convoyForm

This commit is contained in:
kevin 2026-04-22 09:35:37 +03:00
parent 3e6ac4055c
commit d8d2b68421
2 changed files with 281 additions and 7 deletions

View File

@ -5,9 +5,71 @@
<div class="multisteps-form__content"> <div class="multisteps-form__content">
<div class="row mt-3"> <div class="row mt-3">
<div class="col-12 col-sm-6"> <div class="col-12 col-sm-6 position-relative">
<label class="form-label">ID défunt <span class="text-danger">*</span></label> <label class="form-label">Défunt <span class="text-danger">*</span></label>
<soft-input v-model="form.deceased_id" type="number" min="1" /> <div class="input-group">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input
v-model="deceasedSearch"
type="text"
class="form-control"
placeholder="Rechercher un défunt par nom"
@input="handleDeceasedSearch"
/>
<button
v-if="selectedDeceased"
class="btn btn-outline-secondary mb-0"
type="button"
@click="clearSelectedDeceased"
>
<i class="fas fa-times"></i>
</button>
</div>
<div
v-if="deceasedLoading"
class="text-center py-2 position-absolute w-100 bg-white border rounded shadow-sm"
style="z-index: 1000; top: 100%"
>
<div class="spinner-border spinner-border-sm text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
</div>
<div
v-else-if="deceasedResults.length > 0 && showDeceasedResults"
class="list-group position-absolute w-100 mt-1 shadow-lg"
style="z-index: 1000; max-height: 300px; overflow-y: auto"
>
<button
v-for="deceased in deceasedResults"
:key="deceased.id"
type="button"
class="list-group-item list-group-item-action"
@click="selectDeceased(deceased)"
>
<div class="d-flex flex-column">
<span class="font-weight-bold text-sm">
{{ [deceased.first_name, deceased.last_name].filter(Boolean).join(' ') || 'Défunt' }}
</span>
<span class="text-xs text-muted">
{{ deceased.death_date || deceased.birth_date || 'Aucune date renseignée' }}
</span>
</div>
</button>
</div>
<div
v-else-if="deceasedSearch && !deceasedLoading && showDeceasedResults"
class="position-absolute w-100 mt-1 p-3 bg-white border rounded shadow-sm text-center text-sm text-muted"
style="z-index: 1000"
>
Aucun défunt trouvé.
</div>
<div v-if="selectedDeceased" class="mt-2 small text-success">
Sélectionné: {{ [selectedDeceased.first_name, selectedDeceased.last_name].filter(Boolean).join(' ') }}
</div>
</div> </div>
<div class="col-12 col-sm-6 mt-3 mt-sm-0"> <div class="col-12 col-sm-6 mt-3 mt-sm-0">
<label class="form-label">Titre de mission</label> <label class="form-label">Titre de mission</label>
@ -47,9 +109,69 @@
</div> </div>
<div class="row mt-3"> <div class="row mt-3">
<div class="col-12 col-sm-6"> <div class="col-12 col-sm-6 position-relative">
<label class="form-label">Ville de départ</label> <label class="form-label">Lieu de départ</label>
<soft-input v-model="form.departure_city" type="text" placeholder="Ex. Paris" /> <div class="input-group">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input
v-model="locationSearch"
type="text"
class="form-control"
placeholder="Rechercher un lieu par nom, ville..."
@input="handleLocationSearch"
/>
<button
v-if="selectedLocation"
class="btn btn-outline-secondary mb-0"
type="button"
@click="clearSelectedLocation"
>
<i class="fas fa-times"></i>
</button>
</div>
<div
v-if="locationLoading"
class="text-center py-2 position-absolute w-100 bg-white border rounded shadow-sm"
style="z-index: 1000; top: 100%"
>
<div class="spinner-border spinner-border-sm text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
</div>
<div
v-else-if="locationResults.length > 0 && showLocationResults"
class="list-group position-absolute w-100 mt-1 shadow-lg"
style="z-index: 1000; max-height: 300px; overflow-y: auto"
>
<button
v-for="location in locationResults"
:key="location.id"
type="button"
class="list-group-item list-group-item-action"
@click="selectLocation(location)"
>
<div class="d-flex flex-column">
<span class="font-weight-bold text-sm">{{ location.name || 'Lieu sans nom' }}</span>
<span class="text-xs text-muted">
{{ [location.address_line1, location.postal_code, location.city].filter(Boolean).join(', ') }}
</span>
</div>
</button>
</div>
<div
v-else-if="locationSearch && !locationLoading && showLocationResults"
class="position-absolute w-100 mt-1 p-3 bg-white border rounded shadow-sm text-center text-sm text-muted"
style="z-index: 1000"
>
Aucun lieu trouvé.
</div>
<div v-if="selectedLocation" class="mt-2 small text-success">
Sélectionné: {{ selectedLocation.name || 'Lieu' }}
</div>
</div> </div>
<div class="col-12 col-sm-6 mt-3 mt-sm-0"> <div class="col-12 col-sm-6 mt-3 mt-sm-0">
<label class="form-label">Email famille</label> <label class="form-label">Email famille</label>
@ -74,12 +196,27 @@ import { defineEmits, defineProps } from "vue";
import { ref } from "vue"; import { ref } from "vue";
import SoftInput from "@/components/SoftInput.vue"; import SoftInput from "@/components/SoftInput.vue";
import SoftButton from "@/components/SoftButton.vue"; import SoftButton from "@/components/SoftButton.vue";
import { useClientLocationStore } from "@/stores/clientLocation";
import { useDeceasedStore } from "@/stores/deceasedStore";
const props = defineProps({ const props = defineProps({
loading: { type: Boolean, default: false }, loading: { type: Boolean, default: false },
}); });
const emit = defineEmits(["createConvoy"]); const emit = defineEmits(["createConvoy"]);
const clientLocationStore = useClientLocationStore();
const deceasedStore = useDeceasedStore();
const deceasedSearch = ref("");
const deceasedResults = ref([]);
const deceasedLoading = ref(false);
const showDeceasedResults = ref(false);
const selectedDeceased = ref(null);
const locationSearch = ref("");
const locationResults = ref([]);
const locationLoading = ref(false);
const showLocationResults = ref(false);
const selectedLocation = ref(null);
let debounceTimeout = null;
const defaultForm = () => ({ const defaultForm = () => ({
deceased_id: "", deceased_id: "",
@ -88,12 +225,110 @@ const defaultForm = () => ({
transport_mode: "road", transport_mode: "road",
planned_start_at: "", planned_start_at: "",
estimated_end_at: "", estimated_end_at: "",
departure_location_id: null,
departure_name: "",
departure_address: "",
departure_city: "", departure_city: "",
departure_postal_code: "",
departure_country_code: "",
family_email: "", family_email: "",
}); });
const form = ref(defaultForm()); const form = ref(defaultForm());
const handleDeceasedSearch = () => {
showDeceasedResults.value = true;
if (debounceTimeout) clearTimeout(debounceTimeout);
if (!deceasedSearch.value.trim()) {
deceasedResults.value = [];
showDeceasedResults.value = false;
clearSelectedDeceased();
return;
}
deceasedLoading.value = true;
debounceTimeout = setTimeout(async () => {
try {
const results = await deceasedStore.searchDeceased(deceasedSearch.value);
deceasedResults.value = results || [];
} catch (error) {
console.error("Error searching deceased:", error);
deceasedResults.value = [];
} finally {
deceasedLoading.value = false;
}
}, 300);
};
const selectDeceased = (deceased) => {
selectedDeceased.value = deceased;
deceasedSearch.value = [deceased.first_name, deceased.last_name].filter(Boolean).join(" ");
form.value.deceased_id = deceased.id;
deceasedResults.value = [];
showDeceasedResults.value = false;
};
const clearSelectedDeceased = () => {
selectedDeceased.value = null;
form.value.deceased_id = "";
};
const handleLocationSearch = () => {
showLocationResults.value = true;
if (debounceTimeout) clearTimeout(debounceTimeout);
if (!locationSearch.value.trim()) {
locationResults.value = [];
showLocationResults.value = false;
clearSelectedLocation();
return;
}
locationLoading.value = true;
debounceTimeout = setTimeout(async () => {
try {
const response = await clientLocationStore.fetchClientLocations({
search: locationSearch.value,
per_page: 10,
});
locationResults.value = response.data || [];
} catch (error) {
console.error("Error searching locations:", error);
locationResults.value = [];
} finally {
locationLoading.value = false;
}
}, 300);
};
const selectLocation = (location) => {
selectedLocation.value = location;
locationSearch.value = location.name || [location.address_line1, location.city].filter(Boolean).join(", ");
form.value.departure_location_id = location.id;
form.value.departure_name = location.name || null;
form.value.departure_address = location.address_line1 || null;
form.value.departure_city = location.city || null;
form.value.departure_postal_code = location.postal_code || null;
form.value.departure_country_code = location.country_code || null;
locationResults.value = [];
showLocationResults.value = false;
};
const clearSelectedLocation = () => {
selectedLocation.value = null;
form.value.departure_location_id = null;
form.value.departure_name = "";
form.value.departure_address = "";
form.value.departure_city = "";
form.value.departure_postal_code = "";
form.value.departure_country_code = "";
};
const submitForm = () => { const submitForm = () => {
emit("createConvoy", { emit("createConvoy", {
deceased_id: Number(form.value.deceased_id), deceased_id: Number(form.value.deceased_id),
@ -102,12 +337,28 @@ const submitForm = () => {
transport_mode: form.value.transport_mode, transport_mode: form.value.transport_mode,
planned_start_at: form.value.planned_start_at, planned_start_at: form.value.planned_start_at,
estimated_end_at: form.value.estimated_end_at || null, estimated_end_at: form.value.estimated_end_at || null,
departure_location_selection_mode: "place",
departure_location_id: form.value.departure_location_id || null,
departure_name: form.value.departure_name || null,
departure_address: form.value.departure_address || null,
departure_city: form.value.departure_city || null, departure_city: form.value.departure_city || null,
departure_postal_code: form.value.departure_postal_code || null,
departure_country_code: form.value.departure_country_code || null,
family_email: form.value.family_email || null, family_email: form.value.family_email || null,
}); });
}; };
const resetForm = () => { const resetForm = () => {
form.value = defaultForm(); form.value = defaultForm();
deceasedSearch.value = "";
deceasedResults.value = [];
deceasedLoading.value = false;
showDeceasedResults.value = false;
selectedDeceased.value = null;
locationSearch.value = "";
locationResults.value = [];
locationLoading.value = false;
showLocationResults.value = false;
selectedLocation.value = null;
}; };
</script> </script>

View File

@ -63,8 +63,31 @@ export const useDeceasedStore = defineStore("deceased", () => {
success.value = false; success.value = false;
}; };
const normalizeDeceased = (entry: Partial<Deceased> | null | undefined): Deceased | null => {
if (!entry || typeof entry !== "object") {
return null;
}
return {
id: entry.id,
last_name: entry.last_name || "",
first_name: entry.first_name || "",
full_name: entry.full_name,
birth_date: entry.birth_date,
death_date: entry.death_date,
place_of_death: entry.place_of_death,
notes: entry.notes,
documents_count: entry.documents_count,
interventions_count: entry.interventions_count,
created_at: entry.created_at,
updated_at: entry.updated_at,
};
};
const setDeceased = (newDeceased: Deceased[]) => { const setDeceased = (newDeceased: Deceased[]) => {
deceased.value = newDeceased; deceased.value = (newDeceased || [])
.map((entry) => normalizeDeceased(entry))
.filter((entry): entry is Deceased => entry !== null);
}; };
const setCurrentDeceased = (deceased: Deceased | null) => { const setCurrentDeceased = (deceased: Deceased | null) => {