1194 lines
43 KiB
Vue
1194 lines
43 KiB
Vue
<template>
|
||
<div
|
||
id="interventionModal"
|
||
ref="modalRef"
|
||
class="modal fade"
|
||
tabindex="-1"
|
||
aria-labelledby="interventionModalLabel"
|
||
aria-hidden="true"
|
||
>
|
||
<div
|
||
class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-xl"
|
||
>
|
||
<div class="modal-content border-0 shadow-lg">
|
||
<div class="modal-header border-0 pb-0">
|
||
<div>
|
||
<h5 id="interventionModalLabel" class="modal-title mb-1">
|
||
{{
|
||
isEditing ? "Modifier l'intervention" : "Nouvelle Intervention"
|
||
}}
|
||
</h5>
|
||
<p class="text-sm text-secondary mb-0">
|
||
{{
|
||
isEditing
|
||
? "Mettez à jour les informations ci-dessous"
|
||
: "Renseignez les informations de la prise en charge"
|
||
}}
|
||
</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
class="btn-close"
|
||
data-bs-dismiss="modal"
|
||
aria-label="Close"
|
||
></button>
|
||
</div>
|
||
|
||
<div class="modal-body pt-3">
|
||
<div v-if="globalErrors.length" class="alert alert-danger py-2">
|
||
<div v-for="(err, idx) in globalErrors" :key="idx">{{ err }}</div>
|
||
</div>
|
||
|
||
<form @submit.prevent="handleSubmit">
|
||
<div class="card border-0 shadow-sm mb-4">
|
||
<div class="card-header pb-0">
|
||
<p
|
||
class="text-xs text-uppercase text-secondary font-weight-bold mb-0"
|
||
>
|
||
1. Personnes concernées
|
||
</p>
|
||
</div>
|
||
<div class="card-body pt-3">
|
||
<div class="row g-4">
|
||
<div class="col-12 col-lg-6">
|
||
<div
|
||
class="d-flex justify-content-between align-items-center mb-2 gap-2 flex-wrap"
|
||
>
|
||
<label class="form-label mb-0">Défunt *</label>
|
||
<button
|
||
type="button"
|
||
class="btn btn-outline-secondary btn-sm mb-0"
|
||
@click="toggleDeceasedMode"
|
||
>
|
||
{{ deceasedForm.is_existing ? "Créer" : "Rechercher" }}
|
||
</button>
|
||
</div>
|
||
|
||
<div
|
||
v-if="deceasedForm.is_existing"
|
||
class="position-relative"
|
||
>
|
||
<div class="input-group">
|
||
<input
|
||
v-model="deceasedSearchQuery"
|
||
type="text"
|
||
class="form-control"
|
||
:class="{ 'is-invalid': hasError('deceased_id') }"
|
||
placeholder="Nom, prénom..."
|
||
@input="handleDeceasedSearch"
|
||
@focus="showDeceasedResults = true"
|
||
/>
|
||
<button
|
||
v-if="deceasedForm.id"
|
||
class="btn btn-outline-secondary mb-0"
|
||
type="button"
|
||
@click="clearDeceasedSelection"
|
||
>
|
||
Effacer
|
||
</button>
|
||
</div>
|
||
<small
|
||
v-if="getFieldError('deceased_id')"
|
||
class="text-danger d-block mt-1"
|
||
>
|
||
{{ getFieldError("deceased_id") }}
|
||
</small>
|
||
<div
|
||
v-if="
|
||
showDeceasedResults && deceasedSearchResults.length
|
||
"
|
||
class="position-absolute start-0 end-0 mt-2 card border-0 shadow-sm"
|
||
style="
|
||
z-index: 1060;
|
||
max-height: 16rem;
|
||
overflow-y: auto;
|
||
"
|
||
>
|
||
<div class="list-group list-group-flush">
|
||
<button
|
||
v-for="d in deceasedSearchResults"
|
||
:key="d.id"
|
||
type="button"
|
||
class="list-group-item list-group-item-action text-start"
|
||
@click="selectDeceased(d)"
|
||
>
|
||
<div class="fw-semibold text-sm">
|
||
{{ d.first_name }} {{ d.last_name }}
|
||
</div>
|
||
<small class="text-secondary">
|
||
{{ d.birth_date }} - {{ d.death_date }}
|
||
</small>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="row g-3">
|
||
<div class="col-12 col-md-6">
|
||
<label class="form-label">Nom *</label>
|
||
<input
|
||
v-model="deceasedForm.last_name"
|
||
class="form-control"
|
||
:class="{
|
||
'is-invalid': hasError('deceased.last_name'),
|
||
}"
|
||
placeholder="Dupont"
|
||
/>
|
||
</div>
|
||
<div class="col-12 col-md-6">
|
||
<label class="form-label">Prénom</label>
|
||
<input
|
||
v-model="deceasedForm.first_name"
|
||
class="form-control"
|
||
placeholder="Jean"
|
||
/>
|
||
</div>
|
||
<div class="col-12 col-md-6">
|
||
<label class="form-label">Naissance</label>
|
||
<input
|
||
v-model="deceasedForm.birth_date"
|
||
type="date"
|
||
class="form-control"
|
||
/>
|
||
</div>
|
||
<div class="col-12 col-md-6">
|
||
<label class="form-label">Décès</label>
|
||
<input
|
||
v-model="deceasedForm.death_date"
|
||
type="date"
|
||
class="form-control"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<small
|
||
v-if="getFieldError('deceased.last_name')"
|
||
class="text-danger d-block mt-1"
|
||
>
|
||
{{ getFieldError("deceased.last_name") }}
|
||
</small>
|
||
</div>
|
||
|
||
<div class="col-12 col-lg-6">
|
||
<label class="form-label">Client (Donneur d'ordre) *</label>
|
||
<div class="position-relative">
|
||
<div class="input-group">
|
||
<input
|
||
v-model="clientSearchQuery"
|
||
type="text"
|
||
class="form-control"
|
||
:class="{ 'is-invalid': hasError('client') }"
|
||
placeholder="Nom, email..."
|
||
@input="handleClientSearch"
|
||
@focus="handleClientFocus"
|
||
/>
|
||
<button
|
||
v-if="selectedClient"
|
||
class="btn btn-outline-secondary mb-0"
|
||
type="button"
|
||
@click="clearClientSelection"
|
||
>
|
||
Effacer
|
||
</button>
|
||
</div>
|
||
<small
|
||
v-if="getFieldError('client')"
|
||
class="text-danger d-block mt-1"
|
||
>
|
||
{{ getFieldError("client") }}
|
||
</small>
|
||
<div
|
||
v-if="showClientResults"
|
||
class="position-absolute start-0 end-0 mt-2 card border-0 shadow-sm"
|
||
style="
|
||
z-index: 1060;
|
||
max-height: 16rem;
|
||
overflow-y: auto;
|
||
"
|
||
>
|
||
<div class="list-group list-group-flush">
|
||
<button
|
||
v-for="c in clientSearchResults"
|
||
:key="c.id"
|
||
type="button"
|
||
class="list-group-item list-group-item-action text-start"
|
||
@click="selectClient(c)"
|
||
>
|
||
<div class="fw-semibold text-sm">{{ c.name }}</div>
|
||
<small class="text-secondary">{{ c.email }}</small>
|
||
</button>
|
||
<div
|
||
v-if="!clientSearchResults.length"
|
||
class="list-group-item text-secondary text-sm"
|
||
>
|
||
Aucun client trouvé
|
||
</div>
|
||
<div class="list-group-item text-secondary text-xs">
|
||
Écrire le nom du client pour rechercher
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card border-0 shadow-sm mb-4">
|
||
<div class="card-header pb-0">
|
||
<p
|
||
class="text-xs text-uppercase text-secondary font-weight-bold mb-0"
|
||
>
|
||
2. Planification
|
||
</p>
|
||
</div>
|
||
<div class="card-body pt-3">
|
||
<div class="row g-4">
|
||
<div class="col-12 col-lg-4">
|
||
<label class="form-label">Date et heure *</label>
|
||
<input
|
||
v-model="interventionForm.scheduled_at"
|
||
type="datetime-local"
|
||
class="form-control"
|
||
:class="{ 'is-invalid': hasError('scheduled_at') }"
|
||
required
|
||
/>
|
||
<small
|
||
v-if="getFieldError('scheduled_at')"
|
||
class="text-danger d-block mt-1"
|
||
>
|
||
{{ getFieldError("scheduled_at") }}
|
||
</small>
|
||
</div>
|
||
|
||
<div class="col-12 col-lg-4">
|
||
<label class="form-label">Type de soin *</label>
|
||
<div class="position-relative">
|
||
<div class="input-group">
|
||
<input
|
||
v-model="productSearchQuery"
|
||
type="text"
|
||
class="form-control"
|
||
:class="{ 'is-invalid': hasError('product_id') }"
|
||
placeholder="Rechercher un soin..."
|
||
@input="handleProductSearch"
|
||
@focus="handleProductFocus"
|
||
/>
|
||
<button
|
||
v-if="productForm.product_id"
|
||
class="btn btn-outline-secondary mb-0"
|
||
type="button"
|
||
@click="clearProductSelection"
|
||
>
|
||
Effacer
|
||
</button>
|
||
</div>
|
||
<small
|
||
v-if="getFieldError('product_id')"
|
||
class="text-danger d-block mt-1"
|
||
>
|
||
{{ getFieldError("product_id") }}
|
||
</small>
|
||
<div
|
||
v-if="showProductResults && productSearchResults.length"
|
||
class="position-absolute start-0 end-0 mt-2 card border-0 shadow-sm"
|
||
style="
|
||
z-index: 1060;
|
||
max-height: 16rem;
|
||
overflow-y: auto;
|
||
"
|
||
>
|
||
<div class="list-group list-group-flush">
|
||
<button
|
||
v-for="p in productSearchResults"
|
||
:key="p.id"
|
||
type="button"
|
||
class="list-group-item list-group-item-action text-start"
|
||
@click="selectProduct(p)"
|
||
>
|
||
<div class="fw-semibold text-sm">{{ p.nom }}</div>
|
||
<small v-if="p.price" class="text-secondary"
|
||
>{{ p.price }}€</small
|
||
>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-12 col-lg-4">
|
||
<label class="form-label">Statut</label>
|
||
<select
|
||
v-model="interventionForm.status"
|
||
class="form-select"
|
||
>
|
||
<option value="demande">Demande</option>
|
||
<option value="planifie">Planifiée</option>
|
||
<option value="en_cours">En cours</option>
|
||
<option value="terminee">Terminée</option>
|
||
<option value="facturee">Facturée</option>
|
||
<option value="annule">Annulée</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="col-12 col-lg-6">
|
||
<label class="form-label">Thanatopracteur salarié *</label>
|
||
<div class="position-relative">
|
||
<div class="input-group">
|
||
<input
|
||
v-model="practitionerSearchQuery"
|
||
type="text"
|
||
class="form-control"
|
||
:class="{
|
||
'is-invalid': hasError('assigned_practitioner_id'),
|
||
}"
|
||
placeholder="Rechercher par nom d'employé..."
|
||
@input="handlePractitionerSearch"
|
||
@focus="handlePractitionerFocus"
|
||
/>
|
||
<button
|
||
v-if="interventionForm.assigned_practitioner_id"
|
||
class="btn btn-outline-secondary mb-0"
|
||
type="button"
|
||
@click="clearPractitionerSelection"
|
||
>
|
||
Effacer
|
||
</button>
|
||
</div>
|
||
<small
|
||
v-if="getFieldError('assigned_practitioner_id')"
|
||
class="text-danger d-block mt-1"
|
||
>
|
||
{{ getFieldError("assigned_practitioner_id") }}
|
||
</small>
|
||
<div
|
||
v-if="showPractitionerResults"
|
||
class="position-absolute start-0 end-0 mt-2 card border-0 shadow-sm"
|
||
style="
|
||
z-index: 1060;
|
||
max-height: 16rem;
|
||
overflow-y: auto;
|
||
"
|
||
>
|
||
<div class="list-group list-group-flush">
|
||
<button
|
||
v-for="practitioner in practitionerSearchResults"
|
||
:key="practitioner.id"
|
||
type="button"
|
||
class="list-group-item list-group-item-action text-start"
|
||
@click="selectPractitioner(practitioner)"
|
||
>
|
||
<div class="fw-semibold text-sm">
|
||
{{ getPractitionerName(practitioner) }}
|
||
</div>
|
||
<small class="text-secondary">
|
||
{{ practitioner.employee?.email || "Employé" }}
|
||
</small>
|
||
</button>
|
||
<div
|
||
v-if="
|
||
practitionerSearchQuery.trim().length >= 2 &&
|
||
!practitionerSearchResults.length
|
||
"
|
||
class="list-group-item text-secondary text-sm"
|
||
>
|
||
Aucun employé trouvé
|
||
</div>
|
||
<div
|
||
v-else-if="
|
||
practitionerSearchQuery.trim().length < 2
|
||
"
|
||
class="list-group-item text-secondary text-sm"
|
||
>
|
||
Saisir au moins 2 caractères
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<small
|
||
v-if="getFieldError('assigned_practitioner_id')"
|
||
class="text-danger d-block mt-1"
|
||
>
|
||
{{ getFieldError("assigned_practitioner_id") }}
|
||
</small>
|
||
</div>
|
||
|
||
<div class="col-12 col-lg-6">
|
||
<label class="form-label">Sous-traitant</label>
|
||
<select class="form-select" disabled>
|
||
<option>Bientôt disponible</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card border-0 shadow-sm mb-4">
|
||
<div class="card-header pb-0">
|
||
<p
|
||
class="text-xs text-uppercase text-secondary font-weight-bold mb-0"
|
||
>
|
||
3. Lieu & Documents
|
||
</p>
|
||
</div>
|
||
<div class="card-body pt-3">
|
||
<div class="row g-4">
|
||
<div class="col-12 col-lg-6">
|
||
<div
|
||
class="d-flex justify-content-between align-items-center mb-2 gap-2 flex-wrap"
|
||
>
|
||
<label class="form-label mb-0">Lieu d'intervention</label>
|
||
<button
|
||
type="button"
|
||
class="btn btn-outline-secondary btn-sm mb-0"
|
||
@click="toggleLocationMode"
|
||
>
|
||
{{
|
||
locationForm.is_existing ? "Nouveau" : "Rechercher"
|
||
}}
|
||
</button>
|
||
</div>
|
||
|
||
<div
|
||
v-if="locationForm.is_existing"
|
||
class="position-relative"
|
||
>
|
||
<div class="input-group">
|
||
<input
|
||
v-model="locationSearchQuery"
|
||
type="text"
|
||
class="form-control"
|
||
:class="{
|
||
'is-invalid':
|
||
hasError('location.name') && !locationForm.id,
|
||
}"
|
||
placeholder="Domicile, clinique..."
|
||
@input="handleLocationSearch"
|
||
@focus="showLocationResults = true"
|
||
/>
|
||
<button
|
||
v-if="locationForm.id"
|
||
class="btn btn-outline-secondary mb-0"
|
||
type="button"
|
||
@click="clearLocationSelection"
|
||
>
|
||
Effacer
|
||
</button>
|
||
</div>
|
||
<small
|
||
v-if="
|
||
getFieldError('location.name') && !locationForm.id
|
||
"
|
||
class="text-danger d-block mt-1"
|
||
>
|
||
{{ getFieldError("location.name") }}
|
||
</small>
|
||
<div
|
||
v-if="
|
||
showLocationResults && locationSearchResults.length
|
||
"
|
||
class="position-absolute start-0 end-0 mt-2 card border-0 shadow-sm"
|
||
style="
|
||
z-index: 1060;
|
||
max-height: 16rem;
|
||
overflow-y: auto;
|
||
"
|
||
>
|
||
<div class="list-group list-group-flush">
|
||
<button
|
||
v-for="loc in locationSearchResults"
|
||
:key="loc.id"
|
||
type="button"
|
||
class="list-group-item list-group-item-action text-start"
|
||
@click="selectLocation(loc)"
|
||
>
|
||
<div class="fw-semibold text-sm">
|
||
{{ loc.name }}
|
||
</div>
|
||
<small class="text-secondary">{{ loc.city }}</small>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="row g-3">
|
||
<div class="col-12 col-md-6">
|
||
<label class="form-label">Nom du lieu *</label>
|
||
<input
|
||
v-model="locationForm.name"
|
||
class="form-control"
|
||
placeholder="Ex: Domicile"
|
||
:class="{ 'is-invalid': hasError('location.name') }"
|
||
/>
|
||
</div>
|
||
<div class="col-12 col-md-6">
|
||
<label class="form-label">Ville</label>
|
||
<input
|
||
v-model="locationForm.city"
|
||
class="form-control"
|
||
placeholder="Ex: Paris"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-12 col-lg-6">
|
||
<label class="form-label">N° Prescription médicale</label>
|
||
<div class="input-group">
|
||
<input
|
||
v-model="interventionForm.medical_prescription_number"
|
||
type="text"
|
||
class="form-control"
|
||
placeholder="Ex: RX-2024-001"
|
||
/>
|
||
<button
|
||
class="btn btn-outline-secondary mb-0"
|
||
type="button"
|
||
@click="toggleVoiceInput('medical_prescription_number')"
|
||
>
|
||
Vocal
|
||
</button>
|
||
</div>
|
||
<small class="text-secondary d-block mt-1">
|
||
Optionnel - si fourni par la famille
|
||
</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card border-0 shadow-sm mb-4">
|
||
<div class="card-header pb-0">
|
||
<p
|
||
class="text-xs text-uppercase text-secondary font-weight-bold mb-0"
|
||
>
|
||
4. Observations
|
||
</p>
|
||
</div>
|
||
<div class="card-body pt-3">
|
||
<label class="form-label">Observations techniques</label>
|
||
<div class="input-group">
|
||
<textarea
|
||
v-model="interventionForm.notes"
|
||
class="form-control"
|
||
rows="4"
|
||
placeholder="Notes et observations sur l'intervention..."
|
||
></textarea>
|
||
<button
|
||
class="btn btn-outline-secondary mb-0"
|
||
type="button"
|
||
@click="toggleVoiceInput('notes')"
|
||
>
|
||
Vocal
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
class="d-flex justify-content-between align-items-center gap-3 flex-wrap pt-2"
|
||
>
|
||
<div>
|
||
<p
|
||
class="text-xs text-uppercase text-secondary font-weight-bold mb-1"
|
||
>
|
||
Action rapide
|
||
</p>
|
||
<p class="text-sm text-secondary mb-0">
|
||
Vérifiez le client, le soin et l'intervenant avant validation.
|
||
</p>
|
||
</div>
|
||
<div class="d-flex gap-2 flex-wrap">
|
||
<SoftButton
|
||
type="button"
|
||
color="secondary"
|
||
variant="outline"
|
||
data-bs-dismiss="modal"
|
||
:disabled="submitting"
|
||
class="mb-0"
|
||
>
|
||
Annuler
|
||
</SoftButton>
|
||
<SoftButton
|
||
type="submit"
|
||
color="success"
|
||
variant="gradient"
|
||
:disabled="submitting"
|
||
class="mb-0"
|
||
>
|
||
<span
|
||
v-if="submitting"
|
||
class="spinner-border spinner-border-sm me-2"
|
||
role="status"
|
||
aria-hidden="true"
|
||
></span>
|
||
{{ isEditing ? "Mettre à jour" : "Créer l'intervention" }}
|
||
</SoftButton>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import {
|
||
ref,
|
||
watch,
|
||
defineProps,
|
||
defineEmits,
|
||
defineExpose,
|
||
onMounted,
|
||
} from "vue";
|
||
import { Modal } from "bootstrap";
|
||
import DeceasedService from "@/services/deceased";
|
||
import ProductService from "@/services/product";
|
||
import ClientLocationService from "@/services/client_location";
|
||
import { ClientService } from "@/services/client";
|
||
import ThanatopractitionerService from "@/services/thanatopractitioner";
|
||
import SoftButton from "@/components/SoftButton.vue";
|
||
|
||
const props = defineProps({
|
||
isEditing: { type: Boolean, default: false },
|
||
practitioners: { type: Array, default: () => [] },
|
||
initialDate: { type: String, default: "" },
|
||
});
|
||
|
||
const emit = defineEmits(["submit", "close"]);
|
||
|
||
const modalRef = ref(null);
|
||
let modalInstance = null;
|
||
const submitting = ref(false);
|
||
const globalErrors = ref([]);
|
||
const errors = ref([]);
|
||
|
||
const deceasedForm = ref({
|
||
id: null,
|
||
is_existing: true,
|
||
first_name: "",
|
||
last_name: "",
|
||
birth_date: "",
|
||
death_date: "",
|
||
});
|
||
const deceasedSearchQuery = ref("");
|
||
const deceasedSearchResults = ref([]);
|
||
const showDeceasedResults = ref(false);
|
||
let deceasedSearchTimeout;
|
||
|
||
const selectedClient = ref(null);
|
||
const clientSearchQuery = ref("");
|
||
const clientSearchResults = ref([]);
|
||
const showClientResults = ref(false);
|
||
const recentClients = ref([]);
|
||
let clientSearchTimeout;
|
||
|
||
const productForm = ref({ product_id: null });
|
||
const productSearchQuery = ref("");
|
||
const productSearchResults = ref([]);
|
||
const allProducts = ref([]);
|
||
const showProductResults = ref(false);
|
||
let productSearchTimeout;
|
||
|
||
const practitionerSearchQuery = ref("");
|
||
const practitionerSearchResults = ref([]);
|
||
const showPractitionerResults = ref(false);
|
||
let practitionerSearchTimeout;
|
||
|
||
const locationForm = ref({ id: null, is_existing: true, name: "", city: "" });
|
||
const locationSearchQuery = ref("");
|
||
const locationSearchResults = ref([]);
|
||
const showLocationResults = ref(false);
|
||
let locationSearchTimeout;
|
||
|
||
const interventionForm = ref({
|
||
scheduled_at: "",
|
||
status: "planifie",
|
||
assigned_practitioner_id: "",
|
||
notes: "",
|
||
medical_prescription_number: "",
|
||
order_giver: "",
|
||
type: "thanatopraxie",
|
||
});
|
||
|
||
onMounted(async () => {
|
||
if (modalRef.value) {
|
||
modalInstance = new Modal(modalRef.value);
|
||
modalRef.value.addEventListener("hidden.bs.modal", () => emit("close"));
|
||
}
|
||
if (props.initialDate) {
|
||
interventionForm.value.scheduled_at = props.initialDate;
|
||
} else {
|
||
const now = new Date();
|
||
now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
|
||
interventionForm.value.scheduled_at = now.toISOString().slice(0, 16);
|
||
}
|
||
try {
|
||
const pService = new ProductService();
|
||
const response = await pService.getAllProducts({
|
||
per_page: 100,
|
||
is_intervention: true,
|
||
});
|
||
const products = Array.isArray(response?.data)
|
||
? response.data
|
||
: Array.isArray(response?.data?.data)
|
||
? response.data.data
|
||
: [];
|
||
allProducts.value = products;
|
||
productSearchResults.value = products;
|
||
} catch (e) {
|
||
console.error("Failed to load products", e);
|
||
}
|
||
});
|
||
|
||
const show = () => modalInstance?.show();
|
||
const hide = () => modalInstance?.hide();
|
||
|
||
const toggleDeceasedMode = () => {
|
||
deceasedForm.value.is_existing = !deceasedForm.value.is_existing;
|
||
if (!deceasedForm.value.is_existing) clearDeceasedSelection();
|
||
else {
|
||
deceasedSearchQuery.value = "";
|
||
deceasedSearchResults.value = [];
|
||
}
|
||
};
|
||
|
||
const handleDeceasedSearch = () => {
|
||
if (deceasedSearchTimeout) clearTimeout(deceasedSearchTimeout);
|
||
if (deceasedSearchQuery.value.length < 2) {
|
||
deceasedSearchResults.value = [];
|
||
return;
|
||
}
|
||
deceasedSearchTimeout = setTimeout(async () => {
|
||
try {
|
||
deceasedSearchResults.value = await DeceasedService.searchDeceased(
|
||
deceasedSearchQuery.value
|
||
);
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
}, 300);
|
||
};
|
||
|
||
const selectDeceased = (d) => {
|
||
deceasedForm.value.id = d.id;
|
||
deceasedSearchQuery.value = `${d.first_name || ""} ${d.last_name}`;
|
||
deceasedSearchResults.value = [];
|
||
showDeceasedResults.value = false;
|
||
errors.value = errors.value.filter((e) => e.field !== "deceased_id");
|
||
};
|
||
|
||
const clearDeceasedSelection = () => {
|
||
deceasedForm.value.id = null;
|
||
deceasedSearchQuery.value = "";
|
||
Object.assign(deceasedForm.value, {
|
||
first_name: "",
|
||
last_name: "",
|
||
birth_date: "",
|
||
death_date: "",
|
||
});
|
||
deceasedSearchResults.value = [];
|
||
errors.value = errors.value.filter(
|
||
(e) => e.field !== "deceased_id" && e.field !== "deceased.last_name"
|
||
);
|
||
};
|
||
|
||
const handleClientSearch = () => {
|
||
showClientResults.value = true;
|
||
if (clientSearchTimeout) clearTimeout(clientSearchTimeout);
|
||
if (clientSearchQuery.value.length < 2) {
|
||
clientSearchResults.value = recentClients.value;
|
||
return;
|
||
}
|
||
clientSearchTimeout = setTimeout(async () => {
|
||
try {
|
||
clientSearchResults.value = await ClientService.searchClients(
|
||
clientSearchQuery.value
|
||
);
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
}, 300);
|
||
};
|
||
|
||
const loadRecentClients = async () => {
|
||
try {
|
||
const response = await ClientService.getAllClients({
|
||
page: 1,
|
||
per_page: 5,
|
||
});
|
||
const clients = Array.isArray(response?.data)
|
||
? response.data
|
||
: Array.isArray(response?.data?.data)
|
||
? response.data.data
|
||
: [];
|
||
recentClients.value = clients;
|
||
} catch (e) {
|
||
console.error("Failed to load recent clients", e);
|
||
recentClients.value = [];
|
||
}
|
||
};
|
||
|
||
const handleClientFocus = async () => {
|
||
if (!recentClients.value.length) {
|
||
await loadRecentClients();
|
||
}
|
||
if (!clientSearchQuery.value || clientSearchQuery.value.length < 2) {
|
||
clientSearchResults.value = recentClients.value;
|
||
}
|
||
showClientResults.value = true;
|
||
};
|
||
|
||
const selectClient = (c) => {
|
||
selectedClient.value = c;
|
||
clientSearchQuery.value = c.name + (c.email ? ` (${c.email})` : "");
|
||
clientSearchResults.value = [];
|
||
showClientResults.value = false;
|
||
errors.value = errors.value.filter((e) => e.field !== "client");
|
||
};
|
||
|
||
const clearClientSelection = () => {
|
||
selectedClient.value = null;
|
||
clientSearchQuery.value = "";
|
||
clientSearchResults.value = recentClients.value;
|
||
errors.value = errors.value.filter((e) => e.field !== "client");
|
||
};
|
||
|
||
const handleProductSearch = () => {
|
||
showProductResults.value = true;
|
||
if (productSearchTimeout) clearTimeout(productSearchTimeout);
|
||
productSearchTimeout = setTimeout(async () => {
|
||
try {
|
||
const pService = new ProductService();
|
||
const response = await pService.getAllProducts({
|
||
per_page: 100,
|
||
is_intervention: true,
|
||
search: productSearchQuery.value || undefined,
|
||
});
|
||
const products = Array.isArray(response?.data)
|
||
? response.data
|
||
: Array.isArray(response?.data?.data)
|
||
? response.data.data
|
||
: [];
|
||
productSearchResults.value = products;
|
||
} catch (e) {
|
||
console.error(e);
|
||
productSearchResults.value = [];
|
||
}
|
||
}, 250);
|
||
};
|
||
|
||
const handleProductFocus = () => {
|
||
showProductResults.value = true;
|
||
if (!productSearchQuery.value) productSearchResults.value = allProducts.value;
|
||
else handleProductSearch();
|
||
};
|
||
|
||
const selectProduct = (p) => {
|
||
productForm.value.product_id = p.id;
|
||
productSearchQuery.value = p.nom;
|
||
showProductResults.value = false;
|
||
errors.value = errors.value.filter((e) => e.field !== "product_id");
|
||
if (!interventionForm.value.type)
|
||
interventionForm.value.type = "thanatopraxie";
|
||
};
|
||
|
||
const clearProductSelection = () => {
|
||
productForm.value.product_id = null;
|
||
productSearchQuery.value = "";
|
||
productSearchResults.value = allProducts.value;
|
||
errors.value = errors.value.filter((e) => e.field !== "product_id");
|
||
};
|
||
|
||
const getPractitionerName = (practitioner) => {
|
||
const firstName = practitioner?.employee?.first_name || "";
|
||
const lastName = practitioner?.employee?.last_name || "";
|
||
return `${firstName} ${lastName}`.trim() || `#${practitioner?.id || ""}`;
|
||
};
|
||
|
||
const handlePractitionerSearch = () => {
|
||
if (practitionerSearchTimeout) clearTimeout(practitionerSearchTimeout);
|
||
|
||
if (interventionForm.value.assigned_practitioner_id) {
|
||
interventionForm.value.assigned_practitioner_id = "";
|
||
}
|
||
|
||
if (practitionerSearchQuery.value.trim().length < 2) {
|
||
practitionerSearchResults.value = [];
|
||
showPractitionerResults.value = true;
|
||
return;
|
||
}
|
||
|
||
showPractitionerResults.value = true;
|
||
practitionerSearchTimeout = setTimeout(async () => {
|
||
try {
|
||
practitionerSearchResults.value = await ThanatopractitionerService.searchThanatopractitioners(
|
||
practitionerSearchQuery.value.trim()
|
||
);
|
||
} catch (error) {
|
||
console.error("Failed to search practitioners", error);
|
||
practitionerSearchResults.value = [];
|
||
}
|
||
}, 300);
|
||
};
|
||
|
||
const handlePractitionerFocus = () => {
|
||
showPractitionerResults.value = !interventionForm.value
|
||
.assigned_practitioner_id;
|
||
};
|
||
|
||
const selectPractitioner = (practitioner) => {
|
||
interventionForm.value.assigned_practitioner_id = practitioner.id;
|
||
practitionerSearchQuery.value = getPractitionerName(practitioner);
|
||
showPractitionerResults.value = false;
|
||
practitionerSearchResults.value = [];
|
||
errors.value = errors.value.filter(
|
||
(e) => e.field !== "assigned_practitioner_id"
|
||
);
|
||
};
|
||
|
||
const clearPractitionerSelection = () => {
|
||
interventionForm.value.assigned_practitioner_id = "";
|
||
practitionerSearchQuery.value = "";
|
||
practitionerSearchResults.value = [];
|
||
showPractitionerResults.value = false;
|
||
errors.value = errors.value.filter(
|
||
(e) => e.field !== "assigned_practitioner_id"
|
||
);
|
||
};
|
||
|
||
const toggleLocationMode = () => {
|
||
locationForm.value.is_existing = !locationForm.value.is_existing;
|
||
if (!locationForm.value.is_existing) clearLocationSelection();
|
||
else {
|
||
locationForm.value.name = "";
|
||
locationForm.value.city = "";
|
||
locationSearchQuery.value = "";
|
||
locationSearchResults.value = [];
|
||
}
|
||
errors.value = errors.value.filter((e) => e.field === "location.name");
|
||
};
|
||
|
||
const handleLocationSearch = () => {
|
||
if (locationSearchTimeout) clearTimeout(locationSearchTimeout);
|
||
if (locationSearchQuery.value.length < 2) {
|
||
locationSearchResults.value = [];
|
||
return;
|
||
}
|
||
locationSearchTimeout = setTimeout(async () => {
|
||
try {
|
||
const response = await ClientLocationService.getAllClientLocations({
|
||
search: locationSearchQuery.value,
|
||
per_page: 10,
|
||
});
|
||
locationSearchResults.value = response.data;
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
}, 300);
|
||
};
|
||
|
||
const selectLocation = (loc) => {
|
||
locationForm.value.id = loc.id;
|
||
locationForm.value.name = loc.name;
|
||
locationForm.value.city = loc.city || "";
|
||
locationSearchQuery.value = loc.name + (loc.city ? ` (${loc.city})` : "");
|
||
showLocationResults.value = false;
|
||
locationSearchResults.value = [];
|
||
errors.value = errors.value.filter((e) => e.field !== "location.name");
|
||
};
|
||
|
||
const clearLocationSelection = () => {
|
||
Object.assign(locationForm.value, { id: null, name: "", city: "" });
|
||
locationSearchQuery.value = "";
|
||
locationSearchResults.value = [];
|
||
showLocationResults.value = false;
|
||
errors.value = errors.value.filter((e) => e.field !== "location.name");
|
||
};
|
||
|
||
const toggleVoiceInput = (field) =>
|
||
alert(`Saisie vocale pour ${field} – Fonctionnalité à implémenter`);
|
||
|
||
const hasError = (field) => errors.value.some((e) => e.field === field);
|
||
const getFieldError = (field) =>
|
||
errors.value.find((e) => e.field === field)?.message || "";
|
||
|
||
const validate = () => {
|
||
errors.value = [];
|
||
let isValid = true;
|
||
if (deceasedForm.value.is_existing) {
|
||
if (!deceasedForm.value.id) {
|
||
errors.value.push({
|
||
field: "deceased_id",
|
||
message: "Veuillez sélectionner un défunt.",
|
||
});
|
||
isValid = false;
|
||
}
|
||
} else {
|
||
if (!deceasedForm.value.last_name) {
|
||
errors.value.push({
|
||
field: "deceased.last_name",
|
||
message: "Le nom du défunt est requis.",
|
||
});
|
||
isValid = false;
|
||
}
|
||
}
|
||
if (!selectedClient.value) {
|
||
errors.value.push({
|
||
field: "client",
|
||
message: "Veuillez sélectionner un client.",
|
||
});
|
||
isValid = false;
|
||
}
|
||
if (!interventionForm.value.scheduled_at) {
|
||
errors.value.push({ field: "scheduled_at", message: "Date obligatoire." });
|
||
isValid = false;
|
||
}
|
||
if (!productForm.value.product_id) {
|
||
errors.value.push({ field: "product_id", message: "Type de soin requis." });
|
||
isValid = false;
|
||
}
|
||
if (!interventionForm.value.assigned_practitioner_id) {
|
||
errors.value.push({
|
||
field: "assigned_practitioner_id",
|
||
message: "Intervenant requis.",
|
||
});
|
||
isValid = false;
|
||
}
|
||
if (locationForm.value.is_existing) {
|
||
if (!locationForm.value.id) {
|
||
errors.value.push({
|
||
field: "location.name",
|
||
message: "Veuillez sélectionner un lieu.",
|
||
});
|
||
isValid = false;
|
||
}
|
||
} else {
|
||
if (!locationForm.value.name) {
|
||
errors.value.push({
|
||
field: "location.name",
|
||
message: "Le nom du lieu est requis.",
|
||
});
|
||
isValid = false;
|
||
}
|
||
}
|
||
return isValid;
|
||
};
|
||
|
||
const handleSubmit = async () => {
|
||
if (submitting.value || !validate()) return;
|
||
submitting.value = true;
|
||
globalErrors.value = [];
|
||
try {
|
||
const formData = new FormData();
|
||
if (deceasedForm.value.is_existing && deceasedForm.value.id) {
|
||
formData.append("deceased_id", deceasedForm.value.id);
|
||
} else {
|
||
formData.append(
|
||
"deceased[first_name]",
|
||
deceasedForm.value.first_name || ""
|
||
);
|
||
formData.append(
|
||
"deceased[last_name]",
|
||
deceasedForm.value.last_name || ""
|
||
);
|
||
if (deceasedForm.value.birth_date)
|
||
formData.append("deceased[birth_date]", deceasedForm.value.birth_date);
|
||
if (deceasedForm.value.death_date)
|
||
formData.append("deceased[death_date]", deceasedForm.value.death_date);
|
||
}
|
||
if (selectedClient.value) {
|
||
formData.append("client_id", selectedClient.value.id);
|
||
if (selectedClient.value.name)
|
||
formData.append("client[name]", selectedClient.value.name);
|
||
if (selectedClient.value.email)
|
||
formData.append("client[email]", selectedClient.value.email);
|
||
if (selectedClient.value.phone)
|
||
formData.append("client[phone]", selectedClient.value.phone);
|
||
if (selectedClient.value.billing_address) {
|
||
const ba = selectedClient.value.billing_address;
|
||
if (ba.line1)
|
||
formData.append("client[billing_address_line1]", ba.line1);
|
||
if (ba.line2)
|
||
formData.append("client[billing_address_line2]", ba.line2);
|
||
if (ba.postal_code)
|
||
formData.append("client[billing_postal_code]", ba.postal_code);
|
||
if (ba.city) formData.append("client[billing_city]", ba.city);
|
||
if (ba.country_code)
|
||
formData.append("client[billing_country_code]", ba.country_code);
|
||
}
|
||
if (selectedClient.value.vat_number)
|
||
formData.append("client[vat_number]", selectedClient.value.vat_number);
|
||
if (selectedClient.value.siret)
|
||
formData.append("client[siret]", selectedClient.value.siret);
|
||
}
|
||
if (locationForm.value.is_existing && locationForm.value.id) {
|
||
formData.append("location_id", locationForm.value.id);
|
||
} else {
|
||
formData.append("location[name]", locationForm.value.name || "");
|
||
if (locationForm.value.city)
|
||
formData.append("location[city]", locationForm.value.city);
|
||
}
|
||
if (productForm.value.product_id)
|
||
formData.append("product_id", productForm.value.product_id);
|
||
|
||
if (interventionForm.value.assigned_practitioner_id) {
|
||
formData.append(
|
||
"intervention[principal_practitioner_id]",
|
||
interventionForm.value.assigned_practitioner_id
|
||
);
|
||
formData.append(
|
||
"intervention[practitioners][0]",
|
||
interventionForm.value.assigned_practitioner_id
|
||
);
|
||
}
|
||
|
||
Object.keys(interventionForm.value).forEach((key) => {
|
||
if (interventionForm.value[key] != null) {
|
||
let value = interventionForm.value[key];
|
||
if (key === "scheduled_at" && value) {
|
||
value = value.replace("T", " ");
|
||
if (value.length === 16) value += ":00";
|
||
}
|
||
formData.append(`intervention[${key}]`, value);
|
||
}
|
||
});
|
||
emit("submit", formData);
|
||
} catch (e) {
|
||
console.error(e);
|
||
globalErrors.value.push("Erreur lors de la préparation du formulaire.");
|
||
} finally {
|
||
submitting.value = false;
|
||
}
|
||
};
|
||
|
||
watch(
|
||
() => locationForm.value.name,
|
||
() => {
|
||
if (getFieldError("location.name"))
|
||
errors.value = errors.value.filter((e) => e.field !== "location.name");
|
||
}
|
||
);
|
||
watch(
|
||
() => interventionForm.value.assigned_practitioner_id,
|
||
() => {
|
||
if (getFieldError("assigned_practitioner_id"))
|
||
errors.value = errors.value.filter(
|
||
(e) => e.field !== "assigned_practitioner_id"
|
||
);
|
||
|
||
const practitioner = props.practitioners.find(
|
||
(item) => item.id === interventionForm.value.assigned_practitioner_id
|
||
);
|
||
|
||
if (practitioner) {
|
||
practitionerSearchQuery.value = getPractitionerName(practitioner);
|
||
} else if (!interventionForm.value.assigned_practitioner_id) {
|
||
practitionerSearchQuery.value = "";
|
||
}
|
||
}
|
||
);
|
||
|
||
defineExpose({ show, hide });
|
||
</script>
|