intervention
This commit is contained in:
parent
7570f46658
commit
69fbe1a7a1
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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']);
|
||||
|
||||
@ -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"]);
|
||||
|
||||
@ -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();
|
||||
|
||||
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>
|
||||
|
||||
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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
@ -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"
|
||||
>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();
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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é",
|
||||
|
||||
@ -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"),
|
||||
},
|
||||
{
|
||||
path: "/intervention",
|
||||
path: "/intervention/:id",
|
||||
name: "Intervention details",
|
||||
component: () =>
|
||||
import("@/views/pages/Interventions/InterventionDetails.vue"),
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user