From 1da39eb66bbd79af0ef93ae9c6ca96264498aec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20Ioni=C8=9B=C4=83?= Date: Wed, 19 Mar 2025 17:30:36 +0000 Subject: [PATCH 1/2] feat: form versioning --- .../Shelter/Resources/BeneficiaryResource.php | 14 +++- .../Pages/EditBeneficiary.php | 36 ++++----- .../Pages/ListPersonalDataVersions.php | 50 +++++++++++++ .../Pages/ViewDocument.php | 2 - .../Pages/ViewPersonalDataVersion.php | 55 ++++++++++++++ .../Schemas/BeneficiaryDynamicInfolist.php | 73 ++++++++++--------- .../Widgets/PersonalDataVersionsWidget.php | 53 ++++++++++++++ app/Models/Beneficiary.php | 5 ++ lang/en/app.php | 1 + 9 files changed, 234 insertions(+), 55 deletions(-) create mode 100644 app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ListPersonalDataVersions.php create mode 100644 app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ViewPersonalDataVersion.php create mode 100644 app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/PersonalDataVersionsWidget.php diff --git a/app/Filament/Shelter/Resources/BeneficiaryResource.php b/app/Filament/Shelter/Resources/BeneficiaryResource.php index a877f55..ececddf 100644 --- a/app/Filament/Shelter/Resources/BeneficiaryResource.php +++ b/app/Filament/Shelter/Resources/BeneficiaryResource.php @@ -71,9 +71,17 @@ public static function infolist(Infolist $infolist): Infolist ->columns(3) ->heading(__('app.beneficiary.steps.personal_details')) ->headerActions([ + Action::make('data') + ->label(__('app.form.actions.history')) + ->url(fn (Beneficiary $record) => static::getUrl('versions.index', ['record' => $record])) + ->visible(fn (Beneficiary $record) => $record->hasMoreThanOneForm()) + ->icon('heroicon-o-clock') + ->link() + ->outlined(), + Action::make('edit') ->label(__('filament-actions::edit.single.label')) - ->url(fn ($record) => static::getUrl('edit', ['record' => $record])) + ->url(fn (Beneficiary $record) => static::getUrl('edit', ['record' => $record])) ->icon('heroicon-o-pencil-square') ->color('gray') ->outlined(), @@ -171,8 +179,12 @@ public static function getPages(): array 'create' => Pages\CreateBeneficiary::route('/create'), 'view' => Pages\ViewBeneficiary::route('/{record}'), 'edit' => Pages\EditBeneficiary::route('/{record}/edit'), + 'stay' => Pages\ViewStay::route('/{record}/stay/{stay}'), 'document' => Pages\ViewDocument::route('/{record}/document/{document}'), + + 'versions.index' => Pages\ListPersonalDataVersions::route('/{record}/data'), + 'versions.view' => Pages\ViewPersonalDataVersion::route('/{record}/data/{response}'), ]; } } diff --git a/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/EditBeneficiary.php b/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/EditBeneficiary.php index 96a450e..aa29330 100644 --- a/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/EditBeneficiary.php +++ b/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/EditBeneficiary.php @@ -8,6 +8,7 @@ use App\Filament\Shelter\Resources\BeneficiaryResource; use App\Models\Form; use App\Models\Form\FieldResponse; +use App\Models\Form\Response; use Filament\Actions; use Filament\Resources\Pages\EditRecord; @@ -36,24 +37,23 @@ protected function mutateFormDataBeforeFill(array $data): array protected function mutateFormDataBeforeSave(array $data): array { + $form = Form::query() + ->latestPublished(Type::PERSONAL) + ->first(['id']); + + /** @var Response */ $response = $this->getRecord() ->latestPersonal() - ->firstOr(function () { - $form = Form::query() - ->latestPublished(Type::PERSONAL) - ->first(['id']); - - if (blank($form)) { - return; - } + ->where('form_id', $form->id) + ->firstOr( + fn () => $this->getRecord() + ->personal()->create([ + 'form_id' => $form->id, + ]) + ); - return $this->getRecord()->personal()->create([ - 'form_id' => $form->id, - ]); - }); - - $this->wrapInDatabaseTransaction( - fn () => collect(data_get($data, 'form'))->each( + $this->wrapInDatabaseTransaction(function () use ($data, $response) { + collect(data_get($data, 'form'))->each( fn ($value, int $field_id) => FieldResponse::updateOrCreate( [ 'response_id' => $response->id, @@ -63,8 +63,10 @@ protected function mutateFormDataBeforeSave(array $data): array 'value' => $value, ] ) - ) - ); + ); + + $response->touch(); + }); return $data; } diff --git a/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ListPersonalDataVersions.php b/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ListPersonalDataVersions.php new file mode 100644 index 0000000..793d3f9 --- /dev/null +++ b/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ListPersonalDataVersions.php @@ -0,0 +1,50 @@ +getRecord()->hasMoreThanOneForm(), 404); + } + + protected function fillForm(): void + { + // + } + + public function infolist(Infolist $infolist): Infolist + { + return $infolist; + } + + protected function getForms(): array + { + return [ + // Disables form + 'form' => $this->makeForm(), + ]; + } + + protected function getHeaderWidgets(): array + { + return [ + BeneficiaryResource\Widgets\PersonalDataVersionsWidget::class, + ]; + } +} diff --git a/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ViewDocument.php b/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ViewDocument.php index 5b23155..e73497b 100644 --- a/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ViewDocument.php +++ b/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ViewDocument.php @@ -25,7 +25,6 @@ class ViewDocument extends ViewRecord protected function getHeaderActions(): array { return [ - Actions\DeleteAction::make() ->label(__('app.documents.actions.delete')) ->record($this->document) @@ -59,7 +58,6 @@ protected function getHeaderActions(): array return response()->download($mediaItem->getPath(), $mediaItem->file_name); }), - ]; } diff --git a/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ViewPersonalDataVersion.php b/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ViewPersonalDataVersion.php new file mode 100644 index 0000000..b0768b7 --- /dev/null +++ b/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ViewPersonalDataVersion.php @@ -0,0 +1,55 @@ +record($this->response) + ->schema(fn (Response $record) => [ + Section::make() + ->columns(3) + ->schema([ + TextEntry::make('id') + ->label(__('app.field.id')) + ->prefix('#'), + + TextEntry::make('created_at') + ->label(__('app.field.created_at')) + ->dateTime(), + + TextEntry::make('updated_at') + ->label(__('app.field.updated_at')) + ->dateTime(), + ]), + + ...BeneficiaryDynamicInfolist::getSchemaForResponse($record), + ]); + } +} diff --git a/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/BeneficiaryDynamicInfolist.php b/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/BeneficiaryDynamicInfolist.php index 8d52345..5028050 100644 --- a/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/BeneficiaryDynamicInfolist.php +++ b/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/BeneficiaryDynamicInfolist.php @@ -25,47 +25,50 @@ public static function getSchema(): array return []; } - $state->loadMissing('form.sections.fields:id,label,type,section_id'); + return static::getSchemaForResponse($state); + }), + ]; + } - $fieldIndex = 0; + public static function getSchemaForResponse(Response $response): array + { + $fieldIndex = 0; - return $state->form - ->sections - ->map(function ($section) use (&$fieldIndex) { - return Section::make($section->name) - ->description($section->description) - ->columns(3) - ->collapsible() - ->schema( - $section->fields->map(function (Field $field) use (&$fieldIndex) { - $component = TextEntry::make("fields.{$fieldIndex}.value") - ->label($field->label); + return $response->form + ->sections + ->map(function ($section) use (&$fieldIndex) { + return Section::make($section->name) + ->description($section->description) + ->columns(3) + ->collapsible() + ->schema( + $section->fields->map(function (Field $field) use (&$fieldIndex) { + $component = TextEntry::make("fields.{$fieldIndex}.value") + ->label($field->label); - $fieldIndex++; + $fieldIndex++; - switch ($field->type) { - case FieldType::CHECKBOX: - case FieldType::RADIO: - case FieldType::SELECT: - $component->listWithLineBreaks(); - break; + switch ($field->type) { + case FieldType::CHECKBOX: + case FieldType::RADIO: + case FieldType::SELECT: + $component->listWithLineBreaks(); + break; - case FieldType::NUMBER: - $component->numeric(); - break; + case FieldType::NUMBER: + $component->numeric(); + break; - case FieldType::DATE: - $component->formatStateUsing(function ($state) { - return rescue(fn () => Carbon::parse($state)->toFormattedDate(), $state, false); - }); - break; - } + case FieldType::DATE: + $component->formatStateUsing(function ($state) { + return rescue(fn () => Carbon::parse($state)->toFormattedDate(), $state, false); + }); + break; + } - return $component; - })->all() - ); - })->all(); - }), - ]; + return $component; + })->all() + ); + })->all(); } } diff --git a/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/PersonalDataVersionsWidget.php b/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/PersonalDataVersionsWidget.php new file mode 100644 index 0000000..b62166d --- /dev/null +++ b/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/PersonalDataVersionsWidget.php @@ -0,0 +1,53 @@ +query(fn () => $this->record->personal()) + ->columns([ + TextColumn::make('id') + ->label(__('app.field.id')) + ->prefix('#') + ->sortable() + ->shrink(), + + TextColumn::make('created_at') + ->label(__('app.field.created_at')) + ->dateTime() + ->sortable() + ->shrink(), + + TextColumn::make('updated_at') + ->label(__('app.field.updated_at')) + ->dateTime() + ->sortable() + ->shrink(), + + ]) + ->actions([ + ViewAction::make() + ->url(fn (Response $record) => BeneficiaryResource::getUrl('versions.view', [ + 'record' => $this->record, + 'response' => $record, + ])), + ]); + } +} diff --git a/app/Models/Beneficiary.php b/app/Models/Beneficiary.php index a2cf8ce..6184b00 100644 --- a/app/Models/Beneficiary.php +++ b/app/Models/Beneficiary.php @@ -123,4 +123,9 @@ public function scopeWhereInShelter(Builder $query, Shelter $shelter): Builder { return $query->whereRelation('stays', 'shelter_id', $shelter->id); } + + public function hasMoreThanOneForm(): bool + { + return $this->personal()->count() > 1; + } } diff --git a/lang/en/app.php b/lang/en/app.php index 903f2f4..c2dbbc0 100644 --- a/lang/en/app.php +++ b/lang/en/app.php @@ -165,6 +165,7 @@ 'obsolete' => 'Obsolete', ], 'actions' => [ + 'history' => 'Form history', 'draft' => [ 'button' => 'Draft', 'confirm' => [ From 1f81c10b8ffa967f51d6fa590ad2c9bc01cf1de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20Ioni=C8=9B=C4=83?= Date: Thu, 20 Mar 2025 10:04:05 +0000 Subject: [PATCH 2/2] wip --- .../BeneficiaryResource/Pages/ListPersonalDataVersions.php | 5 +++++ .../BeneficiaryResource/Pages/ViewPersonalDataVersion.php | 6 ++---- .../Schemas/BeneficiaryDynamicInfolist.php | 2 ++ .../Widgets/PersonalDataVersionsWidget.php | 5 +++++ lang/en/app.php | 1 + 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ListPersonalDataVersions.php b/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ListPersonalDataVersions.php index 793d3f9..8481ef0 100644 --- a/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ListPersonalDataVersions.php +++ b/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ListPersonalDataVersions.php @@ -23,6 +23,11 @@ protected function authorizeAccess(): void abort_unless($this->getRecord()->hasMoreThanOneForm(), 404); } + public function getTitle(): string + { + return __('app.form.actions.history'); + } + protected function fillForm(): void { // diff --git a/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ViewPersonalDataVersion.php b/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ViewPersonalDataVersion.php index b0768b7..95aa147 100644 --- a/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ViewPersonalDataVersion.php +++ b/app/Filament/Shelter/Resources/BeneficiaryResource/Pages/ViewPersonalDataVersion.php @@ -21,11 +21,9 @@ class ViewPersonalDataVersion extends ViewRecord protected static string $resource = BeneficiaryResource::class; - protected function getHeaderActions(): array + public function getTitle(): string { - return [ - // - ]; + return __('app.form.actions.history') . ' #' . $this->response->id; } public function infolist(Infolist $infolist): Infolist diff --git a/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/BeneficiaryDynamicInfolist.php b/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/BeneficiaryDynamicInfolist.php index 5028050..bc32a03 100644 --- a/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/BeneficiaryDynamicInfolist.php +++ b/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/BeneficiaryDynamicInfolist.php @@ -34,6 +34,8 @@ public static function getSchemaForResponse(Response $response): array { $fieldIndex = 0; + $response->loadMissing('form.sections.fields'); + return $response->form ->sections ->map(function ($section) use (&$fieldIndex) { diff --git a/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/PersonalDataVersionsWidget.php b/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/PersonalDataVersionsWidget.php index b62166d..f9f0017 100644 --- a/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/PersonalDataVersionsWidget.php +++ b/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/PersonalDataVersionsWidget.php @@ -22,6 +22,7 @@ public function table(Table $table): Table { return $table ->query(fn () => $this->record->personal()) + ->heading(__('app.form.actions.history')) ->columns([ TextColumn::make('id') ->label(__('app.field.id')) @@ -29,6 +30,10 @@ public function table(Table $table): Table ->sortable() ->shrink(), + TextColumn::make('form.sections.name') + ->label(__('app.field.form_sections')) + ->wrap(), + TextColumn::make('created_at') ->label(__('app.field.created_at')) ->dateTime() diff --git a/lang/en/app.php b/lang/en/app.php index c2dbbc0..91f1aa3 100644 --- a/lang/en/app.php +++ b/lang/en/app.php @@ -39,6 +39,7 @@ 'enabled' => 'Enabled', 'end_date' => 'End date', 'fields' => 'Fields', + 'form_sections' => 'Form sections', 'form_type' => 'Form type', 'gender' => 'Gender', 'group_size' => 'No.',