intervention
This commit is contained in:
parent
0c4ff92fd5
commit
7570f46658
146
thanasoft-back/app/Http/Controllers/Api/DeceasedController.php
Normal file
146
thanasoft-back/app/Http/Controllers/Api/DeceasedController.php
Normal file
@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreDeceasedRequest;
|
||||
use App\Http\Requests\UpdateDeceasedRequest;
|
||||
use App\Http\Resources\Deceased\DeceasedResource;
|
||||
use App\Http\Resources\Deceased\DeceasedCollection;
|
||||
use App\Models\Deceased;
|
||||
use App\Repositories\DeceasedRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class DeceasedController extends Controller
|
||||
{
|
||||
/**
|
||||
* @var DeceasedRepositoryInterface
|
||||
*/
|
||||
protected $deceasedRepository;
|
||||
|
||||
/**
|
||||
* DeceasedController constructor.
|
||||
*
|
||||
* @param DeceasedRepositoryInterface $deceasedRepository
|
||||
*/
|
||||
public function __construct(DeceasedRepositoryInterface $deceasedRepository)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreInterventionRequest;
|
||||
use App\Http\Requests\UpdateInterventionRequest;
|
||||
use App\Http\Resources\Intervention\InterventionResource;
|
||||
use App\Http\Resources\Intervention\InterventionCollection;
|
||||
use App\Repositories\InterventionRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class InterventionController extends Controller
|
||||
{
|
||||
/**
|
||||
* @var InterventionRepositoryInterface
|
||||
*/
|
||||
protected $interventionRepository;
|
||||
|
||||
/**
|
||||
* InterventionController constructor.
|
||||
*
|
||||
* @param InterventionRepositoryInterface $interventionRepository
|
||||
*/
|
||||
public function __construct(InterventionRepositoryInterface $interventionRepository)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
49
thanasoft-back/app/Http/Requests/StoreDeceasedRequest.php
Normal file
49
thanasoft-back/app/Http/Requests/StoreDeceasedRequest.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreDeceasedRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
// Add authorization logic if needed
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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.'
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreInterventionRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
// Add authorization logic if needed
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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.'
|
||||
];
|
||||
}
|
||||
}
|
||||
49
thanasoft-back/app/Http/Requests/UpdateDeceasedRequest.php
Normal file
49
thanasoft-back/app/Http/Requests/UpdateDeceasedRequest.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateDeceasedRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
// Add authorization logic if needed
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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.'
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateInterventionRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
// Add authorization logic if needed
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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.'
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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'))
|
||||
],
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Deceased;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class DeceasedCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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()
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Deceased;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class DeceasedResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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')
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Intervention;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class InterventionAttachmentResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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')
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Intervention;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class InterventionCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Intervention;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class InterventionNotificationResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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')
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Intervention;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use App\Http\Resources\Deceased\DeceasedResource;
|
||||
use App\Http\Resources\Client\ClientResource;
|
||||
use App\Http\Resources\Employee\ThanatopractitionerResource;
|
||||
|
||||
class InterventionResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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);
|
||||
})
|
||||
];
|
||||
}
|
||||
}
|
||||
59
thanasoft-back/app/Models/Deceased.php
Normal file
59
thanasoft-back/app/Models/Deceased.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Deceased extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'deceased';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'last_name',
|
||||
'first_name',
|
||||
'birth_date',
|
||||
'death_date',
|
||||
'place_of_death',
|
||||
'notes'
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'birth_date' => '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);
|
||||
}
|
||||
}
|
||||
49
thanasoft-back/app/Models/DeceasedDocument.php
Normal file
49
thanasoft-back/app/Models/DeceasedDocument.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class DeceasedDocument extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'deceased_id',
|
||||
'doc_type',
|
||||
'file_id',
|
||||
'generated_at'
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'generated_at' => '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);
|
||||
}
|
||||
}
|
||||
99
thanasoft-back/app/Models/Intervention.php
Normal file
99
thanasoft-back/app/Models/Intervention.php
Normal file
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Intervention extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'client_id',
|
||||
'deceased_id',
|
||||
'order_giver',
|
||||
'location_id',
|
||||
'type',
|
||||
'scheduled_at',
|
||||
'duration_min',
|
||||
'status',
|
||||
'assigned_practitioner_id',
|
||||
'attachments_count',
|
||||
'notes',
|
||||
'created_by'
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'scheduled_at' => '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);
|
||||
}
|
||||
}
|
||||
39
thanasoft-back/app/Models/InterventionAttachment.php
Normal file
39
thanasoft-back/app/Models/InterventionAttachment.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class InterventionAttachment extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'intervention_id',
|
||||
'file_id',
|
||||
'label'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the intervention associated with the attachment.
|
||||
*/
|
||||
public function intervention(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Intervention::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file associated with the attachment.
|
||||
*/
|
||||
public function file(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(File::class);
|
||||
}
|
||||
}
|
||||
44
thanasoft-back/app/Models/InterventionNotification.php
Normal file
44
thanasoft-back/app/Models/InterventionNotification.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class InterventionNotification extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'intervention_id',
|
||||
'channel',
|
||||
'destination',
|
||||
'payload',
|
||||
'status',
|
||||
'sent_at'
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'payload' => 'array',
|
||||
'sent_at' => 'datetime'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the intervention associated with the notification.
|
||||
*/
|
||||
public function intervention(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Intervention::class);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
29
thanasoft-back/app/Providers/RepositoryServiceProvider.php
Normal file
29
thanasoft-back/app/Providers/RepositoryServiceProvider.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Repositories\DeceasedRepositoryInterface;
|
||||
use App\Repositories\DeceasedRepository;
|
||||
use App\Repositories\InterventionRepositoryInterface;
|
||||
use App\Repositories\InterventionRepository;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class RepositoryServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->bind(DeceasedRepositoryInterface::class, DeceasedRepository::class);
|
||||
$this->app->bind(InterventionRepositoryInterface::class, InterventionRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
102
thanasoft-back/app/Repositories/DeceasedRepository.php
Normal file
102
thanasoft-back/app/Repositories/DeceasedRepository.php
Normal file
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Deceased;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DeceasedRepository implements DeceasedRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Get all deceased with optional filtering and pagination
|
||||
*
|
||||
* @param array $filters
|
||||
* @param int $perPage
|
||||
* @return LengthAwarePaginator
|
||||
*/
|
||||
public function getAllPaginated(array $filters = [], int $perPage = 15): LengthAwarePaginator
|
||||
{
|
||||
$query = Deceased::query();
|
||||
|
||||
// Apply filters
|
||||
if (!empty($filters['search'])) {
|
||||
$query->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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Deceased;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
interface DeceasedRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Get all deceased with optional filtering and pagination
|
||||
*
|
||||
* @param array $filters
|
||||
* @param int $perPage
|
||||
* @return LengthAwarePaginator
|
||||
*/
|
||||
public function getAllPaginated(array $filters = [], int $perPage = 15): LengthAwarePaginator;
|
||||
|
||||
/**
|
||||
* Find a deceased by ID
|
||||
*
|
||||
* @param int $id
|
||||
* @return Deceased
|
||||
*/
|
||||
public function findById(int $id): Deceased;
|
||||
|
||||
/**
|
||||
* Create a new deceased record
|
||||
*
|
||||
* @param array $data
|
||||
* @return Deceased
|
||||
*/
|
||||
public function create(array $data): Deceased;
|
||||
|
||||
/**
|
||||
* Update an existing deceased record
|
||||
*
|
||||
* @param Deceased $deceased
|
||||
* @param array $data
|
||||
* @return Deceased
|
||||
*/
|
||||
public function update(Deceased $deceased, array $data): Deceased;
|
||||
|
||||
/**
|
||||
* Delete a deceased record
|
||||
*
|
||||
* @param Deceased $deceased
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(Deceased $deceased): bool;
|
||||
}
|
||||
137
thanasoft-back/app/Repositories/InterventionRepository.php
Normal file
137
thanasoft-back/app/Repositories/InterventionRepository.php
Normal file
@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Intervention;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class InterventionRepository implements InterventionRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Get all interventions with optional filtering and pagination
|
||||
*
|
||||
* @param array $filters
|
||||
* @param int $perPage
|
||||
* @return LengthAwarePaginator
|
||||
*/
|
||||
public function getAllPaginated(array $filters = [], int $perPage = 15): LengthAwarePaginator
|
||||
{
|
||||
$query = Intervention::query();
|
||||
|
||||
// Apply filters
|
||||
if (!empty($filters['client_id'])) {
|
||||
$query->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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Intervention;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
interface InterventionRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Get all interventions with optional filtering and pagination
|
||||
*
|
||||
* @param array $filters
|
||||
* @param int $perPage
|
||||
* @return LengthAwarePaginator
|
||||
*/
|
||||
public function getAllPaginated(array $filters = [], int $perPage = 15): LengthAwarePaginator;
|
||||
|
||||
/**
|
||||
* Find an intervention by ID
|
||||
*
|
||||
* @param int $id
|
||||
* @return Intervention
|
||||
*/
|
||||
public function findById(int $id): Intervention;
|
||||
|
||||
/**
|
||||
* Create a new intervention record
|
||||
*
|
||||
* @param array $data
|
||||
* @return Intervention
|
||||
*/
|
||||
public function create(array $data): Intervention;
|
||||
|
||||
/**
|
||||
* Update an existing intervention record
|
||||
*
|
||||
* @param Intervention $intervention
|
||||
* @param array $data
|
||||
* @return Intervention
|
||||
*/
|
||||
public function update(Intervention $intervention, array $data): Intervention;
|
||||
|
||||
/**
|
||||
* Delete an intervention record
|
||||
*
|
||||
* @param Intervention $intervention
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(Intervention $intervention): bool;
|
||||
|
||||
/**
|
||||
* Change the status of an intervention
|
||||
*
|
||||
* @param Intervention $intervention
|
||||
* @param string $status
|
||||
* @return Intervention
|
||||
*/
|
||||
public function changeStatus(Intervention $intervention, string $status): Intervention;
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('files', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('deceased', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('deceased_documents', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('interventions', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('intervention_attachments', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('intervention_notifications', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -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']);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<add-defunt-template>
|
||||
<template #defunt-form>
|
||||
<defunt-form
|
||||
:loading="loading"
|
||||
:validation-errors="validationErrors"
|
||||
:success="success"
|
||||
@create-defunt="handleCreateDefunt"
|
||||
/>
|
||||
</template>
|
||||
</add-defunt-template>
|
||||
</template>
|
||||
<script setup>
|
||||
import AddDefuntTemplate from "@/components/templates/Defunts/AddDefuntTemplate.vue";
|
||||
import DefuntForm from "@/components/molecules/Defunts/DefuntForm.vue";
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
|
||||
defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
validationErrors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
success: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["createDefunt"]);
|
||||
|
||||
const handleCreateDefunt = (data) => {
|
||||
emit("createDefunt", data);
|
||||
};
|
||||
</script>
|
||||
@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<defunt-detail-template>
|
||||
<template #button-return>
|
||||
<div class="col-12">
|
||||
<router-link
|
||||
to="/defunts"
|
||||
class="btn btn-outline-secondary btn-sm mb-3"
|
||||
>
|
||||
<i class="fas fa-arrow-left me-2"></i>Retour aux défunts
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
<template #loading-state>
|
||||
<div v-if="isLoading" class="text-center p-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Chargement...</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #defunt-detail-sidebar>
|
||||
<DefuntDetailSidebar :deceased="deceased" :is-loading="isLoading" />
|
||||
</template>
|
||||
<template #defunt-detail-content>
|
||||
<DefuntDetailContent
|
||||
:deceased="deceased"
|
||||
:is-loading="isLoading"
|
||||
@update-deceased="handleUpdateDeceased"
|
||||
/>
|
||||
</template>
|
||||
</defunt-detail-template>
|
||||
</template>
|
||||
<script setup>
|
||||
import { defineProps, defineEmits, ref, computed } from "vue";
|
||||
import DefuntDetailTemplate from "@/components/templates/Defunts/DefuntDetailTemplate.vue";
|
||||
import DefuntDetailSidebar from "@/components/molecules/Defunts/DefuntDetailSidebar.vue";
|
||||
import DefuntDetailContent from "@/components/molecules/Defunts/DefuntDetailContent.vue";
|
||||
import { RouterLink } from "vue-router";
|
||||
|
||||
const props = defineProps({
|
||||
deceased: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["updateDeceased"]);
|
||||
|
||||
const handleUpdateDeceased = (updateData) => {
|
||||
emit("updateDeceased", updateData);
|
||||
};
|
||||
|
||||
const getInitials = (firstName, lastName) => {
|
||||
const first = firstName ? firstName.charAt(0).toUpperCase() : "";
|
||||
const last = lastName ? lastName.charAt(0).toUpperCase() : "";
|
||||
return first + last || "?";
|
||||
};
|
||||
|
||||
const getFullName = computed(() => {
|
||||
const parts = [props.deceased.first_name, props.deceased.last_name].filter(
|
||||
Boolean
|
||||
);
|
||||
return parts.join(" ") || "Nom non renseigné";
|
||||
});
|
||||
</script>
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<defunts-template>
|
||||
<template #defunt-new-action>
|
||||
<add-button text="Ajouter" />
|
||||
<add-button text="Ajouter" @click="add" />
|
||||
</template>
|
||||
<template #select-filter>
|
||||
<filter-table />
|
||||
@ -10,7 +10,7 @@
|
||||
<table-action />
|
||||
</template>
|
||||
<template #defunt-table>
|
||||
<defunts-list />
|
||||
<defunts-list :defunts="defunts" />
|
||||
</template>
|
||||
</defunts-template>
|
||||
</template>
|
||||
@ -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" });
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<add-intervention-template>
|
||||
<template #intervention-form>
|
||||
<intervention-form
|
||||
:loading="loading"
|
||||
:validation-errors="validationErrors"
|
||||
:success="success"
|
||||
:deceased-list="deceasedList"
|
||||
:deceased-loading="deceasedLoading"
|
||||
:client-list="clientList"
|
||||
:client-loading="clientLoading"
|
||||
@create-intervention="handleCreateIntervention"
|
||||
/>
|
||||
</template>
|
||||
</add-intervention-template>
|
||||
</template>
|
||||
<script setup>
|
||||
import AddInterventionTemplate from "@/components/templates/Interventions/AddInterventionTemplate.vue";
|
||||
import InterventionForm from "@/components/molecules/Interventions/InterventionForm.vue";
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
|
||||
defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
validationErrors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
success: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
deceasedList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
deceasedLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
clientList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
clientLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["createIntervention"]);
|
||||
|
||||
const handleCreateIntervention = (data) => {
|
||||
emit("createIntervention", data);
|
||||
};
|
||||
</script>
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<interventions-template>
|
||||
<template #intervention-new-action>
|
||||
<add-button text="Ajouter" />
|
||||
<add-button text="Ajouter" @click="go" />
|
||||
</template>
|
||||
<template #select-filter>
|
||||
<filter-table />
|
||||
@ -10,7 +10,24 @@
|
||||
<table-action />
|
||||
</template>
|
||||
<template #intervention-table>
|
||||
<interventions-list />
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Chargement...</span>
|
||||
</div>
|
||||
<p class="mt-2">Chargement des interventions...</p>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<div v-else-if="error" class="alert alert-danger text-center py-4">
|
||||
<p>{{ error }}</p>
|
||||
<button @click="$emit('retry')" class="btn btn-outline-danger">
|
||||
Réessayer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Data table -->
|
||||
<interventions-list v-else :intervention-data="interventions" />
|
||||
</template>
|
||||
</interventions-template>
|
||||
</template>
|
||||
@ -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" });
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -0,0 +1,412 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<!-- Header with Title and Actions -->
|
||||
<div class="row align-items-center mb-4">
|
||||
<div class="col">
|
||||
<h5 class="mb-0">Informations du défunt</h5>
|
||||
<p class="text-sm text-secondary mb-0">
|
||||
Détails et informations personnelles
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<soft-button
|
||||
color="primary"
|
||||
variant="gradient"
|
||||
class="btn-sm"
|
||||
@click="isEditing = !isEditing"
|
||||
>
|
||||
<i class="fas fa-edit me-2"></i>
|
||||
{{ isEditing ? "Annuler" : "Modifier" }}
|
||||
</soft-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="text-center p-4">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Chargement...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div v-else>
|
||||
<!-- View Mode -->
|
||||
<div v-if="!isEditing" class="row">
|
||||
<div class="col-12">
|
||||
<h6
|
||||
class="text-uppercase text-secondary text-xs font-weight-bolder mb-3"
|
||||
>
|
||||
Informations personnelles
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<!-- Personal Info -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="border rounded-3 p-3 h-100">
|
||||
<div class="d-flex align-items-center">
|
||||
<div
|
||||
class="avatar avatar-sm bg-gradient-secondary rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
>
|
||||
<i class="fas fa-user text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-0 text-sm">{{ getFullName }}</h6>
|
||||
<p class="text-xs text-secondary mb-0">Nom complet</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="border rounded-3 p-3 h-100">
|
||||
<div class="d-flex align-items-center">
|
||||
<div
|
||||
class="avatar avatar-sm bg-gradient-info rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
>
|
||||
<i class="fas fa-calendar-alt text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-0 text-sm">
|
||||
{{ formatDate(deceased?.birth_date) }}
|
||||
</h6>
|
||||
<p class="text-xs text-secondary mb-0">Date de naissance</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="border rounded-3 p-3 h-100">
|
||||
<div class="d-flex align-items-center">
|
||||
<div
|
||||
class="avatar avatar-sm bg-gradient-danger rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
>
|
||||
<i class="fas fa-cross text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-0 text-sm">
|
||||
{{ formatDate(deceased?.death_date) }}
|
||||
</h6>
|
||||
<p class="text-xs text-secondary mb-0">Date de décès</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="border rounded-3 p-3 h-100">
|
||||
<div class="d-flex align-items-center">
|
||||
<div
|
||||
class="avatar avatar-sm bg-gradient-warning rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
>
|
||||
<i class="fas fa-map-marker-alt text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-0 text-sm">
|
||||
{{ deceased?.place_of_death || "Non renseigné" }}
|
||||
</h6>
|
||||
<p class="text-xs text-secondary mb-0">Lieu de décès</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Age at death -->
|
||||
<div class="col-12 mb-4">
|
||||
<div class="border rounded-3 p-3">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<div
|
||||
class="avatar avatar-lg bg-gradient-success rounded-circle d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<i class="fas fa-birthday-cake text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3 class="mb-0">{{ calculateAgeAtDeath }}</h3>
|
||||
<p class="text-sm text-secondary mb-0">Âge au décès</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="col-12">
|
||||
<h6
|
||||
class="text-uppercase text-secondary text-xs font-weight-bolder mb-3"
|
||||
>
|
||||
Notes et observations
|
||||
</h6>
|
||||
<div class="border rounded-3 p-3">
|
||||
<p class="text-sm mb-0">
|
||||
{{ deceased?.notes || "Aucune note renseignée" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Mode -->
|
||||
<div v-else class="row">
|
||||
<div class="col-12">
|
||||
<h6
|
||||
class="text-uppercase text-secondary text-xs font-weight-bolder mb-3"
|
||||
>
|
||||
Modifier les informations
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label"
|
||||
>Nom de famille <span class="text-danger">*</span></label
|
||||
>
|
||||
<soft-input
|
||||
v-model="editForm.last_name"
|
||||
type="text"
|
||||
placeholder="Nom de famille"
|
||||
:class="{ 'is-invalid': validationErrors.last_name }"
|
||||
/>
|
||||
<div v-if="validationErrors.last_name" class="invalid-feedback">
|
||||
{{ validationErrors.last_name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Prénom(s)</label>
|
||||
<soft-input
|
||||
v-model="editForm.first_name"
|
||||
type="text"
|
||||
placeholder="Prénom(s)"
|
||||
:class="{ 'is-invalid': validationErrors.first_name }"
|
||||
/>
|
||||
<div v-if="validationErrors.first_name" class="invalid-feedback">
|
||||
{{ validationErrors.first_name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Date de naissance</label>
|
||||
<soft-input
|
||||
v-model="editForm.birth_date"
|
||||
type="date"
|
||||
:class="{ 'is-invalid': validationErrors.birth_date }"
|
||||
/>
|
||||
<div v-if="validationErrors.birth_date" class="invalid-feedback">
|
||||
{{ validationErrors.birth_date }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Date de décès</label>
|
||||
<soft-input
|
||||
v-model="editForm.death_date"
|
||||
type="date"
|
||||
:class="{ 'is-invalid': validationErrors.death_date }"
|
||||
/>
|
||||
<div v-if="validationErrors.death_date" class="invalid-feedback">
|
||||
{{ validationErrors.death_date }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mb-3">
|
||||
<label class="form-label">Lieu de décès</label>
|
||||
<soft-input
|
||||
v-model="editForm.place_of_death"
|
||||
type="text"
|
||||
placeholder="Lieu de décès"
|
||||
:class="{ 'is-invalid': validationErrors.place_of_death }"
|
||||
/>
|
||||
<div
|
||||
v-if="validationErrors.place_of_death"
|
||||
class="invalid-feedback"
|
||||
>
|
||||
{{ validationErrors.place_of_death }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mb-4">
|
||||
<label class="form-label">Notes et observations</label>
|
||||
<textarea
|
||||
v-model="editForm.notes"
|
||||
class="form-control"
|
||||
rows="4"
|
||||
placeholder="Notes complémentaires..."
|
||||
:class="{ 'is-invalid': validationErrors.notes }"
|
||||
></textarea>
|
||||
<div v-if="validationErrors.notes" class="invalid-feedback">
|
||||
{{ validationErrors.notes }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="d-flex gap-2">
|
||||
<soft-button
|
||||
color="primary"
|
||||
variant="gradient"
|
||||
@click="saveChanges"
|
||||
:disabled="isSaving"
|
||||
>
|
||||
<span
|
||||
v-if="isSaving"
|
||||
class="spinner-border spinner-border-sm me-2"
|
||||
role="status"
|
||||
></span>
|
||||
{{ isSaving ? "Sauvegarde..." : "Sauvegarder" }}
|
||||
</soft-button>
|
||||
<soft-button
|
||||
color="secondary"
|
||||
variant="outline"
|
||||
@click="cancelEdit"
|
||||
:disabled="isSaving"
|
||||
>
|
||||
Annuler
|
||||
</soft-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
import SoftInput from "@/components/SoftInput.vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
|
||||
const props = defineProps({
|
||||
deceased: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update-deceased"]);
|
||||
|
||||
const isEditing = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const validationErrors = ref({});
|
||||
|
||||
const editForm = ref({
|
||||
last_name: "",
|
||||
first_name: "",
|
||||
birth_date: "",
|
||||
death_date: "",
|
||||
place_of_death: "",
|
||||
notes: "",
|
||||
});
|
||||
|
||||
const getFullName = computed(() => {
|
||||
if (!props.deceased) return "Nom non renseigné";
|
||||
const parts = [props.deceased.first_name, props.deceased.last_name].filter(
|
||||
Boolean
|
||||
);
|
||||
return parts.join(" ") || "Nom non renseigné";
|
||||
});
|
||||
|
||||
const calculateAgeAtDeath = computed(() => {
|
||||
if (
|
||||
!props.deceased ||
|
||||
!props.deceased.birth_date ||
|
||||
!props.deceased.death_date
|
||||
) {
|
||||
return "N/A";
|
||||
}
|
||||
|
||||
const birthDate = new Date(props.deceased.birth_date);
|
||||
const deathDate = new Date(props.deceased.death_date);
|
||||
const age = deathDate.getFullYear() - birthDate.getFullYear();
|
||||
|
||||
// Adjust if birthday hasn't occurred yet in the year of death
|
||||
const monthDiff = deathDate.getMonth() - birthDate.getMonth();
|
||||
if (
|
||||
monthDiff < 0 ||
|
||||
(monthDiff === 0 && deathDate.getDate() < birthDate.getDate())
|
||||
) {
|
||||
return age - 1;
|
||||
}
|
||||
|
||||
return age;
|
||||
});
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return "Non renseignée";
|
||||
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString("fr-FR", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const populateEditForm = () => {
|
||||
if (!props.deceased) return;
|
||||
editForm.value = {
|
||||
last_name: props.deceased.last_name || "",
|
||||
first_name: props.deceased.first_name || "",
|
||||
birth_date: props.deceased.birth_date || "",
|
||||
death_date: props.deceased.death_date || "",
|
||||
place_of_death: props.deceased.place_of_death || "",
|
||||
notes: props.deceased.notes || "",
|
||||
};
|
||||
validationErrors.value = {};
|
||||
};
|
||||
|
||||
const saveChanges = async () => {
|
||||
isSaving.value = true;
|
||||
validationErrors.value = {};
|
||||
|
||||
try {
|
||||
await emit("update-deceased", { ...editForm.value });
|
||||
isEditing.value = false;
|
||||
} catch (error) {
|
||||
if (error.response?.data?.errors) {
|
||||
validationErrors.value = error.response.data.errors;
|
||||
}
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
isEditing.value = false;
|
||||
validationErrors.value = {};
|
||||
populateEditForm();
|
||||
};
|
||||
|
||||
// Watch for deceased changes
|
||||
watch(
|
||||
() => props.deceased,
|
||||
(newDeceased) => {
|
||||
if (newDeceased && isEditing.value) {
|
||||
populateEditForm();
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// Initialize form when component mounts
|
||||
populateEditForm();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.avatar-sm {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.avatar-lg {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
.border {
|
||||
border: 1px solid #dee2e6 !important;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<div class="card position-sticky top-1">
|
||||
<!-- Defunt Profile Card -->
|
||||
<div class="card-body text-center">
|
||||
<div
|
||||
class="avatar avatar-xl bg-gradient-dark border-radius-md d-flex align-items-center justify-content-center mx-auto mb-3"
|
||||
>
|
||||
<i class="fa fa-user text-white fa-2x"></i>
|
||||
</div>
|
||||
<h5 class="mb-1">{{ getFullName }}</h5>
|
||||
<p class="text-xs text-secondary mb-2">
|
||||
Âge au décès: {{ calculateAgeAtDeath }}
|
||||
</p>
|
||||
<div class="row text-center">
|
||||
<div class="col-4">
|
||||
<h6 class="mb-0 text-sm">{{ deceased?.documents_count || 0 }}</h6>
|
||||
<p class="mb-0 text-xs text-secondary">Documents</p>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<h6 class="mb-0 text-sm">{{ deceased?.interventions_count || 0 }}</h6>
|
||||
<p class="mb-0 text-xs text-secondary">Interventions</p>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<h6 class="mb-0 text-sm">{{ formatDate(deceased?.created_at) }}</h6>
|
||||
<p class="mb-0 text-xs text-secondary">Créé le</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="horizontal dark my-3 mx-3" />
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="card-body pt-0">
|
||||
<h6 class="text-uppercase text-secondary text-xs font-weight-bolder mb-3">
|
||||
Actions rapides
|
||||
</h6>
|
||||
<div class="d-grid gap-2">
|
||||
<soft-button
|
||||
color="primary"
|
||||
variant="gradient"
|
||||
class="btn-sm"
|
||||
@click="$emit('create-intervention')"
|
||||
>
|
||||
<i class="fas fa-plus me-2"></i>
|
||||
Nouvelle intervention
|
||||
</soft-button>
|
||||
<soft-button
|
||||
color="secondary"
|
||||
variant="outline"
|
||||
class="btn-sm"
|
||||
@click="$emit('edit-deceased')"
|
||||
>
|
||||
<i class="fas fa-edit me-2"></i>
|
||||
Modifier
|
||||
</soft-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="horizontal dark my-3 mx-3" />
|
||||
|
||||
<!-- Key Dates -->
|
||||
<div class="card-body pt-0">
|
||||
<h6 class="text-uppercase text-secondary text-xs font-weight-bolder mb-3">
|
||||
Dates importantes
|
||||
</h6>
|
||||
<div class="timeline timeline-one-side">
|
||||
<div class="timeline-block mb-3">
|
||||
<span class="timeline-step">
|
||||
<i class="fas fa-calendar-alt text-primary"></i>
|
||||
</span>
|
||||
<div class="timeline-content">
|
||||
<h6 class="text-dark text-sm font-weight-bold mb-0">Naissance</h6>
|
||||
<p class="text-secondary text-xs mb-0">
|
||||
{{ formatDate(deceased?.birth_date) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timeline-block mb-0">
|
||||
<span class="timeline-step">
|
||||
<i class="fas fa-cross text-dark"></i>
|
||||
</span>
|
||||
<div class="timeline-content">
|
||||
<h6 class="text-dark text-sm font-weight-bold mb-0">Décès</h6>
|
||||
<p class="text-secondary text-xs mb-0">
|
||||
{{ formatDate(deceased?.death_date) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
|
||||
const props = defineProps({
|
||||
deceased: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(["create-intervention", "edit-deceased"]);
|
||||
|
||||
const getFullName = computed(() => {
|
||||
if (!props.deceased) return "Nom non renseigné";
|
||||
const parts = [props.deceased.first_name, props.deceased.last_name].filter(
|
||||
Boolean
|
||||
);
|
||||
return parts.join(" ") || "Nom non renseigné";
|
||||
});
|
||||
|
||||
const calculateAgeAtDeath = computed(() => {
|
||||
if (
|
||||
!props.deceased ||
|
||||
!props.deceased.birth_date ||
|
||||
!props.deceased.death_date
|
||||
) {
|
||||
return "N/A";
|
||||
}
|
||||
|
||||
const birthDate = new Date(props.deceased.birth_date);
|
||||
const deathDate = new Date(props.deceased.death_date);
|
||||
const age = deathDate.getFullYear() - birthDate.getFullYear();
|
||||
|
||||
// Adjust if birthday hasn't occurred yet in the year of death
|
||||
const monthDiff = deathDate.getMonth() - birthDate.getMonth();
|
||||
if (
|
||||
monthDiff < 0 ||
|
||||
(monthDiff === 0 && deathDate.getDate() < birthDate.getDate())
|
||||
) {
|
||||
return age - 1;
|
||||
}
|
||||
|
||||
return age;
|
||||
});
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return "Non renseignée";
|
||||
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString("fr-FR", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.position-sticky {
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 0;
|
||||
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, 0.15);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
min-width: 80px;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.timeline-one-side {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-block {
|
||||
position: relative;
|
||||
padding-left: 2.5rem;
|
||||
}
|
||||
|
||||
.timeline-step {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0.25rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
border: 2px solid #dee2e6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-block:not(:last-child)::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0.75rem;
|
||||
top: 2rem;
|
||||
width: 2px;
|
||||
height: calc(100% - 2rem);
|
||||
background: #dee2e6;
|
||||
}
|
||||
</style>
|
||||
310
thanasoft-front/src/components/molecules/Defunts/DefuntForm.vue
Normal file
310
thanasoft-front/src/components/molecules/Defunts/DefuntForm.vue
Normal file
@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<div class="card p-3 border-radius-xl bg-white" data-animation="FadeIn">
|
||||
<h5 class="font-weight-bolder mb-0">Informations du défunt</h5>
|
||||
<p class="mb-0 text-sm">Informations de la personne décédée</p>
|
||||
|
||||
<div class="multisteps-form__content">
|
||||
<!-- Nom du défunt -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label"
|
||||
>Nom de famille <span class="text-danger">*</span></label
|
||||
>
|
||||
<soft-input
|
||||
v-model="form.last_name"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.last_name }"
|
||||
type="text"
|
||||
placeholder="ex. MARTIN"
|
||||
/>
|
||||
<div v-if="fieldErrors.last_name" class="invalid-feedback">
|
||||
{{ fieldErrors.last_name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prénom du défunt -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Prénom(s)</label>
|
||||
<soft-input
|
||||
v-model="form.first_name"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.first_name }"
|
||||
type="text"
|
||||
placeholder="ex. Jean Pierre"
|
||||
/>
|
||||
<div v-if="fieldErrors.first_name" class="invalid-feedback">
|
||||
{{ fieldErrors.first_name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dates de naissance et de décès -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12 col-sm-6">
|
||||
<label class="form-label">Date de naissance</label>
|
||||
<soft-input
|
||||
v-model="form.birth_date"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.birth_date }"
|
||||
type="date"
|
||||
/>
|
||||
<div v-if="fieldErrors.birth_date" class="invalid-feedback">
|
||||
{{ fieldErrors.birth_date }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
||||
<label class="form-label">Date de décès</label>
|
||||
<soft-input
|
||||
v-model="form.death_date"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.death_date }"
|
||||
type="date"
|
||||
/>
|
||||
<div v-if="fieldErrors.death_date" class="invalid-feedback">
|
||||
{{ fieldErrors.death_date }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lieu de décès -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Lieu de décès</label>
|
||||
<soft-input
|
||||
v-model="form.place_of_death"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.place_of_death }"
|
||||
type="text"
|
||||
placeholder="ex. Hôpital Saint-Louis, Paris"
|
||||
/>
|
||||
<div v-if="fieldErrors.place_of_death" class="invalid-feedback">
|
||||
{{ fieldErrors.place_of_death }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Notes et observations</label>
|
||||
<textarea
|
||||
v-model="form.notes"
|
||||
class="form-control multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.notes }"
|
||||
rows="4"
|
||||
placeholder="Informations complémentaires, causes du décès, circonstances particulières..."
|
||||
maxlength="2000"
|
||||
></textarea>
|
||||
<div v-if="fieldErrors.notes" class="invalid-feedback">
|
||||
{{ fieldErrors.notes }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Validation des dates -->
|
||||
<div v-if="dateValidationError" class="row mt-3">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
{{ dateValidationError }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Boutons -->
|
||||
<div class="button-row d-flex mt-4">
|
||||
<soft-button
|
||||
type="button"
|
||||
color="secondary"
|
||||
variant="outline"
|
||||
class="me-2 mb-0"
|
||||
@click="resetForm"
|
||||
>
|
||||
Réinitialiser
|
||||
</soft-button>
|
||||
<soft-button
|
||||
type="button"
|
||||
color="dark"
|
||||
variant="gradient"
|
||||
class="ms-auto mb-0"
|
||||
:disabled="props.loading || !!dateValidationError"
|
||||
@click="submitForm"
|
||||
>
|
||||
<span
|
||||
v-if="props.loading"
|
||||
class="spinner-border spinner-border-sm me-2"
|
||||
role="status"
|
||||
></span>
|
||||
{{ props.loading ? "Enregistrement..." : "Enregistrer" }}
|
||||
</soft-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits, watch, computed } from "vue";
|
||||
import SoftInput from "@/components/SoftInput.vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
validationErrors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
success: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["createDefunt"]);
|
||||
|
||||
// Reactive data
|
||||
const errors = ref([]);
|
||||
const fieldErrors = ref({});
|
||||
|
||||
const form = ref({
|
||||
last_name: "",
|
||||
first_name: "",
|
||||
birth_date: "",
|
||||
death_date: "",
|
||||
place_of_death: "",
|
||||
notes: "",
|
||||
});
|
||||
|
||||
// Computed property to validate date logic
|
||||
const dateValidationError = computed(() => {
|
||||
if (!form.value.birth_date || !form.value.death_date) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const birthDate = new Date(form.value.birth_date);
|
||||
const deathDate = new Date(form.value.death_date);
|
||||
|
||||
if (birthDate >= deathDate) {
|
||||
return "La date de décès doit être postérieure à la date de naissance";
|
||||
}
|
||||
|
||||
// Check if death date is not too far in the future
|
||||
const now = new Date();
|
||||
if (deathDate > now) {
|
||||
return "La date de décès ne peut pas être dans le futur";
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
// Watch for validation errors from parent
|
||||
watch(
|
||||
() => props.validationErrors,
|
||||
(newErrors) => {
|
||||
fieldErrors.value = { ...newErrors };
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// Watch for success from parent
|
||||
watch(
|
||||
() => props.success,
|
||||
(newSuccess) => {
|
||||
if (newSuccess) {
|
||||
resetForm();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const submitForm = async () => {
|
||||
// Clear errors before submitting
|
||||
fieldErrors.value = {};
|
||||
errors.value = [];
|
||||
|
||||
// Check for date validation errors
|
||||
if (dateValidationError.value) {
|
||||
errors.value.push(dateValidationError.value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!form.value.last_name || form.value.last_name.trim() === "") {
|
||||
fieldErrors.value.last_name = "Le nom de famille est obligatoire";
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up form data: convert empty strings to null
|
||||
const cleanedForm = {};
|
||||
const formData = form.value;
|
||||
|
||||
for (const [key, value] of Object.entries(formData)) {
|
||||
if (value === "" || value === null || value === undefined) {
|
||||
cleanedForm[key] = null;
|
||||
} else {
|
||||
cleanedForm[key] = value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Defunt form data being emitted:", cleanedForm);
|
||||
|
||||
// Emit the cleaned form data to parent
|
||||
emit("createDefunt", cleanedForm);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
form.value = {
|
||||
last_name: "",
|
||||
first_name: "",
|
||||
birth_date: "",
|
||||
death_date: "",
|
||||
place_of_death: "",
|
||||
notes: "",
|
||||
};
|
||||
clearErrors();
|
||||
};
|
||||
|
||||
const clearErrors = () => {
|
||||
errors.value = [];
|
||||
fieldErrors.value = {};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #f5365c;
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.spinner-border-sm {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.alert i {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
@ -1,17 +1,27 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div v-if="!defunts || defunts.length === 0" class="empty-state">
|
||||
<div class="empty-message">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<h3>Aucun défunt trouvé</h3>
|
||||
<p>Il semble qu'il n'y ait pas encore de défunts dans cette liste.</p>
|
||||
<soft-button @click="addDeceased" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Ajouter un défunt
|
||||
</soft-button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(defunt, index) in defunts"
|
||||
:key="index"
|
||||
class="col-xl-4 col-md-6 col-12 mb-4"
|
||||
>
|
||||
<defunt-card
|
||||
:nom="defunt.nom"
|
||||
:prenom="defunt.prenom"
|
||||
:date_naissance="defunt.date_naissance"
|
||||
:date_deces="defunt.date_deces"
|
||||
:lieu_deces="defunt.lieu_deces"
|
||||
:description="defunt.description"
|
||||
:nom="defunt.last_name"
|
||||
:prenom="defunt.first_name"
|
||||
:date_naissance="defunt.birth_date"
|
||||
:date_deces="defunt.death_date"
|
||||
:lieu_deces="defunt.place_of_death"
|
||||
:description="defunt.notes"
|
||||
:dropdown="dropdownOptions"
|
||||
@dropdown-action="(action) => handleAction(action, defunt)"
|
||||
/>
|
||||
@ -21,101 +31,32 @@
|
||||
<script setup>
|
||||
import DefuntCard from "@/components/atoms/Defunts/DefuntCard.vue";
|
||||
import { ref } from "vue";
|
||||
import { defineProps } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
|
||||
// Données d'exemple
|
||||
const defunts = ref([
|
||||
// Router
|
||||
const router = useRouter();
|
||||
|
||||
// Options du dropdown
|
||||
const dropdownOptions = ref([
|
||||
{ label: "Voir les détails", action: "view", route: "#" },
|
||||
{ label: "Modifier", action: "edit", route: "#" },
|
||||
{
|
||||
id: 1,
|
||||
nom: "Dupont",
|
||||
prenom: "Jean",
|
||||
date_naissance: "1950-03-15",
|
||||
date_deces: "2024-01-10",
|
||||
lieu_deces: "Hôpital Saint-Louis, Paris 10ème",
|
||||
description:
|
||||
"Décédé des suites d'une longue maladie. Souhaitait une cérémonie religieuse catholique. Famille très présente.",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
nom: "Martin",
|
||||
prenom: "Sophie",
|
||||
date_naissance: "1965-08-22",
|
||||
date_deces: "2024-01-08",
|
||||
lieu_deces: "Domicile, 92 Rue de Rivoli, Paris 4ème",
|
||||
description:
|
||||
"Décès naturel à domicile. Cérémonie laïque souhaitée. Environ 50 personnes attendues.",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
nom: "Bernard",
|
||||
prenom: "Pierre",
|
||||
date_naissance: "1942-11-30",
|
||||
date_deces: "2024-01-05",
|
||||
lieu_deces: "Clinique du Val de Seine, Issy-les-Moulineaux",
|
||||
description:
|
||||
"Ancien militaire, décédé des suites d'un cancer. Demande d'honneurs militaires. Cérémonie au crématorium.",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
nom: "Moreau",
|
||||
prenom: "Marie",
|
||||
date_naissance: "1938-07-14",
|
||||
date_deces: "2024-01-03",
|
||||
lieu_deces: "EHPAD Les Jardins Fleuris, Vincennes",
|
||||
description:
|
||||
"Décédée paisiblement à l'EHPAD. Cérémonie religieuse à l'église Saint-Louis de Vincennes. Famille nombreuse.",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
nom: "Leroy",
|
||||
prenom: "Philippe",
|
||||
date_naissance: "1972-12-05",
|
||||
date_deces: "2024-01-01",
|
||||
lieu_deces: "Centre Hospitalier Universitaire, Créteil",
|
||||
description:
|
||||
"Décès accidentel. Jeune famille endeuillée. Cérémonie intime souhaitée. Crémation prévue.",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
nom: "Petit",
|
||||
prenom: "Claire",
|
||||
date_naissance: "1980-04-18",
|
||||
date_deces: "2023-12-28",
|
||||
lieu_deces: "Hôpital Necker, Paris 15ème",
|
||||
description:
|
||||
"Décédée après une courte maladie. Professeure de musique. Cérémonie avec prestation musicale des élèves.",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
nom: "Roux",
|
||||
prenom: "Antoine",
|
||||
date_naissance: "1955-09-12",
|
||||
date_deces: "2023-12-25",
|
||||
lieu_deces: "Maison de retraite Les Tilleuls, Sceaux",
|
||||
description:
|
||||
"Ancien artisan ébéniste. Décès pendant la nuit de Noël. Cérémonie au cimetière du Père Lachaise.",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
nom: "Garcia",
|
||||
prenom: "Elena",
|
||||
date_naissance: "1968-02-28",
|
||||
date_deces: "2023-12-20",
|
||||
lieu_deces: "Hôpital Européen Georges Pompidou, Paris",
|
||||
description:
|
||||
"D'origine espagnole. Décédée des suites d'une opération. Double cérémonie française et espagnole prévue.",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
nom: "Fournier",
|
||||
prenom: "Michel",
|
||||
date_naissance: "1948-06-08",
|
||||
date_deces: "2023-12-18",
|
||||
lieu_deces: "Centre de Soins Palliatifs, Boulogne-Billancourt",
|
||||
description:
|
||||
"Ancien chef d'entreprise. Décès après une longue maladie. Cérémonie civile au funérarium.",
|
||||
label: "Créer une intervention",
|
||||
action: "create_intervention",
|
||||
route: "#",
|
||||
},
|
||||
{ label: "Supprimer", action: "delete", route: "#" },
|
||||
]);
|
||||
|
||||
defineProps({
|
||||
defunts: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Gestion des actions du dropdown
|
||||
const handleAction = (action, defunt) => {
|
||||
switch (action) {
|
||||
@ -124,8 +65,8 @@ const handleAction = (action, defunt) => {
|
||||
// Ouvrir modal d'édition
|
||||
break;
|
||||
case "view":
|
||||
console.log("Voir les détails:", defunt);
|
||||
// Naviguer vers la page détail
|
||||
router.push({ name: "Defunt details", params: { id: defunt.id } });
|
||||
break;
|
||||
case "create_intervention":
|
||||
console.log("Créer une intervention pour:", defunt);
|
||||
@ -136,27 +77,16 @@ const handleAction = (action, defunt) => {
|
||||
// Confirmer la suppression
|
||||
if (
|
||||
confirm(
|
||||
`Êtes-vous sûr de vouloir supprimer ${defunt.prenom} ${defunt.nom} ?`
|
||||
`Êtes-vous sûr de vouloir supprimer ${defunt.first_name} ${defunt.last_name} ?`
|
||||
)
|
||||
) {
|
||||
defunts.value = defunts.value.filter((d) => d.id !== defunt.id);
|
||||
}
|
||||
break;
|
||||
)
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Fonction pour ajouter un nouveau défunt (exemple)
|
||||
const ajouterDefunt = () => {
|
||||
const nouveauDefunt = {
|
||||
id: defunts.value.length + 1,
|
||||
nom: "Nouveau",
|
||||
prenom: "Défunt",
|
||||
date_naissance: "1950-01-01",
|
||||
date_deces: "2024-01-15",
|
||||
lieu_deces: "Lieu à définir",
|
||||
description: "Description à compléter",
|
||||
};
|
||||
defunts.value.unshift(nouveauDefunt);
|
||||
// Navigation to add deceased page
|
||||
const addDeceased = () => {
|
||||
router.push({ name: "Add Defunts" });
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@ -0,0 +1,440 @@
|
||||
<template>
|
||||
<div class="card p-3 border-radius-xl bg-white" data-animation="FadeIn">
|
||||
<h5 class="font-weight-bolder mb-0">Informations de l'intervention</h5>
|
||||
<p class="mb-0 text-sm">Créez une nouvelle intervention funéraire</p>
|
||||
|
||||
<div class="multisteps-form__content">
|
||||
<!-- Client selection -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label"
|
||||
>Client <span class="text-danger">*</span></label
|
||||
>
|
||||
<select
|
||||
v-model="form.client_id"
|
||||
class="form-select multisteps-form__select"
|
||||
:class="{ 'is-invalid': fieldErrors.client_id }"
|
||||
>
|
||||
<option value="">Sélectionnez un client</option>
|
||||
<option
|
||||
v-for="client in clientList"
|
||||
:key="client.id"
|
||||
:value="client.id"
|
||||
>
|
||||
{{ client.name }}
|
||||
</option>
|
||||
</select>
|
||||
<div v-if="fieldErrors.client_id" class="invalid-feedback">
|
||||
{{ fieldErrors.client_id }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Deceased selection -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Personne décédée</label>
|
||||
<select
|
||||
v-model="form.deceased_id"
|
||||
class="form-select multisteps-form__select"
|
||||
:class="{ 'is-invalid': fieldErrors.deceased_id }"
|
||||
>
|
||||
<option value="">Sélectionnez une personne décédée</option>
|
||||
<option
|
||||
v-for="deceased in deceasedList"
|
||||
:key="deceased.id"
|
||||
:value="deceased.id"
|
||||
>
|
||||
{{ deceased.last_name }} {{ deceased.first_name || "" }}
|
||||
</option>
|
||||
</select>
|
||||
<div v-if="fieldErrors.deceased_id" class="invalid-feedback">
|
||||
{{ fieldErrors.deceased_id }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Intervention type -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label"
|
||||
>Type d'intervention <span class="text-danger">*</span></label
|
||||
>
|
||||
<select
|
||||
v-model="form.type"
|
||||
class="form-select multisteps-form__select"
|
||||
:class="{ 'is-invalid': fieldErrors.type }"
|
||||
>
|
||||
<option value="">Sélectionnez un type d'intervention</option>
|
||||
<option value="thanatopraxie">Thanatopraxie</option>
|
||||
<option value="toilette_mortuaire">Toilette mortuaire</option>
|
||||
<option value="exhumation">Exhumation</option>
|
||||
<option value="retrait_pacemaker">Retrait pacemaker</option>
|
||||
<option value="retrait_bijoux">Retrait bijoux</option>
|
||||
<option value="autre">Autre</option>
|
||||
</select>
|
||||
<div v-if="fieldErrors.type" class="invalid-feedback">
|
||||
{{ fieldErrors.type }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date et heure de l'intervention -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12 col-sm-6">
|
||||
<label class="form-label">Date de l'intervention</label>
|
||||
<soft-input
|
||||
v-model="form.scheduled_date"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.scheduled_at }"
|
||||
type="date"
|
||||
/>
|
||||
<div v-if="fieldErrors.scheduled_at" class="invalid-feedback">
|
||||
{{ fieldErrors.scheduled_at }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
||||
<label class="form-label">Heure de l'intervention</label>
|
||||
<soft-input
|
||||
v-model="form.scheduled_time"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.scheduled_at }"
|
||||
type="time"
|
||||
/>
|
||||
<div v-if="fieldErrors.scheduled_at" class="invalid-feedback">
|
||||
{{ fieldErrors.scheduled_at }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Duration and status -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12 col-sm-6">
|
||||
<label class="form-label">Durée (minutes)</label>
|
||||
<soft-input
|
||||
v-model="form.duration_min"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.duration_min }"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="ex. 90"
|
||||
/>
|
||||
<div v-if="fieldErrors.duration_min" class="invalid-feedback">
|
||||
{{ fieldErrors.duration_min }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
||||
<label class="form-label">Statut</label>
|
||||
<select
|
||||
v-model="form.status"
|
||||
class="form-select multisteps-form__select"
|
||||
:class="{ 'is-invalid': fieldErrors.status }"
|
||||
>
|
||||
<option value="">Sélectionnez un statut</option>
|
||||
<option value="demande">Demande</option>
|
||||
<option value="planifie">Planifié</option>
|
||||
<option value="en_cours">En cours</option>
|
||||
<option value="termine">Terminé</option>
|
||||
<option value="annule">Annulé</option>
|
||||
</select>
|
||||
<div v-if="fieldErrors.status" class="invalid-feedback">
|
||||
{{ fieldErrors.status }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order giver and notes -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Donneur d'ordre</label>
|
||||
<soft-input
|
||||
v-model="form.order_giver"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.order_giver }"
|
||||
type="text"
|
||||
placeholder="Nom du donneur d'ordre"
|
||||
/>
|
||||
<div v-if="fieldErrors.order_giver" class="invalid-feedback">
|
||||
{{ fieldErrors.order_giver }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Notes et observations</label>
|
||||
<textarea
|
||||
v-model="form.notes"
|
||||
class="form-control multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.notes }"
|
||||
rows="4"
|
||||
placeholder="Informations complémentaires, instructions spéciales..."
|
||||
maxlength="2000"
|
||||
></textarea>
|
||||
<div v-if="fieldErrors.notes" class="invalid-feedback">
|
||||
{{ fieldErrors.notes }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Validation errors -->
|
||||
<div v-if="formValidationError" class="row mt-3">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
{{ formValidationError }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Boutons -->
|
||||
<div class="button-row d-flex mt-4">
|
||||
<soft-button
|
||||
type="button"
|
||||
color="secondary"
|
||||
variant="outline"
|
||||
class="me-2 mb-0"
|
||||
@click="resetForm"
|
||||
>
|
||||
Réinitialiser
|
||||
</soft-button>
|
||||
<soft-button
|
||||
type="button"
|
||||
color="dark"
|
||||
variant="gradient"
|
||||
class="ms-auto mb-0"
|
||||
:disabled="props.loading || !!formValidationError"
|
||||
@click="submitForm"
|
||||
>
|
||||
<span
|
||||
v-if="props.loading"
|
||||
class="spinner-border spinner-border-sm me-2"
|
||||
role="status"
|
||||
></span>
|
||||
{{ props.loading ? "Enregistrement..." : "Enregistrer" }}
|
||||
</soft-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits, watch, computed } from "vue";
|
||||
import SoftInput from "@/components/SoftInput.vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
validationErrors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
success: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
deceasedList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
deceasedLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
clientList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
clientLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["createIntervention"]);
|
||||
|
||||
// Reactive data
|
||||
const errors = ref([]);
|
||||
const fieldErrors = ref({});
|
||||
|
||||
const form = ref({
|
||||
client_id: "",
|
||||
deceased_id: "",
|
||||
type: "",
|
||||
scheduled_date: "",
|
||||
scheduled_time: "",
|
||||
duration_min: "",
|
||||
status: "",
|
||||
order_giver: "",
|
||||
notes: "",
|
||||
});
|
||||
|
||||
// Computed property to validate form
|
||||
const formValidationError = computed(() => {
|
||||
if (!form.value.client_id) {
|
||||
return "Le client est obligatoire";
|
||||
}
|
||||
|
||||
if (!form.value.type || form.value.type === "") {
|
||||
return "Le type d'intervention est obligatoire";
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
// Watch for validation errors from parent
|
||||
watch(
|
||||
() => props.validationErrors,
|
||||
(newErrors) => {
|
||||
fieldErrors.value = { ...newErrors };
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// Watch for success from parent
|
||||
watch(
|
||||
() => props.success,
|
||||
(newSuccess) => {
|
||||
if (newSuccess) {
|
||||
resetForm();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const submitForm = async () => {
|
||||
// Clear errors before submitting
|
||||
fieldErrors.value = {};
|
||||
errors.value = [];
|
||||
|
||||
// Check for form validation errors
|
||||
if (formValidationError.value) {
|
||||
errors.value.push(formValidationError.value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!form.value.client_id || form.value.client_id === "") {
|
||||
fieldErrors.value.client_id = "Le client est obligatoire";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!form.value.type || form.value.type === "") {
|
||||
fieldErrors.value.type = "Le type d'intervention est obligatoire";
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up form data: convert empty strings to null
|
||||
const cleanedForm = {};
|
||||
const formData = form.value;
|
||||
|
||||
for (const [key, value] of Object.entries(formData)) {
|
||||
if (value === "" || value === null || value === undefined) {
|
||||
cleanedForm[key] = null;
|
||||
} else {
|
||||
cleanedForm[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Combine date and time into scheduled_at (backend expects Y-m-d H:i:s format)
|
||||
if (cleanedForm.scheduled_date && cleanedForm.scheduled_time) {
|
||||
// Format: Y-m-d H:i:s (e.g., "2024-12-15 14:30:00")
|
||||
const formattedDate = cleanedForm.scheduled_date;
|
||||
const formattedTime = cleanedForm.scheduled_time;
|
||||
cleanedForm.scheduled_at = `${formattedDate} ${formattedTime}:00`;
|
||||
delete cleanedForm.scheduled_date;
|
||||
delete cleanedForm.scheduled_time;
|
||||
}
|
||||
|
||||
// Convert string numbers to integers
|
||||
if (cleanedForm.client_id) {
|
||||
cleanedForm.client_id = parseInt(cleanedForm.client_id);
|
||||
}
|
||||
if (cleanedForm.deceased_id) {
|
||||
cleanedForm.deceased_id = parseInt(cleanedForm.deceased_id);
|
||||
}
|
||||
if (cleanedForm.duration_min) {
|
||||
cleanedForm.duration_min = parseInt(cleanedForm.duration_min);
|
||||
}
|
||||
|
||||
console.log("Intervention form data being emitted:", cleanedForm);
|
||||
|
||||
// Emit the cleaned form data to parent
|
||||
emit("createIntervention", cleanedForm);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
form.value = {
|
||||
client_id: "",
|
||||
deceased_id: "",
|
||||
type: "",
|
||||
scheduled_date: "",
|
||||
scheduled_time: "",
|
||||
duration_min: "",
|
||||
status: "",
|
||||
order_giver: "",
|
||||
notes: "",
|
||||
};
|
||||
clearErrors();
|
||||
};
|
||||
|
||||
const clearErrors = () => {
|
||||
errors.value = [];
|
||||
fieldErrors.value = {};
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #f5365c;
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.spinner-border-sm {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.alert i {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.multisteps-form__select {
|
||||
background-color: white;
|
||||
border: 1px solid #d2d6da;
|
||||
border-radius: 0.5rem;
|
||||
color: #495057;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.multisteps-form__select:focus {
|
||||
border-color: #5e72e4;
|
||||
box-shadow: 0 0 0 0.2rem rgba(94, 114, 228, 0.25);
|
||||
outline: 0;
|
||||
}
|
||||
</style>
|
||||
@ -29,155 +29,50 @@
|
||||
|
||||
<script setup>
|
||||
import CardInterventions from "@/components/atoms/Interventions/CardInterventions.vue";
|
||||
import { defineProps } from "vue";
|
||||
// Exemple de données
|
||||
const interventions = [
|
||||
{
|
||||
title: "Cérémonie religieuse",
|
||||
status: {
|
||||
label: "Confirmé",
|
||||
color: "success",
|
||||
variant: "fill",
|
||||
size: "md",
|
||||
},
|
||||
date: "15 Décembre 2024 - 14:00",
|
||||
defuntName: "Jean Dupont",
|
||||
lieux: "Église Saint-Pierre, Paris",
|
||||
duree: "1h30",
|
||||
description:
|
||||
"Cérémonie religieuse traditionnelle suivie d'une bénédiction.",
|
||||
action: {
|
||||
label: "Voir détails",
|
||||
color: "primary",
|
||||
},
|
||||
members: [
|
||||
{
|
||||
image: "/images/avatar1.jpg",
|
||||
name: "Marie Curie",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar2.jpg",
|
||||
name: "Pierre Durand",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Crémation",
|
||||
status: {
|
||||
label: "En attente",
|
||||
color: "warning",
|
||||
variant: "fill",
|
||||
size: "md",
|
||||
},
|
||||
date: "16 Décembre 2024 - 10:00",
|
||||
defuntName: "Sophie Martin",
|
||||
lieux: "Crématorium de Lyon",
|
||||
duree: "45 minutes",
|
||||
description: "Cérémonie de crémation avec discours des proches.",
|
||||
action: {
|
||||
label: "Modifier",
|
||||
color: "info",
|
||||
},
|
||||
members: [
|
||||
{
|
||||
image: "/images/avatar3.jpg",
|
||||
name: "Luc Bernard",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Inhumation",
|
||||
status: {
|
||||
label: "Annulé",
|
||||
color: "danger",
|
||||
variant: "fill",
|
||||
size: "md",
|
||||
},
|
||||
date: "17 Décembre 2024 - 11:00",
|
||||
defuntName: "Paul Lefebvre",
|
||||
lieux: "Cimetière du Père Lachaise, Paris",
|
||||
duree: "2 heures",
|
||||
description: "Inhumation suivie d'une réception familiale.",
|
||||
action: {
|
||||
label: "Voir raisons",
|
||||
color: "secondary",
|
||||
},
|
||||
members: [
|
||||
{
|
||||
image: "/images/avatar4.jpg",
|
||||
name: "Alice Moreau",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar5.jpg",
|
||||
name: "Robert Petit",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar6.jpg",
|
||||
name: "Claire Blanc",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Cérémonie laïque",
|
||||
status: {
|
||||
label: "Planifié",
|
||||
color: "info",
|
||||
variant: "gradient",
|
||||
size: "md",
|
||||
},
|
||||
date: "18 Décembre 2024 - 15:30",
|
||||
defuntName: "Michelle Rousseau",
|
||||
lieux: "Salle des fêtes, Marseille",
|
||||
duree: "1 heure",
|
||||
description: "Cérémonie laïque avec partage de souvenirs et musique.",
|
||||
action: {
|
||||
label: "Préparer",
|
||||
color: "success",
|
||||
},
|
||||
members: [
|
||||
{
|
||||
image: "/images/avatar7.jpg",
|
||||
name: "Thomas Leroy",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar8.jpg",
|
||||
name: "Isabelle Marchand",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Transport funéraire",
|
||||
status: {
|
||||
label: "Terminé",
|
||||
color: "dark",
|
||||
variant: "fill",
|
||||
size: "sm",
|
||||
},
|
||||
date: "14 Décembre 2024 - 09:00",
|
||||
defuntName: "Henri Garnier",
|
||||
lieux: "De l'hôpital au funérarium",
|
||||
duree: "30 minutes",
|
||||
description: "Transport du défunt vers le funérarium.",
|
||||
action: {
|
||||
label: "Voir rapport",
|
||||
color: "dark",
|
||||
},
|
||||
members: [
|
||||
{
|
||||
image: "/images/avatar9.jpg",
|
||||
name: "David Morel",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Props optionnelles pour permettre le passage de données externes
|
||||
defineProps({
|
||||
import { defineProps, computed } from "vue";
|
||||
const props = defineProps({
|
||||
interventionData: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
// Use provided data if available, otherwise fall back to default
|
||||
const interventions = computed(() => {
|
||||
return props.interventionData.length > 0
|
||||
? props.interventionData
|
||||
: [
|
||||
{
|
||||
title: "Cérémonie religieuse",
|
||||
status: {
|
||||
label: "Confirmé",
|
||||
color: "success",
|
||||
variant: "fill",
|
||||
size: "md",
|
||||
},
|
||||
date: "15 Décembre 2024 - 14:00",
|
||||
defuntName: "Jean Dupont",
|
||||
lieux: "Église Saint-Pierre, Paris",
|
||||
duree: "1h30",
|
||||
description:
|
||||
"Cérémonie religieuse traditionnelle suivie d'une bénédiction.",
|
||||
action: {
|
||||
label: "Voir détails",
|
||||
color: "primary",
|
||||
},
|
||||
members: [
|
||||
{
|
||||
image: "/images/avatar1.jpg",
|
||||
name: "Marie Curie",
|
||||
},
|
||||
{
|
||||
image: "/images/avatar2.jpg",
|
||||
name: "Pierre Durand",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="multisteps-form mb-5">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-8 mx-auto my-5">
|
||||
<slot name="multi-step" />
|
||||
</div>
|
||||
</div>
|
||||
<!--form panels-->
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-8 m-auto">
|
||||
<slot name="defunt-form" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row mb-4">
|
||||
<slot name="button-return" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
<slot name="defunt-detail-sidebar" />
|
||||
<slot name="file-input" />
|
||||
</div>
|
||||
<div class="col-lg-9 mt-lg-0 mt-4">
|
||||
<slot name="defunt-detail-content" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="multisteps-form mb-5">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-8 mx-auto my-5">
|
||||
<slot name="multi-step" />
|
||||
</div>
|
||||
</div>
|
||||
<!--form panels-->
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-8 m-auto">
|
||||
<slot name="intervention-form" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script></script>
|
||||
@ -574,6 +574,11 @@ const routes = [
|
||||
name: "Interventions",
|
||||
component: () => import("@/views/pages/Interventions/Interventions.vue"),
|
||||
},
|
||||
{
|
||||
path: "/interventions/new",
|
||||
name: "Add Intervention",
|
||||
component: () => import("@/views/pages/Interventions/AddIntervention.vue"),
|
||||
},
|
||||
{
|
||||
path: "/intervention",
|
||||
name: "Intervention details",
|
||||
@ -585,6 +590,16 @@ const routes = [
|
||||
name: "Defunts",
|
||||
component: () => import("@/views/pages/Defunts/ListeDefunts.vue"),
|
||||
},
|
||||
{
|
||||
path: "/defunts/new",
|
||||
name: "Add Defunts",
|
||||
component: () => import("@/views/pages/Defunts/AddDefunt.vue"),
|
||||
},
|
||||
{
|
||||
path: "/defunts/:id",
|
||||
name: "Defunt details",
|
||||
component: () => import("@/views/pages/Defunts/DefuntDetails.vue"),
|
||||
},
|
||||
// Paramétrage
|
||||
{
|
||||
path: "/parametrage/droits",
|
||||
|
||||
148
thanasoft-front/src/services/deceased.ts
Normal file
148
thanasoft-front/src/services/deceased.ts
Normal file
@ -0,0 +1,148 @@
|
||||
import { request } from "./http";
|
||||
|
||||
export interface Deceased {
|
||||
id?: number;
|
||||
last_name: string;
|
||||
first_name?: string;
|
||||
full_name?: string;
|
||||
birth_date?: string;
|
||||
death_date?: string;
|
||||
place_of_death?: string;
|
||||
notes?: string;
|
||||
documents_count?: number;
|
||||
interventions_count?: number;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface DeceasedListResponse {
|
||||
data: Deceased[];
|
||||
meta?: {
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DeceasedResponse {
|
||||
data: Deceased;
|
||||
}
|
||||
|
||||
export interface CreateDeceasedPayload {
|
||||
last_name: string;
|
||||
first_name?: string;
|
||||
birth_date?: string;
|
||||
death_date?: string;
|
||||
place_of_death?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface UpdateDeceasedPayload extends Partial<CreateDeceasedPayload> {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const DeceasedService = {
|
||||
/**
|
||||
* Get all deceased with pagination
|
||||
*/
|
||||
async getAllDeceased(params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
search?: string;
|
||||
sort_by?: string;
|
||||
sort_order?: "asc" | "desc";
|
||||
}): Promise<DeceasedListResponse> {
|
||||
const response = await request<DeceasedListResponse>({
|
||||
url: "/api/deceased",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a specific deceased by ID
|
||||
*/
|
||||
async getDeceased(id: number): Promise<Deceased> {
|
||||
const response = await request<Deceased>({
|
||||
url: `/api/deceased/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new deceased
|
||||
*/
|
||||
async createDeceased(
|
||||
payload: CreateDeceasedPayload
|
||||
): Promise<DeceasedResponse> {
|
||||
const response = await request<DeceasedResponse>({
|
||||
url: "/api/deceased",
|
||||
method: "post",
|
||||
data: payload,
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an existing deceased
|
||||
*/
|
||||
async updateDeceased(
|
||||
payload: UpdateDeceasedPayload
|
||||
): Promise<DeceasedResponse> {
|
||||
const { id, ...updateData } = payload;
|
||||
|
||||
const response = await request<DeceasedResponse>({
|
||||
url: `/api/deceased/${id}`,
|
||||
method: "put",
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a deceased
|
||||
*/
|
||||
async deleteDeceased(
|
||||
id: number
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
const response = await request<{ success: boolean; message: string }>({
|
||||
url: `/api/deceased/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Search deceased by name or other criteria
|
||||
*/
|
||||
async searchDeceased(
|
||||
query: string,
|
||||
params?: {
|
||||
exact_match?: boolean;
|
||||
}
|
||||
): Promise<Deceased[]> {
|
||||
const response = await request<{
|
||||
data: Deceased[];
|
||||
count: number;
|
||||
message: string;
|
||||
}>({
|
||||
url: "/api/deceased/searchBy",
|
||||
method: "get",
|
||||
params: {
|
||||
search: query,
|
||||
exact_match: params?.exact_match || false,
|
||||
},
|
||||
});
|
||||
return response.data; // Return just the data array
|
||||
},
|
||||
};
|
||||
|
||||
export default DeceasedService;
|
||||
237
thanasoft-front/src/services/intervention.ts
Normal file
237
thanasoft-front/src/services/intervention.ts
Normal file
@ -0,0 +1,237 @@
|
||||
import { request } from "./http";
|
||||
|
||||
export interface Intervention {
|
||||
id?: number;
|
||||
client_id: number;
|
||||
deceased_id: number;
|
||||
order_giver?: string;
|
||||
location_id?: number;
|
||||
type?: string;
|
||||
scheduled_at?: string;
|
||||
duration_min?: number;
|
||||
status?: string;
|
||||
assigned_practitioner_id?: number;
|
||||
attachments_count?: number;
|
||||
notes?: string;
|
||||
created_by?: number;
|
||||
// Relations
|
||||
client?: any;
|
||||
deceased?: any;
|
||||
practitioner?: any;
|
||||
location?: any;
|
||||
// Timestamps
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface InterventionListResponse {
|
||||
data: Intervention[];
|
||||
meta?: {
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InterventionResponse {
|
||||
data: Intervention;
|
||||
}
|
||||
|
||||
export interface CreateInterventionPayload {
|
||||
client_id: number;
|
||||
deceased_id: number;
|
||||
order_giver?: string;
|
||||
location_id?: number;
|
||||
type?: string;
|
||||
scheduled_at?: string;
|
||||
duration_min?: number;
|
||||
status?: string;
|
||||
assigned_practitioner_id?: number;
|
||||
notes?: string;
|
||||
created_by?: number;
|
||||
}
|
||||
|
||||
export interface UpdateInterventionPayload
|
||||
extends Partial<CreateInterventionPayload> {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const InterventionService = {
|
||||
/**
|
||||
* Get all interventions with pagination and filters
|
||||
*/
|
||||
async getAllInterventions(params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
search?: string;
|
||||
deceased_id?: number;
|
||||
client_id?: number;
|
||||
status?: string;
|
||||
scheduled_at?: string;
|
||||
sort_by?: string;
|
||||
sort_order?: "asc" | "desc";
|
||||
}): Promise<InterventionListResponse> {
|
||||
const response = await request<InterventionListResponse>({
|
||||
url: "/api/interventions",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a specific intervention by ID
|
||||
*/
|
||||
async getIntervention(id: number): Promise<Intervention> {
|
||||
const response = await request<Intervention>({
|
||||
url: `/api/interventions/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new intervention
|
||||
*/
|
||||
async createIntervention(
|
||||
payload: CreateInterventionPayload
|
||||
): Promise<Intervention> {
|
||||
const response = await request<Intervention>({
|
||||
url: "/api/interventions",
|
||||
method: "post",
|
||||
data: payload,
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an existing intervention
|
||||
*/
|
||||
async updateIntervention(
|
||||
payload: UpdateInterventionPayload
|
||||
): Promise<Intervention> {
|
||||
const { id, ...updateData } = payload;
|
||||
|
||||
const response = await request<Intervention>({
|
||||
url: `/api/interventions/${id}`,
|
||||
method: "put",
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete an intervention
|
||||
*/
|
||||
async deleteIntervention(
|
||||
id: number
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
const response = await request<{ success: boolean; message: string }>({
|
||||
url: `/api/interventions/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get interventions by deceased ID
|
||||
*/
|
||||
async getInterventionsByDeceased(
|
||||
deceasedId: number,
|
||||
params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
}
|
||||
): Promise<InterventionListResponse> {
|
||||
const response = await request<InterventionListResponse>({
|
||||
url: `/api/deceased/${deceasedId}/interventions`,
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get interventions by client ID
|
||||
*/
|
||||
async getInterventionsByClient(
|
||||
clientId: number,
|
||||
params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
}
|
||||
): Promise<InterventionListResponse> {
|
||||
const response = await request<InterventionListResponse>({
|
||||
url: `/api/clients/${clientId}/interventions`,
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Search interventions by various criteria
|
||||
*/
|
||||
async searchInterventions(
|
||||
query: string,
|
||||
params?: {
|
||||
exact_match?: boolean;
|
||||
}
|
||||
): Promise<Intervention[]> {
|
||||
const response = await request<{
|
||||
data: Intervention[];
|
||||
count: number;
|
||||
message: string;
|
||||
}>({
|
||||
url: "/api/interventions/search",
|
||||
method: "get",
|
||||
params: {
|
||||
search: query,
|
||||
exact_match: params?.exact_match || false,
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update intervention status
|
||||
*/
|
||||
async updateInterventionStatus(
|
||||
id: number,
|
||||
status: string
|
||||
): Promise<Intervention> {
|
||||
const response = await request<Intervention>({
|
||||
url: `/api/interventions/${id}/status`,
|
||||
method: "patch",
|
||||
data: { status },
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Assign practitioner to intervention
|
||||
*/
|
||||
async assignPractitioner(
|
||||
id: number,
|
||||
practitionerId: number
|
||||
): Promise<Intervention> {
|
||||
const response = await request<Intervention>({
|
||||
url: `/api/interventions/${id}/assign`,
|
||||
method: "patch",
|
||||
data: { assigned_practitioner_id: practitionerId },
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
};
|
||||
|
||||
export default InterventionService;
|
||||
327
thanasoft-front/src/stores/deceasedStore.ts
Normal file
327
thanasoft-front/src/stores/deceasedStore.ts
Normal file
@ -0,0 +1,327 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
import DeceasedService, {
|
||||
Deceased,
|
||||
CreateDeceasedPayload,
|
||||
UpdateDeceasedPayload,
|
||||
DeceasedListResponse,
|
||||
} from "@/services/deceased";
|
||||
|
||||
export const useDeceasedStore = defineStore("deceased", () => {
|
||||
// State
|
||||
const deceased = ref<Deceased[]>([]);
|
||||
const currentDeceased = ref<Deceased | null>(null);
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const success = ref(false);
|
||||
|
||||
// Pagination state
|
||||
const pagination = ref({
|
||||
current_page: 1,
|
||||
last_page: 1,
|
||||
per_page: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
// Getters
|
||||
const allDeceased = computed(() => deceased.value);
|
||||
|
||||
const isLoading = computed(() => loading.value);
|
||||
const hasError = computed(() => error.value !== null);
|
||||
const getError = computed(() => error.value);
|
||||
const isSuccess = computed(() => success.value);
|
||||
|
||||
const getDeceasedById = computed(() => (id: number) =>
|
||||
deceased.value.find((d) => d.id === id)
|
||||
);
|
||||
|
||||
const getPagination = computed(() => pagination.value);
|
||||
|
||||
const getDeceasedFullName = computed(() => (deceased: Deceased) => {
|
||||
const parts = [deceased.first_name, deceased.last_name].filter(Boolean);
|
||||
return parts.join(" ").trim();
|
||||
});
|
||||
|
||||
// Actions
|
||||
const setLoading = (isLoading: boolean) => {
|
||||
loading.value = isLoading;
|
||||
};
|
||||
|
||||
const setError = (err: string | null) => {
|
||||
error.value = err;
|
||||
};
|
||||
|
||||
const clearError = () => {
|
||||
error.value = null;
|
||||
};
|
||||
|
||||
const setSuccess = (isSuccess: boolean) => {
|
||||
success.value = isSuccess;
|
||||
};
|
||||
|
||||
const clearSuccess = () => {
|
||||
success.value = false;
|
||||
};
|
||||
|
||||
const setDeceased = (newDeceased: Deceased[]) => {
|
||||
deceased.value = newDeceased;
|
||||
};
|
||||
|
||||
const setCurrentDeceased = (deceased: Deceased | null) => {
|
||||
currentDeceased.value = deceased;
|
||||
};
|
||||
|
||||
const setPagination = (meta: any) => {
|
||||
if (meta) {
|
||||
pagination.value = {
|
||||
current_page: meta.current_page || 1,
|
||||
last_page: meta.last_page || 1,
|
||||
per_page: meta.per_page || 10,
|
||||
total: meta.total || 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all deceased with pagination and filters
|
||||
*/
|
||||
const fetchDeceased = async (params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
search?: string;
|
||||
sort_by?: string;
|
||||
sort_order?: "asc" | "desc";
|
||||
}) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const response = await DeceasedService.getAllDeceased(params);
|
||||
setDeceased(response.data);
|
||||
if (response.meta) {
|
||||
setPagination(response.meta);
|
||||
}
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Échec du chargement des personnes décédées";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a single deceased by ID
|
||||
*/
|
||||
const fetchDeceasedById = async (id: number) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const deceased = await DeceasedService.getDeceased(id);
|
||||
setCurrentDeceased(deceased);
|
||||
return deceased;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Échec du chargement de la personne décédée";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new deceased
|
||||
*/
|
||||
const createDeceased = async (payload: CreateDeceasedPayload) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const response = await DeceasedService.createDeceased(payload);
|
||||
// Add the new deceased to the list
|
||||
deceased.value.unshift(response.data);
|
||||
setCurrentDeceased(response.data);
|
||||
setSuccess(true);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Échec de la création de la personne décédée";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update an existing deceased
|
||||
*/
|
||||
const updateDeceased = async (payload: UpdateDeceasedPayload) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const response = await DeceasedService.updateDeceased(payload);
|
||||
const updatedDeceased = response.data;
|
||||
|
||||
// Update in the deceased list
|
||||
const index = deceased.value.findIndex(
|
||||
(d) => d.id === updatedDeceased.id
|
||||
);
|
||||
if (index !== -1) {
|
||||
deceased.value[index] = updatedDeceased;
|
||||
}
|
||||
|
||||
// Update current deceased if it's the one being edited
|
||||
if (
|
||||
currentDeceased.value &&
|
||||
currentDeceased.value.id === updatedDeceased.id
|
||||
) {
|
||||
setCurrentDeceased(updatedDeceased);
|
||||
}
|
||||
|
||||
setSuccess(true);
|
||||
return updatedDeceased;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Échec de la mise à jour de la personne décédée";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a deceased
|
||||
*/
|
||||
const deleteDeceased = async (id: number) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const response = await DeceasedService.deleteDeceased(id);
|
||||
|
||||
// Remove from the deceased list
|
||||
deceased.value = deceased.value.filter((d) => d.id !== id);
|
||||
|
||||
// Clear current deceased if it's the one being deleted
|
||||
if (currentDeceased.value && currentDeceased.value.id === id) {
|
||||
setCurrentDeceased(null);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Échec de la suppression de la personne décédée";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Search deceased by name or other criteria
|
||||
*/
|
||||
const searchDeceased = async (
|
||||
query: string,
|
||||
params?: {
|
||||
exact_match?: boolean;
|
||||
}
|
||||
) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const results = await DeceasedService.searchDeceased(query, params);
|
||||
setDeceased(results);
|
||||
return results;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Échec de la recherche de personnes décédées";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the state
|
||||
*/
|
||||
const resetState = () => {
|
||||
deceased.value = [];
|
||||
currentDeceased.value = null;
|
||||
loading.value = false;
|
||||
error.value = null;
|
||||
success.value = false;
|
||||
pagination.value = {
|
||||
current_page: 1,
|
||||
last_page: 1,
|
||||
per_page: 10,
|
||||
total: 0,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
deceased,
|
||||
currentDeceased,
|
||||
loading,
|
||||
error,
|
||||
success,
|
||||
pagination,
|
||||
|
||||
// Getters
|
||||
allDeceased,
|
||||
isLoading,
|
||||
hasError,
|
||||
getError,
|
||||
isSuccess,
|
||||
getDeceasedById,
|
||||
getPagination,
|
||||
getDeceasedFullName,
|
||||
|
||||
// Actions
|
||||
setLoading,
|
||||
setError,
|
||||
clearError,
|
||||
setSuccess,
|
||||
clearSuccess,
|
||||
setDeceased,
|
||||
setCurrentDeceased,
|
||||
setPagination,
|
||||
fetchDeceased,
|
||||
fetchDeceasedById,
|
||||
createDeceased,
|
||||
updateDeceased,
|
||||
deleteDeceased,
|
||||
searchDeceased,
|
||||
resetState,
|
||||
};
|
||||
});
|
||||
|
||||
export default useDeceasedStore;
|
||||
523
thanasoft-front/src/stores/interventionStore.ts
Normal file
523
thanasoft-front/src/stores/interventionStore.ts
Normal file
@ -0,0 +1,523 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
import InterventionService from "@/services/intervention";
|
||||
import type {
|
||||
Intervention,
|
||||
CreateInterventionPayload,
|
||||
UpdateInterventionPayload,
|
||||
InterventionListResponse,
|
||||
} from "@/services/intervention";
|
||||
|
||||
export const useInterventionStore = defineStore("intervention", () => {
|
||||
// State
|
||||
const interventions = ref<Intervention[]>([]);
|
||||
const currentIntervention = ref<Intervention | null>(null);
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const success = ref(false);
|
||||
|
||||
// Pagination state
|
||||
const pagination = ref({
|
||||
current_page: 1,
|
||||
last_page: 1,
|
||||
per_page: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
// Getters
|
||||
const allInterventions = computed(() => interventions.value);
|
||||
|
||||
const isLoading = computed(() => loading.value);
|
||||
const hasError = computed(() => error.value !== null);
|
||||
const getError = computed(() => error.value);
|
||||
const isSuccess = computed(() => success.value);
|
||||
|
||||
const getInterventionById = computed(() => (id: number) =>
|
||||
interventions.value.find((i) => i.id === id)
|
||||
);
|
||||
|
||||
const getPagination = computed(() => pagination.value);
|
||||
|
||||
const getInterventionsByDeceased = computed(() => (deceasedId: number) =>
|
||||
interventions.value.filter((i) => i.deceased_id === deceasedId)
|
||||
);
|
||||
|
||||
const getInterventionsByClient = computed(() => (clientId: number) =>
|
||||
interventions.value.filter((i) => i.client_id === clientId)
|
||||
);
|
||||
|
||||
const getScheduledInterventions = computed(() =>
|
||||
interventions.value.filter(
|
||||
(i) => i.scheduled_at && new Date(i.scheduled_at) > new Date()
|
||||
)
|
||||
);
|
||||
|
||||
const getCompletedInterventions = computed(() =>
|
||||
interventions.value.filter((i) => i.status === "completed")
|
||||
);
|
||||
|
||||
const getPendingInterventions = computed(() =>
|
||||
interventions.value.filter((i) => i.status === "pending")
|
||||
);
|
||||
|
||||
// Actions
|
||||
const setLoading = (isLoading: boolean) => {
|
||||
loading.value = isLoading;
|
||||
};
|
||||
|
||||
const setError = (err: string | null) => {
|
||||
error.value = err;
|
||||
};
|
||||
|
||||
const clearError = () => {
|
||||
error.value = null;
|
||||
};
|
||||
|
||||
const setSuccess = (isSuccess: boolean) => {
|
||||
success.value = isSuccess;
|
||||
};
|
||||
|
||||
const clearSuccess = () => {
|
||||
success.value = false;
|
||||
};
|
||||
|
||||
const setInterventions = (newInterventions: Intervention[]) => {
|
||||
interventions.value = newInterventions;
|
||||
};
|
||||
|
||||
const setCurrentIntervention = (intervention: Intervention | null) => {
|
||||
currentIntervention.value = intervention;
|
||||
};
|
||||
|
||||
const setPagination = (meta: any) => {
|
||||
if (meta) {
|
||||
pagination.value = {
|
||||
current_page: meta.current_page || 1,
|
||||
last_page: meta.last_page || 1,
|
||||
per_page: meta.per_page || 10,
|
||||
total: meta.total || 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all interventions with pagination and filters
|
||||
*/
|
||||
const fetchInterventions = async (params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
search?: string;
|
||||
deceased_id?: number;
|
||||
client_id?: number;
|
||||
status?: string;
|
||||
scheduled_at?: string;
|
||||
sort_by?: string;
|
||||
sort_order?: "asc" | "desc";
|
||||
}) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const response = await InterventionService.getAllInterventions(params);
|
||||
setInterventions(response.data);
|
||||
if (response.meta) {
|
||||
setPagination(response.meta);
|
||||
}
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Échec du chargement des interventions";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a single intervention by ID
|
||||
*/
|
||||
const fetchInterventionById = async (id: number) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const intervention = await InterventionService.getIntervention(id);
|
||||
setCurrentIntervention(intervention);
|
||||
return intervention;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Échec du chargement de l'intervention";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new intervention
|
||||
*/
|
||||
const createIntervention = async (payload: CreateInterventionPayload) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const intervention = await InterventionService.createIntervention(
|
||||
payload
|
||||
);
|
||||
// Add the new intervention to the list
|
||||
interventions.value.unshift(intervention);
|
||||
setCurrentIntervention(intervention);
|
||||
setSuccess(true);
|
||||
return intervention;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Échec de la création de l'intervention";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update an existing intervention
|
||||
*/
|
||||
const updateIntervention = async (payload: UpdateInterventionPayload) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const intervention = await InterventionService.updateIntervention(
|
||||
payload
|
||||
);
|
||||
|
||||
// Update in the interventions list
|
||||
const index = interventions.value.findIndex(
|
||||
(i) => i.id === intervention.id
|
||||
);
|
||||
if (index !== -1) {
|
||||
interventions.value[index] = intervention;
|
||||
}
|
||||
|
||||
// Update current intervention if it's the one being edited
|
||||
if (
|
||||
currentIntervention.value &&
|
||||
currentIntervention.value.id === intervention.id
|
||||
) {
|
||||
setCurrentIntervention(intervention);
|
||||
}
|
||||
|
||||
setSuccess(true);
|
||||
return intervention;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Échec de la mise à jour de l'intervention";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete an intervention
|
||||
*/
|
||||
const deleteIntervention = async (id: number) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const response = await InterventionService.deleteIntervention(id);
|
||||
|
||||
// Remove from the interventions list
|
||||
interventions.value = interventions.value.filter((i) => i.id !== id);
|
||||
|
||||
// Clear current intervention if it's the one being deleted
|
||||
if (currentIntervention.value && currentIntervention.value.id === id) {
|
||||
setCurrentIntervention(null);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Échec de la suppression de l'intervention";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get interventions by deceased ID
|
||||
*/
|
||||
const fetchInterventionsByDeceased = async (
|
||||
deceasedId: number,
|
||||
params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
}
|
||||
) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const response = await InterventionService.getInterventionsByDeceased(
|
||||
deceasedId,
|
||||
params
|
||||
);
|
||||
setInterventions(response.data);
|
||||
if (response.meta) {
|
||||
setPagination(response.meta);
|
||||
}
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Échec du chargement des interventions du défunt";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get interventions by client ID
|
||||
*/
|
||||
const fetchInterventionsByClient = async (
|
||||
clientId: number,
|
||||
params?: {
|
||||
page?: number;
|
||||
per_page?: number;
|
||||
}
|
||||
) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const response = await InterventionService.getInterventionsByClient(
|
||||
clientId,
|
||||
params
|
||||
);
|
||||
setInterventions(response.data);
|
||||
if (response.meta) {
|
||||
setPagination(response.meta);
|
||||
}
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Échec du chargement des interventions du client";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Search interventions by various criteria
|
||||
*/
|
||||
const searchInterventions = async (
|
||||
query: string,
|
||||
params?: {
|
||||
exact_match?: boolean;
|
||||
}
|
||||
) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const results = await InterventionService.searchInterventions(
|
||||
query,
|
||||
params
|
||||
);
|
||||
setInterventions(results);
|
||||
return results;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Échec de la recherche d'interventions";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update intervention status
|
||||
*/
|
||||
const updateInterventionStatus = async (id: number, status: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const intervention = await InterventionService.updateInterventionStatus(
|
||||
id,
|
||||
status
|
||||
);
|
||||
|
||||
// Update in the interventions list
|
||||
const index = interventions.value.findIndex(
|
||||
(i) => i.id === intervention.id
|
||||
);
|
||||
if (index !== -1) {
|
||||
interventions.value[index] = intervention;
|
||||
}
|
||||
|
||||
// Update current intervention if it's the one being updated
|
||||
if (
|
||||
currentIntervention.value &&
|
||||
currentIntervention.value.id === intervention.id
|
||||
) {
|
||||
setCurrentIntervention(intervention);
|
||||
}
|
||||
|
||||
setSuccess(true);
|
||||
return intervention;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Échec de la mise à jour du statut";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Assign practitioner to intervention
|
||||
*/
|
||||
const assignPractitioner = async (id: number, practitionerId: number) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const intervention = await InterventionService.assignPractitioner(
|
||||
id,
|
||||
practitionerId
|
||||
);
|
||||
|
||||
// Update in the interventions list
|
||||
const index = interventions.value.findIndex(
|
||||
(i) => i.id === intervention.id
|
||||
);
|
||||
if (index !== -1) {
|
||||
interventions.value[index] = intervention;
|
||||
}
|
||||
|
||||
// Update current intervention if it's the one being updated
|
||||
if (
|
||||
currentIntervention.value &&
|
||||
currentIntervention.value.id === intervention.id
|
||||
) {
|
||||
setCurrentIntervention(intervention);
|
||||
}
|
||||
|
||||
setSuccess(true);
|
||||
return intervention;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Échec de l'assignation du praticien";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the state
|
||||
*/
|
||||
const resetState = () => {
|
||||
interventions.value = [];
|
||||
currentIntervention.value = null;
|
||||
loading.value = false;
|
||||
error.value = null;
|
||||
success.value = false;
|
||||
pagination.value = {
|
||||
current_page: 1,
|
||||
last_page: 1,
|
||||
per_page: 10,
|
||||
total: 0,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
interventions,
|
||||
currentIntervention,
|
||||
loading,
|
||||
error,
|
||||
success,
|
||||
pagination,
|
||||
|
||||
// Getters
|
||||
allInterventions,
|
||||
isLoading,
|
||||
hasError,
|
||||
getError,
|
||||
isSuccess,
|
||||
getInterventionById,
|
||||
getPagination,
|
||||
getInterventionsByDeceased,
|
||||
getInterventionsByClient,
|
||||
getScheduledInterventions,
|
||||
getCompletedInterventions,
|
||||
getPendingInterventions,
|
||||
|
||||
// Actions
|
||||
setLoading,
|
||||
setError,
|
||||
clearError,
|
||||
setSuccess,
|
||||
clearSuccess,
|
||||
setInterventions,
|
||||
setCurrentIntervention,
|
||||
setPagination,
|
||||
fetchInterventions,
|
||||
fetchInterventionById,
|
||||
createIntervention,
|
||||
updateIntervention,
|
||||
deleteIntervention,
|
||||
fetchInterventionsByDeceased,
|
||||
fetchInterventionsByClient,
|
||||
searchInterventions,
|
||||
updateInterventionStatus,
|
||||
assignPractitioner,
|
||||
resetState,
|
||||
};
|
||||
});
|
||||
|
||||
export default useInterventionStore;
|
||||
59
thanasoft-front/src/views/pages/Defunts/AddDefunt.vue
Normal file
59
thanasoft-front/src/views/pages/Defunts/AddDefunt.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<add-defunt-presentation
|
||||
:loading="deceasedStore.isLoading"
|
||||
:validation-errors="validationErrors"
|
||||
:success="showSuccess"
|
||||
@create-defunt="handleCreateDefunt"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import AddDefuntPresentation from "@/components/Organism/Defunts/AddDefuntPresentation.vue";
|
||||
import { useDeceasedStore } from "@/stores/deceasedStore";
|
||||
import { useNotificationStore } from "@/stores/notification";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
const deceasedStore = useDeceasedStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
const validationErrors = ref({});
|
||||
const showSuccess = ref(false);
|
||||
|
||||
const handleCreateDefunt = async (form) => {
|
||||
try {
|
||||
// Clear previous errors
|
||||
validationErrors.value = {};
|
||||
showSuccess.value = false;
|
||||
|
||||
// Call the store to create deceased
|
||||
const deceased = await deceasedStore.createDeceased(form);
|
||||
|
||||
// Show success notification
|
||||
notificationStore.created("Personne décédée");
|
||||
showSuccess.value = true;
|
||||
|
||||
// Redirect after 2 seconds
|
||||
setTimeout(() => {
|
||||
router.push({ name: "Liste Defunts" });
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error("Error creating deceased:", error);
|
||||
|
||||
// Handle validation errors from Laravel
|
||||
if (error.response && error.response.status === 422) {
|
||||
validationErrors.value = error.response.data.errors || {};
|
||||
notificationStore.error(
|
||||
"Erreur de validation",
|
||||
"Veuillez corriger les erreurs dans le formulaire"
|
||||
);
|
||||
} else if (error.response && error.response.data) {
|
||||
// Handle other API errors
|
||||
const errorMessage =
|
||||
error.response.data.message || "Une erreur est survenue";
|
||||
notificationStore.error("Erreur", errorMessage);
|
||||
} else {
|
||||
notificationStore.error("Erreur", "Une erreur inattendue s'est produite");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
61
thanasoft-front/src/views/pages/Defunts/DefuntDetails.vue
Normal file
61
thanasoft-front/src/views/pages/Defunts/DefuntDetails.vue
Normal file
@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div v-if="deceasedStore.currentDeceased" class="container-fluid">
|
||||
<defunt-detail-presentation
|
||||
:deceased="deceasedStore.currentDeceased"
|
||||
:is-loading="deceasedStore.isLoading"
|
||||
@update-deceased="updateDeceased"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="!deceasedStore.isLoading" class="text-center p-5">
|
||||
<h4 class="text-muted">Personne décédée introuvable</h4>
|
||||
<router-link to="/defunts" class="btn btn-primary mt-3">
|
||||
<i class="fas fa-arrow-left me-2"></i>Retour à la liste
|
||||
</router-link>
|
||||
</div>
|
||||
<div v-else class="text-center p-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Chargement...</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useDeceasedStore } from "@/stores/deceasedStore";
|
||||
import { useNotificationStore } from "@/stores/notification";
|
||||
import DefuntDetailPresentation from "@/components/Organism/Defunts/DefuntDetailPresentation.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const deceasedStore = useDeceasedStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
|
||||
// Ensure deceased_id is a number
|
||||
const deceased_id = Number(route.params.id);
|
||||
|
||||
onMounted(async () => {
|
||||
if (deceased_id) {
|
||||
const cc = await deceasedStore.fetchDeceasedById(deceased_id);
|
||||
console.log(cc);
|
||||
}
|
||||
});
|
||||
|
||||
const updateDeceased = async (data) => {
|
||||
if (!deceased_id) {
|
||||
console.error("Missing deceased id");
|
||||
notificationStore.error("Erreur", "ID du défunt manquant");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deceasedStore.updateDeceased({ id: deceased_id, ...data });
|
||||
notificationStore.updated("Personne décédée");
|
||||
} catch (error) {
|
||||
console.error("Error updating deceased:", error);
|
||||
notificationStore.error(
|
||||
"Erreur",
|
||||
"Impossible de mettre à jour la personne décédée"
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,6 +1,14 @@
|
||||
<template>
|
||||
<defunt-presentation />
|
||||
<defunt-presentation :defunts="deceasedStore.deceased" />
|
||||
</template>
|
||||
<script setup>
|
||||
import DefuntPresentation from "@/components/Organism/Defunts/DefuntPresentation.vue";
|
||||
import { useDeceasedStore } from "@/stores/deceasedStore";
|
||||
import { onMounted } from "vue";
|
||||
|
||||
const deceasedStore = useDeceasedStore();
|
||||
|
||||
onMounted(async () => {
|
||||
await deceasedStore.fetchDeceased();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<add-intervention-presentation
|
||||
:loading="interventionStore.isLoading"
|
||||
:validation-errors="validationErrors"
|
||||
:success="showSuccess"
|
||||
:deceased-list="deceasedStore.allDeceased"
|
||||
:deceased-loading="deceasedStore.isLoading"
|
||||
:client-list="clientStore.allClients"
|
||||
:client-loading="clientStore.isLoading"
|
||||
@create-intervention="handleCreateIntervention"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import AddInterventionPresentation from "@/components/Organism/Interventions/AddInterventionPresentation.vue";
|
||||
import { useInterventionStore } from "@/stores/interventionStore";
|
||||
import { useDeceasedStore } from "@/stores/deceasedStore";
|
||||
import { useClientStore } from "@/stores/clientStore";
|
||||
import { useNotificationStore } from "@/stores/notification";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
const interventionStore = useInterventionStore();
|
||||
const deceasedStore = useDeceasedStore();
|
||||
const clientStore = useClientStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
const validationErrors = ref({});
|
||||
const showSuccess = ref(false);
|
||||
|
||||
const handleCreateIntervention = async (form) => {
|
||||
try {
|
||||
// Clear previous errors
|
||||
validationErrors.value = {};
|
||||
showSuccess.value = false;
|
||||
|
||||
// Call the store to create intervention
|
||||
const intervention = await interventionStore.createIntervention(form);
|
||||
|
||||
// Show success notification
|
||||
notificationStore.created("Intervention");
|
||||
showSuccess.value = true;
|
||||
|
||||
// Redirect after 2 seconds
|
||||
setTimeout(() => {
|
||||
router.push({ name: "Interventions" });
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error("Error creating intervention:", error);
|
||||
|
||||
// Handle validation errors from Laravel
|
||||
if (error.response && error.response.status === 422) {
|
||||
validationErrors.value = error.response.data.errors || {};
|
||||
notificationStore.error(
|
||||
"Erreur de validation",
|
||||
"Veuillez corriger les erreurs dans le formulaire"
|
||||
);
|
||||
} else if (error.response && error.response.data) {
|
||||
// Handle other API errors
|
||||
const errorMessage =
|
||||
error.response.data.message || "Une erreur est survenue";
|
||||
notificationStore.error("Erreur", errorMessage);
|
||||
} else {
|
||||
notificationStore.error("Erreur", "Une erreur inattendue s'est produite");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Load deceased and client lists for selection
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
deceasedStore.fetchDeceased(),
|
||||
clientStore.fetchClients(),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("Error loading data:", error);
|
||||
notificationStore.error(
|
||||
"Erreur",
|
||||
"Impossible de charger les données nécessaires"
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -1,6 +1,85 @@
|
||||
<template>
|
||||
<intervention-presentation />
|
||||
<intervention-presentation
|
||||
:interventions="transformedInterventions"
|
||||
:loading="interventionStore.isLoading"
|
||||
:error="interventionStore.getError"
|
||||
@retry="loadInterventions"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, computed } from "vue";
|
||||
import InterventionPresentation from "@/components/Organism/Interventions/InterventionPresentation.vue";
|
||||
import { useInterventionStore } from "@/stores/interventionStore";
|
||||
|
||||
const interventionStore = useInterventionStore();
|
||||
|
||||
// Load interventions on component mount
|
||||
const loadInterventions = async () => {
|
||||
try {
|
||||
await interventionStore.fetchInterventions();
|
||||
} catch (error) {
|
||||
console.error("Failed to load interventions:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Transform store data to match component expectations
|
||||
const transformedInterventions = computed(() => {
|
||||
return interventionStore.interventions.map((intervention) => ({
|
||||
title: intervention.title || intervention.type || "Intervention",
|
||||
status: {
|
||||
label: intervention.status || "En attente",
|
||||
color: getStatusColor(intervention.status),
|
||||
variant: "fill",
|
||||
size: "md",
|
||||
},
|
||||
date: formatDate(intervention.scheduled_at || intervention.date),
|
||||
defuntName:
|
||||
intervention.deceased_name ||
|
||||
intervention.defuntName ||
|
||||
"Nom non disponible",
|
||||
lieux: intervention.location || intervention.lieux || "Lieu non spécifié",
|
||||
duree: intervention.duration || intervention.duree || "Durée non définie",
|
||||
description: intervention.description || "Aucune description disponible",
|
||||
action: {
|
||||
label: "Voir détails",
|
||||
color: "primary",
|
||||
},
|
||||
members: intervention.members || [],
|
||||
}));
|
||||
});
|
||||
|
||||
// Helper function to get status color
|
||||
const getStatusColor = (status) => {
|
||||
const statusColors = {
|
||||
pending: "warning",
|
||||
confirmed: "success",
|
||||
completed: "success",
|
||||
cancelled: "danger",
|
||||
planned: "info",
|
||||
in_progress: "info",
|
||||
finished: "dark",
|
||||
};
|
||||
return statusColors[status?.toLowerCase()] || "secondary";
|
||||
};
|
||||
|
||||
// Helper function to format date
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return "Date non définie";
|
||||
|
||||
const date = new Date(dateString);
|
||||
return date
|
||||
.toLocaleDateString("fr-FR", {
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
.replace(",", " -");
|
||||
};
|
||||
|
||||
// Load data on component mount
|
||||
onMounted(() => {
|
||||
loadInterventions();
|
||||
});
|
||||
</script>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user