intervention

This commit is contained in:
Nyavokevin 2025-11-12 16:44:12 +03:00
parent 7570f46658
commit 69fbe1a7a1
27 changed files with 2696 additions and 291 deletions

View File

@ -143,4 +143,41 @@ class DeceasedController extends Controller
], 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);
}
}
}

View File

@ -99,4 +99,21 @@ class DeceasedRepository implements DeceasedRepositoryInterface
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();
}
}

View File

@ -49,4 +49,12 @@ interface DeceasedRepositoryInterface
* @return bool
*/
public function delete(Deceased $deceased): bool;
/**
* Search deceased by name
*
* @param string $name
* @return Collection
*/
public function searchByName(string $name): Collection;
}

View File

@ -98,6 +98,7 @@ Route::middleware('auth:sanctum')->group(function () {
// Deceased Routes
Route::prefix('deceased')->group(function () {
Route::get('/searchBy', [DeceasedController::class, 'searchBy']);
Route::get('/', [DeceasedController::class, 'index']);
Route::post('/', [DeceasedController::class, 'store']);
Route::get('/{deceased}', [DeceasedController::class, 'show']);

View File

@ -9,6 +9,10 @@
:deceased-loading="deceasedLoading"
:client-list="clientList"
:client-loading="clientLoading"
:search-clients="searchClients"
:on-client-select="onClientSelect"
:search-deceased="searchDeceased"
:on-deceased-select="onDeceasedSelect"
@create-intervention="handleCreateIntervention"
/>
</template>
@ -48,6 +52,22 @@ defineProps({
type: Boolean,
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"]);

View File

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

View File

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

View File

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

View File

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

View File

@ -59,7 +59,8 @@ import { useRouter } from "vue-router";
const router = useRouter();
defineProps({
const props = defineProps({
id: { type: [Number, String], required: false, default: null },
title: {
type: String,
default: "",
@ -108,6 +109,10 @@ defineProps({
});
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>

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

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

View File

@ -27,6 +27,57 @@
/>
</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>
<script setup>
import DefuntCard from "@/components/atoms/Defunts/DefuntCard.vue";
@ -34,10 +85,26 @@ import { ref } from "vue";
import { defineProps } from "vue";
import { useRouter } from "vue-router";
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
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
const dropdownOptions = ref([
{ label: "Voir les détails", action: "view", route: "#" },
@ -50,6 +117,66 @@ const dropdownOptions = ref([
{ 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({
defunts: {
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
const handleAction = (action, defunt) => {
switch (action) {
@ -69,8 +215,7 @@ const handleAction = (action, defunt) => {
router.push({ name: "Defunt details", params: { id: defunt.id } });
break;
case "create_intervention":
console.log("Créer une intervention pour:", defunt);
// Ouvrir wizard de création d'intervention
openInterventionModal(defunt);
break;
case "delete":
console.log("Supprimer le défunt:", defunt);
@ -88,6 +233,22 @@ const handleAction = (action, defunt) => {
const addDeceased = () => {
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>
<style scoped>
@ -105,4 +266,63 @@ const addDeceased = () => {
.pagination {
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>

View File

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

View File

@ -10,20 +10,20 @@
<label class="form-label"
>Client <span class="text-danger">*</span></label
>
<select
v-model="form.client_id"
class="form-select multisteps-form__select"
:class="{ 'is-invalid': fieldErrors.client_id }"
>
<option value="">Sélectionnez un client</option>
<option
v-for="client in clientList"
:key="client.id"
:value="client.id"
>
{{ client.name }}
</option>
</select>
<search-input
v-model="selectedItem"
:search-action="props.searchClients"
:min-chars="0"
item-key="id"
item-label="name"
@search="handleSearch"
@select="handleSelect"
/>
<div v-if="selectedItem" class="selected-item">
Sélectionné: {{ selectedItem.name }} ({{
selectedItem.email || "Pas d'email"
}})
</div>
<div v-if="fieldErrors.client_id" class="invalid-feedback">
{{ fieldErrors.client_id }}
</div>
@ -34,20 +34,19 @@
<div class="row mt-3">
<div class="col-12">
<label class="form-label">Personne décédée</label>
<select
v-model="form.deceased_id"
class="form-select multisteps-form__select"
:class="{ 'is-invalid': fieldErrors.deceased_id }"
>
<option value="">Sélectionnez une personne décédée</option>
<option
v-for="deceased in deceasedList"
:key="deceased.id"
:value="deceased.id"
>
{{ deceased.last_name }} {{ deceased.first_name || "" }}
</option>
</select>
<search-input
v-model="selectedDeceased"
:search-action="props.searchDeceased"
:min-chars="0"
item-key="id"
:item-label="getDeceasedFullName"
@search="handleSearchDeceased"
@select="handleSelectDeceased"
/>
<div v-if="selectedDeceased" class="selected-item">
Sélectionné: {{ selectedDeceased.last_name }}
{{ selectedDeceased.first_name || "" }}
</div>
<div v-if="fieldErrors.deceased_id" class="invalid-feedback">
{{ fieldErrors.deceased_id }}
</div>
@ -223,6 +222,7 @@
import { ref, defineProps, defineEmits, watch, computed } from "vue";
import SoftInput from "@/components/SoftInput.vue";
import SoftButton from "@/components/SoftButton.vue";
import SearchInput from "@/components/atoms/input/SearchInput.vue";
// Props
const props = defineProps({
@ -254,6 +254,22 @@ const props = defineProps({
type: Boolean,
default: false,
},
searchClients: {
type: Function,
required: true,
},
onClientSelect: {
type: Function,
required: true,
},
searchDeceased: {
type: Function,
required: true,
},
onDeceasedSelect: {
type: Function,
required: true,
},
});
// Emits
@ -261,6 +277,51 @@ const emit = defineEmits(["createIntervention"]);
// Reactive data
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 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 () => {
// Clear errors before submitting
fieldErrors.value = {};
@ -380,6 +461,9 @@ const resetForm = () => {
order_giver: "",
notes: "",
};
// Clear the selected items
selectedItem.value = null;
selectedDeceased.value = null;
clearErrors();
};

View File

@ -1,190 +1,226 @@
<template>
<div class="mt-4 card">
<div class="p-3 card-body">
<!-- En-tête avec titre et badge de statut -->
<div class="d-flex align-items-center justify-content-between mb-4">
<h5 class="mb-0">Détails de l'Intervention</h5>
<SoftBadge
:color="intervention.status.color"
:variant="intervention.status.variant"
:size="intervention.status.size"
>
{{ intervention.status.label }}
</SoftBadge>
<!-- 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>
<!-- Informations Client -->
<div class="mb-4">
<div class="d-flex align-items-center justify-content-between mb-3">
<h6 class="mb-0">Informations Client</h6>
<button
type="button"
class="btn btn-sm bg-gradient-secondary"
@click="toggleEditMode"
>
{{ editMode ? "Sauvegarder" : "Modifier" }}
</button>
<!-- 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>
<div class="row">
<!-- Colonne gauche -->
<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>
<!-- Colonne droite -->
<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>
<!-- Description -->
<div class="mb-4">
<h6 class="mb-3">Description</h6>
<SoftInput
type="textarea"
rows="3"
v-model="localIntervention.description"
:disabled="!editMode"
placeholder="Description détaillée de l'intervention..."
/>
</div>
<hr class="horizontal dark" />
<!-- Informations supplémentaires -->
<div class="mb-4">
<h6 class="mb-3">Informations Complémentaires</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"
/>
<SoftInput
label="Coordonnées du contact"
v-model="localIntervention.coordonneesContact"
:disabled="!editMode"
class="mb-3"
/>
</div>
<div class="col-md-6">
<SoftInput
label="Prestations supplémentaires"
type="textarea"
rows="2"
v-model="localIntervention.prestationsSupplementaires"
:disabled="!editMode"
class="mb-3"
/>
</div>
</div>
</div>
<!-- Équipe assignée -->
<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="showTeamModal = true"
>
Gérer l'équipe
</button>
</div>
<div class="avatar-group">
<a
v-for="(member, index) in localIntervention.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" />
</a>
</div>
</div>
<hr class="horizontal dark" />
<!-- Actions -->
<div class="d-flex justify-content-between">
<button
type="button"
class="btn btn-sm bg-gradient-danger"
class="btn btn-sm bg-gradient-secondary"
@click="$emit('cancel')"
>
Annuler
<i class="fas fa-arrow-left me-2"></i>Retour
</button>
</div>
<div>
<button
type="button"
class="btn btn-sm bg-gradient-secondary me-2"
@click="resetChanges"
:disabled="!hasChanges"
<!-- Content -->
<template v-else-if="mappedIntervention">
<!-- En-tête avec titre et badge de statut -->
<div class="d-flex align-items-center justify-content-between mb-4">
<h5 class="mb-0">Détails de l'Intervention</h5>
<SoftBadge
:color="mappedIntervention.status.color"
:variant="mappedIntervention.status.variant"
:size="mappedIntervention.status.size"
>
Réinitialiser
</button>
<button
type="button"
class="btn btn-sm"
:class="`bg-gradient-${intervention.action.color}`"
@click="saveChanges"
:disabled="!hasChanges"
>
{{ intervention.action.label }}
</button>
{{ mappedIntervention.status.label }}
</SoftBadge>
</div>
<!-- Informations Client -->
<div class="mb-4">
<div class="d-flex align-items-center justify-content-between mb-3">
<h6 class="mb-0">Informations Client</h6>
<button
type="button"
class="btn btn-sm bg-gradient-secondary"
@click="toggleEditMode"
:disabled="loading"
>
{{ editMode ? "Sauvegarder" : "Modifier" }}
</button>
</div>
<div class="row">
<!-- Colonne gauche -->
<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>
<!-- Colonne droite -->
<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>
<!-- Description -->
<div class="mb-4">
<h6 class="mb-3">Description</h6>
<SoftInput
type="textarea"
rows="3"
v-model="localIntervention.description"
:disabled="!editMode"
placeholder="Description détaillée de l'intervention..."
/>
</div>
<hr class="horizontal dark" />
<!-- Informations supplémentaires -->
<div class="mb-4">
<h6 class="mb-3">Informations Complémentaires</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"
/>
<SoftInput
label="Coordonnées du contact"
v-model="localIntervention.coordonneesContact"
:disabled="!editMode"
class="mb-3"
/>
</div>
<div class="col-md-6">
<SoftInput
label="Prestations supplémentaires"
type="textarea"
rows="2"
v-model="localIntervention.prestationsSupplementaires"
:disabled="!editMode"
class="mb-3"
/>
</div>
</div>
</div>
<!-- Équipe assignée -->
<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="showTeamModal = true"
:disabled="loading"
>
Gérer l'équipe
</button>
</div>
<div class="avatar-group">
<a
v-for="(member, index) in localIntervention.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" />
</a>
</div>
</div>
<hr class="horizontal dark" />
<!-- Actions -->
<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-${mappedIntervention.action.color}`"
@click="saveChanges"
:disabled="!hasChanges || loading"
>
{{ mappedIntervention.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>
</div>
@ -231,7 +267,15 @@ import SoftBadge from "@/components/SoftBadge.vue";
const props = defineProps({
intervention: {
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
const editMode = 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
const hasChanges = computed(() => {
if (!props.intervention || !localIntervention.value) return false;
return (
JSON.stringify(localIntervention.value) !==
JSON.stringify(props.intervention)
@ -252,28 +387,45 @@ const hasChanges = computed(() => {
// Méthodes
const toggleEditMode = () => {
if (editMode.value) {
if (editMode.value && hasChanges.value) {
saveChanges();
} else {
editMode.value = !editMode.value;
}
editMode.value = !editMode.value;
};
const saveChanges = () => {
emit("update", localIntervention.value);
if (hasChanges.value) {
emit("update", localIntervention.value);
}
editMode.value = false;
};
const resetChanges = () => {
localIntervention.value = { ...props.intervention };
if (props.intervention) {
localIntervention.value = { ...props.intervention };
}
};
// Watch pour mettre à jour les données locales quand les props changent
watch(
() => props.intervention,
(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>

View File

@ -7,6 +7,7 @@
class="col-lg-4 col-md-6 col-12 mb-4"
>
<card-interventions
:id="intervention.id"
:title="intervention.title"
:status="intervention.status"
:date="intervention.date"
@ -43,6 +44,7 @@ const interventions = computed(() => {
? props.interventionData
: [
{
id: 1, // Add required id for navigation
title: "Cérémonie religieuse",
status: {
label: "Confirmé",

View File

@ -1,4 +0,0 @@
<template></template>
<script setup>
import SoftInput from "@/components/SoftInput.vue";
</script>

View File

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

View File

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

View File

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

View File

@ -580,7 +580,7 @@ const routes = [
component: () => import("@/views/pages/Interventions/AddIntervention.vue"),
},
{
path: "/intervention",
path: "/intervention/:id",
name: "Intervention details",
component: () =>
import("@/views/pages/Interventions/InterventionDetails.vue"),

View File

@ -226,6 +226,7 @@ export const useClientStore = defineStore("client", () => {
* Search clients
*/
const searchClients = async (query: string, exactMatch: boolean = false) => {
console.log("ClientStore: searchClients called with query:", query);
setLoading(true);
error.value = null;
@ -233,7 +234,11 @@ export const useClientStore = defineStore("client", () => {
const results = await ClientService.searchClients(query, {
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);
console.log("ClientStore: Set searchResults to:", searchResults.value);
return results;
} catch (err) {
error.value = "Erreur lors de la recherche des clients";

View File

@ -147,6 +147,7 @@ export const useInterventionStore = defineStore("intervention", () => {
try {
const intervention = await InterventionService.getIntervention(id);
console.log(intervention);
setCurrentIntervention(intervention);
return intervention;
} catch (err: any) {

View File

@ -7,6 +7,10 @@
:deceased-loading="deceasedStore.isLoading"
:client-list="clientStore.allClients"
:client-loading="clientStore.isLoading"
:search-clients="handleSearchClients"
:on-client-select="handleClientSelect"
:search-deceased="handleSearchDeceased"
:on-deceased-select="handleDeceasedSelect"
@create-intervention="handleCreateIntervention"
/>
</template>
@ -27,6 +31,26 @@ const notificationStore = useNotificationStore();
const validationErrors = ref({});
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) => {
try {
// Clear previous errors

View File

@ -1,6 +1,105 @@
<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>
<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>

View File

@ -25,6 +25,7 @@ const loadInterventions = async () => {
// Transform store data to match component expectations
const transformedInterventions = computed(() => {
return interventionStore.interventions.map((intervention) => ({
id: intervention.id,
title: intervention.title || intervention.type || "Intervention",
status: {
label: intervention.status || "En attente",