diff --git a/thanasoft-back/app/Http/Controllers/Api/ClientLocationController.php b/thanasoft-back/app/Http/Controllers/Api/ClientLocationController.php index 8df46da..6021150 100644 --- a/thanasoft-back/app/Http/Controllers/Api/ClientLocationController.php +++ b/thanasoft-back/app/Http/Controllers/Api/ClientLocationController.php @@ -29,7 +29,7 @@ class ClientLocationController extends Controller { try { $filters = $request->only(['client_id', 'is_default', 'search']); - $perPage = $request->get('per_page', 10); + $perPage = (int) $request->get('per_page', 10); $clientLocations = $this->clientLocationRepository->getPaginated($filters, $perPage); return new ClientLocationCollection($clientLocations); diff --git a/thanasoft-back/app/Repositories/ClientLocationRepository.php b/thanasoft-back/app/Repositories/ClientLocationRepository.php index 07fb233..bd88b47 100644 --- a/thanasoft-back/app/Repositories/ClientLocationRepository.php +++ b/thanasoft-back/app/Repositories/ClientLocationRepository.php @@ -43,7 +43,8 @@ class ClientLocationRepository extends BaseRepository implements ClientLocationR $search = $filters['search']; $query->where(function ($q) use ($search) { $q->where('name', 'LIKE', "%{$search}%") - ->orWhere('address', 'LIKE', "%{$search}%") + ->orWhere('address_line1', 'LIKE', "%{$search}%") + ->orWhere('address_line2', 'LIKE', "%{$search}%") ->orWhere('city', 'LIKE', "%{$search}%") ->orWhere('postal_code', 'LIKE', "%{$search}%"); }); @@ -71,7 +72,8 @@ class ClientLocationRepository extends BaseRepository implements ClientLocationR $search = $filters['search']; $query->where(function ($q) use ($search) { $q->where('name', 'LIKE', "%{$search}%") - ->orWhere('address', 'LIKE', "%{$search}%") + ->orWhere('address_line1', 'LIKE', "%{$search}%") + ->orWhere('address_line2', 'LIKE', "%{$search}%") ->orWhere('city', 'LIKE', "%{$search}%") ->orWhere('postal_code', 'LIKE', "%{$search}%"); }); diff --git a/thanasoft-front/src/components/Organism/Agenda/InterventionMultiStepModal.vue b/thanasoft-front/src/components/Organism/Agenda/InterventionMultiStepModal.vue index 62fb578..5c10aa5 100644 --- a/thanasoft-front/src/components/Organism/Agenda/InterventionMultiStepModal.vue +++ b/thanasoft-front/src/components/Organism/Agenda/InterventionMultiStepModal.vue @@ -24,95 +24,381 @@ aria-label="Close" > - @@ -128,15 +414,13 @@ import { defineEmits, defineExpose, onMounted, + nextTick } from "vue"; import { Modal } from "bootstrap"; -import WizardProgress from "./WizardSteps/WizardProgress.vue"; -import StepDeceased from "./WizardSteps/StepDeceased.vue"; -import StepClient from "./WizardSteps/StepClient.vue"; -import StepLocation from "./WizardSteps/StepLocation.vue"; -import StepProductSelection from "./WizardSteps/StepProductSelection.vue"; -import StepDocuments from "./WizardSteps/StepDocuments.vue"; -import StepIntervention from "./WizardSteps/StepIntervention.vue"; +import DeceasedService from "@/services/deceased"; +import ProductService from "@/services/product"; +import ClientLocationService from "@/services/client_location"; +import ClientService from "@/services/client"; const props = defineProps({ isEditing: { @@ -157,86 +441,93 @@ const emit = defineEmits(["submit", "close"]); const modalRef = ref(null); let modalInstance = null; -const activeStep = ref(0); const submitting = ref(false); -const validatingStep = ref(false); -const uploadedFiles = ref([]); -// Step definitions -const steps = [ - { label: "Défunt", title: "Information du Défunt" }, - { label: "Client", title: "Information du Client" }, - { label: "Lieu", title: "Lieu de l'intervention" }, - { label: "Type de soins", title: "Sélection du type de soins" }, - { label: "Documents", title: "Documents à joindre" }, - { label: "Intervention", title: "Détails de l'intervention" }, -]; - -// Error tracking -const stepErrors = ref([[], [], [], [], [], []]); const globalErrors = ref([]); +const errors = ref([]); -// Form data +// --- SEARCH & DATA STATE --- +// Deceased const deceasedForm = ref({ id: null, - is_existing: false, // UI state to track mode + is_existing: true, // Start with search mode first_name: "", last_name: "", birth_date: "", death_date: "", - place_of_death: "", - gender: "", - notes: "", }); +const deceasedSearchQuery = ref(""); +const deceasedSearchResults = ref([]); +const showDeceasedResults = ref(false); +let deceasedSearchTimeout; -const clientForm = ref({ - name: "", - email: "", - phone: "", - billing_address_line1: "", - billing_address_line2: "", - billing_city: "", - billing_postal_code: "", - billing_country_code: "MG", - vat_number: "", - siret: "", - notes: "", - is_active: true, -}); - -const locationForm = ref({ - id: null, - is_existing: false, - name: "", - address: "", - city: "", - postal_code: "", - country_code: "MG", - access_instructions: "", - notes: "", -}); +// Client +const selectedClient = ref(null); +const clientSearchQuery = ref(""); +const clientSearchResults = ref([]); +const showClientResults = ref(false); +let clientSearchTimeout; +// Product const productForm = ref({ - product_id: "", + product_id: null }); +const productSearchQuery = ref(""); +const productSearchResults = ref([]); +const allProducts = ref([]); +const showProductResults = ref(false); +// Location - FIXED: Start with search mode (true = search existing, false = create new) +const locationForm = ref({ + id: null, + is_existing: true, // Start with search mode + name: "", + city: "", +}); +const locationSearchQuery = ref(""); +const locationSearchResults = ref([]); +const showLocationResults = ref(false); +let locationSearchTimeout; + +// Intervention const interventionForm = ref({ - type: "thanatopraxie", // Default type scheduled_at: "", - status: "demande", + status: "planifie", assigned_practitioner_id: "", notes: "", - order_giver: "", - duration_min: null, + medical_prescription_number: "", + order_giver: "", + type: "thanatopraxie", }); -onMounted(() => { - if (modalRef.value) { - modalInstance = new Modal(modalRef.value); - modalRef.value.addEventListener("hidden.bs.modal", () => { - emit("close"); - }); - } +// --- METHODS --- +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 + }); + allProducts.value = response.data; + productSearchResults.value = allProducts.value; + } catch (e) { + console.error("Failed to load products", e); + } }); const show = () => { @@ -251,240 +542,344 @@ const hide = () => { } }; -// Watch for product selection -watch( - () => productForm.value.product_id, - (newId) => { - if (newId) { - if (!interventionForm.value.type) { - interventionForm.value.type = "thanatopraxie"; - } +// --- DECEASED LOGIC --- +const toggleDeceasedMode = () => { + deceasedForm.value.is_existing = !deceasedForm.value.is_existing; + if (!deceasedForm.value.is_existing) { + clearDeceasedSelection(); + } else { + deceasedSearchQuery.value = ""; + deceasedSearchResults.value = []; } - } -); - -// Helper methods for errors -const clearStepErrors = (stepIndex) => { - stepErrors.value[stepIndex] = []; - globalErrors.value = []; }; -const addStepError = (stepIndex, field, message) => { - stepErrors.value[stepIndex].push({ field, message }); +const handleDeceasedSearch = () => { + if (deceasedSearchTimeout) clearTimeout(deceasedSearchTimeout); + if (deceasedSearchQuery.value.length < 2) { + deceasedSearchResults.value = []; + return; + } + deceasedSearchTimeout = setTimeout(async () => { + try { + const results = await DeceasedService.searchDeceased(deceasedSearchQuery.value); + deceasedSearchResults.value = results; + } catch(e) { console.error(e); } + }, 300); }; -// Validations -const validateDeceasedStep = () => { - clearStepErrors(0); - let isValid = true; - if (!deceasedForm.value.is_existing && !deceasedForm.value.last_name) { - addStepError(0, "last_name", "Le nom est obligatoire."); - isValid = false; - } - if (deceasedForm.value.is_existing && !deceasedForm.value.id) { - addStepError(0, "deceased_id", "Veuillez sélectionner un défunt."); - isValid = false; - } - return isValid; +const selectDeceased = (d) => { + deceasedForm.value.id = d.id; + deceasedSearchQuery.value = `${d.first_name || ''} ${d.last_name}`; + deceasedSearchResults.value = []; + showDeceasedResults.value = false; + + // Clear any error for deceased + errors.value = errors.value.filter(e => e.field !== 'deceased_id'); }; -const validateClientStep = () => { - clearStepErrors(1); - let isValid = true; - if (!clientForm.value.name) { - addStepError(1, "name", "Le nom du client est obligatoire."); - isValid = false; - } - return isValid; -}; - -const validateLocationStep = () => { - clearStepErrors(2); - let isValid = true; - if (!locationForm.value.is_existing && !locationForm.value.city) { - addStepError(2, "city", "La ville est obligatoire."); - isValid = false; - } - if (locationForm.value.is_existing && !locationForm.value.id) { - addStepError(2, "location_id", "Veuillez sélectionner un lieu."); - isValid = false; - } - return isValid; -}; - -const validateProductStep = () => { - clearStepErrors(3); - let isValid = true; - if (!productForm.value.product_id) { - addStepError(3, "product_id", "Veuillez sélectionner un type de soins."); - isValid = false; - } - return isValid; -}; - -const validateDocumentsStep = () => { - clearStepErrors(4); - return true; // Optional for now -}; - -const validateInterventionStep = () => { - clearStepErrors(5); - let isValid = true; - if (!interventionForm.value.scheduled_at) { - addStepError( - 5, - "scheduled_at", - "La date de l'intervention est obligatoire." +const clearDeceasedSelection = () => { + deceasedForm.value.id = null; + deceasedSearchQuery.value = ""; + deceasedForm.value.first_name = ""; + deceasedForm.value.last_name = ""; + deceasedForm.value.birth_date = ""; + deceasedForm.value.death_date = ""; + deceasedSearchResults.value = []; + + // Clear errors + errors.value = errors.value.filter(e => + e.field !== 'deceased_id' && + e.field !== 'deceased.last_name' ); - isValid = false; - } - return isValid; }; -const validateAllSteps = () => { - const v0 = validateDeceasedStep(); - const v1 = validateClientStep(); - const v2 = validateLocationStep(); - const v3 = validateProductStep(); - const v4 = validateDocumentsStep(); - const v5 = validateInterventionStep(); - return v0 && v1 && v2 && v3 && v4 && v5; -}; - -// Navigation -const validateAndNext = async (stepIndex) => { - validatingStep.value = true; - // Simulate async validation if needed - await new Promise((resolve) => setTimeout(resolve, 300)); - - let isValid = false; - switch (stepIndex) { - case 0: - isValid = validateDeceasedStep(); - break; - case 1: - isValid = validateClientStep(); - break; - case 2: - isValid = validateLocationStep(); - break; - case 3: - isValid = validateProductStep(); - break; - case 4: - isValid = validateDocumentsStep(); - break; - default: - isValid = true; - } - - validatingStep.value = false; - if (isValid) { - if (activeStep.value < steps.length - 1) { - activeStep.value++; +// --- CLIENT LOGIC --- +const handleClientSearch = () => { + if (clientSearchTimeout) clearTimeout(clientSearchTimeout); + if (clientSearchQuery.value.length < 2) { + clientSearchResults.value = []; + return; } - } + clientSearchTimeout = setTimeout(async () => { + try { + const results = await ClientService.searchClients(clientSearchQuery.value); + clientSearchResults.value = results; + } catch(e) { console.error(e); } + }, 300); }; -const prevStep = () => { - if (activeStep.value > 0) { - activeStep.value--; - } +const selectClient = (c) => { + selectedClient.value = c; + clientSearchQuery.value = c.name + (c.email ? ` (${c.email})` : ''); + clientSearchResults.value = []; + showClientResults.value = false; + + // Clear any error for client + errors.value = errors.value.filter(e => e.field !== 'client'); }; -const goToStep = (index) => { - // Simple logic: allow going back, or going forward only if current step is valid? - // For now, let's just allow navigation like prevStep, but maybe restrict forward jumping unless validated. - // Implementation: restrict to <= activeStep to force sequential progress, or just allow it. - // Standard wizard behavior: usually allows clicking previous steps. - if (index < activeStep.value) { - activeStep.value = index; - } +const clearClientSelection = () => { + selectedClient.value = null; + clientSearchQuery.value = ""; + clientSearchResults.value = []; + + // Clear error + errors.value = errors.value.filter(e => e.field !== 'client'); }; -// File Handling -const handleFileUpload = (fileObj) => { - // fileObj = { file: File, type: string, preview: string } - uploadedFiles.value.push(fileObj); -}; - -const removeFile = (index) => { - uploadedFiles.value.splice(index, 1); -}; - -// Submit -const handleSubmit = async () => { - submitting.value = true; - try { - const isValid = validateAllSteps(); - if (!isValid) { - // Find first step with errors - for (let i = 0; i < stepErrors.value.length; i++) { - if (stepErrors.value[i].length > 0) { - activeStep.value = i; - break; - } - } - return; +// --- PRODUCT LOGIC --- +const handleProductSearch = () => { + const query = productSearchQuery.value.toLowerCase(); + if (!query) { + productSearchResults.value = allProducts.value; + return; } + productSearchResults.value = allProducts.value.filter(p => + p.nom.toLowerCase().includes(query) + ); +}; - const formData = new FormData(); +const selectProduct = (p) => { + productForm.value.product_id = p.id; + productSearchQuery.value = p.nom; + showProductResults.value = false; + + // Clear error + 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; + + // Clear error + errors.value = errors.value.filter(e => e.field !== 'product_id'); +}; + +// --- LOCATION LOGIC --- (FIXED) +const toggleLocationMode = () => { + locationForm.value.is_existing = !locationForm.value.is_existing; + if (!locationForm.value.is_existing) { + // Switching to create mode: clear search selection + clearLocationSelection(); + } else { + // Switching to search mode: clear create fields + locationForm.value.name = ""; + locationForm.value.city = ""; + locationSearchQuery.value = ""; + locationSearchResults.value = []; + } + + // Clear location errors + 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 || ""; + let display = loc.name || ""; + if (loc.city) display += ` (${loc.city})`; + locationSearchQuery.value = display; + showLocationResults.value = false; + locationSearchResults.value = []; + + // Clear error + errors.value = errors.value.filter(e => e.field !== 'location.name'); +}; + +const clearLocationSelection = () => { + locationForm.value.id = null; + locationForm.value.name = ""; + locationForm.value.city = ""; + locationSearchQuery.value = ""; + locationSearchResults.value = []; + showLocationResults.value = false; + + // Clear error + errors.value = errors.value.filter(e => e.field !== 'location.name'); +}; + +// --- VOICE INPUT --- +const toggleVoiceInput = (field) => { + alert(`Saisie vocale pour ${field} - Fonctionnalité à implémenter`); + // You would implement actual voice recognition here +}; + +// --- VALIDATION --- +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; + // Deceased - if (deceasedForm.value.is_existing && deceasedForm.value.id) { - formData.append("deceased_id", deceasedForm.value.id); - } else { - Object.keys(deceasedForm.value).forEach((key) => { - if (deceasedForm.value[key] != null && key !== 'id' && key !== 'is_existing') - formData.append(`deceased[${key}]`, deceasedForm.value[key]); - }); - } - // Client - Object.keys(clientForm.value).forEach((key) => { - if (clientForm.value[key] != null) - formData.append(`client[${key}]`, clientForm.value[key]); - }); - // Location - if (locationForm.value.is_existing && locationForm.value.id) { - formData.append("location_id", locationForm.value.id); - } else { - Object.keys(locationForm.value).forEach((key) => { - if (locationForm.value[key] != null && key !== 'id' && key !== 'is_existing') - formData.append(`location[${key}]`, locationForm.value[key]); - }); - } - // Intervention - Object.keys(interventionForm.value).forEach((key) => { - if (interventionForm.value[key] != null) { - let value = interventionForm.value[key]; - // Fix date format for scheduled_at - if (key === "scheduled_at" && value) { - value = value.replace("T", " "); - if (value.length === 16) { - value += ":00"; - } + 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; } - formData.append(`intervention[${key}]`, value); - } - }); - - // Product - if (productForm.value.product_id) { - formData.append("product_id", productForm.value.product_id); } - // Documents - uploadedFiles.value.forEach((fileObj, index) => { - formData.append(`documents[${index}][file]`, fileObj.file); - formData.append(`documents[${index}][type]`, fileObj.type || "other"); - }); + // Client + if (!selectedClient.value) { + errors.value.push({ field: 'client', message: "Veuillez sélectionner un client." }); + isValid = false; + } - emit("submit", formData); - } finally { - submitting.value = false; - } + // Intervention fields + 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; + } + + // Location + 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; }; +// --- SUBMIT --- +const handleSubmit = async () => { + if (submitting.value) return; + + if (!validate()) { + return; + } + + submitting.value = true; + globalErrors.value = []; + + try { + const formData = new FormData(); + + // Deceased + 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); + } + + // Client + 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) { + if (selectedClient.value.billing_address.line1) formData.append("client[billing_address_line1]", selectedClient.value.billing_address.line1); + if (selectedClient.value.billing_address.line2) formData.append("client[billing_address_line2]", selectedClient.value.billing_address.line2); + if (selectedClient.value.billing_address.postal_code) formData.append("client[billing_postal_code]", selectedClient.value.billing_address.postal_code); + if (selectedClient.value.billing_address.city) formData.append("client[billing_city]", selectedClient.value.billing_address.city); + if (selectedClient.value.billing_address.country_code) formData.append("client[billing_country_code]", selectedClient.value.billing_address.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); + } + + // Location + 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); + } + + // Product + if (productForm.value.product_id) { + formData.append("product_id", productForm.value.product_id); + } + + // Intervention + 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 for changes to clear errors +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'); + } +}); + defineExpose({ show, hide, @@ -492,96 +887,16 @@ defineExpose({ + \ No newline at end of file diff --git a/thanasoft-front/src/components/SoftTextarea.vue b/thanasoft-front/src/components/SoftTextarea.vue index 711efe2..e69de29 100644 --- a/thanasoft-front/src/components/SoftTextarea.vue +++ b/thanasoft-front/src/components/SoftTextarea.vue @@ -1,29 +0,0 @@ - - - diff --git a/thanasoft-front/src/components/molecules/location/LocationModal.vue b/thanasoft-front/src/components/molecules/location/LocationModal.vue index 93b8607..2cd205e 100644 --- a/thanasoft-front/src/components/molecules/location/LocationModal.vue +++ b/thanasoft-front/src/components/molecules/location/LocationModal.vue @@ -240,6 +240,8 @@ + +