540 lines
14 KiB
Vue
540 lines
14 KiB
Vue
<template>
|
|
<div class="card p-3 border-radius-xl bg-white" data-animation="FadeIn">
|
|
<h5 class="font-weight-bolder mb-0">Informations de l'intervention</h5>
|
|
<p class="mb-0 text-sm">Créez une nouvelle intervention funéraire</p>
|
|
|
|
<div class="multisteps-form__content">
|
|
<!-- Client selection -->
|
|
<div class="row mt-3">
|
|
<div class="col-12">
|
|
<label class="form-label"
|
|
>Client <span class="text-danger">*</span></label
|
|
>
|
|
<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 small-error"
|
|
>
|
|
{{ fieldErrors.client_id }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Deceased selection -->
|
|
<div class="row mt-3">
|
|
<div class="col-12">
|
|
<label class="form-label">Personne décédée</label>
|
|
<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 small-error"
|
|
>
|
|
{{ fieldErrors.deceased_id }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Intervention type -->
|
|
<div class="row mt-3">
|
|
<div class="col-12">
|
|
<label class="form-label"
|
|
>Type d'intervention <span class="text-danger">*</span></label
|
|
>
|
|
<select
|
|
v-model="form.type"
|
|
class="form-select multisteps-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 small-error">
|
|
{{ fieldErrors.type }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Date et heure de l'intervention -->
|
|
<div class="row mt-3">
|
|
<div class="col-12 col-sm-6">
|
|
<label class="form-label">Date de l'intervention</label>
|
|
<soft-input
|
|
v-model="form.scheduled_date"
|
|
class="multisteps-form__input"
|
|
:class="{ 'is-invalid': fieldErrors.scheduled_at }"
|
|
type="date"
|
|
/>
|
|
<div
|
|
v-if="fieldErrors.scheduled_at"
|
|
class="invalid-feedback small-error"
|
|
>
|
|
{{ fieldErrors.scheduled_at }}
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
|
<label class="form-label">Heure de l'intervention</label>
|
|
<soft-input
|
|
v-model="form.scheduled_time"
|
|
class="multisteps-form__input"
|
|
:class="{ 'is-invalid': fieldErrors.scheduled_at }"
|
|
type="time"
|
|
/>
|
|
<div
|
|
v-if="fieldErrors.scheduled_at"
|
|
class="invalid-feedback small-error"
|
|
>
|
|
{{ fieldErrors.scheduled_at }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Duration and status -->
|
|
<div class="row mt-3">
|
|
<div class="col-12 col-sm-6">
|
|
<label class="form-label">Durée (minutes)</label>
|
|
<soft-input
|
|
v-model="form.duration_min"
|
|
class="multisteps-form__input"
|
|
:class="{ 'is-invalid': fieldErrors.duration_min }"
|
|
type="number"
|
|
min="1"
|
|
placeholder="ex. 90"
|
|
/>
|
|
<div
|
|
v-if="fieldErrors.duration_min"
|
|
class="invalid-feedback small-error"
|
|
>
|
|
{{ fieldErrors.duration_min }}
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
|
<label class="form-label">Statut</label>
|
|
<select
|
|
v-model="form.status"
|
|
class="form-select multisteps-form__select"
|
|
:class="{ 'is-invalid': fieldErrors.status }"
|
|
>
|
|
<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 v-if="fieldErrors.status" class="invalid-feedback small-error">
|
|
{{ fieldErrors.status }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Order giver and notes -->
|
|
<div class="row mt-3">
|
|
<div class="col-12">
|
|
<label class="form-label">Donneur d'ordre</label>
|
|
<soft-input
|
|
v-model="form.order_giver"
|
|
class="multisteps-form__input"
|
|
:class="{ 'is-invalid': fieldErrors.order_giver }"
|
|
type="text"
|
|
placeholder="Nom du donneur d'ordre"
|
|
/>
|
|
<div
|
|
v-if="fieldErrors.order_giver"
|
|
class="invalid-feedback small-error"
|
|
>
|
|
{{ fieldErrors.order_giver }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notes -->
|
|
<div class="row mt-3">
|
|
<div class="col-12">
|
|
<label class="form-label">Notes et observations</label>
|
|
<textarea
|
|
v-model="form.notes"
|
|
class="form-control multisteps-form__input"
|
|
:class="{ 'is-invalid': fieldErrors.notes }"
|
|
rows="4"
|
|
placeholder="Informations complémentaires, instructions spéciales..."
|
|
maxlength="2000"
|
|
></textarea>
|
|
<div v-if="fieldErrors.notes" class="invalid-feedback small-error">
|
|
{{ fieldErrors.notes }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Boutons -->
|
|
<div class="button-row d-flex mt-4">
|
|
<soft-button
|
|
type="button"
|
|
color="secondary"
|
|
variant="outline"
|
|
class="me-2 mb-0"
|
|
@click="resetForm"
|
|
>
|
|
Réinitialiser
|
|
</soft-button>
|
|
<soft-button
|
|
type="button"
|
|
color="dark"
|
|
variant="gradient"
|
|
class="ms-auto mb-0"
|
|
:disabled="props.loading"
|
|
@click="submitForm"
|
|
>
|
|
<span
|
|
v-if="props.loading"
|
|
class="spinner-border spinner-border-sm me-2"
|
|
role="status"
|
|
></span>
|
|
{{ props.loading ? "Enregistrement..." : "Enregistrer" }}
|
|
</soft-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, defineProps, defineEmits, watch } 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({
|
|
loading: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
validationErrors: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
success: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
deceasedList: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
deceasedLoading: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
clientList: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
clientLoading: {
|
|
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
|
|
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({
|
|
client_id: "",
|
|
deceased_id: "",
|
|
type: "",
|
|
scheduled_date: "",
|
|
scheduled_time: "",
|
|
duration_min: "",
|
|
status: "",
|
|
order_giver: "",
|
|
notes: "",
|
|
});
|
|
|
|
// Watch for validation errors from parent
|
|
watch(
|
|
() => props.validationErrors,
|
|
(newErrors) => {
|
|
fieldErrors.value = { ...newErrors };
|
|
},
|
|
{ deep: true }
|
|
);
|
|
|
|
// Watch for success from parent
|
|
watch(
|
|
() => props.success,
|
|
(newSuccess) => {
|
|
if (newSuccess) {
|
|
resetForm();
|
|
}
|
|
}
|
|
);
|
|
|
|
// Watch for client_id changes to update selectedItem
|
|
watch(
|
|
() => form.value.client_id,
|
|
(newClientId) => {
|
|
if (newClientId && fieldErrors.value.client_id) {
|
|
delete fieldErrors.value.client_id;
|
|
}
|
|
if (!newClientId) {
|
|
selectedItem.value = null;
|
|
}
|
|
}
|
|
);
|
|
|
|
// Watch for deceased_id changes to update selectedDeceased
|
|
watch(
|
|
() => form.value.deceased_id,
|
|
(newDeceasedId) => {
|
|
if (newDeceasedId && fieldErrors.value.deceased_id) {
|
|
delete fieldErrors.value.deceased_id;
|
|
}
|
|
if (!newDeceasedId) {
|
|
selectedDeceased.value = null;
|
|
}
|
|
}
|
|
);
|
|
|
|
watch(
|
|
() => form.value.type,
|
|
(newType) => {
|
|
if (newType && fieldErrors.value.type) {
|
|
delete fieldErrors.value.type;
|
|
}
|
|
}
|
|
);
|
|
|
|
const submitForm = async () => {
|
|
// Clear errors before submitting
|
|
fieldErrors.value = {};
|
|
errors.value = [];
|
|
|
|
// Validate required fields
|
|
let hasErrors = false;
|
|
|
|
if (!form.value.client_id || form.value.client_id === "") {
|
|
fieldErrors.value.client_id = "Le client est obligatoire";
|
|
hasErrors = true;
|
|
}
|
|
|
|
if (!form.value.type || form.value.type === "") {
|
|
fieldErrors.value.type = "Le type d'intervention est obligatoire";
|
|
hasErrors = true;
|
|
}
|
|
|
|
if (hasErrors) {
|
|
return;
|
|
}
|
|
|
|
// Clean up form data: convert empty strings to null
|
|
const cleanedForm = {};
|
|
const formData = form.value;
|
|
|
|
for (const [key, value] of Object.entries(formData)) {
|
|
if (value === "" || value === null || value === undefined) {
|
|
cleanedForm[key] = null;
|
|
} else {
|
|
cleanedForm[key] = value;
|
|
}
|
|
}
|
|
|
|
// Combine date and time into scheduled_at (backend expects Y-m-d H:i:s format)
|
|
if (cleanedForm.scheduled_date && cleanedForm.scheduled_time) {
|
|
// Format: Y-m-d H:i:s (e.g., "2024-12-15 14:30:00")
|
|
const formattedDate = cleanedForm.scheduled_date;
|
|
const formattedTime = cleanedForm.scheduled_time;
|
|
cleanedForm.scheduled_at = `${formattedDate} ${formattedTime}:00`;
|
|
delete cleanedForm.scheduled_date;
|
|
delete cleanedForm.scheduled_time;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
console.log("Intervention form data being emitted:", cleanedForm);
|
|
|
|
// Emit the cleaned form data to parent
|
|
emit("createIntervention", cleanedForm);
|
|
};
|
|
|
|
const resetForm = () => {
|
|
form.value = {
|
|
client_id: "",
|
|
deceased_id: "",
|
|
type: "",
|
|
scheduled_date: "",
|
|
scheduled_time: "",
|
|
duration_min: "",
|
|
status: "",
|
|
order_giver: "",
|
|
notes: "",
|
|
};
|
|
// Clear the selected items
|
|
selectedItem.value = null;
|
|
selectedDeceased.value = null;
|
|
clearErrors();
|
|
};
|
|
|
|
const clearErrors = () => {
|
|
errors.value = [];
|
|
fieldErrors.value = {};
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.form-label {
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.text-danger {
|
|
color: #f5365c;
|
|
}
|
|
|
|
.invalid-feedback {
|
|
display: block;
|
|
}
|
|
|
|
.small-error {
|
|
font-size: 0.75rem;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.spinner-border-sm {
|
|
width: 1rem;
|
|
height: 1rem;
|
|
}
|
|
|
|
.alert {
|
|
border-radius: 0.75rem;
|
|
}
|
|
|
|
.alert-warning {
|
|
background-color: #fff3cd;
|
|
border: 1px solid #ffeaa7;
|
|
color: #856404;
|
|
}
|
|
|
|
.alert i {
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.multisteps-form__select {
|
|
background-color: white;
|
|
border: 1px solid #d2d6da;
|
|
border-radius: 0.5rem;
|
|
color: #495057;
|
|
padding: 0.5rem 0.75rem;
|
|
font-size: 0.875rem;
|
|
transition: all 0.15s ease-in-out;
|
|
}
|
|
|
|
.multisteps-form__select:focus {
|
|
border-color: #5e72e4;
|
|
box-shadow: 0 0 0 0.2rem rgba(94, 114, 228, 0.25);
|
|
outline: 0;
|
|
}
|
|
</style>
|