diff --git a/thanasoft-back/app/Http/Controllers/Api/DeceasedController.php b/thanasoft-back/app/Http/Controllers/Api/DeceasedController.php new file mode 100644 index 0000000..b866f71 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/DeceasedController.php @@ -0,0 +1,146 @@ +deceasedRepository = $deceasedRepository; + } + + /** + * Display a listing of the resource. + */ + public function index(Request $request): JsonResponse + { + try { + $filters = $request->only([ + 'search', + 'start_date', + 'end_date', + 'sort_by', + 'sort_order' + ]); + + $perPage = $request->input('per_page', 15); + + $deceased = $this->deceasedRepository->getAllPaginated($filters, $perPage); + + return response()->json(new DeceasedCollection($deceased)); + } catch (\Exception $e) { + Log::error('Error fetching deceased list: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des défunts.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Store a newly created resource in storage. + */ + public function store(StoreDeceasedRequest $request): JsonResponse + { + try { + $validated = $request->validated(); + + $deceased = $this->deceasedRepository->create($validated); + + return response()->json(new DeceasedResource($deceased), Response::HTTP_CREATED); + } catch (\Exception $e) { + Log::error('Error creating deceased: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du défunt.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Display the specified resource. + */ + public function show(int $id): JsonResponse + { + try { + $deceased = $this->deceasedRepository->findById($id); + + return response()->json(new DeceasedResource($deceased)); + } catch (\Exception $e) { + Log::error('Error fetching deceased details: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Défunt non trouvé ou une erreur est survenue.', + 'error' => $e->getMessage() + ], Response::HTTP_NOT_FOUND); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(UpdateDeceasedRequest $request, int $id): JsonResponse + { + try { + $deceased = $this->deceasedRepository->findById($id); + + $validated = $request->validated(); + + $updatedDeceased = $this->deceasedRepository->update($deceased, $validated); + + return response()->json(new DeceasedResource($updatedDeceased)); + } catch (\Exception $e) { + Log::error('Error updating deceased: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du défunt.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(int $id): JsonResponse + { + try { + $deceased = $this->deceasedRepository->findById($id); + + $this->deceasedRepository->delete($deceased); + + return response()->json(null, Response::HTTP_NO_CONTENT); + } catch (\Exception $e) { + Log::error('Error deleting deceased: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du défunt.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/InterventionController.php b/thanasoft-back/app/Http/Controllers/Api/InterventionController.php new file mode 100644 index 0000000..26cfad8 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/InterventionController.php @@ -0,0 +1,176 @@ +interventionRepository = $interventionRepository; + } + + /** + * Display a listing of the resource. + */ + public function index(Request $request): JsonResponse + { + try { + $filters = $request->only([ + 'client_id', + 'deceased_id', + 'status', + 'type', + 'start_date', + 'end_date', + 'sort_by', + 'sort_order' + ]); + + $perPage = $request->input('per_page', 15); + + $interventions = $this->interventionRepository->getAllPaginated($filters, $perPage); + + return response()->json(new InterventionCollection($interventions)); + } catch (\Exception $e) { + Log::error('Error fetching interventions list: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des interventions.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Store a newly created resource in storage. + */ + public function store(StoreInterventionRequest $request): JsonResponse + { + try { + $validated = $request->validated(); + + $intervention = $this->interventionRepository->create($validated); + + return response()->json(new InterventionResource($intervention), Response::HTTP_CREATED); + } catch (\Exception $e) { + Log::error('Error creating intervention: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création de l\'intervention.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Display the specified resource. + */ + public function show(int $id): JsonResponse + { + try { + $intervention = $this->interventionRepository->findById($id); + + return response()->json(new InterventionResource($intervention)); + } catch (\Exception $e) { + Log::error('Error fetching intervention details: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Intervention non trouvée ou une erreur est survenue.', + 'error' => $e->getMessage() + ], Response::HTTP_NOT_FOUND); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(UpdateInterventionRequest $request, int $id): JsonResponse + { + try { + $intervention = $this->interventionRepository->findById($id); + + $validated = $request->validated(); + + $updatedIntervention = $this->interventionRepository->update($intervention, $validated); + + return response()->json(new InterventionResource($updatedIntervention)); + } catch (\Exception $e) { + Log::error('Error updating intervention: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour de l\'intervention.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(int $id): JsonResponse + { + try { + $intervention = $this->interventionRepository->findById($id); + + $this->interventionRepository->delete($intervention); + + return response()->json(null, Response::HTTP_NO_CONTENT); + } catch (\Exception $e) { + Log::error('Error deleting intervention: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression de l\'intervention.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Change the status of an intervention. + */ + public function changeStatus(Request $request, int $id): JsonResponse + { + try { + $validated = $request->validate([ + 'status' => 'required|in:demande,planifie,en_cours,termine,annule' + ]); + + $intervention = $this->interventionRepository->findById($id); + + $updatedIntervention = $this->interventionRepository->changeStatus( + $intervention, + $validated['status'] + ); + + return response()->json(new InterventionResource($updatedIntervention)); + } catch (\Exception $e) { + Log::error('Error changing intervention status: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la modification du statut de l\'intervention.', + 'error' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreDeceasedRequest.php b/thanasoft-back/app/Http/Requests/StoreDeceasedRequest.php new file mode 100644 index 0000000..1f75f24 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreDeceasedRequest.php @@ -0,0 +1,49 @@ +|string> + */ + public function rules(): array + { + return [ + 'last_name' => ['required', 'string', 'max:191'], + 'first_name' => ['nullable', 'string', 'max:191'], + 'birth_date' => ['nullable', 'date'], + 'death_date' => ['nullable', 'date', 'after_or_equal:birth_date'], + 'place_of_death' => ['nullable', 'string', 'max:255'], + 'notes' => ['nullable', 'string'] + ]; + } + + /** + * Get custom error messages for validator errors. + */ + public function messages(): array + { + return [ + 'last_name.required' => 'Le nom de famille est obligatoire.', + 'last_name.max' => 'Le nom de famille ne peut pas dépasser 191 caractères.', + 'first_name.max' => 'Le prénom ne peut pas dépasser 191 caractères.', + 'death_date.after_or_equal' => 'La date de décès doit être postérieure ou égale à la date de naissance.', + 'place_of_death.max' => 'Le lieu de décès ne peut pas dépasser 255 caractères.' + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreInterventionRequest.php b/thanasoft-back/app/Http/Requests/StoreInterventionRequest.php new file mode 100644 index 0000000..3298451 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreInterventionRequest.php @@ -0,0 +1,75 @@ +|string> + */ + public function rules(): array + { + return [ + 'client_id' => ['required', 'exists:clients,id'], + 'deceased_id' => ['nullable', 'exists:deceased,id'], + 'order_giver' => ['nullable', 'string', 'max:255'], + 'location_id' => ['nullable', 'exists:client_locations,id'], + 'type' => ['required', Rule::in([ + 'thanatopraxie', + 'toilette_mortuaire', + 'exhumation', + 'retrait_pacemaker', + 'retrait_bijoux', + 'autre' + ])], + 'scheduled_at' => ['nullable', 'date_format:Y-m-d H:i:s'], + 'duration_min' => ['nullable', 'integer', 'min:0'], + 'status' => ['sometimes', Rule::in([ + 'demande', + 'planifie', + 'en_cours', + 'termine', + 'annule' + ])], + 'assigned_practitioner_id' => ['nullable', 'exists:thanatopractitioners,id'], + 'notes' => ['nullable', 'string'], + 'created_by' => ['nullable', 'exists:users,id'] + ]; + } + + /** + * Get custom error messages for validator errors. + */ + public function messages(): array + { + return [ + 'client_id.required' => 'Le client est obligatoire.', + 'client_id.exists' => 'Le client sélectionné est invalide.', + 'deceased_id.exists' => 'Le défunt sélectionné est invalide.', + 'order_giver.max' => 'Le donneur d\'ordre ne peut pas dépasser 255 caractères.', + 'location_id.exists' => 'Le lieu sélectionné est invalide.', + 'type.required' => 'Le type d\'intervention est obligatoire.', + 'type.in' => 'Le type d\'intervention est invalide.', + 'scheduled_at.date_format' => 'Le format de la date programmée est invalide.', + 'duration_min.integer' => 'La durée doit être un nombre entier.', + 'duration_min.min' => 'La durée ne peut pas être négative.', + 'status.in' => 'Le statut de l\'intervention est invalide.', + 'assigned_practitioner_id.exists' => 'Le praticien sélectionné est invalide.', + 'created_by.exists' => 'L\'utilisateur créateur est invalide.' + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateDeceasedRequest.php b/thanasoft-back/app/Http/Requests/UpdateDeceasedRequest.php new file mode 100644 index 0000000..dcafcd4 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateDeceasedRequest.php @@ -0,0 +1,49 @@ +|string> + */ + public function rules(): array + { + return [ + 'last_name' => ['sometimes', 'required', 'string', 'max:191'], + 'first_name' => ['nullable', 'string', 'max:191'], + 'birth_date' => ['nullable', 'date'], + 'death_date' => ['nullable', 'date', 'after_or_equal:birth_date'], + 'place_of_death' => ['nullable', 'string', 'max:255'], + 'notes' => ['nullable', 'string'] + ]; + } + + /** + * Get custom error messages for validator errors. + */ + public function messages(): array + { + return [ + 'last_name.required' => 'Le nom de famille est obligatoire.', + 'last_name.max' => 'Le nom de famille ne peut pas dépasser 191 caractères.', + 'first_name.max' => 'Le prénom ne peut pas dépasser 191 caractères.', + 'death_date.after_or_equal' => 'La date de décès doit être postérieure ou égale à la date de naissance.', + 'place_of_death.max' => 'Le lieu de décès ne peut pas dépasser 255 caractères.' + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateInterventionRequest.php b/thanasoft-back/app/Http/Requests/UpdateInterventionRequest.php new file mode 100644 index 0000000..4f5a99a --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateInterventionRequest.php @@ -0,0 +1,75 @@ +|string> + */ + public function rules(): array + { + return [ + 'client_id' => ['sometimes', 'required', 'exists:clients,id'], + 'deceased_id' => ['nullable', 'exists:deceased,id'], + 'order_giver' => ['nullable', 'string', 'max:255'], + 'location_id' => ['nullable', 'exists:client_locations,id'], + 'type' => ['sometimes', 'required', Rule::in([ + 'thanatopraxie', + 'toilette_mortuaire', + 'exhumation', + 'retrait_pacemaker', + 'retrait_bijoux', + 'autre' + ])], + 'scheduled_at' => ['nullable', 'date_format:Y-m-d H:i:s'], + 'duration_min' => ['nullable', 'integer', 'min:0'], + 'status' => ['sometimes', Rule::in([ + 'demande', + 'planifie', + 'en_cours', + 'termine', + 'annule' + ])], + 'assigned_practitioner_id' => ['nullable', 'exists:thanatopractitioners,id'], + 'notes' => ['nullable', 'string'], + 'created_by' => ['nullable', 'exists:users,id'] + ]; + } + + /** + * Get custom error messages for validator errors. + */ + public function messages(): array + { + return [ + 'client_id.required' => 'Le client est obligatoire.', + 'client_id.exists' => 'Le client sélectionné est invalide.', + 'deceased_id.exists' => 'Le défunt sélectionné est invalide.', + 'order_giver.max' => 'Le donneur d\'ordre ne peut pas dépasser 255 caractères.', + 'location_id.exists' => 'Le lieu sélectionné est invalide.', + 'type.required' => 'Le type d\'intervention est obligatoire.', + 'type.in' => 'Le type d\'intervention est invalide.', + 'scheduled_at.date_format' => 'Le format de la date programmée est invalide.', + 'duration_min.integer' => 'La durée doit être un nombre entier.', + 'duration_min.min' => 'La durée ne peut pas être négative.', + 'status.in' => 'Le statut de l\'intervention est invalide.', + 'assigned_practitioner_id.exists' => 'Le praticien sélectionné est invalide.', + 'created_by.exists' => 'L\'utilisateur créateur est invalide.' + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateThanatopractitionerRequest.php b/thanasoft-back/app/Http/Requests/UpdateThanatopractitionerRequest.php index bb9aff4..74ff3c8 100644 --- a/thanasoft-back/app/Http/Requests/UpdateThanatopractitionerRequest.php +++ b/thanasoft-back/app/Http/Requests/UpdateThanatopractitionerRequest.php @@ -24,7 +24,7 @@ class UpdateThanatopractitionerRequest extends FormRequest { return [ 'employee_id' => [ - 'required', + 'nullable', 'exists:employees,id', Rule::unique('thanatopractitioners', 'employee_id')->ignore($this->route('thanatopractitioner')) ], diff --git a/thanasoft-back/app/Http/Resources/Deceased/DeceasedCollection.php b/thanasoft-back/app/Http/Resources/Deceased/DeceasedCollection.php new file mode 100644 index 0000000..9ea0fdf --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Deceased/DeceasedCollection.php @@ -0,0 +1,29 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'data' => $this->collection, + 'meta' => [ + 'total' => $this->total(), + 'per_page' => $this->perPage(), + 'current_page' => $this->currentPage(), + 'last_page' => $this->lastPage(), + 'from' => $this->firstItem(), + 'to' => $this->lastItem() + ] + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Deceased/DeceasedResource.php b/thanasoft-back/app/Http/Resources/Deceased/DeceasedResource.php new file mode 100644 index 0000000..b4db6b7 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Deceased/DeceasedResource.php @@ -0,0 +1,32 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'last_name' => $this->last_name, + 'first_name' => $this->first_name, + 'full_name' => trim($this->first_name . ' ' . $this->last_name), + 'birth_date' => $this->birth_date ? $this->birth_date->format('Y-m-d') : null, + 'death_date' => $this->death_date ? $this->death_date->format('Y-m-d') : null, + 'place_of_death' => $this->place_of_death, + 'notes' => $this->notes, + 'documents_count' => $this->documents_count ?? $this->documents()->count(), + 'interventions_count' => $this->interventions_count ?? $this->interventions()->count(), + 'created_at' => $this->created_at->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at->format('Y-m-d H:i:s') + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Intervention/InterventionAttachmentResource.php b/thanasoft-back/app/Http/Resources/Intervention/InterventionAttachmentResource.php new file mode 100644 index 0000000..ad4b398 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Intervention/InterventionAttachmentResource.php @@ -0,0 +1,33 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'intervention_id' => $this->intervention_id, + 'file' => $this->whenLoaded('file', function () { + return [ + 'id' => $this->file->id, + 'name' => $this->file->name, + 'path' => $this->file->path, + 'mime_type' => $this->file->mime_type, + 'size' => $this->file->size + ]; + }), + 'label' => $this->label, + 'created_at' => $this->created_at->format('Y-m-d H:i:s') + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Intervention/InterventionCollection.php b/thanasoft-back/app/Http/Resources/Intervention/InterventionCollection.php new file mode 100644 index 0000000..72fb989 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Intervention/InterventionCollection.php @@ -0,0 +1,46 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'data' => $this->collection, + 'meta' => [ + 'total' => $this->total(), + 'per_page' => $this->perPage(), + 'current_page' => $this->currentPage(), + 'last_page' => $this->lastPage(), + 'from' => $this->firstItem(), + 'to' => $this->lastItem(), + 'status_summary' => $this->calculateStatusSummary() + ] + ]; + } + + /** + * Calculate summary of intervention statuses. + * + * @return array + */ + protected function calculateStatusSummary(): array + { + $statusCounts = $this->collection->groupBy('status') + ->map(function ($group) { + return $group->count(); + }) + ->toArray(); + + return $statusCounts; + } +} diff --git a/thanasoft-back/app/Http/Resources/Intervention/InterventionNotificationResource.php b/thanasoft-back/app/Http/Resources/Intervention/InterventionNotificationResource.php new file mode 100644 index 0000000..dfd7da0 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Intervention/InterventionNotificationResource.php @@ -0,0 +1,28 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'intervention_id' => $this->intervention_id, + 'channel' => $this->channel, + 'destination' => $this->destination, + 'payload' => $this->payload, + 'status' => $this->status, + 'sent_at' => $this->sent_at ? $this->sent_at->format('Y-m-d H:i:s') : null, + 'created_at' => $this->created_at->format('Y-m-d H:i:s') + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Intervention/InterventionResource.php b/thanasoft-back/app/Http/Resources/Intervention/InterventionResource.php new file mode 100644 index 0000000..0b9a040 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Intervention/InterventionResource.php @@ -0,0 +1,55 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'client' => $this->whenLoaded('client', function () { + return new ClientResource($this->client); + }), + 'deceased' => $this->whenLoaded('deceased', function () { + return new DeceasedResource($this->deceased); + }), + 'order_giver' => $this->order_giver, + 'location' => $this->whenLoaded('location', function () { + return [ + 'id' => $this->location->id, + 'name' => $this->location->name + ]; + }), + 'type' => $this->type, + 'scheduled_at' => $this->scheduled_at ? $this->scheduled_at->format('Y-m-d H:i:s') : null, + 'duration_min' => $this->duration_min, + 'status' => $this->status, + 'assigned_practitioner' => $this->whenLoaded('assignedPractitioner', function () { + return new ThanatopractitionerResource($this->assignedPractitioner); + }), + 'attachments_count' => $this->attachments_count, + 'notes' => $this->notes, + 'created_by' => $this->created_by, + 'created_at' => $this->created_at->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at->format('Y-m-d H:i:s'), + 'attachments' => $this->whenLoaded('attachments', function () { + return InterventionAttachmentResource::collection($this->attachments); + }), + 'notifications' => $this->whenLoaded('notifications', function () { + return InterventionNotificationResource::collection($this->notifications); + }) + ]; + } +} diff --git a/thanasoft-back/app/Models/Deceased.php b/thanasoft-back/app/Models/Deceased.php new file mode 100644 index 0000000..77c5f92 --- /dev/null +++ b/thanasoft-back/app/Models/Deceased.php @@ -0,0 +1,59 @@ + 'date', + 'death_date' => 'date', + ]; + + /** + * Get the documents associated with the deceased. + */ + public function documents(): HasMany + { + return $this->hasMany(DeceasedDocument::class); + } + + /** + * Get the interventions associated with the deceased. + */ + public function interventions(): HasMany + { + return $this->hasMany(Intervention::class); + } +} diff --git a/thanasoft-back/app/Models/DeceasedDocument.php b/thanasoft-back/app/Models/DeceasedDocument.php new file mode 100644 index 0000000..b7b15ac --- /dev/null +++ b/thanasoft-back/app/Models/DeceasedDocument.php @@ -0,0 +1,49 @@ + 'datetime' + ]; + + /** + * Get the deceased associated with the document. + */ + public function deceased(): BelongsTo + { + return $this->belongsTo(Deceased::class); + } + + /** + * Get the file associated with the document. + */ + public function file(): BelongsTo + { + return $this->belongsTo(File::class); + } +} diff --git a/thanasoft-back/app/Models/Intervention.php b/thanasoft-back/app/Models/Intervention.php new file mode 100644 index 0000000..7470b99 --- /dev/null +++ b/thanasoft-back/app/Models/Intervention.php @@ -0,0 +1,99 @@ + 'datetime', + 'attachments_count' => 'integer' + ]; + + /** + * Get the client associated with the intervention. + */ + public function client(): BelongsTo + { + return $this->belongsTo(Client::class); + } + + /** + * Get the deceased associated with the intervention. + */ + public function deceased(): BelongsTo + { + return $this->belongsTo(Deceased::class); + } + + /** + * Get the location associated with the intervention. + */ + public function location(): BelongsTo + { + return $this->belongsTo(ClientLocation::class); + } + + /** + * Get the practitioner assigned to the intervention. + */ + public function assignedPractitioner(): BelongsTo + { + return $this->belongsTo(Thanatopractitioner::class, 'assigned_practitioner_id'); + } + + /** + * Get the user who created the intervention. + */ + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Get the attachments for the intervention. + */ + public function attachments(): HasMany + { + return $this->hasMany(InterventionAttachment::class); + } + + /** + * Get the notifications for the intervention. + */ + public function notifications(): HasMany + { + return $this->hasMany(InterventionNotification::class); + } +} diff --git a/thanasoft-back/app/Models/InterventionAttachment.php b/thanasoft-back/app/Models/InterventionAttachment.php new file mode 100644 index 0000000..f0f939d --- /dev/null +++ b/thanasoft-back/app/Models/InterventionAttachment.php @@ -0,0 +1,39 @@ +belongsTo(Intervention::class); + } + + /** + * Get the file associated with the attachment. + */ + public function file(): BelongsTo + { + return $this->belongsTo(File::class); + } +} diff --git a/thanasoft-back/app/Models/InterventionNotification.php b/thanasoft-back/app/Models/InterventionNotification.php new file mode 100644 index 0000000..63a9800 --- /dev/null +++ b/thanasoft-back/app/Models/InterventionNotification.php @@ -0,0 +1,44 @@ + 'array', + 'sent_at' => 'datetime' + ]; + + /** + * Get the intervention associated with the notification. + */ + public function intervention(): BelongsTo + { + return $this->belongsTo(Intervention::class); + } +} diff --git a/thanasoft-back/app/Providers/AppServiceProvider.php b/thanasoft-back/app/Providers/AppServiceProvider.php index 76de503..b77805f 100644 --- a/thanasoft-back/app/Providers/AppServiceProvider.php +++ b/thanasoft-back/app/Providers/AppServiceProvider.php @@ -56,6 +56,10 @@ class AppServiceProvider extends ServiceProvider $this->app->bind(\App\Repositories\PractitionerDocumentRepositoryInterface::class, function ($app) { return new \App\Repositories\PractitionerDocumentRepository($app->make(\App\Models\PractitionerDocument::class)); }); + + $this->app->bind(\App\Repositories\InterventionRepositoryInterface::class, \App\Repositories\InterventionRepository::class); + + $this->app->bind(\App\Repositories\DeceasedRepositoryInterface::class, \App\Repositories\DeceasedRepository::class); } /** diff --git a/thanasoft-back/app/Providers/RepositoryServiceProvider.php b/thanasoft-back/app/Providers/RepositoryServiceProvider.php new file mode 100644 index 0000000..924eb69 --- /dev/null +++ b/thanasoft-back/app/Providers/RepositoryServiceProvider.php @@ -0,0 +1,29 @@ +app->bind(DeceasedRepositoryInterface::class, DeceasedRepository::class); + $this->app->bind(InterventionRepositoryInterface::class, InterventionRepository::class); + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + // + } +} diff --git a/thanasoft-back/app/Repositories/DeceasedRepository.php b/thanasoft-back/app/Repositories/DeceasedRepository.php new file mode 100644 index 0000000..2da7bb7 --- /dev/null +++ b/thanasoft-back/app/Repositories/DeceasedRepository.php @@ -0,0 +1,102 @@ +where(function($q) use ($filters) { + $q->where('last_name', 'LIKE', "%{$filters['search']}%") + ->orWhere('first_name', 'LIKE', "%{$filters['search']}%"); + }); + } + + // Apply date range filters + if (!empty($filters['start_date'])) { + $query->where('death_date', '>=', $filters['start_date']); + } + + if (!empty($filters['end_date'])) { + $query->where('death_date', '<=', $filters['end_date']); + } + + // Apply sorting + $sortBy = $filters['sort_by'] ?? 'created_at'; + $sortOrder = $filters['sort_order'] ?? 'desc'; + $query->orderBy($sortBy, $sortOrder); + + // Eager load related counts + $query->withCount(['documents', 'interventions']); + + return $query->paginate($perPage); + } + + /** + * Find a deceased by ID + * + * @param int $id + * @return Deceased + */ + public function findById(int $id): Deceased + { + return Deceased::findOrFail($id); + } + + /** + * Create a new deceased record + * + * @param array $data + * @return Deceased + */ + public function create(array $data): Deceased + { + return DB::transaction(function () use ($data) { + return Deceased::create($data); + }); + } + + /** + * Update an existing deceased record + * + * @param Deceased $deceased + * @param array $data + * @return Deceased + */ + public function update(Deceased $deceased, array $data): Deceased + { + return DB::transaction(function () use ($deceased, $data) { + $deceased->update($data); + return $deceased; + }); + } + + /** + * Delete a deceased record + * + * @param Deceased $deceased + * @return bool + */ + public function delete(Deceased $deceased): bool + { + return DB::transaction(function () use ($deceased) { + return $deceased->delete(); + }); + } +} diff --git a/thanasoft-back/app/Repositories/DeceasedRepositoryInterface.php b/thanasoft-back/app/Repositories/DeceasedRepositoryInterface.php new file mode 100644 index 0000000..a7f5840 --- /dev/null +++ b/thanasoft-back/app/Repositories/DeceasedRepositoryInterface.php @@ -0,0 +1,52 @@ +where('client_id', $filters['client_id']); + } + + if (!empty($filters['deceased_id'])) { + $query->where('deceased_id', $filters['deceased_id']); + } + + if (!empty($filters['status'])) { + $query->where('status', $filters['status']); + } + + if (!empty($filters['type'])) { + $query->where('type', $filters['type']); + } + + // Date range filters + if (!empty($filters['start_date'])) { + $query->where('scheduled_at', '>=', $filters['start_date']); + } + + if (!empty($filters['end_date'])) { + $query->where('scheduled_at', '<=', $filters['end_date']); + } + + // Apply sorting + $sortBy = $filters['sort_by'] ?? 'created_at'; + $sortOrder = $filters['sort_order'] ?? 'desc'; + $query->orderBy($sortBy, $sortOrder); + + // Eager load related models + $query->with([ + 'client', + 'deceased', + 'location', + 'assignedPractitioner' + ]); + + return $query->paginate($perPage); + } + + /** + * Find an intervention by ID + * + * @param int $id + * @return Intervention + */ + public function findById(int $id): Intervention + { + return Intervention::with([ + 'client', + 'deceased', + 'location', + 'assignedPractitioner', + 'attachments', + 'notifications' + ])->findOrFail($id); + } + + /** + * Create a new intervention record + * + * @param array $data + * @return Intervention + */ + public function create(array $data): Intervention + { + return DB::transaction(function () use ($data) { + return Intervention::create($data); + }); + } + + /** + * Update an existing intervention record + * + * @param Intervention $intervention + * @param array $data + * @return Intervention + */ + public function update(Intervention $intervention, array $data): Intervention + { + return DB::transaction(function () use ($intervention, $data) { + $intervention->update($data); + return $intervention; + }); + } + + /** + * Delete an intervention record + * + * @param Intervention $intervention + * @return bool + */ + public function delete(Intervention $intervention): bool + { + return DB::transaction(function () use ($intervention) { + return $intervention->delete(); + }); + } + + /** + * Change the status of an intervention + * + * @param Intervention $intervention + * @param string $status + * @return Intervention + */ + public function changeStatus(Intervention $intervention, string $status): Intervention + { + return DB::transaction(function () use ($intervention, $status) { + $intervention->update(['status' => $status]); + return $intervention; + }); + } +} diff --git a/thanasoft-back/app/Repositories/InterventionRepositoryInterface.php b/thanasoft-back/app/Repositories/InterventionRepositoryInterface.php new file mode 100644 index 0000000..a4df6df --- /dev/null +++ b/thanasoft-back/app/Repositories/InterventionRepositoryInterface.php @@ -0,0 +1,60 @@ +id(); // This creates an auto-incrementing BIGINT primary key + $table->string('file_name', 255); + $table->string('mime_type', 191)->nullable(); + $table->bigInteger('size_bytes')->nullable(); + $table->text('storage_uri'); + $table->char('sha256', 64)->nullable(); + $table->foreignId('uploaded_by')->nullable()->constrained('users')->onDelete('set null'); + $table->timestamp('uploaded_at')->useCurrent(); + $table->timestamps(); // Optional: adds created_at and updated_at columns + + // Add index for better performance on frequently searched columns + $table->index('sha256'); + $table->index('uploaded_by'); + $table->index('uploaded_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('files'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_11_100000_create_deceased_table.php b/thanasoft-back/database/migrations/2025_11_11_100000_create_deceased_table.php new file mode 100644 index 0000000..e076375 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_11_100000_create_deceased_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('last_name', 191)->nullable(false); + $table->string('first_name', 191)->nullable(); + $table->date('birth_date')->nullable(); + $table->date('death_date')->nullable(); + $table->string('place_of_death', 255)->nullable(); + $table->text('notes')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('deceased'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_11_100001_create_deceased_documents_table.php b/thanasoft-back/database/migrations/2025_11_11_100001_create_deceased_documents_table.php new file mode 100644 index 0000000..75f32c4 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_11_100001_create_deceased_documents_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('deceased_id')->constrained('deceased')->cascadeOnDelete(); + $table->string('doc_type', 64); + $table->foreignId('file_id')->nullable()->constrained('files')->nullOnDelete(); + $table->timestamp('generated_at')->useCurrent(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('deceased_documents'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_11_100002_create_interventions_table.php b/thanasoft-back/database/migrations/2025_11_11_100002_create_interventions_table.php new file mode 100644 index 0000000..8d468d1 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_11_100002_create_interventions_table.php @@ -0,0 +1,57 @@ +id(); + $table->foreignId('client_id')->constrained('clients'); + $table->foreignId('deceased_id')->nullable()->constrained('deceased')->nullOnDelete(); + $table->string('order_giver', 255)->nullable(); + $table->foreignId('location_id')->nullable()->constrained('client_locations')->nullOnDelete(); + $table->enum('type', [ + 'thanatopraxie', + 'toilette_mortuaire', + 'exhumation', + 'retrait_pacemaker', + 'retrait_bijoux', + 'autre' + ])->nullable(false); + $table->dateTime('scheduled_at')->nullable(); + $table->integer('duration_min')->nullable(); + $table->enum('status', [ + 'demande', + 'planifie', + 'en_cours', + 'termine', + 'annule' + ])->default('demande'); + $table->foreignId('assigned_practitioner_id')->nullable()->constrained('thanatopractitioners')->nullOnDelete(); + $table->integer('attachments_count')->default(0); + $table->text('notes')->nullable(); + $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + + // Indexes + $table->index(['client_id', 'status'], 'idx_interventions_company_status'); + $table->index('scheduled_at', 'idx_interventions_scheduled'); + $table->index(['client_id', 'scheduled_at'], 'idx_interventions_client_date'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('interventions'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_11_100003_create_intervention_attachments_table.php b/thanasoft-back/database/migrations/2025_11_11_100003_create_intervention_attachments_table.php new file mode 100644 index 0000000..7880e75 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_11_100003_create_intervention_attachments_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('intervention_id')->constrained('interventions')->cascadeOnDelete(); + $table->foreignId('file_id')->nullable()->constrained('files')->nullOnDelete(); + $table->string('label', 191)->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('intervention_attachments'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_11_11_100004_create_intervention_notifications_table.php b/thanasoft-back/database/migrations/2025_11_11_100004_create_intervention_notifications_table.php new file mode 100644 index 0000000..9bb620d --- /dev/null +++ b/thanasoft-back/database/migrations/2025_11_11_100004_create_intervention_notifications_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('intervention_id')->constrained('interventions')->cascadeOnDelete(); + $table->enum('channel', ['sms', 'mobile', 'email'])->nullable(false); + $table->string('destination', 191)->nullable(false); + $table->json('payload')->nullable(); + $table->string('status', 32)->default('queued'); + $table->timestamp('sent_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('intervention_notifications'); + } +}; diff --git a/thanasoft-back/routes/api.php b/thanasoft-back/routes/api.php index fee0164..95da23c 100644 --- a/thanasoft-back/routes/api.php +++ b/thanasoft-back/routes/api.php @@ -13,6 +13,9 @@ use App\Http\Controllers\Api\ProductCategoryController; use App\Http\Controllers\Api\EmployeeController; use App\Http\Controllers\Api\ThanatopractitionerController; use App\Http\Controllers\Api\PractitionerDocumentController; +use App\Http\Controllers\Api\DeceasedController; +use App\Http\Controllers\Api\InterventionController; + /* |-------------------------------------------------------------------------- @@ -92,4 +95,24 @@ Route::middleware('auth:sanctum')->group(function () { Route::get('/practitioner-documents/expiring', [PractitionerDocumentController::class, 'getExpiringDocuments']); Route::apiResource('practitioner-documents', PractitionerDocumentController::class); Route::patch('/practitioner-documents/{id}/verify', [PractitionerDocumentController::class, 'verifyDocument']); + + // Deceased Routes + Route::prefix('deceased')->group(function () { + Route::get('/', [DeceasedController::class, 'index']); + Route::post('/', [DeceasedController::class, 'store']); + Route::get('/{deceased}', [DeceasedController::class, 'show']); + Route::put('/{deceased}', [DeceasedController::class, 'update']); + Route::delete('/{deceased}', [DeceasedController::class, 'destroy']); + }); + + // Intervention Routes + Route::prefix('interventions')->group(function () { + Route::get('/', [InterventionController::class, 'index']); + Route::post('/', [InterventionController::class, 'store']); + Route::get('/{intervention}', [InterventionController::class, 'show']); + Route::put('/{intervention}', [InterventionController::class, 'update']); + Route::delete('/{intervention}', [InterventionController::class, 'destroy']); + Route::patch('/{intervention}/status', [InterventionController::class, 'changeStatus']); + }); + }); diff --git a/thanasoft-front/src/components/Organism/Defunts/AddDefuntPresentation.vue b/thanasoft-front/src/components/Organism/Defunts/AddDefuntPresentation.vue new file mode 100644 index 0000000..fbfb045 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Defunts/AddDefuntPresentation.vue @@ -0,0 +1,38 @@ + + diff --git a/thanasoft-front/src/components/Organism/Defunts/DefuntDetailPresentation.vue b/thanasoft-front/src/components/Organism/Defunts/DefuntDetailPresentation.vue new file mode 100644 index 0000000..db180b0 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Defunts/DefuntDetailPresentation.vue @@ -0,0 +1,68 @@ + + diff --git a/thanasoft-front/src/components/Organism/Defunts/DefuntPresentation.vue b/thanasoft-front/src/components/Organism/Defunts/DefuntPresentation.vue index 8853f07..677e3fa 100644 --- a/thanasoft-front/src/components/Organism/Defunts/DefuntPresentation.vue +++ b/thanasoft-front/src/components/Organism/Defunts/DefuntPresentation.vue @@ -1,7 +1,7 @@ @@ -20,4 +20,19 @@ import addButton from "@/components/molecules/new-button/addButton.vue"; import FilterTable from "@/components/molecules/Tables/FilterTable.vue"; import TableAction from "@/components/molecules/Tables/TableAction.vue"; import DefuntsList from "@/components/molecules/Defunts/DefuntsList.vue"; +import { defineProps } from "vue"; + +import { useRouter } from "vue-router"; +const router = useRouter(); + +defineProps({ + defunts: { + type: Array, + required: true, + }, +}); + +const add = () => { + router.push({ name: "Add Defunts" }); +}; diff --git a/thanasoft-front/src/components/Organism/Interventions/AddInterventionPresentation.vue b/thanasoft-front/src/components/Organism/Interventions/AddInterventionPresentation.vue new file mode 100644 index 0000000..b19646c --- /dev/null +++ b/thanasoft-front/src/components/Organism/Interventions/AddInterventionPresentation.vue @@ -0,0 +1,58 @@ + + diff --git a/thanasoft-front/src/components/Organism/Interventions/InterventionPresentation.vue b/thanasoft-front/src/components/Organism/Interventions/InterventionPresentation.vue index 68a4caa..d6b34ed 100644 --- a/thanasoft-front/src/components/Organism/Interventions/InterventionPresentation.vue +++ b/thanasoft-front/src/components/Organism/Interventions/InterventionPresentation.vue @@ -1,7 +1,7 @@ @@ -20,4 +37,30 @@ import addButton from "@/components/molecules/new-button/addButton.vue"; import FilterTable from "@/components/molecules/Tables/FilterTable.vue"; import TableAction from "@/components/molecules/Tables/TableAction.vue"; import interventionsList from "@/components/molecules/Interventions/interventionsList.vue"; +import { defineProps, defineEmits } from "vue"; + +import { useRouter } from "vue-router"; +const router = useRouter(); +// Props +defineProps({ + interventions: { + type: Array, + default: () => [], + }, + loading: { + type: Boolean, + default: false, + }, + error: { + type: String, + default: null, + }, +}); + +// Emits +defineEmits(["retry"]); + +const go = () => { + router.push({ name: "Add Intervention" }); +}; diff --git a/thanasoft-front/src/components/molecules/Defunts/DefuntDetailContent.vue b/thanasoft-front/src/components/molecules/Defunts/DefuntDetailContent.vue new file mode 100644 index 0000000..0f25da4 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Defunts/DefuntDetailContent.vue @@ -0,0 +1,412 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Defunts/DefuntDetailSidebar.vue b/thanasoft-front/src/components/molecules/Defunts/DefuntDetailSidebar.vue new file mode 100644 index 0000000..ebe9b7a --- /dev/null +++ b/thanasoft-front/src/components/molecules/Defunts/DefuntDetailSidebar.vue @@ -0,0 +1,209 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Defunts/DefuntForm.vue b/thanasoft-front/src/components/molecules/Defunts/DefuntForm.vue new file mode 100644 index 0000000..6afdfff --- /dev/null +++ b/thanasoft-front/src/components/molecules/Defunts/DefuntForm.vue @@ -0,0 +1,310 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Defunts/DefuntsList.vue b/thanasoft-front/src/components/molecules/Defunts/DefuntsList.vue index aa333eb..59a3a29 100644 --- a/thanasoft-front/src/components/molecules/Defunts/DefuntsList.vue +++ b/thanasoft-front/src/components/molecules/Defunts/DefuntsList.vue @@ -1,17 +1,27 @@