intervention
This commit is contained in:
parent
7570f46658
commit
69fbe1a7a1
@ -143,4 +143,41 @@ class DeceasedController extends Controller
|
|||||||
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Search deceased by name or other criteria.
|
||||||
|
*/
|
||||||
|
public function searchBy(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$search = $request->get('search', '');
|
||||||
|
|
||||||
|
if (empty($search)) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Le paramètre "search" est requis.',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$deceased = $this->deceasedRepository->searchByName($search);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => $deceased,
|
||||||
|
'count' => $deceased->count(),
|
||||||
|
'message' => $deceased->count() > 0
|
||||||
|
? 'Défunts trouvés avec succès.'
|
||||||
|
: 'Aucun défunt trouvé.',
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error searching deceased by name: ' . $e->getMessage(), [
|
||||||
|
'exception' => $e,
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
'search_term' => $search ?? '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la recherche des défunts.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -99,4 +99,21 @@ class DeceasedRepository implements DeceasedRepositoryInterface
|
|||||||
return $deceased->delete();
|
return $deceased->delete();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Search deceased by name
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
* @return Collection
|
||||||
|
*/
|
||||||
|
public function searchByName(string $name): Collection
|
||||||
|
{
|
||||||
|
return Deceased::where(function($query) use ($name) {
|
||||||
|
$query->where('last_name', 'LIKE', "%{$name}%")
|
||||||
|
->orWhere('first_name', 'LIKE', "%{$name}%")
|
||||||
|
->orWhere(DB::raw("CONCAT(last_name, ' ', first_name)"), 'LIKE', "%{$name}%");
|
||||||
|
})
|
||||||
|
->orderBy('last_name', 'asc')
|
||||||
|
->orderBy('first_name', 'asc')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,4 +49,12 @@ interface DeceasedRepositoryInterface
|
|||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function delete(Deceased $deceased): bool;
|
public function delete(Deceased $deceased): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search deceased by name
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
* @return Collection
|
||||||
|
*/
|
||||||
|
public function searchByName(string $name): Collection;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -98,6 +98,7 @@ Route::middleware('auth:sanctum')->group(function () {
|
|||||||
|
|
||||||
// Deceased Routes
|
// Deceased Routes
|
||||||
Route::prefix('deceased')->group(function () {
|
Route::prefix('deceased')->group(function () {
|
||||||
|
Route::get('/searchBy', [DeceasedController::class, 'searchBy']);
|
||||||
Route::get('/', [DeceasedController::class, 'index']);
|
Route::get('/', [DeceasedController::class, 'index']);
|
||||||
Route::post('/', [DeceasedController::class, 'store']);
|
Route::post('/', [DeceasedController::class, 'store']);
|
||||||
Route::get('/{deceased}', [DeceasedController::class, 'show']);
|
Route::get('/{deceased}', [DeceasedController::class, 'show']);
|
||||||
|
|||||||
@ -9,6 +9,10 @@
|
|||||||
:deceased-loading="deceasedLoading"
|
:deceased-loading="deceasedLoading"
|
||||||
:client-list="clientList"
|
:client-list="clientList"
|
||||||
:client-loading="clientLoading"
|
:client-loading="clientLoading"
|
||||||
|
:search-clients="searchClients"
|
||||||
|
:on-client-select="onClientSelect"
|
||||||
|
:search-deceased="searchDeceased"
|
||||||
|
:on-deceased-select="onDeceasedSelect"
|
||||||
@create-intervention="handleCreateIntervention"
|
@create-intervention="handleCreateIntervention"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@ -48,6 +52,22 @@ defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
searchClients: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
onClientSelect: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
searchDeceased: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
onDeceasedSelect: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["createIntervention"]);
|
const emit = defineEmits(["createIntervention"]);
|
||||||
|
|||||||
@ -0,0 +1,202 @@
|
|||||||
|
<template>
|
||||||
|
<intervention-detail-template>
|
||||||
|
<template #button-return>
|
||||||
|
<div class="col-12">
|
||||||
|
<router-link
|
||||||
|
to="/interventions"
|
||||||
|
class="btn btn-outline-secondary btn-sm mb-3"
|
||||||
|
>
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>Retour aux interventions
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #loading-state>
|
||||||
|
<div v-if="loading" class="text-center p-5">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #intervention-detail-sidebar>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<InterventionDetailSidebar
|
||||||
|
:intervention="mappedIntervention"
|
||||||
|
:active-tab="activeTab"
|
||||||
|
:practitioners="practitioners"
|
||||||
|
@change-tab="activeTab = $event"
|
||||||
|
@assign-practitioner="handleAssignPractitioner"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #intervention-detail-content>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<InterventionDetailContent
|
||||||
|
:active-tab="activeTab"
|
||||||
|
:intervention="mappedIntervention"
|
||||||
|
:loading="loading"
|
||||||
|
:error="error"
|
||||||
|
@change-tab="activeTab = $event"
|
||||||
|
@update-intervention="handleUpdateIntervention"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</intervention-detail-template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits, ref, computed } from "vue";
|
||||||
|
import InterventionDetailTemplate from "@/components/templates/Interventions/InterventionDetailTemplate.vue";
|
||||||
|
import InterventionDetailSidebar from "./intervention/InterventionDetailSidebar.vue";
|
||||||
|
import InterventionDetailContent from "./intervention/InterventionDetailContent.vue";
|
||||||
|
import { RouterLink } from "vue-router";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
intervention: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
activeTab: {
|
||||||
|
type: String,
|
||||||
|
default: "overview",
|
||||||
|
},
|
||||||
|
practitioners: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
"update-intervention",
|
||||||
|
"cancel",
|
||||||
|
"assign-practitioner",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const localActiveTab = ref(props.activeTab);
|
||||||
|
|
||||||
|
// Map API data to expected format
|
||||||
|
const mappedIntervention = computed(() => {
|
||||||
|
if (!props.intervention) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...props.intervention,
|
||||||
|
// Map API fields to component expected fields
|
||||||
|
defuntName: props.intervention.deceased
|
||||||
|
? `${props.intervention.deceased.last_name || ""} ${
|
||||||
|
props.intervention.deceased.first_name || ""
|
||||||
|
}`.trim()
|
||||||
|
: `Personne ${props.intervention.deceased_id || "inconnue"}`,
|
||||||
|
date: props.intervention.scheduled_at
|
||||||
|
? new Date(props.intervention.scheduled_at).toLocaleString("fr-FR")
|
||||||
|
: "Non définie",
|
||||||
|
lieux: props.intervention.location
|
||||||
|
? props.intervention.location.name || "Lieu non défini"
|
||||||
|
: "Lieu non défini",
|
||||||
|
duree: props.intervention.duration_min
|
||||||
|
? `${props.intervention.duration_min} minutes`
|
||||||
|
: "Non définie",
|
||||||
|
title: props.intervention.type
|
||||||
|
? getInterventionTypeLabel(props.intervention.type)
|
||||||
|
: "Type non défini",
|
||||||
|
contactFamilial: props.intervention.order_giver || "Non renseigné",
|
||||||
|
description: props.intervention.notes || "Aucune description disponible",
|
||||||
|
nombrePersonnes: props.intervention.attachments_count || 0,
|
||||||
|
coordonneesContact: props.intervention.client
|
||||||
|
? props.intervention.client.email ||
|
||||||
|
props.intervention.client.phone ||
|
||||||
|
"Non disponible"
|
||||||
|
: "Non disponible",
|
||||||
|
prestationsSupplementaires: "À définir",
|
||||||
|
members: props.intervention.practitioner
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: `${props.intervention.practitioner.first_name || ""} ${
|
||||||
|
props.intervention.practitioner.last_name || ""
|
||||||
|
}`.trim(),
|
||||||
|
image: "/images/avatar-default.png",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
|
||||||
|
// Map status from API string to expected object format
|
||||||
|
status: props.intervention.status
|
||||||
|
? {
|
||||||
|
label: getStatusLabel(props.intervention.status),
|
||||||
|
color: getStatusColor(props.intervention.status),
|
||||||
|
variant: "fill",
|
||||||
|
size: "md",
|
||||||
|
}
|
||||||
|
: { label: "En attente", color: "warning", variant: "fill", size: "md" },
|
||||||
|
|
||||||
|
// Map action (add if missing)
|
||||||
|
action: {
|
||||||
|
color: "primary",
|
||||||
|
label: "Modifier",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
const getStatusLabel = (status) => {
|
||||||
|
const statusLabels = {
|
||||||
|
demande: "Demande",
|
||||||
|
planifie: "Planifié",
|
||||||
|
en_cours: "En cours",
|
||||||
|
termine: "Terminé",
|
||||||
|
annule: "Annulé",
|
||||||
|
};
|
||||||
|
return statusLabels[status] || status;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
const statusColors = {
|
||||||
|
demande: "warning",
|
||||||
|
planifie: "info",
|
||||||
|
en_cours: "primary",
|
||||||
|
termine: "success",
|
||||||
|
annule: "danger",
|
||||||
|
};
|
||||||
|
return statusColors[status] || "secondary";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInterventionTypeLabel = (type) => {
|
||||||
|
const typeLabels = {
|
||||||
|
thanatopraxie: "Thanatopraxie",
|
||||||
|
toilette_mortuaire: "Toilette mortuaire",
|
||||||
|
exhumation: "Exhumation",
|
||||||
|
retrait_pacemaker: "Retrait pacemaker",
|
||||||
|
retrait_bijoux: "Retrait bijoux",
|
||||||
|
autre: "Autre",
|
||||||
|
};
|
||||||
|
return typeLabels[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateIntervention = (updateData) => {
|
||||||
|
emit("update-intervention", updateData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
emit("cancel");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAssignPractitioner = (practitionerData) => {
|
||||||
|
emit("assign-practitioner", practitionerData);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch for prop changes to sync local state
|
||||||
|
const updateLocalActiveTab = () => {
|
||||||
|
localActiveTab.value = props.activeTab;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -0,0 +1,425 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div v-if="loading" class="text-center py-5">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-3 text-muted">Chargement des détails de l'intervention...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error State -->
|
||||||
|
<div v-else-if="error" class="text-center py-5">
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm bg-gradient-secondary"
|
||||||
|
@click="$emit('cancel')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>Retour
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Content -->
|
||||||
|
<template v-else-if="intervention">
|
||||||
|
<!-- Overview Tab -->
|
||||||
|
<div v-if="activeTab === 'overview'" class="tab-pane fade show active">
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="mb-3">Informations Générales</h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="info-horizontal">
|
||||||
|
<div class="icon-sm text-center">
|
||||||
|
<i class="fas fa-user text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div class="description">
|
||||||
|
<p class="text-xs mb-0">
|
||||||
|
<b>Nom du défunt:</b> {{ intervention.defuntName }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="info-horizontal">
|
||||||
|
<div class="icon-sm text-center">
|
||||||
|
<i class="fas fa-calendar text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div class="description">
|
||||||
|
<p class="text-xs mb-0">
|
||||||
|
<b>Date:</b> {{ intervention.date }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="info-horizontal">
|
||||||
|
<div class="icon-sm text-center">
|
||||||
|
<i class="fas fa-map-marker text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div class="description">
|
||||||
|
<p class="text-xs mb-0">
|
||||||
|
<b>Lieu:</b> {{ intervention.lieux }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="info-horizontal">
|
||||||
|
<div class="icon-sm text-center">
|
||||||
|
<i class="fas fa-clock text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div class="description">
|
||||||
|
<p class="text-xs mb-0">
|
||||||
|
<b>Durée:</b> {{ intervention.duree }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="mb-3">Contact et Communication</h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="info-horizontal">
|
||||||
|
<div class="icon-sm text-center">
|
||||||
|
<i class="fas fa-phone text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div class="description">
|
||||||
|
<p class="text-xs mb-0">
|
||||||
|
<b>Contact:</b> {{ intervention.contactFamilial }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="info-horizontal">
|
||||||
|
<div class="icon-sm text-center">
|
||||||
|
<i class="fas fa-envelope text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div class="description">
|
||||||
|
<p class="text-xs mb-0">
|
||||||
|
<b>Coordonnées:</b> {{ intervention.coordonneesContact }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="mb-3">Description</h6>
|
||||||
|
<p class="text-sm">{{ intervention.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Details Tab -->
|
||||||
|
<div v-if="activeTab === 'details'" class="tab-pane fade show active">
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
|
<h6 class="mb-0">Détails Complets</h6>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm bg-gradient-secondary"
|
||||||
|
@click="toggleEditMode"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
{{ editMode ? "Sauvegarder" : "Modifier" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<SoftInput
|
||||||
|
label="Nom du défunt"
|
||||||
|
v-model="localIntervention.defuntName"
|
||||||
|
:disabled="!editMode"
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
|
<SoftInput
|
||||||
|
label="Date de l'intervention"
|
||||||
|
type="datetime-local"
|
||||||
|
v-model="localIntervention.date"
|
||||||
|
:disabled="!editMode"
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
|
<SoftInput
|
||||||
|
label="Lieu"
|
||||||
|
v-model="localIntervention.lieux"
|
||||||
|
:disabled="!editMode"
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<SoftInput
|
||||||
|
label="Durée prévue"
|
||||||
|
v-model="localIntervention.duree"
|
||||||
|
:disabled="!editMode"
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
|
<SoftInput
|
||||||
|
label="Type de cérémonie"
|
||||||
|
v-model="localIntervention.title"
|
||||||
|
:disabled="!editMode"
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
|
<SoftInput
|
||||||
|
label="Contact familial"
|
||||||
|
v-model="localIntervention.contactFamilial"
|
||||||
|
:disabled="!editMode"
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="mb-3">Informations Suplementaires</h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<SoftInput
|
||||||
|
label="Nombre de personnes attendues"
|
||||||
|
type="number"
|
||||||
|
v-model="localIntervention.nombrePersonnes"
|
||||||
|
:disabled="!editMode"
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<SoftInput
|
||||||
|
label="Prestations supplémentaires"
|
||||||
|
type="textarea"
|
||||||
|
rows="3"
|
||||||
|
v-model="localIntervention.prestationsSupplementaires"
|
||||||
|
:disabled="!editMode"
|
||||||
|
class="mb-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="d-flex justify-content-end mt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm bg-gradient-secondary me-2"
|
||||||
|
@click="resetChanges"
|
||||||
|
:disabled="!hasChanges || loading"
|
||||||
|
>
|
||||||
|
Réinitialiser
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm bg-gradient-primary"
|
||||||
|
@click="saveChanges"
|
||||||
|
:disabled="!hasChanges || loading"
|
||||||
|
>
|
||||||
|
Sauvegarder
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Team Tab -->
|
||||||
|
<div v-if="activeTab === 'team'" class="tab-pane fade show active">
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
|
<h6 class="mb-0">Équipe Assignée</h6>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm bg-gradient-info"
|
||||||
|
@click="$emit('assign-practitioner')"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
Gérer l'équipe
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="intervention.members && intervention.members.length > 0"
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(member, index) in intervention.members"
|
||||||
|
:key="index"
|
||||||
|
class="col-md-4 mb-3"
|
||||||
|
>
|
||||||
|
<div class="card border-0">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="avatar avatar-xl mb-3">
|
||||||
|
<img
|
||||||
|
alt="Image placeholder"
|
||||||
|
:src="member.image || '/images/avatar-default.png'"
|
||||||
|
class="rounded-circle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h6 class="text-sm">{{ member.name }}</h6>
|
||||||
|
<p class="text-xs text-muted">Praticien</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="text-center py-4">
|
||||||
|
<div class="avatar avatar-xl mb-3">
|
||||||
|
<div
|
||||||
|
class="avatar-title bg-gradient-secondary text-white h5 mb-0"
|
||||||
|
>
|
||||||
|
<i class="fas fa-user-plus"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h6 class="text-sm text-muted">Aucun praticien assigné</h6>
|
||||||
|
<p class="text-xs text-muted">
|
||||||
|
Cliquez sur "Gérer l'équipe" pour assigner des praticiens
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Documents Tab -->
|
||||||
|
<div v-if="activeTab === 'documents'" class="tab-pane fade show active">
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<div class="avatar avatar-xl mb-3">
|
||||||
|
<div class="avatar-title bg-gradient-info text-white h5 mb-0">
|
||||||
|
<i class="fas fa-file-alt"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h6 class="text-sm text-muted">Documents</h6>
|
||||||
|
<p class="text-xs text-muted">
|
||||||
|
Interface de gestion des documents à implémenter...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- History Tab -->
|
||||||
|
<div v-if="activeTab === 'history'" class="tab-pane fade show active">
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<div class="avatar avatar-xl mb-3">
|
||||||
|
<div class="avatar-title bg-gradient-warning text-white h5 mb-0">
|
||||||
|
<i class="fas fa-history"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h6 class="text-sm text-muted">Historique</h6>
|
||||||
|
<p class="text-xs text-muted">
|
||||||
|
Interface d'historique des modifications à implémenter...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation Actions -->
|
||||||
|
<hr class="horizontal dark" />
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm bg-gradient-danger"
|
||||||
|
@click="$emit('cancel')"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm bg-gradient-secondary me-2"
|
||||||
|
@click="resetChanges"
|
||||||
|
:disabled="!hasChanges || loading"
|
||||||
|
>
|
||||||
|
Réinitialiser
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="`bg-gradient-${intervention.action.color}`"
|
||||||
|
@click="saveChanges"
|
||||||
|
:disabled="!hasChanges || loading"
|
||||||
|
>
|
||||||
|
{{ intervention.action.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- No data state -->
|
||||||
|
<div v-else class="text-center py-5">
|
||||||
|
<p class="text-muted">Aucune donnée d'intervention disponible</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from "vue";
|
||||||
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
activeTab: {
|
||||||
|
type: String,
|
||||||
|
default: "overview",
|
||||||
|
},
|
||||||
|
intervention: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["change-tab", "update-intervention", "cancel"]);
|
||||||
|
|
||||||
|
// État local pour l'édition
|
||||||
|
const editMode = ref(false);
|
||||||
|
const localIntervention = ref({});
|
||||||
|
|
||||||
|
// Computed pour détecter les changements
|
||||||
|
const hasChanges = computed(() => {
|
||||||
|
if (!props.intervention || !localIntervention.value) return false;
|
||||||
|
return (
|
||||||
|
JSON.stringify(localIntervention.value) !==
|
||||||
|
JSON.stringify(props.intervention)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Méthodes
|
||||||
|
const toggleEditMode = () => {
|
||||||
|
if (editMode.value && hasChanges.value) {
|
||||||
|
saveChanges();
|
||||||
|
} else {
|
||||||
|
editMode.value = !editMode.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveChanges = () => {
|
||||||
|
if (hasChanges.value) {
|
||||||
|
emit("update-intervention", localIntervention.value);
|
||||||
|
}
|
||||||
|
editMode.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetChanges = () => {
|
||||||
|
if (props.intervention) {
|
||||||
|
localIntervention.value = { ...props.intervention };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch pour mettre à jour les données locales quand les props changent
|
||||||
|
watch(
|
||||||
|
() => props.intervention,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
localIntervention.value = { ...newVal };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card position-sticky top-1">
|
||||||
|
<!-- Intervention Profile Card -->
|
||||||
|
<InterventionProfileCard :intervention="intervention" />
|
||||||
|
|
||||||
|
<hr class="horizontal dark my-3 mx-3" />
|
||||||
|
|
||||||
|
<!-- Tab Navigation -->
|
||||||
|
<div class="card-body pt-0">
|
||||||
|
<InterventionTabNavigation
|
||||||
|
:active-tab="activeTab"
|
||||||
|
:team-count="practitioners.length"
|
||||||
|
:documents-count="0"
|
||||||
|
@change-tab="changeTab"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Assign Practitioner Button -->
|
||||||
|
<div v-if="!practitioners.length" class="mx-3 mb-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-outline-primary w-100"
|
||||||
|
@click="assignPractitioner"
|
||||||
|
>
|
||||||
|
<i class="fas fa-user-plus me-2"></i>Assigner un praticien
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import InterventionProfileCard from "@/components/molecules/intervention/InterventionProfileCard.vue";
|
||||||
|
import InterventionTabNavigation from "@/components/molecules/intervention/InterventionTabNavigation.vue";
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
intervention: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
activeTab: {
|
||||||
|
type: String,
|
||||||
|
default: "overview",
|
||||||
|
},
|
||||||
|
practitioners: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["change-tab", "assign-practitioner"]);
|
||||||
|
|
||||||
|
const changeTab = (tab) => {
|
||||||
|
emit("change-tab", tab);
|
||||||
|
};
|
||||||
|
|
||||||
|
const assignPractitioner = () => {
|
||||||
|
emit("assign-practitioner");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.position-sticky {
|
||||||
|
top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: 0;
|
||||||
|
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, 0.15);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,71 +0,0 @@
|
|||||||
<template>
|
|
||||||
<intervention-details-template>
|
|
||||||
<template #preceding-action>
|
|
||||||
<div class="col-12">
|
|
||||||
<router-link
|
|
||||||
to="/interventions"
|
|
||||||
class="btn btn-outline-secondary btn-sm mb-3"
|
|
||||||
>
|
|
||||||
<i class="fas fa-arrow-left me-2"></i>Retour aux interventions
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #intervention-details>
|
|
||||||
<intervention-details
|
|
||||||
:intervention="currentIntervention"
|
|
||||||
@update="handleUpdate"
|
|
||||||
@cancel="handleCancel"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</intervention-details-template>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import { ref } from "vue";
|
|
||||||
import { RouterLink } from "vue-router";
|
|
||||||
import InterventionDetailsTemplate from "@/components/templates/Interventions/InterventionDetailsTemplate.vue";
|
|
||||||
import interventionDetails from "@/components/molecules/Interventions/interventionDetails.vue";
|
|
||||||
|
|
||||||
const currentIntervention = ref({
|
|
||||||
id: 1,
|
|
||||||
title: "Cérémonie religieuse catholique",
|
|
||||||
status: {
|
|
||||||
label: "Confirmé",
|
|
||||||
color: "success",
|
|
||||||
variant: "fill",
|
|
||||||
size: "md",
|
|
||||||
},
|
|
||||||
date: "2024-12-15T14:00",
|
|
||||||
defuntName: "Jean Dupont",
|
|
||||||
lieux: "Église Saint-Pierre, 75008 Paris",
|
|
||||||
duree: "1h30",
|
|
||||||
description:
|
|
||||||
"Messe funéraire traditionnelle suivie de la bénédiction du corps. Prévoyez environ 80 personnes.",
|
|
||||||
contactFamilial: "Marie Dupont - 06 12 34 56 78",
|
|
||||||
coordonneesContact: "01 42 34 56 78 - contact@eglise-stpierre.fr",
|
|
||||||
nombrePersonnes: 80,
|
|
||||||
prestationsSupplementaires: "Fleurs, musique d'orgue, livret de cérémonie",
|
|
||||||
action: {
|
|
||||||
label: "Mettre à jour",
|
|
||||||
color: "primary",
|
|
||||||
},
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
image: "/images/pretre.jpg",
|
|
||||||
name: "Père Martin",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
image: "/images/organiste.jpg",
|
|
||||||
name: "Claire Organiste",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleUpdate = (updatedIntervention) => {
|
|
||||||
console.log("Intervention mise à jour:", updatedIntervention);
|
|
||||||
currentIntervention.value = updatedIntervention;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
console.log("Édition annulée");
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@ -59,7 +59,8 @@ import { useRouter } from "vue-router";
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
|
id: { type: [Number, String], required: false, default: null },
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
@ -108,6 +109,10 @@ defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const goToDetail = () => {
|
const goToDetail = () => {
|
||||||
router.push({ name: "Intervention details" });
|
if (props.id) {
|
||||||
|
router.push({ name: "Intervention details", params: { id: props.id } });
|
||||||
|
} else {
|
||||||
|
console.warn("Cannot navigate to details: intervention ID is missing");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
304
thanasoft-front/src/components/atoms/input/ModalSearch.vue
Normal file
304
thanasoft-front/src/components/atoms/input/ModalSearch.vue
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
<template>
|
||||||
|
<div class="modal-search-component">
|
||||||
|
<!-- Search Interface - Always Visible -->
|
||||||
|
<div class="search-interface">
|
||||||
|
<!-- Search Input -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">{{ searchLabel || "Rechercher" }}</label>
|
||||||
|
<div class="position-relative">
|
||||||
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:placeholder="searchPlaceholder || 'Entrez votre recherche...'"
|
||||||
|
@input="handleSearch"
|
||||||
|
@focus="showResults = true"
|
||||||
|
/>
|
||||||
|
<div v-if="isLoading" class="position-absolute end-0 top-0 mt-2 me-3">
|
||||||
|
<div
|
||||||
|
class="spinner-border spinner-border-sm text-primary"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
<span class="visually-hidden">Recherche...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Results -->
|
||||||
|
<div v-if="showResults && results.length > 0" class="results-container">
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
<div
|
||||||
|
v-for="item in results"
|
||||||
|
:key="getItemKey(item)"
|
||||||
|
class="list-group-item list-group-item-action"
|
||||||
|
@click="selectItem(item)"
|
||||||
|
>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold">{{ getItemLabel(item) }}</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ getItemDescription(item) }}
|
||||||
|
</small>
|
||||||
|
<div v-if="getItemMeta(item)" class="text-xs text-info">
|
||||||
|
{{ getItemMeta(item) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-outline-success"
|
||||||
|
@click.stop="confirmSelection(item)"
|
||||||
|
>
|
||||||
|
Sélectionner
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No Results -->
|
||||||
|
<div
|
||||||
|
v-if="showResults && results.length === 0 && searchQuery"
|
||||||
|
class="text-center text-muted py-3"
|
||||||
|
>
|
||||||
|
<i class="fas fa-search mb-2"></i>
|
||||||
|
<p class="mb-0">Aucun résultat trouvé</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selected Item Display -->
|
||||||
|
<div v-if="selectedItem" class="selected-display mt-3">
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<strong>{{ getItemLabel(selectedItem) }}</strong>
|
||||||
|
<div class="text-sm text-muted">
|
||||||
|
{{ getItemDescription(selectedItem) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-success">Sélectionné</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close btn-close-sm ms-2"
|
||||||
|
@click="clearSelection"
|
||||||
|
aria-label="Clear selection"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// Search function that will be called with {query}
|
||||||
|
searchAction: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
// Currently selected item
|
||||||
|
selectedItem: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
// Modal config
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: "Recherche",
|
||||||
|
},
|
||||||
|
searchLabel: {
|
||||||
|
type: String,
|
||||||
|
default: "Rechercher",
|
||||||
|
},
|
||||||
|
searchPlaceholder: {
|
||||||
|
type: String,
|
||||||
|
default: "Entrez votre recherche...",
|
||||||
|
},
|
||||||
|
triggerText: {
|
||||||
|
type: String,
|
||||||
|
default: "Rechercher",
|
||||||
|
},
|
||||||
|
triggerColor: {
|
||||||
|
type: String,
|
||||||
|
default: "primary",
|
||||||
|
},
|
||||||
|
// Item mapping
|
||||||
|
itemKey: {
|
||||||
|
type: String,
|
||||||
|
default: "id",
|
||||||
|
},
|
||||||
|
itemLabel: {
|
||||||
|
type: [String, Function],
|
||||||
|
default: "name",
|
||||||
|
},
|
||||||
|
itemDescription: {
|
||||||
|
type: [String, Function],
|
||||||
|
default: (item) => item.description || "",
|
||||||
|
},
|
||||||
|
itemMeta: {
|
||||||
|
type: [String, Function],
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
// Search settings
|
||||||
|
debounceDelay: {
|
||||||
|
type: Number,
|
||||||
|
default: 300,
|
||||||
|
},
|
||||||
|
minSearchLength: {
|
||||||
|
type: Number,
|
||||||
|
default: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["select", "confirm"]);
|
||||||
|
|
||||||
|
// Component state
|
||||||
|
const searchQuery = ref("");
|
||||||
|
const results = ref([]);
|
||||||
|
const showResults = ref(false);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const selectedItem = ref(props.selectedItem);
|
||||||
|
|
||||||
|
// Search timeout
|
||||||
|
let searchTimeout = null;
|
||||||
|
|
||||||
|
// Item extraction helpers
|
||||||
|
const getItemKey = (item) => {
|
||||||
|
return typeof props.itemKey === "function"
|
||||||
|
? props.itemKey(item)
|
||||||
|
: item[props.itemKey];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getItemLabel = (item) => {
|
||||||
|
return typeof props.itemLabel === "function"
|
||||||
|
? props.itemLabel(item)
|
||||||
|
: item[props.itemLabel];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getItemDescription = (item) => {
|
||||||
|
return typeof props.itemDescription === "function"
|
||||||
|
? props.itemDescription(item)
|
||||||
|
: item[props.itemDescription];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getItemMeta = (item) => {
|
||||||
|
return typeof props.itemMeta === "function"
|
||||||
|
? props.itemMeta(item)
|
||||||
|
: props.itemMeta
|
||||||
|
? item[props.itemMeta]
|
||||||
|
: "";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Search handling
|
||||||
|
const handleSearch = () => {
|
||||||
|
// Clear previous timeout
|
||||||
|
if (searchTimeout) {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new timeout for debouncing
|
||||||
|
searchTimeout = setTimeout(async () => {
|
||||||
|
if (searchQuery.value.length >= props.minSearchLength) {
|
||||||
|
await performSearch();
|
||||||
|
} else {
|
||||||
|
results.value = [];
|
||||||
|
showResults.value = false;
|
||||||
|
}
|
||||||
|
}, props.debounceDelay);
|
||||||
|
};
|
||||||
|
|
||||||
|
const performSearch = async () => {
|
||||||
|
if (!props.searchAction || !searchQuery.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
showResults.value = true;
|
||||||
|
const searchResults = await props.searchAction({
|
||||||
|
query: searchQuery.value,
|
||||||
|
});
|
||||||
|
results.value = searchResults || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Search error:", error);
|
||||||
|
results.value = [];
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Item selection
|
||||||
|
const selectItem = (item) => {
|
||||||
|
selectedItem.value = item;
|
||||||
|
emit("select", item);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmSelection = (item) => {
|
||||||
|
selectedItem.value = item;
|
||||||
|
emit("confirm", item);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearSelection = () => {
|
||||||
|
selectedItem.value = null;
|
||||||
|
searchQuery.value = "";
|
||||||
|
results.value = [];
|
||||||
|
showResults.value = false;
|
||||||
|
emit("select", null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch for external selected item changes
|
||||||
|
watch(
|
||||||
|
() => props.selectedItem,
|
||||||
|
(newValue) => {
|
||||||
|
selectedItem.value = newValue;
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal-search-component {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-interface {
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-container {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-display {
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item {
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #f1f3f4;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close-sm {
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
215
thanasoft-front/src/components/atoms/input/SearchInput.vue
Normal file
215
thanasoft-front/src/components/atoms/input/SearchInput.vue
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
<template>
|
||||||
|
<div class="search-container">
|
||||||
|
<!-- Search Input -->
|
||||||
|
<SoftInput
|
||||||
|
v-model="searchQuery"
|
||||||
|
placeholder="Search..."
|
||||||
|
@input="handleSearch"
|
||||||
|
@focus="showResults = true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Search Results Dropdown -->
|
||||||
|
<div
|
||||||
|
v-if="showResults && searchResults.length > 0"
|
||||||
|
class="results-dropdown"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="item in searchResults"
|
||||||
|
:key="getItemKey(item)"
|
||||||
|
class="result-item"
|
||||||
|
@click="selectItem(item)"
|
||||||
|
>
|
||||||
|
{{ getItemLabel(item) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No Results Message -->
|
||||||
|
<div
|
||||||
|
v-if="showResults && searchResults.length === 0 && searchQuery"
|
||||||
|
class="no-results"
|
||||||
|
>
|
||||||
|
Pas de résultats trouvés.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, defineProps, defineEmits } from "vue";
|
||||||
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps({
|
||||||
|
// Store action to search data
|
||||||
|
searchAction: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
// Key to use as unique identifier
|
||||||
|
itemKey: {
|
||||||
|
type: String,
|
||||||
|
default: "id",
|
||||||
|
},
|
||||||
|
// Key to display as label
|
||||||
|
itemLabel: {
|
||||||
|
type: [String, Function],
|
||||||
|
default: "name",
|
||||||
|
},
|
||||||
|
// Debounce delay in ms
|
||||||
|
debounceDelay: {
|
||||||
|
type: Number,
|
||||||
|
default: 300,
|
||||||
|
},
|
||||||
|
// Minimum characters before searching
|
||||||
|
minChars: {
|
||||||
|
type: Number,
|
||||||
|
default: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits(["search", "select", "update:modelValue"]);
|
||||||
|
|
||||||
|
// Reactive data
|
||||||
|
const searchQuery = ref("");
|
||||||
|
const searchResults = ref([]);
|
||||||
|
const showResults = ref(false);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
let searchTimeout = null;
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const shouldSearch = computed(() => {
|
||||||
|
return searchQuery.value.length >= props.minChars;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const handleSearch = () => {
|
||||||
|
// Clear previous timeout
|
||||||
|
if (searchTimeout) {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new timeout for debouncing
|
||||||
|
searchTimeout = setTimeout(async () => {
|
||||||
|
if (shouldSearch.value) {
|
||||||
|
await performSearch();
|
||||||
|
} else {
|
||||||
|
searchResults.value = [];
|
||||||
|
}
|
||||||
|
}, props.debounceDelay);
|
||||||
|
};
|
||||||
|
|
||||||
|
const performSearch = async () => {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
emit("search", searchQuery.value);
|
||||||
|
|
||||||
|
// Call the store search action
|
||||||
|
const results = await props.searchAction(searchQuery.value);
|
||||||
|
|
||||||
|
searchResults.value = results || [];
|
||||||
|
|
||||||
|
// Force show results for debugging
|
||||||
|
showResults.value = true;
|
||||||
|
} catch (error) {
|
||||||
|
searchResults.value = [];
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectItem = (item) => {
|
||||||
|
emit("select", item);
|
||||||
|
emit("update:modelValue", item);
|
||||||
|
searchQuery.value = getItemLabel(item);
|
||||||
|
showResults.value = false;
|
||||||
|
searchResults.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getItemKey = (item) => {
|
||||||
|
return item[props.itemKey];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getItemLabel = (item) => {
|
||||||
|
if (typeof props.itemLabel === "function") {
|
||||||
|
const result = props.itemLabel(item);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = item[props.itemLabel];
|
||||||
|
console.log("SearchInput: Property label result:", result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close results when clicking outside
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
const searchContainer = document.querySelector(".search-container");
|
||||||
|
if (searchContainer && !searchContainer.contains(event.target)) {
|
||||||
|
showResults.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
import { onMounted, onUnmounted } from "vue";
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener("click", handleClickOutside);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener("click", handleClickOutside);
|
||||||
|
if (searchTimeout) {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1000;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item:hover {
|
||||||
|
background-color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -27,6 +27,57 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Intervention Add Modal -->
|
||||||
|
<div v-if="showModal" class="modal-overlay" @click="closeInterventionModal">
|
||||||
|
<div class="modal-container" @click.stop>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Créer une intervention</h5>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
@click="closeInterventionModal"
|
||||||
|
aria-label="Close"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<InterventationAddModal
|
||||||
|
ref="interventionModalRef"
|
||||||
|
:search-clients="handleSearchClients"
|
||||||
|
:search-deceased="handleSearchDeceased"
|
||||||
|
:on-client-select="handleClientSelect"
|
||||||
|
:on-deceased-select="handleDeceasedSelect"
|
||||||
|
:selected-deceased="selectedDefunt"
|
||||||
|
:loading="interventionStore.isLoading"
|
||||||
|
@create-intervention="handleCreateIntervention"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
@click="closeInterventionModal"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary"
|
||||||
|
:disabled="interventionStore.isLoading"
|
||||||
|
@click="createIntervention"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="interventionStore.isLoading"
|
||||||
|
class="spinner-border spinner-border-sm me-2"
|
||||||
|
role="status"
|
||||||
|
></span>
|
||||||
|
{{
|
||||||
|
interventionStore.isLoading ? "Création..." : "Créer l'intervention"
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import DefuntCard from "@/components/atoms/Defunts/DefuntCard.vue";
|
import DefuntCard from "@/components/atoms/Defunts/DefuntCard.vue";
|
||||||
@ -34,10 +85,26 @@ import { ref } from "vue";
|
|||||||
import { defineProps } from "vue";
|
import { defineProps } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import SoftButton from "@/components/SoftButton.vue";
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
import InterventationAddModal from "@/components/molecules/Interventions/InterventationAddModal.vue";
|
||||||
|
import { useInterventionStore } from "@/stores/interventionStore";
|
||||||
|
import { useDeceasedStore } from "@/stores/deceasedStore";
|
||||||
|
import { useClientStore } from "@/stores/clientStore";
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
|
||||||
// Router
|
// Router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Stores
|
||||||
|
const interventionStore = useInterventionStore();
|
||||||
|
const deceasedStore = useDeceasedStore();
|
||||||
|
const clientStore = useClientStore();
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
const validationErrors = ref({});
|
||||||
|
const showSuccess = ref(false);
|
||||||
|
|
||||||
// Options du dropdown
|
// Options du dropdown
|
||||||
const dropdownOptions = ref([
|
const dropdownOptions = ref([
|
||||||
{ label: "Voir les détails", action: "view", route: "#" },
|
{ label: "Voir les détails", action: "view", route: "#" },
|
||||||
@ -50,6 +117,66 @@ const dropdownOptions = ref([
|
|||||||
{ label: "Supprimer", action: "delete", route: "#" },
|
{ label: "Supprimer", action: "delete", route: "#" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Modal state
|
||||||
|
const showModal = ref(false);
|
||||||
|
const selectedDefunt = ref(null);
|
||||||
|
const interventionModalRef = ref(null);
|
||||||
|
|
||||||
|
// Store functions
|
||||||
|
const handleSearchClients = async (query) => {
|
||||||
|
return await clientStore.searchClients(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClientSelect = (client) => {
|
||||||
|
return client;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchDeceased = async (query) => {
|
||||||
|
return await deceasedStore.searchDeceased(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeceasedSelect = (deceased) => {
|
||||||
|
return deceased;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Close modal after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
closeInterventionModal();
|
||||||
|
}, 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
defunts: {
|
defunts: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@ -57,6 +184,25 @@ defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Modal management functions
|
||||||
|
const openInterventionModal = (defunt) => {
|
||||||
|
selectedDefunt.value = defunt;
|
||||||
|
showModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeInterventionModal = () => {
|
||||||
|
showModal.value = false;
|
||||||
|
selectedDefunt.value = null;
|
||||||
|
validationErrors.value = {};
|
||||||
|
showSuccess.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createIntervention = () => {
|
||||||
|
if (interventionModalRef.value) {
|
||||||
|
interventionModalRef.value.submitForm();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Gestion des actions du dropdown
|
// Gestion des actions du dropdown
|
||||||
const handleAction = (action, defunt) => {
|
const handleAction = (action, defunt) => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@ -69,8 +215,7 @@ const handleAction = (action, defunt) => {
|
|||||||
router.push({ name: "Defunt details", params: { id: defunt.id } });
|
router.push({ name: "Defunt details", params: { id: defunt.id } });
|
||||||
break;
|
break;
|
||||||
case "create_intervention":
|
case "create_intervention":
|
||||||
console.log("Créer une intervention pour:", defunt);
|
openInterventionModal(defunt);
|
||||||
// Ouvrir wizard de création d'intervention
|
|
||||||
break;
|
break;
|
||||||
case "delete":
|
case "delete":
|
||||||
console.log("Supprimer le défunt:", defunt);
|
console.log("Supprimer le défunt:", defunt);
|
||||||
@ -88,6 +233,22 @@ const handleAction = (action, defunt) => {
|
|||||||
const addDeceased = () => {
|
const addDeceased = () => {
|
||||||
router.push({ name: "Add Defunts" });
|
router.push({ name: "Add Defunts" });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Load data on mount
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -105,4 +266,63 @@ const addDeceased = () => {
|
|||||||
.pagination {
|
.pagination {
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Custom Modal Styles */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
max-width: 800px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||||
|
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -0,0 +1,428 @@
|
|||||||
|
<template>
|
||||||
|
<div class="modal-intervention-form">
|
||||||
|
<!-- Client Selection -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">
|
||||||
|
Client <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
<modal-search
|
||||||
|
:search-action="searchClients"
|
||||||
|
:selected-item="selectedClient"
|
||||||
|
title="Rechercher un client"
|
||||||
|
search-label="Nom du client"
|
||||||
|
search-placeholder="Entrez le nom du client..."
|
||||||
|
item-key="id"
|
||||||
|
item-label="name"
|
||||||
|
:item-description="getClientDescription"
|
||||||
|
:item-meta="getClientMeta"
|
||||||
|
@select="handleClientSelect"
|
||||||
|
@confirm="handleClientConfirm"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.client_id" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.client_id }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selected Client Display -->
|
||||||
|
<div v-if="selectedClient" class="selected-display mb-3">
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<strong>{{ selectedClient.name }}</strong>
|
||||||
|
<div class="text-sm text-muted">
|
||||||
|
{{ getClientDescription(selectedClient) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="badge bg-success">Sélectionné</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Deceased Selection -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Personne décédée</label>
|
||||||
|
|
||||||
|
<!-- Selected Deceased Display -->
|
||||||
|
<div v-if="selectedDeceased" class="selected-display mb-2">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<strong>{{ getDeceasedFullName(selectedDeceased) }}</strong>
|
||||||
|
<div class="text-sm text-muted">
|
||||||
|
{{
|
||||||
|
selectedDeceased.birth_date
|
||||||
|
? `Né(e) le ${formatDate(selectedDeceased.birth_date)}`
|
||||||
|
: "Date de naissance inconnue"
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="badge bg-info">Pré-sélectionné</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search for deceased if needed -->
|
||||||
|
<div v-if="!selectedDeceased">
|
||||||
|
<modal-search
|
||||||
|
:search-action="searchDeceased"
|
||||||
|
:selected-item="selectedDeceased"
|
||||||
|
title="Rechercher une personne décédée"
|
||||||
|
search-label="Nom de la personne"
|
||||||
|
search-placeholder="Entrez le nom de la personne..."
|
||||||
|
item-key="id"
|
||||||
|
:item-label="getDeceasedFullName"
|
||||||
|
:item-description="getDeceasedDescription"
|
||||||
|
:item-meta="getDeceasedMeta"
|
||||||
|
@select="handleDeceasedSelect"
|
||||||
|
@confirm="handleDeceasedConfirm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Intervention Type -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">
|
||||||
|
Type d'intervention <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
v-model="form.type"
|
||||||
|
class="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>
|
||||||
|
|
||||||
|
<!-- Date and Time -->
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Date de l'intervention</label>
|
||||||
|
<input
|
||||||
|
v-model="form.scheduled_date"
|
||||||
|
type="date"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.scheduled_at }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Heure de l'intervention</label>
|
||||||
|
<input
|
||||||
|
v-model="form.scheduled_time"
|
||||||
|
type="time"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.scheduled_at }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="fieldErrors.scheduled_at" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.scheduled_at }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Duration and Status -->
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Durée (minutes)</label>
|
||||||
|
<input
|
||||||
|
v-model="form.duration_min"
|
||||||
|
type="number"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="ex. 90"
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Statut</label>
|
||||||
|
<select v-model="form.status" class="form-select">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Order Giver -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Donneur d'ordre</label>
|
||||||
|
<input
|
||||||
|
v-model="form.order_giver"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Nom du donneur d'ordre"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Notes et observations</label>
|
||||||
|
<textarea
|
||||||
|
v-model="form.notes"
|
||||||
|
class="form-control"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Informations complémentaires, instructions spéciales..."
|
||||||
|
maxlength="2000"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form Validation Errors -->
|
||||||
|
<div v-if="formValidationError" class="alert alert-warning">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||||
|
{{ formValidationError }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from "vue";
|
||||||
|
import { defineProps, defineEmits, defineExpose } from "vue";
|
||||||
|
import ModalSearch from "@/components/atoms/input/ModalSearch.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
searchClients: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
searchDeceased: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
onClientSelect: {
|
||||||
|
type: Function,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
onDeceasedSelect: {
|
||||||
|
type: Function,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
selectedDeceased: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["create-intervention"]);
|
||||||
|
|
||||||
|
// Form data
|
||||||
|
const form = ref({
|
||||||
|
client_id: "",
|
||||||
|
deceased_id: "",
|
||||||
|
type: "",
|
||||||
|
scheduled_date: "",
|
||||||
|
scheduled_time: "",
|
||||||
|
duration_min: "",
|
||||||
|
status: "",
|
||||||
|
order_giver: "",
|
||||||
|
notes: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Field errors
|
||||||
|
const fieldErrors = ref({});
|
||||||
|
|
||||||
|
// Selected items
|
||||||
|
const selectedClient = ref(null);
|
||||||
|
const selectedDeceased = ref(props.selectedDeceased);
|
||||||
|
|
||||||
|
// Store functions
|
||||||
|
const searchClients = async (params) => {
|
||||||
|
return await props.searchClients(params.query);
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchDeceased = async (params) => {
|
||||||
|
return await props.searchDeceased(params.query);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Client methods
|
||||||
|
const handleClientSelect = (client) => {
|
||||||
|
selectedClient.value = client;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClientConfirm = (client) => {
|
||||||
|
selectedClient.value = client;
|
||||||
|
form.value.client_id = client.id.toString();
|
||||||
|
if (props.onClientSelect) {
|
||||||
|
props.onClientSelect(client);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClientDescription = (client) => {
|
||||||
|
return client.email || "Pas d'email";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClientMeta = (client) => {
|
||||||
|
return client.phone ? `Tél: ${client.phone}` : "Pas de téléphone";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Deceased methods
|
||||||
|
const handleDeceasedSelect = (deceased) => {
|
||||||
|
selectedDeceased.value = deceased;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeceasedConfirm = (deceased) => {
|
||||||
|
selectedDeceased.value = deceased;
|
||||||
|
form.value.deceased_id = deceased.id.toString();
|
||||||
|
if (props.onDeceasedSelect) {
|
||||||
|
props.onDeceasedSelect(deceased);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDeceasedFullName = (deceased) => {
|
||||||
|
return `${deceased.last_name} ${deceased.first_name || ""}`.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDeceasedDescription = (deceased) => {
|
||||||
|
return `Né(e) le ${formatDate(deceased.birth_date)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDeceasedMeta = (deceased) => {
|
||||||
|
return `Décédé(e) le ${formatDate(deceased.death_date)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return "Date inconnue";
|
||||||
|
return new Date(dateString).toLocaleDateString("fr-FR");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Form validation
|
||||||
|
const formValidationError = computed(() => {
|
||||||
|
if (!form.value.client_id) {
|
||||||
|
return "Le client est obligatoire";
|
||||||
|
}
|
||||||
|
if (!form.value.type) {
|
||||||
|
return "Le type d'intervention est obligatoire";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form submission
|
||||||
|
const submitForm = () => {
|
||||||
|
// Clear field errors
|
||||||
|
fieldErrors.value = {};
|
||||||
|
|
||||||
|
// Check for validation errors
|
||||||
|
if (formValidationError.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields and set field errors
|
||||||
|
if (!selectedClient.value) {
|
||||||
|
fieldErrors.value.client_id = "Le client est obligatoire";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!form.value.type) {
|
||||||
|
fieldErrors.value.type = "Le type d'intervention est obligatoire";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine date and time into scheduled_at
|
||||||
|
const cleanedForm = { ...form.value };
|
||||||
|
if (cleanedForm.scheduled_date && cleanedForm.scheduled_time) {
|
||||||
|
cleanedForm.scheduled_at = `${cleanedForm.scheduled_date} ${cleanedForm.scheduled_time}:00`;
|
||||||
|
delete cleanedForm.scheduled_date;
|
||||||
|
delete cleanedForm.scheduled_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set IDs
|
||||||
|
cleanedForm.client_id = selectedClient.value.id;
|
||||||
|
if (selectedDeceased.value) {
|
||||||
|
cleanedForm.deceased_id = selectedDeceased.value.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit the create-intervention event
|
||||||
|
emit("create-intervention", cleanedForm);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch for pre-selected deceased from parent
|
||||||
|
watch(
|
||||||
|
() => props.selectedDeceased,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
selectedDeceased.value = newValue;
|
||||||
|
form.value.deceased_id = newValue.id.toString();
|
||||||
|
if (props.onDeceasedSelect) {
|
||||||
|
props.onDeceasedSelect(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expose submitForm function to parent component
|
||||||
|
defineExpose({
|
||||||
|
submitForm,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal-intervention-form {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-display .alert {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-danger {
|
||||||
|
color: #f5365c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid-feedback {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #dc3545;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select.is-invalid,
|
||||||
|
.form-control.is-invalid {
|
||||||
|
border-color: #dc3545;
|
||||||
|
padding-right: calc(1.5em + 0.75rem);
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath d='M5.8 4.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right calc(0.375em + 0.1875rem) center;
|
||||||
|
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert i {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -10,20 +10,20 @@
|
|||||||
<label class="form-label"
|
<label class="form-label"
|
||||||
>Client <span class="text-danger">*</span></label
|
>Client <span class="text-danger">*</span></label
|
||||||
>
|
>
|
||||||
<select
|
<search-input
|
||||||
v-model="form.client_id"
|
v-model="selectedItem"
|
||||||
class="form-select multisteps-form__select"
|
:search-action="props.searchClients"
|
||||||
:class="{ 'is-invalid': fieldErrors.client_id }"
|
:min-chars="0"
|
||||||
>
|
item-key="id"
|
||||||
<option value="">Sélectionnez un client</option>
|
item-label="name"
|
||||||
<option
|
@search="handleSearch"
|
||||||
v-for="client in clientList"
|
@select="handleSelect"
|
||||||
:key="client.id"
|
/>
|
||||||
:value="client.id"
|
<div v-if="selectedItem" class="selected-item">
|
||||||
>
|
Sélectionné: {{ selectedItem.name }} ({{
|
||||||
{{ client.name }}
|
selectedItem.email || "Pas d'email"
|
||||||
</option>
|
}})
|
||||||
</select>
|
</div>
|
||||||
<div v-if="fieldErrors.client_id" class="invalid-feedback">
|
<div v-if="fieldErrors.client_id" class="invalid-feedback">
|
||||||
{{ fieldErrors.client_id }}
|
{{ fieldErrors.client_id }}
|
||||||
</div>
|
</div>
|
||||||
@ -34,20 +34,19 @@
|
|||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label">Personne décédée</label>
|
<label class="form-label">Personne décédée</label>
|
||||||
<select
|
<search-input
|
||||||
v-model="form.deceased_id"
|
v-model="selectedDeceased"
|
||||||
class="form-select multisteps-form__select"
|
:search-action="props.searchDeceased"
|
||||||
:class="{ 'is-invalid': fieldErrors.deceased_id }"
|
:min-chars="0"
|
||||||
>
|
item-key="id"
|
||||||
<option value="">Sélectionnez une personne décédée</option>
|
:item-label="getDeceasedFullName"
|
||||||
<option
|
@search="handleSearchDeceased"
|
||||||
v-for="deceased in deceasedList"
|
@select="handleSelectDeceased"
|
||||||
:key="deceased.id"
|
/>
|
||||||
:value="deceased.id"
|
<div v-if="selectedDeceased" class="selected-item">
|
||||||
>
|
Sélectionné: {{ selectedDeceased.last_name }}
|
||||||
{{ deceased.last_name }} {{ deceased.first_name || "" }}
|
{{ selectedDeceased.first_name || "" }}
|
||||||
</option>
|
</div>
|
||||||
</select>
|
|
||||||
<div v-if="fieldErrors.deceased_id" class="invalid-feedback">
|
<div v-if="fieldErrors.deceased_id" class="invalid-feedback">
|
||||||
{{ fieldErrors.deceased_id }}
|
{{ fieldErrors.deceased_id }}
|
||||||
</div>
|
</div>
|
||||||
@ -223,6 +222,7 @@
|
|||||||
import { ref, defineProps, defineEmits, watch, computed } from "vue";
|
import { ref, defineProps, defineEmits, watch, computed } from "vue";
|
||||||
import SoftInput from "@/components/SoftInput.vue";
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
import SoftButton from "@/components/SoftButton.vue";
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
import SearchInput from "@/components/atoms/input/SearchInput.vue";
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -254,6 +254,22 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
searchClients: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
onClientSelect: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
searchDeceased: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
onDeceasedSelect: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emits
|
// Emits
|
||||||
@ -261,6 +277,51 @@ const emit = defineEmits(["createIntervention"]);
|
|||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
const errors = ref([]);
|
const errors = ref([]);
|
||||||
|
// Search input data
|
||||||
|
const selectedItem = ref(null);
|
||||||
|
const selectedDeceased = ref(null);
|
||||||
|
|
||||||
|
// Handle client search event
|
||||||
|
const handleSearch = (query) => {
|
||||||
|
console.log("Searching for client:", query);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle client select event
|
||||||
|
const handleSelect = (item) => {
|
||||||
|
console.log("Selected client:", item);
|
||||||
|
// Call the parent callback for client selection if provided
|
||||||
|
if (props.onClientSelect) {
|
||||||
|
props.onClientSelect(item);
|
||||||
|
}
|
||||||
|
// Set the client_id in the form
|
||||||
|
if (item && item.id) {
|
||||||
|
form.value.client_id = item.id.toString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle deceased search event
|
||||||
|
const handleSearchDeceased = (query) => {
|
||||||
|
console.log("Searching for deceased:", query);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDeceasedFullName = (deceased) => {
|
||||||
|
const parts = [deceased.last_name, deceased.first_name].filter(Boolean);
|
||||||
|
return parts.join(" ").trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle deceased select event
|
||||||
|
const handleSelectDeceased = (item) => {
|
||||||
|
console.log("Selected deceased:", item);
|
||||||
|
// Call the parent callback for deceased selection if provided
|
||||||
|
if (props.onDeceasedSelect) {
|
||||||
|
props.onDeceasedSelect(item);
|
||||||
|
}
|
||||||
|
// Set the deceased_id in the form
|
||||||
|
if (item && item.id) {
|
||||||
|
form.value.deceased_id = item.id.toString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fieldErrors = ref({});
|
const fieldErrors = ref({});
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
@ -307,6 +368,26 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Watch for client_id changes to update selectedItem
|
||||||
|
watch(
|
||||||
|
() => form.value.client_id,
|
||||||
|
(newClientId) => {
|
||||||
|
if (!newClientId) {
|
||||||
|
selectedItem.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Watch for deceased_id changes to update selectedDeceased
|
||||||
|
watch(
|
||||||
|
() => form.value.deceased_id,
|
||||||
|
(newDeceasedId) => {
|
||||||
|
if (!newDeceasedId) {
|
||||||
|
selectedDeceased.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
// Clear errors before submitting
|
// Clear errors before submitting
|
||||||
fieldErrors.value = {};
|
fieldErrors.value = {};
|
||||||
@ -380,6 +461,9 @@ const resetForm = () => {
|
|||||||
order_giver: "",
|
order_giver: "",
|
||||||
notes: "",
|
notes: "",
|
||||||
};
|
};
|
||||||
|
// Clear the selected items
|
||||||
|
selectedItem.value = null;
|
||||||
|
selectedDeceased.value = null;
|
||||||
clearErrors();
|
clearErrors();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mt-4 card">
|
<div class="mt-4 card">
|
||||||
<div class="p-3 card-body">
|
<div class="p-3 card-body">
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div v-if="loading" class="text-center py-5">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-3 text-muted">
|
||||||
|
Chargement des détails de l'intervention...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error State -->
|
||||||
|
<div v-else-if="error" class="text-center py-5">
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm bg-gradient-secondary"
|
||||||
|
@click="$emit('cancel')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>Retour
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<template v-else-if="mappedIntervention">
|
||||||
<!-- En-tête avec titre et badge de statut -->
|
<!-- En-tête avec titre et badge de statut -->
|
||||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||||
<h5 class="mb-0">Détails de l'Intervention</h5>
|
<h5 class="mb-0">Détails de l'Intervention</h5>
|
||||||
<SoftBadge
|
<SoftBadge
|
||||||
:color="intervention.status.color"
|
:color="mappedIntervention.status.color"
|
||||||
:variant="intervention.status.variant"
|
:variant="mappedIntervention.status.variant"
|
||||||
:size="intervention.status.size"
|
:size="mappedIntervention.status.size"
|
||||||
>
|
>
|
||||||
{{ intervention.status.label }}
|
{{ mappedIntervention.status.label }}
|
||||||
</SoftBadge>
|
</SoftBadge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -21,6 +48,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm bg-gradient-secondary"
|
class="btn btn-sm bg-gradient-secondary"
|
||||||
@click="toggleEditMode"
|
@click="toggleEditMode"
|
||||||
|
:disabled="loading"
|
||||||
>
|
>
|
||||||
{{ editMode ? "Sauvegarder" : "Modifier" }}
|
{{ editMode ? "Sauvegarder" : "Modifier" }}
|
||||||
</button>
|
</button>
|
||||||
@ -133,6 +161,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm bg-gradient-info"
|
class="btn btn-sm bg-gradient-info"
|
||||||
@click="showTeamModal = true"
|
@click="showTeamModal = true"
|
||||||
|
:disabled="loading"
|
||||||
>
|
>
|
||||||
Gérer l'équipe
|
Gérer l'équipe
|
||||||
</button>
|
</button>
|
||||||
@ -161,6 +190,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm bg-gradient-danger"
|
class="btn btn-sm bg-gradient-danger"
|
||||||
@click="$emit('cancel')"
|
@click="$emit('cancel')"
|
||||||
|
:disabled="loading"
|
||||||
>
|
>
|
||||||
Annuler
|
Annuler
|
||||||
</button>
|
</button>
|
||||||
@ -170,7 +200,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm bg-gradient-secondary me-2"
|
class="btn btn-sm bg-gradient-secondary me-2"
|
||||||
@click="resetChanges"
|
@click="resetChanges"
|
||||||
:disabled="!hasChanges"
|
:disabled="!hasChanges || loading"
|
||||||
>
|
>
|
||||||
Réinitialiser
|
Réinitialiser
|
||||||
</button>
|
</button>
|
||||||
@ -178,14 +208,20 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm"
|
class="btn btn-sm"
|
||||||
:class="`bg-gradient-${intervention.action.color}`"
|
:class="`bg-gradient-${mappedIntervention.action.color}`"
|
||||||
@click="saveChanges"
|
@click="saveChanges"
|
||||||
:disabled="!hasChanges"
|
:disabled="!hasChanges || loading"
|
||||||
>
|
>
|
||||||
{{ intervention.action.label }}
|
{{ mappedIntervention.action.label }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- No data state -->
|
||||||
|
<div v-else class="text-center py-5">
|
||||||
|
<p class="text-muted">Aucune donnée d'intervention disponible</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -231,7 +267,15 @@ import SoftBadge from "@/components/SoftBadge.vue";
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
intervention: {
|
intervention: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
default: () => null,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -240,10 +284,101 @@ const emit = defineEmits(["update", "cancel"]);
|
|||||||
// État local pour l'édition
|
// État local pour l'édition
|
||||||
const editMode = ref(false);
|
const editMode = ref(false);
|
||||||
const showTeamModal = ref(false);
|
const showTeamModal = ref(false);
|
||||||
const localIntervention = ref({ ...props.intervention });
|
const localIntervention = ref({});
|
||||||
|
|
||||||
|
// Map API data to expected format
|
||||||
|
const mappedIntervention = computed(() => {
|
||||||
|
if (!props.intervention) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...props.intervention,
|
||||||
|
// Map API fields to component expected fields
|
||||||
|
defuntName: props.intervention.deceased
|
||||||
|
? `${props.intervention.deceased.last_name || ""} ${
|
||||||
|
props.intervention.deceased.first_name || ""
|
||||||
|
}`.trim()
|
||||||
|
: `Personne ${props.intervention.deceased_id || "inconnue"}`,
|
||||||
|
date: props.intervention.scheduled_at
|
||||||
|
? new Date(props.intervention.scheduled_at).toLocaleString("fr-FR")
|
||||||
|
: "Non définie",
|
||||||
|
lieux: props.intervention.location
|
||||||
|
? props.intervention.location.name || "Lieu non défini"
|
||||||
|
: "Lieu non défini",
|
||||||
|
duree: props.intervention.duration_min
|
||||||
|
? `${props.intervention.duration_min} minutes`
|
||||||
|
: "Non définie",
|
||||||
|
title: props.intervention.type
|
||||||
|
? getInterventionTypeLabel(props.intervention.type)
|
||||||
|
: "Type non défini",
|
||||||
|
contactFamilial: props.intervention.order_giver || "Non renseigné",
|
||||||
|
description: props.intervention.notes || "Aucune description disponible",
|
||||||
|
nombrePersonnes: props.intervention.attachments_count || 0,
|
||||||
|
coordonneesContact: props.intervention.client
|
||||||
|
? props.intervention.client.email ||
|
||||||
|
props.intervention.client.phone ||
|
||||||
|
"Non disponible"
|
||||||
|
: "Non disponible",
|
||||||
|
prestationsSupplementaires: "À définir",
|
||||||
|
members: [], // Could be populated from practitioner data if available
|
||||||
|
|
||||||
|
// Map status from API string to expected object format
|
||||||
|
status: props.intervention.status
|
||||||
|
? {
|
||||||
|
label: getStatusLabel(props.intervention.status),
|
||||||
|
color: getStatusColor(props.intervention.status),
|
||||||
|
variant: "fill",
|
||||||
|
size: "md",
|
||||||
|
}
|
||||||
|
: { label: "En attente", color: "warning", variant: "fill", size: "md" },
|
||||||
|
|
||||||
|
// Map action (add if missing)
|
||||||
|
action: {
|
||||||
|
color: "primary",
|
||||||
|
label: "Modifier",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper function to map status string to readable label
|
||||||
|
const getStatusLabel = (status) => {
|
||||||
|
const statusLabels = {
|
||||||
|
demande: "Demande",
|
||||||
|
planifie: "Planifié",
|
||||||
|
en_cours: "En cours",
|
||||||
|
termine: "Terminé",
|
||||||
|
annule: "Annulé",
|
||||||
|
};
|
||||||
|
return statusLabels[status] || status;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to map intervention type to readable label
|
||||||
|
const getInterventionTypeLabel = (type) => {
|
||||||
|
const typeLabels = {
|
||||||
|
thanatopraxie: "Thanatopraxie",
|
||||||
|
toilette_mortuaire: "Toilette mortuaire",
|
||||||
|
exhumation: "Exhumation",
|
||||||
|
retrait_pacemaker: "Retrait pacemaker",
|
||||||
|
retrait_bijoux: "Retrait bijoux",
|
||||||
|
autre: "Autre",
|
||||||
|
};
|
||||||
|
return typeLabels[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to map status string to color
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
const statusColors = {
|
||||||
|
demande: "warning",
|
||||||
|
planifie: "info",
|
||||||
|
en_cours: "primary",
|
||||||
|
termine: "success",
|
||||||
|
annule: "danger",
|
||||||
|
};
|
||||||
|
return statusColors[status] || "secondary";
|
||||||
|
};
|
||||||
|
|
||||||
// Computed pour détecter les changements
|
// Computed pour détecter les changements
|
||||||
const hasChanges = computed(() => {
|
const hasChanges = computed(() => {
|
||||||
|
if (!props.intervention || !localIntervention.value) return false;
|
||||||
return (
|
return (
|
||||||
JSON.stringify(localIntervention.value) !==
|
JSON.stringify(localIntervention.value) !==
|
||||||
JSON.stringify(props.intervention)
|
JSON.stringify(props.intervention)
|
||||||
@ -252,28 +387,45 @@ const hasChanges = computed(() => {
|
|||||||
|
|
||||||
// Méthodes
|
// Méthodes
|
||||||
const toggleEditMode = () => {
|
const toggleEditMode = () => {
|
||||||
if (editMode.value) {
|
if (editMode.value && hasChanges.value) {
|
||||||
saveChanges();
|
saveChanges();
|
||||||
}
|
} else {
|
||||||
editMode.value = !editMode.value;
|
editMode.value = !editMode.value;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveChanges = () => {
|
const saveChanges = () => {
|
||||||
|
if (hasChanges.value) {
|
||||||
emit("update", localIntervention.value);
|
emit("update", localIntervention.value);
|
||||||
|
}
|
||||||
editMode.value = false;
|
editMode.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetChanges = () => {
|
const resetChanges = () => {
|
||||||
|
if (props.intervention) {
|
||||||
localIntervention.value = { ...props.intervention };
|
localIntervention.value = { ...props.intervention };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Watch pour mettre à jour les données locales quand les props changent
|
// Watch pour mettre à jour les données locales quand les props changent
|
||||||
watch(
|
watch(
|
||||||
() => props.intervention,
|
() => props.intervention,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
localIntervention.value = { ...newVal };
|
if (newVal) {
|
||||||
|
localIntervention.value = mappedIntervention.value || {};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true, immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Watch pour gérer le mode édition
|
||||||
|
watch(
|
||||||
|
() => props.loading,
|
||||||
|
(newLoading) => {
|
||||||
|
if (newLoading) {
|
||||||
|
editMode.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
class="col-lg-4 col-md-6 col-12 mb-4"
|
class="col-lg-4 col-md-6 col-12 mb-4"
|
||||||
>
|
>
|
||||||
<card-interventions
|
<card-interventions
|
||||||
|
:id="intervention.id"
|
||||||
:title="intervention.title"
|
:title="intervention.title"
|
||||||
:status="intervention.status"
|
:status="intervention.status"
|
||||||
:date="intervention.date"
|
:date="intervention.date"
|
||||||
@ -43,6 +44,7 @@ const interventions = computed(() => {
|
|||||||
? props.interventionData
|
? props.interventionData
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
|
id: 1, // Add required id for navigation
|
||||||
title: "Cérémonie religieuse",
|
title: "Cérémonie religieuse",
|
||||||
status: {
|
status: {
|
||||||
label: "Confirmé",
|
label: "Confirmé",
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
<template></template>
|
|
||||||
<script setup>
|
|
||||||
import SoftInput from "@/components/SoftInput.vue";
|
|
||||||
</script>
|
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<!-- Intervention Icon/Avatar -->
|
||||||
|
<div class="avatar avatar-xl mb-3">
|
||||||
|
<div class="avatar-title bg-gradient-primary text-white h5 mb-0">
|
||||||
|
<i class="fas fa-procedures"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Intervention Title -->
|
||||||
|
<h5 class="font-weight-bolder mb-0">
|
||||||
|
{{ intervention.title || "Intervention" }}
|
||||||
|
</h5>
|
||||||
|
<p class="text-sm text-secondary mb-3">
|
||||||
|
{{ intervention.defuntName }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Status Badge -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<SoftBadge
|
||||||
|
:color="intervention.status.color"
|
||||||
|
:variant="intervention.status.variant"
|
||||||
|
:size="intervention.status.size"
|
||||||
|
>
|
||||||
|
{{ intervention.status.label }}
|
||||||
|
</SoftBadge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Stats -->
|
||||||
|
<div class="row text-center mt-3">
|
||||||
|
<div class="col-6 border-end">
|
||||||
|
<h6 class="text-sm font-weight-bolder mb-0">
|
||||||
|
{{ intervention.duree }}
|
||||||
|
</h6>
|
||||||
|
<p class="text-xs text-secondary mb-0">Durée</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<h6 class="text-sm font-weight-bolder mb-0">
|
||||||
|
{{ intervention.members ? intervention.members.length : 0 }}
|
||||||
|
</h6>
|
||||||
|
<p class="text-xs text-secondary mb-0">Équipe</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Team Members -->
|
||||||
|
<div
|
||||||
|
v-if="intervention.members && intervention.members.length > 0"
|
||||||
|
class="mt-3"
|
||||||
|
>
|
||||||
|
<div class="avatar-group">
|
||||||
|
<a
|
||||||
|
v-for="(member, index) in intervention.members"
|
||||||
|
:key="index"
|
||||||
|
href="javascript:;"
|
||||||
|
class="avatar avatar-sm rounded-circle"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-placement="bottom"
|
||||||
|
:title="member.name"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="Image placeholder"
|
||||||
|
:src="member.image || '/images/avatar-default.png'"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import SoftBadge from "@/components/SoftBadge.vue";
|
||||||
|
import { defineProps } from "vue";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
intervention: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<ul class="nav nav-pills flex-column">
|
||||||
|
<TabNavigationItem
|
||||||
|
icon="fas fa-eye"
|
||||||
|
label="Vue d'ensemble"
|
||||||
|
:is-active="activeTab === 'overview'"
|
||||||
|
spacing=""
|
||||||
|
@click="$emit('change-tab', 'overview')"
|
||||||
|
/>
|
||||||
|
<TabNavigationItem
|
||||||
|
icon="fas fa-list"
|
||||||
|
label="Détails"
|
||||||
|
:is-active="activeTab === 'details'"
|
||||||
|
@click="$emit('change-tab', 'details')"
|
||||||
|
/>
|
||||||
|
<TabNavigationItem
|
||||||
|
icon="fas fa-users"
|
||||||
|
label="Équipe"
|
||||||
|
:is-active="activeTab === 'team'"
|
||||||
|
:badge="teamCount > 0 ? teamCount : null"
|
||||||
|
@click="$emit('change-tab', 'team')"
|
||||||
|
/>
|
||||||
|
<TabNavigationItem
|
||||||
|
icon="fas fa-file-alt"
|
||||||
|
label="Documents"
|
||||||
|
:is-active="activeTab === 'documents'"
|
||||||
|
:badge="documentsCount > 0 ? documentsCount : null"
|
||||||
|
@click="$emit('change-tab', 'documents')"
|
||||||
|
/>
|
||||||
|
<TabNavigationItem
|
||||||
|
icon="fas fa-history"
|
||||||
|
label="Historique"
|
||||||
|
:is-active="activeTab === 'history'"
|
||||||
|
@click="$emit('change-tab', 'history')"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import TabNavigationItem from "@/components/atoms/client/TabNavigationItem.vue";
|
||||||
|
|
||||||
|
import { defineProps, defineEmits, computed } from "vue";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
activeTab: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
teamCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
documentsCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(["change-tab"]);
|
||||||
|
</script>
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<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="intervention-detail-sidebar" />
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-9 mt-lg-0 mt-4">
|
||||||
|
<slot name="intervention-detail-content" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// This is a simple template component that provides the layout structure
|
||||||
|
</script>
|
||||||
@ -580,7 +580,7 @@ const routes = [
|
|||||||
component: () => import("@/views/pages/Interventions/AddIntervention.vue"),
|
component: () => import("@/views/pages/Interventions/AddIntervention.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/intervention",
|
path: "/intervention/:id",
|
||||||
name: "Intervention details",
|
name: "Intervention details",
|
||||||
component: () =>
|
component: () =>
|
||||||
import("@/views/pages/Interventions/InterventionDetails.vue"),
|
import("@/views/pages/Interventions/InterventionDetails.vue"),
|
||||||
|
|||||||
@ -226,6 +226,7 @@ export const useClientStore = defineStore("client", () => {
|
|||||||
* Search clients
|
* Search clients
|
||||||
*/
|
*/
|
||||||
const searchClients = async (query: string, exactMatch: boolean = false) => {
|
const searchClients = async (query: string, exactMatch: boolean = false) => {
|
||||||
|
console.log("ClientStore: searchClients called with query:", query);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
error.value = null;
|
error.value = null;
|
||||||
|
|
||||||
@ -233,7 +234,11 @@ export const useClientStore = defineStore("client", () => {
|
|||||||
const results = await ClientService.searchClients(query, {
|
const results = await ClientService.searchClients(query, {
|
||||||
exact_match: exactMatch,
|
exact_match: exactMatch,
|
||||||
});
|
});
|
||||||
|
console.log("ClientStore: Raw results from ClientService:", results);
|
||||||
|
console.log("ClientStore: Results type:", typeof results);
|
||||||
|
console.log("ClientStore: Results length:", results?.length);
|
||||||
setSearchClient(results);
|
setSearchClient(results);
|
||||||
|
console.log("ClientStore: Set searchResults to:", searchResults.value);
|
||||||
return results;
|
return results;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = "Erreur lors de la recherche des clients";
|
error.value = "Erreur lors de la recherche des clients";
|
||||||
|
|||||||
@ -147,6 +147,7 @@ export const useInterventionStore = defineStore("intervention", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const intervention = await InterventionService.getIntervention(id);
|
const intervention = await InterventionService.getIntervention(id);
|
||||||
|
console.log(intervention);
|
||||||
setCurrentIntervention(intervention);
|
setCurrentIntervention(intervention);
|
||||||
return intervention;
|
return intervention;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@ -7,6 +7,10 @@
|
|||||||
:deceased-loading="deceasedStore.isLoading"
|
:deceased-loading="deceasedStore.isLoading"
|
||||||
:client-list="clientStore.allClients"
|
:client-list="clientStore.allClients"
|
||||||
:client-loading="clientStore.isLoading"
|
:client-loading="clientStore.isLoading"
|
||||||
|
:search-clients="handleSearchClients"
|
||||||
|
:on-client-select="handleClientSelect"
|
||||||
|
:search-deceased="handleSearchDeceased"
|
||||||
|
:on-deceased-select="handleDeceasedSelect"
|
||||||
@create-intervention="handleCreateIntervention"
|
@create-intervention="handleCreateIntervention"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@ -27,6 +31,26 @@ const notificationStore = useNotificationStore();
|
|||||||
const validationErrors = ref({});
|
const validationErrors = ref({});
|
||||||
const showSuccess = ref(false);
|
const showSuccess = ref(false);
|
||||||
|
|
||||||
|
// Client search handler passed down to form
|
||||||
|
const handleSearchClients = async (query) => {
|
||||||
|
return await clientStore.searchClients(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Client selection handler to pass down to form
|
||||||
|
const handleClientSelect = (client) => {
|
||||||
|
return client;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Deceased search handler passed down to form
|
||||||
|
const handleSearchDeceased = async (query) => {
|
||||||
|
return await deceasedStore.searchDeceased(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Deceased selection handler to pass down to form
|
||||||
|
const handleDeceasedSelect = (deceased) => {
|
||||||
|
return deceased;
|
||||||
|
};
|
||||||
|
|
||||||
const handleCreateIntervention = async (form) => {
|
const handleCreateIntervention = async (form) => {
|
||||||
try {
|
try {
|
||||||
// Clear previous errors
|
// Clear previous errors
|
||||||
|
|||||||
@ -1,6 +1,105 @@
|
|||||||
<template>
|
<template>
|
||||||
<intervention-details-presentation />
|
<InterventionDetailPresentation
|
||||||
|
v-if="intervention"
|
||||||
|
:intervention="intervention"
|
||||||
|
:loading="interventionStore.isLoading"
|
||||||
|
:error="interventionStore.getError"
|
||||||
|
:active-tab="activeTab"
|
||||||
|
:practitioners="practitioners"
|
||||||
|
@update-intervention="handleUpdate"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
@assign-practitioner="handleAssignPractitioner"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import interventionDetailsPresentation from "@/components/Organism/Interventions/interventionDetailsPresentation.vue";
|
import { ref, onMounted, watch } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import InterventionDetailPresentation from "@/components/Organism/Interventions/InterventionDetailPresentation.vue";
|
||||||
|
import { useInterventionStore } from "@/stores/interventionStore";
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const interventionStore = useInterventionStore();
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
|
||||||
|
// Reactive data
|
||||||
|
const intervention = ref(null);
|
||||||
|
const activeTab = ref("overview");
|
||||||
|
const practitioners = ref([]);
|
||||||
|
|
||||||
|
// Fetch intervention data
|
||||||
|
const fetchIntervention = async () => {
|
||||||
|
try {
|
||||||
|
const interventionId = parseInt(route.params.id);
|
||||||
|
if (interventionId) {
|
||||||
|
const result = await interventionStore.fetchInterventionById(
|
||||||
|
interventionId
|
||||||
|
);
|
||||||
|
intervention.value = result; // Store method returns the intervention directly
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading intervention:", error);
|
||||||
|
notificationStore.error(
|
||||||
|
"Erreur",
|
||||||
|
"Impossible de charger les détails de l'intervention"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle practitioner assignment
|
||||||
|
const handleAssignPractitioner = async (practitionerData) => {
|
||||||
|
try {
|
||||||
|
if (intervention.value?.id) {
|
||||||
|
await interventionStore.assignPractitioner(
|
||||||
|
intervention.value.id,
|
||||||
|
practitionerData.practitionerId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refresh intervention data to get updated practitioner info
|
||||||
|
await fetchIntervention();
|
||||||
|
notificationStore.created("Praticien assigné");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error assigning practitioner:", error);
|
||||||
|
notificationStore.error("Erreur", "Impossible d'assigner le praticien");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle update from child components
|
||||||
|
const handleUpdate = async (updatedIntervention) => {
|
||||||
|
try {
|
||||||
|
const result = await interventionStore.updateIntervention(
|
||||||
|
updatedIntervention
|
||||||
|
);
|
||||||
|
intervention.value = result; // Store method returns the intervention directly
|
||||||
|
notificationStore.updated("Intervention");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating intervention:", error);
|
||||||
|
notificationStore.error(
|
||||||
|
"Erreur",
|
||||||
|
"Impossible de mettre à jour l'intervention"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle cancel
|
||||||
|
const handleCancel = () => {
|
||||||
|
console.log("Édition annulée");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch for changes in intervention store to update local state
|
||||||
|
watch(
|
||||||
|
() => interventionStore.currentIntervention,
|
||||||
|
(newIntervention) => {
|
||||||
|
intervention.value = newIntervention;
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load data on component mount
|
||||||
|
onMounted(() => {
|
||||||
|
console.log("test");
|
||||||
|
fetchIntervention();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -25,6 +25,7 @@ const loadInterventions = async () => {
|
|||||||
// Transform store data to match component expectations
|
// Transform store data to match component expectations
|
||||||
const transformedInterventions = computed(() => {
|
const transformedInterventions = computed(() => {
|
||||||
return interventionStore.interventions.map((intervention) => ({
|
return interventionStore.interventions.map((intervention) => ({
|
||||||
|
id: intervention.id,
|
||||||
title: intervention.title || intervention.type || "Intervention",
|
title: intervention.title || intervention.type || "Intervention",
|
||||||
status: {
|
status: {
|
||||||
label: intervention.status || "En attente",
|
label: intervention.status || "En attente",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user