intervention

This commit is contained in:
Nyavokevin 2025-11-11 17:45:58 +03:00
parent 0c4ff92fd5
commit 7570f46658
56 changed files with 4995 additions and 265 deletions

View 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);
}
}
}

View File

@ -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);
}
}
}

View 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.'
];
}
}

View File

@ -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.'
];
}
}

View 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.'
];
}
}

View File

@ -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.'
];
}
}

View File

@ -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'))
],

View File

@ -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()
]
];
}
}

View File

@ -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')
];
}
}

View File

@ -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')
];
}
}

View File

@ -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;
}
}

View File

@ -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')
];
}
}

View File

@ -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);
})
];
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View File

@ -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);
}
/**

View 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
{
//
}
}

View 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();
});
}
}

View File

@ -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;
}

View 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;
});
}
}

View File

@ -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;
}

View File

@ -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');
}
};

View File

@ -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');
}
};

View File

@ -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');
}
};

View File

@ -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');
}
};

View File

@ -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');
}
};

View File

@ -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');
}
};

View File

@ -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']);
});
});

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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",

View 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;

View 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;

View 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;

View 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;

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>