assigner thanato
This commit is contained in:
parent
4b7e075918
commit
a51e05559a
@ -359,4 +359,66 @@ class InterventionController extends Controller
|
|||||||
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign a practitioner to an intervention
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @param int $id
|
||||||
|
* @return JsonResponse
|
||||||
|
*/
|
||||||
|
public function assignPractitioner(Request $request, int $id): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$validated = $request->validate([
|
||||||
|
'principal_practitioner_id' => 'nullable|integer|exists:thanatopractitioners,id',
|
||||||
|
'assistant_practitioner_ids' => 'nullable|array',
|
||||||
|
'assistant_practitioner_ids.*' => 'integer|exists:thanatopractitioners,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$intervention = $this->interventionRepository->findById($id);
|
||||||
|
|
||||||
|
if (!$intervention) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Intervention non trouvée.'
|
||||||
|
], Response::HTTP_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync practitioners with their roles
|
||||||
|
$practitioners = [];
|
||||||
|
|
||||||
|
if (isset($validated['principal_practitioner_id'])) {
|
||||||
|
$practitioners[$validated['principal_practitioner_id']] = ['role' => 'principal'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($validated['assistant_practitioner_ids'])) {
|
||||||
|
foreach ($validated['assistant_practitioner_ids'] as $assistantId) {
|
||||||
|
$practitioners[$assistantId] = ['role' => 'assistant'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync the practitioners (this will replace existing assignments)
|
||||||
|
$intervention->practitioners()->sync($practitioners);
|
||||||
|
|
||||||
|
// Reload the intervention with relationships
|
||||||
|
$intervention = $this->interventionRepository->findById($id);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => new InterventionResource($intervention),
|
||||||
|
'message' => 'Praticien(s) assigné(s) avec succès.'
|
||||||
|
], Response::HTTP_OK);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error assigning practitioner to intervention: ' . $e->getMessage(), [
|
||||||
|
'intervention_id' => $id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de l\'assignation du praticien.',
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -258,6 +258,41 @@ class ThanatopractitionerController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search thanatopractitioners by employee name.
|
||||||
|
*/
|
||||||
|
public function searchByEmployeeName(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$query = $request->get('query', '');
|
||||||
|
|
||||||
|
if (strlen($query) < 2) {
|
||||||
|
return response()->json([
|
||||||
|
'data' => [],
|
||||||
|
'message' => 'Veuillez entrer au moins 2 caractères pour la recherche.',
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
$thanatopractitioners = $this->thanatopractitionerRepository->searchByEmployeeName($query);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => new ThanatopractitionerCollection($thanatopractitioners),
|
||||||
|
'message' => 'Recherche effectuée avec succès.',
|
||||||
|
], 200);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error searching thanatopractitioners by employee name: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
'query' => $request->get('query'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la recherche.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the specified thanatopractitioner.
|
* Update the specified thanatopractitioner.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -45,7 +45,11 @@ class StoreInterventionRequest extends FormRequest
|
|||||||
'termine',
|
'termine',
|
||||||
'annule'
|
'annule'
|
||||||
])],
|
])],
|
||||||
'assigned_practitioner_id' => ['nullable', 'exists:thanatopractitioners,id'],
|
'practitioners' => ['nullable', 'array'],
|
||||||
|
'practitioners.*' => ['exists:thanatopractitioners,id'],
|
||||||
|
'principal_practitioner_id' => ['nullable', 'exists:thanatopractitioners,id'],
|
||||||
|
'assistant_practitioner_ids' => ['nullable', 'array'],
|
||||||
|
'assistant_practitioner_ids.*' => ['exists:thanatopractitioners,id'],
|
||||||
'notes' => ['nullable', 'string'],
|
'notes' => ['nullable', 'string'],
|
||||||
'created_by' => ['nullable', 'exists:users,id']
|
'created_by' => ['nullable', 'exists:users,id']
|
||||||
];
|
];
|
||||||
@ -68,7 +72,11 @@ class StoreInterventionRequest extends FormRequest
|
|||||||
'duration_min.integer' => 'La durée doit être un nombre entier.',
|
'duration_min.integer' => 'La durée doit être un nombre entier.',
|
||||||
'duration_min.min' => 'La durée ne peut pas être négative.',
|
'duration_min.min' => 'La durée ne peut pas être négative.',
|
||||||
'status.in' => 'Le statut de l\'intervention est invalide.',
|
'status.in' => 'Le statut de l\'intervention est invalide.',
|
||||||
'assigned_practitioner_id.exists' => 'Le praticien sélectionné est invalide.',
|
'practitioners.array' => 'Les praticiens doivent être un tableau.',
|
||||||
|
'practitioners.*.exists' => 'Un des praticiens sélectionnés est invalide.',
|
||||||
|
'principal_practitioner_id.exists' => 'Le praticien principal sélectionné est invalide.',
|
||||||
|
'assistant_practitioner_ids.array' => 'Les praticiens assistants doivent être un tableau.',
|
||||||
|
'assistant_practitioner_ids.*.exists' => 'Un des praticiens assistants est invalide.',
|
||||||
'created_by.exists' => 'L\'utilisateur créateur est invalide.'
|
'created_by.exists' => 'L\'utilisateur créateur est invalide.'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,7 +83,11 @@ class StoreInterventionWithAllDataRequest extends FormRequest
|
|||||||
'termine',
|
'termine',
|
||||||
'annule'
|
'annule'
|
||||||
])],
|
])],
|
||||||
'intervention.assigned_practitioner_id' => ['nullable', 'exists:thanatopractitioners,id'],
|
'intervention.practitioners' => ['nullable', 'array'],
|
||||||
|
'intervention.practitioners.*' => ['exists:thanatopractitioners,id'],
|
||||||
|
'intervention.principal_practitioner_id' => ['nullable', 'exists:thanatopractitioners,id'],
|
||||||
|
'intervention.assistant_practitioner_ids' => ['nullable', 'array'],
|
||||||
|
'intervention.assistant_practitioner_ids.*' => ['exists:thanatopractitioners,id'],
|
||||||
'intervention.order_giver' => ['nullable', 'string', 'max:255'],
|
'intervention.order_giver' => ['nullable', 'string', 'max:255'],
|
||||||
'intervention.notes' => ['nullable', 'string'],
|
'intervention.notes' => ['nullable', 'string'],
|
||||||
'intervention.created_by' => ['nullable', 'exists:users,id']
|
'intervention.created_by' => ['nullable', 'exists:users,id']
|
||||||
@ -131,7 +135,11 @@ class StoreInterventionWithAllDataRequest extends FormRequest
|
|||||||
'intervention.duration_min.integer' => 'La durée doit être un nombre entier.',
|
'intervention.duration_min.integer' => 'La durée doit être un nombre entier.',
|
||||||
'intervention.duration_min.min' => 'La durée ne peut pas être négative.',
|
'intervention.duration_min.min' => 'La durée ne peut pas être négative.',
|
||||||
'intervention.status.in' => 'Le statut de l\'intervention est invalide.',
|
'intervention.status.in' => 'Le statut de l\'intervention est invalide.',
|
||||||
'intervention.assigned_practitioner_id.exists' => 'Le praticien sélectionné est invalide.',
|
'intervention.practitioners.array' => 'Les praticiens doivent être un tableau.',
|
||||||
|
'intervention.practitioners.*.exists' => 'Un des praticiens sélectionnés est invalide.',
|
||||||
|
'intervention.principal_practitioner_id.exists' => 'Le praticien principal sélectionné est invalide.',
|
||||||
|
'intervention.assistant_practitioner_ids.array' => 'Les praticiens assistants doivent être un tableau.',
|
||||||
|
'intervention.assistant_practitioner_ids.*.exists' => 'Un des praticiens assistants est invalide.',
|
||||||
'intervention.order_giver.max' => 'Le donneur d\'ordre ne peut pas dépasser 255 caractères.',
|
'intervention.order_giver.max' => 'Le donneur d\'ordre ne peut pas dépasser 255 caractères.',
|
||||||
'intervention.created_by.exists' => 'L\'utilisateur créateur est invalide.'
|
'intervention.created_by.exists' => 'L\'utilisateur créateur est invalide.'
|
||||||
];
|
];
|
||||||
|
|||||||
@ -45,7 +45,11 @@ class UpdateInterventionRequest extends FormRequest
|
|||||||
'termine',
|
'termine',
|
||||||
'annule'
|
'annule'
|
||||||
])],
|
])],
|
||||||
'assigned_practitioner_id' => ['nullable', 'exists:thanatopractitioners,id'],
|
'practitioners' => ['nullable', 'array'],
|
||||||
|
'practitioners.*' => ['exists:thanatopractitioners,id'],
|
||||||
|
'principal_practitioner_id' => ['nullable', 'exists:thanatopractitioners,id'],
|
||||||
|
'assistant_practitioner_ids' => ['nullable', 'array'],
|
||||||
|
'assistant_practitioner_ids.*' => ['exists:thanatopractitioners,id'],
|
||||||
'notes' => ['nullable', 'string'],
|
'notes' => ['nullable', 'string'],
|
||||||
'created_by' => ['nullable', 'exists:users,id']
|
'created_by' => ['nullable', 'exists:users,id']
|
||||||
];
|
];
|
||||||
@ -68,7 +72,11 @@ class UpdateInterventionRequest extends FormRequest
|
|||||||
'duration_min.integer' => 'La durée doit être un nombre entier.',
|
'duration_min.integer' => 'La durée doit être un nombre entier.',
|
||||||
'duration_min.min' => 'La durée ne peut pas être négative.',
|
'duration_min.min' => 'La durée ne peut pas être négative.',
|
||||||
'status.in' => 'Le statut de l\'intervention est invalide.',
|
'status.in' => 'Le statut de l\'intervention est invalide.',
|
||||||
'assigned_practitioner_id.exists' => 'Le praticien sélectionné est invalide.',
|
'practitioners.array' => 'Les praticiens doivent être un tableau.',
|
||||||
|
'practitioners.*.exists' => 'Un des praticiens sélectionnés est invalide.',
|
||||||
|
'principal_practitioner_id.exists' => 'Le praticien principal sélectionné est invalide.',
|
||||||
|
'assistant_practitioner_ids.array' => 'Les praticiens assistants doivent être un tableau.',
|
||||||
|
'assistant_practitioner_ids.*.exists' => 'Un des praticiens assistants est invalide.',
|
||||||
'created_by.exists' => 'L\'utilisateur créateur est invalide.'
|
'created_by.exists' => 'L\'utilisateur créateur est invalide.'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,8 +36,12 @@ class InterventionResource extends JsonResource
|
|||||||
'scheduled_at' => $this->scheduled_at ? $this->scheduled_at->format('Y-m-d H:i:s') : null,
|
'scheduled_at' => $this->scheduled_at ? $this->scheduled_at->format('Y-m-d H:i:s') : null,
|
||||||
'duration_min' => $this->duration_min,
|
'duration_min' => $this->duration_min,
|
||||||
'status' => $this->status,
|
'status' => $this->status,
|
||||||
'assigned_practitioner' => $this->whenLoaded('assignedPractitioner', function () {
|
'practitioners' => $this->whenLoaded('practitioners', function () {
|
||||||
return new ThanatopractitionerResource($this->assignedPractitioner);
|
return ThanatopractitionerResource::collection($this->practitioners);
|
||||||
|
}),
|
||||||
|
'principal_practitioner' => $this->whenLoaded('practitioners', function () {
|
||||||
|
$principal = $this->practitioners->where('pivot.role', 'principal')->first();
|
||||||
|
return $principal ? new ThanatopractitionerResource($principal) : null;
|
||||||
}),
|
}),
|
||||||
'attachments_count' => $this->attachments_count,
|
'attachments_count' => $this->attachments_count,
|
||||||
'notes' => $this->notes,
|
'notes' => $this->notes,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ namespace App\Models;
|
|||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
class Intervention extends Model
|
class Intervention extends Model
|
||||||
@ -25,7 +26,6 @@ class Intervention extends Model
|
|||||||
'scheduled_at',
|
'scheduled_at',
|
||||||
'duration_min',
|
'duration_min',
|
||||||
'status',
|
'status',
|
||||||
'assigned_practitioner_id',
|
|
||||||
'attachments_count',
|
'attachments_count',
|
||||||
'notes',
|
'notes',
|
||||||
'created_by'
|
'created_by'
|
||||||
@ -66,11 +66,43 @@ class Intervention extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the practitioner assigned to the intervention.
|
* Get the practitioners assigned to the intervention.
|
||||||
*/
|
*/
|
||||||
public function assignedPractitioner(): BelongsTo
|
public function practitioners(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Thanatopractitioner::class, 'assigned_practitioner_id');
|
return $this->belongsToMany(Thanatopractitioner::class, 'intervention_practitioner', 'intervention_id', 'practitioner_id')
|
||||||
|
->withPivot('role', 'assigned_at')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias for practitioners relationship (for backward compatibility).
|
||||||
|
*/
|
||||||
|
public function assignedPractitioner(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->practitioners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the principal practitioner assigned to the intervention.
|
||||||
|
*/
|
||||||
|
public function principalPractitioner(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Thanatopractitioner::class, 'intervention_practitioner', 'intervention_id', 'practitioner_id')
|
||||||
|
->wherePivot('role', 'principal')
|
||||||
|
->withPivot('role', 'assigned_at')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the assistant practitioners assigned to the intervention.
|
||||||
|
*/
|
||||||
|
public function assistantPractitioners(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Thanatopractitioner::class, 'intervention_practitioner', 'intervention_id', 'practitioner_id')
|
||||||
|
->wherePivot('role', 'assistant')
|
||||||
|
->withPivot('role', 'assigned_at')
|
||||||
|
->withTimestamps();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -5,6 +5,7 @@ namespace App\Models;
|
|||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
class Thanatopractitioner extends Model
|
class Thanatopractitioner extends Model
|
||||||
@ -55,6 +56,38 @@ class Thanatopractitioner extends Model
|
|||||||
return $this->hasMany(PractitionerDocument::class, 'practitioner_id');
|
return $this->hasMany(PractitionerDocument::class, 'practitioner_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the interventions assigned to the thanatopractitioner.
|
||||||
|
*/
|
||||||
|
public function interventions(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Intervention::class, 'intervention_practitioner')
|
||||||
|
->withPivot('role', 'assigned_at')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the interventions where this practitioner is the principal.
|
||||||
|
*/
|
||||||
|
public function principalInterventions(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Intervention::class, 'intervention_practitioner')
|
||||||
|
->wherePivot('role', 'principal')
|
||||||
|
->withPivot('role', 'assigned_at')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the interventions where this practitioner is an assistant.
|
||||||
|
*/
|
||||||
|
public function assistantInterventions(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Intervention::class, 'intervention_practitioner')
|
||||||
|
->wherePivot('role', 'assistant')
|
||||||
|
->withPivot('role', 'assigned_at')
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scope a query to only include practitioners with valid authorization.
|
* Scope a query to only include practitioners with valid authorization.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -73,7 +73,7 @@ class InterventionRepository implements InterventionRepositoryInterface
|
|||||||
'client',
|
'client',
|
||||||
'deceased',
|
'deceased',
|
||||||
'location',
|
'location',
|
||||||
'assignedPractitioner',
|
'practitioners',
|
||||||
'attachments',
|
'attachments',
|
||||||
'notifications'
|
'notifications'
|
||||||
])->findOrFail($id);
|
])->findOrFail($id);
|
||||||
|
|||||||
@ -132,4 +132,20 @@ class ThanatopractitionerRepository extends BaseRepository implements Thanatopra
|
|||||||
'with_documents' => $this->model->newQuery()->has('documents')->count(),
|
'with_documents' => $this->model->newQuery()->has('documents')->count(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search thanatopractitioners by employee name.
|
||||||
|
*/
|
||||||
|
public function searchByEmployeeName(string $query): Collection
|
||||||
|
{
|
||||||
|
return $this->model->newQuery()
|
||||||
|
->with(['employee'])
|
||||||
|
->whereHas('employee', function ($q) use ($query) {
|
||||||
|
$q->where('first_name', 'LIKE', "%{$query}%")
|
||||||
|
->orWhere('last_name', 'LIKE', "%{$query}%")
|
||||||
|
->orWhereRaw("CONCAT(first_name, ' ', last_name) LIKE ?", ["%{$query}%"]);
|
||||||
|
})
|
||||||
|
->limit(10)
|
||||||
|
->get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -70,5 +70,14 @@ interface ThanatopractitionerRepositoryInterface
|
|||||||
*
|
*
|
||||||
* @return array<string, int>
|
* @return array<string, int>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search thanatopractitioners by employee name.
|
||||||
|
*
|
||||||
|
* @param string $query
|
||||||
|
* @return Collection<int, Thanatopractitioner>
|
||||||
|
*/
|
||||||
|
public function searchByEmployeeName(string $query): Collection;
|
||||||
|
|
||||||
public function getStatistics(): array;
|
public function getStatistics(): array;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ return [
|
|||||||
|
|
||||||
// IMPORTANT: Do NOT use '*' when sending credentials. List explicit origins.
|
// IMPORTANT: Do NOT use '*' when sending credentials. List explicit origins.
|
||||||
// Set FRONTEND_URL in .env to override the default if needed.
|
// Set FRONTEND_URL in .env to override the default if needed.
|
||||||
'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:8080', 'http://localhost:8081')],
|
'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:8081')],
|
||||||
|
|
||||||
// Alternatively, use patterns (kept empty for clarity)
|
// Alternatively, use patterns (kept empty for clarity)
|
||||||
'allowed_origins_patterns' => [],
|
'allowed_origins_patterns' => [],
|
||||||
|
|||||||
@ -0,0 +1,38 @@
|
|||||||
|
<?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_practitioner', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('intervention_id')->constrained('interventions')->onDelete('cascade');
|
||||||
|
$table->foreignId('practitioner_id')->constrained('thanatopractitioners')->onDelete('cascade');
|
||||||
|
$table->enum('role', ['principal', 'assistant'])->default('principal');
|
||||||
|
$table->timestamp('assigned_at')->useCurrent();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Unique constraint to prevent duplicate assignments
|
||||||
|
$table->unique(['intervention_id', 'practitioner_id']);
|
||||||
|
|
||||||
|
// Indexes for better query performance
|
||||||
|
$table->index('practitioner_id', 'idx_intervention_practitioner_practitioner');
|
||||||
|
$table->index('intervention_id', 'idx_intervention_practitioner_intervention');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('intervention_practitioner');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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
|
||||||
|
{
|
||||||
|
// First, let's migrate existing data from assigned_practitioner_id to the new pivot table
|
||||||
|
Schema::table('interventions', function (Blueprint $table) {
|
||||||
|
// Add temporary columns to handle data migration
|
||||||
|
$table->dropForeign(['assigned_practitioner_id']);
|
||||||
|
$table->dropColumn('assigned_practitioner_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// This migration is not easily reversible since we'd lose the many-to-many relationship
|
||||||
|
// In a real scenario, you'd want to handle this differently
|
||||||
|
Schema::table('interventions', function (Blueprint $table) {
|
||||||
|
$table->foreignId('assigned_practitioner_id')->nullable()->constrained('thanatopractitioners')->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -87,6 +87,7 @@ Route::middleware('auth:sanctum')->group(function () {
|
|||||||
Route::apiResource('employees', EmployeeController::class);
|
Route::apiResource('employees', EmployeeController::class);
|
||||||
|
|
||||||
// Thanatopractitioner management
|
// Thanatopractitioner management
|
||||||
|
Route::get('/thanatopractitioners/search', [ThanatopractitionerController::class, 'searchByEmployeeName']);
|
||||||
Route::apiResource('thanatopractitioners', ThanatopractitionerController::class);
|
Route::apiResource('thanatopractitioners', ThanatopractitionerController::class);
|
||||||
Route::get('employees/{employeeId}/thanatopractitioners', [ThanatopractitionerController::class, 'getByEmployee']);
|
Route::get('employees/{employeeId}/thanatopractitioners', [ThanatopractitionerController::class, 'getByEmployee']);
|
||||||
Route::get('/thanatopractitioners/{id}/documents', [PractitionerDocumentController::class, 'getByThanatopractitioner']);
|
Route::get('/thanatopractitioners/{id}/documents', [PractitionerDocumentController::class, 'getByThanatopractitioner']);
|
||||||
@ -130,6 +131,7 @@ Route::middleware('auth:sanctum')->group(function () {
|
|||||||
Route::put('/{intervention}', [InterventionController::class, 'update']);
|
Route::put('/{intervention}', [InterventionController::class, 'update']);
|
||||||
Route::delete('/{intervention}', [InterventionController::class, 'destroy']);
|
Route::delete('/{intervention}', [InterventionController::class, 'destroy']);
|
||||||
Route::patch('/{intervention}/status', [InterventionController::class, 'changeStatus']);
|
Route::patch('/{intervention}/status', [InterventionController::class, 'changeStatus']);
|
||||||
|
Route::patch('/{intervention}/assign', [InterventionController::class, 'assignPractitioner']);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -41,6 +41,7 @@
|
|||||||
@change-tab="activeTab = $event"
|
@change-tab="activeTab = $event"
|
||||||
@update-intervention="handleUpdateIntervention"
|
@update-intervention="handleUpdateIntervention"
|
||||||
@cancel="handleCancel"
|
@cancel="handleCancel"
|
||||||
|
@assign-practitioner="handleAssignPractitioner"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -119,16 +120,19 @@ const mappedIntervention = computed(() => {
|
|||||||
"Non disponible"
|
"Non disponible"
|
||||||
: "Non disponible",
|
: "Non disponible",
|
||||||
prestationsSupplementaires: "À définir",
|
prestationsSupplementaires: "À définir",
|
||||||
members: props.intervention.practitioner
|
members:
|
||||||
? [
|
props.intervention.practitioners &&
|
||||||
{
|
props.intervention.practitioners.length > 0
|
||||||
name: `${props.intervention.practitioner.first_name || ""} ${
|
? props.intervention.practitioners.map((p) => ({
|
||||||
props.intervention.practitioner.last_name || ""
|
name: p.employee
|
||||||
}`.trim(),
|
? `${p.employee.first_name || ""} ${
|
||||||
|
p.employee.last_name || ""
|
||||||
|
}`.trim()
|
||||||
|
: `${p.first_name || ""} ${p.last_name || ""}`.trim(),
|
||||||
image: "/images/avatar-default.png",
|
image: "/images/avatar-default.png",
|
||||||
},
|
role: p.pivot?.role || "assistant",
|
||||||
]
|
}))
|
||||||
: [],
|
: [],
|
||||||
|
|
||||||
// Map status from API string to expected object format
|
// Map status from API string to expected object format
|
||||||
status: props.intervention.status
|
status: props.intervention.status
|
||||||
|
|||||||
@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Modal -->
|
||||||
|
<div
|
||||||
|
class="modal fade"
|
||||||
|
:class="{ show: show, 'd-block': show }"
|
||||||
|
:style="{ display: show ? 'block' : 'none' }"
|
||||||
|
tabindex="-1"
|
||||||
|
role="dialog"
|
||||||
|
aria-labelledby="addPractitionerModalLabel"
|
||||||
|
:aria-hidden="!show"
|
||||||
|
>
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="addPractitionerModalLabel">
|
||||||
|
<i class="fas fa-user-plus me-2"></i>
|
||||||
|
Ajouter un praticien
|
||||||
|
</h5>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
@click="handleClose"
|
||||||
|
aria-label="Close"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<PractitionerSearchInput
|
||||||
|
:loading="loading"
|
||||||
|
:results="searchResults"
|
||||||
|
@search="handleSearch"
|
||||||
|
@select="handleSelect"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm bg-gradient-secondary"
|
||||||
|
@click="handleClose"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Backdrop -->
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
|
class="modal-backdrop fade"
|
||||||
|
:class="{ show: show }"
|
||||||
|
@click="handleClose"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from 'vue';
|
||||||
|
import PractitionerSearchInput from '@/components/molecules/thanatopractitioner/PractitionerSearchInput.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
searchResults: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['close', 'search', 'select']);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (query) => {
|
||||||
|
emit('search', query);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = (practitioner) => {
|
||||||
|
emit('select', practitioner);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1040;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop.show {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -239,26 +239,30 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="intervention.members && intervention.members.length > 0"
|
v-if="
|
||||||
class="row"
|
intervention.practitioners &&
|
||||||
|
intervention.practitioners.length > 0
|
||||||
|
"
|
||||||
|
class="list-group list-group-flush"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(member, index) in intervention.members"
|
v-for="(practitioner, index) in intervention.practitioners"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="col-md-4 mb-3"
|
class="list-group-item d-flex justify-content-between align-items-center border-0 px-0"
|
||||||
>
|
>
|
||||||
<div class="card border-0">
|
<div>
|
||||||
<div class="card-body text-center">
|
<h6 class="text-sm mb-0">
|
||||||
<div class="avatar avatar-xl mb-3">
|
{{
|
||||||
<img
|
practitioner.employee?.full_name ||
|
||||||
alt="Image placeholder"
|
(practitioner.employee?.first_name &&
|
||||||
:src="member.image || '/images/avatar-default.png'"
|
practitioner.employee?.last_name
|
||||||
class="rounded-circle"
|
? practitioner.employee.first_name +
|
||||||
/>
|
" " +
|
||||||
</div>
|
practitioner.employee.last_name
|
||||||
<h6 class="text-sm">{{ member.name }}</h6>
|
: "Praticien " + (index + 1))
|
||||||
<p class="text-xs text-muted">Praticien</p>
|
}}
|
||||||
</div>
|
</h6>
|
||||||
|
<p class="text-xs text-muted mb-0">Praticien</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -375,7 +379,12 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["change-tab", "update-intervention", "cancel"]);
|
const emit = defineEmits([
|
||||||
|
"change-tab",
|
||||||
|
"update-intervention",
|
||||||
|
"cancel",
|
||||||
|
"assign-practitioner",
|
||||||
|
]);
|
||||||
|
|
||||||
// État local pour l'édition
|
// État local pour l'édition
|
||||||
const editMode = ref(false);
|
const editMode = ref(false);
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Assign Practitioner Button -->
|
<!-- Assign Practitioner Button -->
|
||||||
<div v-if="!practitioners.length" class="mx-3 mb-3">
|
<div class="mx-3 mb-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-outline-primary w-100"
|
class="btn btn-sm btn-outline-primary w-100"
|
||||||
|
|||||||
@ -235,7 +235,7 @@
|
|||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">Gérer l'équipe</h5>
|
<h5 class="modal-title">Ajouter équipe</h5>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn-close"
|
class="btn-close"
|
||||||
|
|||||||
@ -0,0 +1,241 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="modal fade"
|
||||||
|
:class="{ show: isOpen }"
|
||||||
|
:style="{ display: isOpen ? 'block' : 'none' }"
|
||||||
|
tabindex="-1"
|
||||||
|
role="dialog"
|
||||||
|
@click.self="close"
|
||||||
|
>
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Assigner un thanatopracteur</h5>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
@click="close"
|
||||||
|
aria-label="Fermer"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- Search Input -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Rechercher par nom</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Tapez le nom du thanatopracteur..."
|
||||||
|
@input="handleSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div v-if="loading" class="text-center py-3">
|
||||||
|
<div
|
||||||
|
class="spinner-border spinner-border-sm text-primary"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
<span class="ms-2">Recherche en cours...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Results List -->
|
||||||
|
<div v-else-if="searchResults.length > 0" class="list-group">
|
||||||
|
<button
|
||||||
|
v-for="practitioner in searchResults"
|
||||||
|
:key="practitioner.id"
|
||||||
|
type="button"
|
||||||
|
class="list-group-item list-group-item-action d-flex align-items-center"
|
||||||
|
:class="{ active: selectedPractitioner?.id === practitioner.id }"
|
||||||
|
@click="selectPractitioner(practitioner)"
|
||||||
|
>
|
||||||
|
<div class="avatar avatar-sm me-3">
|
||||||
|
<img
|
||||||
|
:src="practitioner.avatar || '/images/avatar-default.png'"
|
||||||
|
alt="Avatar"
|
||||||
|
class="rounded-circle"
|
||||||
|
style="width: 40px; height: 40px; object-fit: cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
{{
|
||||||
|
practitioner.employee.full_name ||
|
||||||
|
`${practitioner.employee.first_name} ${practitioner.employee.last_name}`
|
||||||
|
}}
|
||||||
|
</h6>
|
||||||
|
<small class="text-muted">{{
|
||||||
|
practitioner.employee.email || "Email non disponible"
|
||||||
|
}}</small>
|
||||||
|
</div>
|
||||||
|
<i
|
||||||
|
v-if="selectedPractitioner?.id === practitioner.id"
|
||||||
|
class="fas fa-check text-success"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No Results -->
|
||||||
|
<div
|
||||||
|
v-else-if="searchQuery.length >= 2 && !loading"
|
||||||
|
class="text-center text-muted py-3"
|
||||||
|
>
|
||||||
|
<i class="fas fa-user-slash fa-2x mb-2"></i>
|
||||||
|
<p class="mb-0">Aucun thanatopracteur trouvé</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Initial State -->
|
||||||
|
<div v-else class="text-center text-muted py-3">
|
||||||
|
<i class="fas fa-search fa-2x mb-2"></i>
|
||||||
|
<p class="mb-0">Tapez au moins 2 caractères pour rechercher</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Role Selection -->
|
||||||
|
<div v-if="selectedPractitioner" class="mt-3">
|
||||||
|
<label class="form-label">Rôle dans l'intervention</label>
|
||||||
|
<select v-model="selectedRole" class="form-select">
|
||||||
|
<option value="principal">Principal</option>
|
||||||
|
<option value="assistant">Assistant</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" @click="close">
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary"
|
||||||
|
:disabled="!selectedPractitioner"
|
||||||
|
@click="confirmAssignment"
|
||||||
|
>
|
||||||
|
<i class="fas fa-user-plus me-2"></i>Assigner
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<div v-if="isOpen" class="modal-backdrop fade show"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
import { useThanatopractitionerStore } from "@/stores/thanatopractitionerStore";
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["close", "assign"]);
|
||||||
|
|
||||||
|
const thanatopractitionerStore = useThanatopractitionerStore();
|
||||||
|
|
||||||
|
const searchQuery = ref("");
|
||||||
|
const searchResults = ref([]);
|
||||||
|
const selectedPractitioner = ref(null);
|
||||||
|
const selectedRole = ref("principal");
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
let searchTimeout = null;
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
if (searchTimeout) {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchQuery.value.length < 2) {
|
||||||
|
searchResults.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchTimeout = setTimeout(async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await thanatopractitionerStore.searchThanatopractitioners(
|
||||||
|
searchQuery.value
|
||||||
|
);
|
||||||
|
// The service returns the data directly, not wrapped in response.data
|
||||||
|
searchResults.value = Array.isArray(response) ? response : [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error searching practitioners:", error);
|
||||||
|
searchResults.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectPractitioner = (practitioner) => {
|
||||||
|
selectedPractitioner.value = practitioner;
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmAssignment = () => {
|
||||||
|
if (selectedPractitioner.value) {
|
||||||
|
emit("assign", {
|
||||||
|
practitionerId: selectedPractitioner.value.id,
|
||||||
|
role: selectedRole.value,
|
||||||
|
});
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
resetForm();
|
||||||
|
emit("close");
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
searchQuery.value = "";
|
||||||
|
searchResults.value = [];
|
||||||
|
selectedPractitioner.value = null;
|
||||||
|
selectedRole.value = "principal";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset form when modal closes
|
||||||
|
watch(
|
||||||
|
() => props.isOpen,
|
||||||
|
(newValue) => {
|
||||||
|
if (!newValue) {
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal.show {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item.active {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
border-color: #2196f3;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item.active:hover {
|
||||||
|
background-color: #bbdefb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<div class="practitioner-search-input">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Rechercher un praticien..."
|
||||||
|
v-model="searchQuery"
|
||||||
|
@input="handleInput"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
:disabled="loading"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-primary"
|
||||||
|
type="button"
|
||||||
|
@click="handleSearch"
|
||||||
|
:disabled="loading || !searchQuery.trim()"
|
||||||
|
>
|
||||||
|
<i v-if="!loading" class="fas fa-search"></i>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="spinner-border spinner-border-sm"
|
||||||
|
role="status"
|
||||||
|
aria-hidden="true"
|
||||||
|
></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Results Dropdown -->
|
||||||
|
<div
|
||||||
|
v-if="results && results.length > 0"
|
||||||
|
class="search-results-dropdown card"
|
||||||
|
>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
<button
|
||||||
|
v-for="practitioner in results"
|
||||||
|
:key="practitioner.id"
|
||||||
|
type="button"
|
||||||
|
class="list-group-item list-group-item-action"
|
||||||
|
@click="handleSelect(practitioner)"
|
||||||
|
>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="avatar avatar-sm me-3">
|
||||||
|
<img
|
||||||
|
:src="practitioner.image || '/images/avatar-default.png'"
|
||||||
|
:alt="practitioner.full_name"
|
||||||
|
class="rounded-circle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="mb-0 text-sm">{{ practitioner.full_name }}</h6>
|
||||||
|
<p class="text-xs text-muted mb-0">
|
||||||
|
{{ practitioner.job_title || 'Praticien' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No Results Message -->
|
||||||
|
<div
|
||||||
|
v-else-if="results && results.length === 0 && searchQuery.trim()"
|
||||||
|
class="alert alert-info text-sm mb-0"
|
||||||
|
>
|
||||||
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
|
Aucun praticien trouvé pour "{{ searchQuery }}"
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, defineProps, defineEmits } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
results: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['search', 'select']);
|
||||||
|
|
||||||
|
const searchQuery = ref('');
|
||||||
|
|
||||||
|
const handleInput = () => {
|
||||||
|
// Optional: Implement debounce here if needed
|
||||||
|
// For now, we'll just search on button click or Enter
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
if (searchQuery.value.trim()) {
|
||||||
|
emit('search', searchQuery.value.trim());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = (practitioner) => {
|
||||||
|
emit('select', practitioner);
|
||||||
|
searchQuery.value = ''; // Clear search after selection
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.practitioner-search-input {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -10,14 +10,14 @@ export interface Intervention {
|
|||||||
scheduled_at?: string;
|
scheduled_at?: string;
|
||||||
duration_min?: number;
|
duration_min?: number;
|
||||||
status?: string;
|
status?: string;
|
||||||
assigned_practitioner_id?: number;
|
|
||||||
attachments_count?: number;
|
attachments_count?: number;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
created_by?: number;
|
created_by?: number;
|
||||||
// Relations
|
// Relations
|
||||||
client?: any;
|
client?: any;
|
||||||
deceased?: any;
|
deceased?: any;
|
||||||
practitioner?: any;
|
practitioners?: any[];
|
||||||
|
principal_practitioner?: any;
|
||||||
location?: any;
|
location?: any;
|
||||||
// Timestamps
|
// Timestamps
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
@ -47,7 +47,9 @@ export interface CreateInterventionPayload {
|
|||||||
scheduled_at?: string;
|
scheduled_at?: string;
|
||||||
duration_min?: number;
|
duration_min?: number;
|
||||||
status?: string;
|
status?: string;
|
||||||
assigned_practitioner_id?: number;
|
practitioners?: number[];
|
||||||
|
principal_practitioner_id?: number;
|
||||||
|
assistant_practitioner_ids?: number[];
|
||||||
notes?: string;
|
notes?: string;
|
||||||
created_by?: number;
|
created_by?: number;
|
||||||
}
|
}
|
||||||
@ -233,16 +235,54 @@ export const InterventionService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assign practitioner to intervention
|
* Assign practitioner(s) to intervention
|
||||||
*/
|
*/
|
||||||
async assignPractitioner(
|
async assignPractitioner(
|
||||||
id: number,
|
id: number,
|
||||||
practitionerId: number
|
practitionerData: {
|
||||||
|
practitioners?: number[];
|
||||||
|
principal_practitioner_id?: number;
|
||||||
|
assistant_practitioner_ids?: number[];
|
||||||
|
}
|
||||||
): Promise<Intervention> {
|
): Promise<Intervention> {
|
||||||
const response = await request<Intervention>({
|
const response = await request<Intervention>({
|
||||||
url: `/api/interventions/${id}/assign`,
|
url: `/api/interventions/${id}/assign`,
|
||||||
method: "patch",
|
method: "patch",
|
||||||
data: { assigned_practitioner_id: practitionerId },
|
data: practitionerData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign multiple practitioners to intervention
|
||||||
|
*/
|
||||||
|
async assignPractitioners(
|
||||||
|
id: number,
|
||||||
|
practitionerIds: number[],
|
||||||
|
principalPractitionerId?: number
|
||||||
|
): Promise<Intervention> {
|
||||||
|
return this.assignPractitioner(id, {
|
||||||
|
practitioners: practitionerIds,
|
||||||
|
principal_practitioner_id: principalPractitionerId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update practitioners for intervention (replace all existing)
|
||||||
|
*/
|
||||||
|
async updatePractitioners(
|
||||||
|
id: number,
|
||||||
|
practitionerData: {
|
||||||
|
practitioners?: number[];
|
||||||
|
principal_practitioner_id?: number;
|
||||||
|
assistant_practitioner_ids?: number[];
|
||||||
|
}
|
||||||
|
): Promise<Intervention> {
|
||||||
|
const response = await request<Intervention>({
|
||||||
|
url: `/api/interventions/${id}/practitioners`,
|
||||||
|
method: "patch",
|
||||||
|
data: practitionerData,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
@ -263,10 +263,10 @@ export const ThanatopractitionerService = {
|
|||||||
data: Thanatopractitioner[];
|
data: Thanatopractitioner[];
|
||||||
};
|
};
|
||||||
}>({
|
}>({
|
||||||
url: "/api/thanatopractitioners",
|
url: "/api/thanatopractitioners/search",
|
||||||
method: "get",
|
method: "get",
|
||||||
params: {
|
params: {
|
||||||
search: query,
|
query: query,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
|
|||||||
@ -444,9 +444,16 @@ export const useInterventionStore = defineStore("intervention", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assign practitioner to intervention
|
* Assign practitioner(s) to intervention
|
||||||
*/
|
*/
|
||||||
const assignPractitioner = async (id: number, practitionerId: number) => {
|
const assignPractitioner = async (
|
||||||
|
id: number,
|
||||||
|
practitionerData: {
|
||||||
|
practitioners?: number[];
|
||||||
|
principal_practitioner_id?: number;
|
||||||
|
assistant_practitioner_ids?: number[];
|
||||||
|
}
|
||||||
|
) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setSuccess(false);
|
setSuccess(false);
|
||||||
@ -454,7 +461,7 @@ export const useInterventionStore = defineStore("intervention", () => {
|
|||||||
try {
|
try {
|
||||||
const intervention = await InterventionService.assignPractitioner(
|
const intervention = await InterventionService.assignPractitioner(
|
||||||
id,
|
id,
|
||||||
practitionerId
|
practitionerData
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update in the interventions list
|
// Update in the interventions list
|
||||||
@ -479,7 +486,72 @@ export const useInterventionStore = defineStore("intervention", () => {
|
|||||||
const errorMessage =
|
const errorMessage =
|
||||||
err.response?.data?.message ||
|
err.response?.data?.message ||
|
||||||
err.message ||
|
err.message ||
|
||||||
"Échec de l'assignation du praticien";
|
"Échec de l'assignation des praticiens";
|
||||||
|
setError(errorMessage);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign multiple practitioners to intervention
|
||||||
|
*/
|
||||||
|
const assignPractitioners = async (
|
||||||
|
id: number,
|
||||||
|
practitionerIds: number[],
|
||||||
|
principalPractitionerId?: number
|
||||||
|
) => {
|
||||||
|
return assignPractitioner(id, {
|
||||||
|
practitioners: practitionerIds,
|
||||||
|
principal_practitioner_id: principalPractitionerId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update practitioners for intervention (replace all existing)
|
||||||
|
*/
|
||||||
|
const updatePractitioners = async (
|
||||||
|
id: number,
|
||||||
|
practitionerData: {
|
||||||
|
practitioners?: number[];
|
||||||
|
principal_practitioner_id?: number;
|
||||||
|
assistant_practitioner_ids?: number[];
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setSuccess(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const intervention = await InterventionService.updatePractitioners(
|
||||||
|
id,
|
||||||
|
practitionerData
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 des praticiens";
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
@ -577,6 +649,8 @@ export const useInterventionStore = defineStore("intervention", () => {
|
|||||||
searchInterventions,
|
searchInterventions,
|
||||||
updateInterventionStatus,
|
updateInterventionStatus,
|
||||||
assignPractitioner,
|
assignPractitioner,
|
||||||
|
assignPractitioners,
|
||||||
|
updatePractitioners,
|
||||||
fetchInterventionsByMonth,
|
fetchInterventionsByMonth,
|
||||||
resetState,
|
resetState,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,7 +8,14 @@
|
|||||||
:practitioners="practitioners"
|
:practitioners="practitioners"
|
||||||
@update-intervention="handleUpdate"
|
@update-intervention="handleUpdate"
|
||||||
@cancel="handleCancel"
|
@cancel="handleCancel"
|
||||||
@assign-practitioner="handleAssignPractitioner"
|
@assign-practitioner="openAssignModal"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Assign Practitioner Modal -->
|
||||||
|
<AssignPractitionerModal
|
||||||
|
:is-open="isModalOpen"
|
||||||
|
@close="closeAssignModal"
|
||||||
|
@assign="handleAssignPractitioner"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -16,6 +23,7 @@
|
|||||||
import { ref, onMounted, watch } from "vue";
|
import { ref, onMounted, watch } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import InterventionDetailPresentation from "@/components/Organism/Interventions/InterventionDetailPresentation.vue";
|
import InterventionDetailPresentation from "@/components/Organism/Interventions/InterventionDetailPresentation.vue";
|
||||||
|
import AssignPractitionerModal from "@/components/molecules/intervention/AssignPractitionerModal.vue";
|
||||||
import { useInterventionStore } from "@/stores/interventionStore";
|
import { useInterventionStore } from "@/stores/interventionStore";
|
||||||
import { useNotificationStore } from "@/stores/notification";
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
|
|
||||||
@ -27,6 +35,7 @@ const notificationStore = useNotificationStore();
|
|||||||
const intervention = ref(null);
|
const intervention = ref(null);
|
||||||
const activeTab = ref("overview");
|
const activeTab = ref("overview");
|
||||||
const practitioners = ref([]);
|
const practitioners = ref([]);
|
||||||
|
const isModalOpen = ref(false);
|
||||||
|
|
||||||
// Fetch intervention data
|
// Fetch intervention data
|
||||||
const fetchIntervention = async () => {
|
const fetchIntervention = async () => {
|
||||||
@ -47,18 +56,37 @@ const fetchIntervention = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Open assign modal
|
||||||
|
const openAssignModal = () => {
|
||||||
|
isModalOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close assign modal
|
||||||
|
const closeAssignModal = () => {
|
||||||
|
isModalOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
// Handle practitioner assignment
|
// Handle practitioner assignment
|
||||||
const handleAssignPractitioner = async (practitionerData) => {
|
const handleAssignPractitioner = async (practitionerData) => {
|
||||||
try {
|
try {
|
||||||
if (intervention.value?.id) {
|
if (intervention.value?.id) {
|
||||||
|
// Build the assignment payload based on role
|
||||||
|
const payload = {};
|
||||||
|
if (practitionerData.role === "principal") {
|
||||||
|
payload.principal_practitioner_id = practitionerData.practitionerId;
|
||||||
|
} else {
|
||||||
|
payload.assistant_practitioner_ids = [practitionerData.practitionerId];
|
||||||
|
}
|
||||||
|
|
||||||
await interventionStore.assignPractitioner(
|
await interventionStore.assignPractitioner(
|
||||||
intervention.value.id,
|
intervention.value.id,
|
||||||
practitionerData.practitionerId
|
payload
|
||||||
);
|
);
|
||||||
|
|
||||||
// Refresh intervention data to get updated practitioner info
|
// Refresh intervention data to get updated practitioner info
|
||||||
await fetchIntervention();
|
await fetchIntervention();
|
||||||
notificationStore.created("Praticien assigné");
|
notificationStore.created("Praticien assigné");
|
||||||
|
closeAssignModal();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error assigning practitioner:", error);
|
console.error("Error assigning practitioner:", error);
|
||||||
|
|||||||
@ -30,23 +30,21 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<SoftInput
|
<SoftInput
|
||||||
id="email"
|
id="email"
|
||||||
:value="email"
|
v-model="email"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
name="email"
|
name="email"
|
||||||
:is-required="true"
|
:is-required="true"
|
||||||
@input="email = $event.target.value"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<SoftInput
|
<SoftInput
|
||||||
id="password"
|
id="password"
|
||||||
:value="password"
|
v-model="password"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Mot de passe"
|
placeholder="Mot de passe"
|
||||||
:is-required="true"
|
:is-required="true"
|
||||||
@input="password = $event.target.value"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SoftSwitch
|
<SoftSwitch
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user