diff --git a/frontend/components/ui/ConfirmDeleteReportModal.vue b/frontend/components/ui/ConfirmDeleteReportModal.vue deleted file mode 100644 index 5010d48..0000000 --- a/frontend/components/ui/ConfirmDeleteReportModal.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - - - diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index dd42cb2..9690649 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -1004,10 +1004,12 @@ "empty": "Aucun prestataire trouvé." }, "contacts": { - "add": "Ajouter un contact", + "add": "Nouveau contact", "item": "Contact {n}", "saved": "Contact enregistré.", "deleted": "Contact supprimé.", + "deleteConfirmTitle": "Supprimer le contact", + "deleteConfirmMessage": "Êtes-vous sûr de vouloir supprimer ce contact ? Cette action est irréversible.", "fields": { "lastName": "Nom", "firstName": "Prénom", @@ -1018,11 +1020,14 @@ } }, "addresses": { - "add": "Ajouter une adresse", + "add": "Nouvelle adresse", "item": "Adresse {n}", "saved": "Adresse enregistrée.", "deleted": "Adresse supprimée.", + "deleteConfirmTitle": "Supprimer l'adresse", + "deleteConfirmMessage": "Êtes-vous sûr de vouloir supprimer cette adresse ? Cette action est irréversible.", "streetNotFound": "Aucune adresse trouvée — saisie libre possible.", + "streetHint": "Renseignez d'abord le code postal et la ville.", "autocompleteUnavailable": "Recherche d'adresse indisponible : saisissez l'adresse manuellement.", "fields": { "label": "Libellé", @@ -1044,6 +1049,8 @@ "deleted": "Compte-rendu supprimé.", "confirmDeleteTitle": "Supprimer ce compte-rendu ?", "confirmDeleteMessage": "Cette action est irréversible. Les documents joints seront également supprimés.", + "documentDeleteTitle": "Supprimer le document", + "documentDeleteMessage": "Êtes-vous sûr de vouloir supprimer ce document ? Cette action est irréversible.", "fields": { "subject": "Objet", "type": "Type d'échange", diff --git a/frontend/modules/directory/components/CommercialReportTab.vue b/frontend/modules/directory/components/CommercialReportTab.vue index 7a61e51..7a0a3e3 100644 --- a/frontend/modules/directory/components/CommercialReportTab.vue +++ b/frontend/modules/directory/components/CommercialReportTab.vue @@ -9,8 +9,7 @@ v-if="canManage" icon-name="mdi:plus" icon-position="left" - button-class="w-auto px-4" - :label="$t('directory.reports.add')" + :label="$t('common.add')" @click="openCreate" /> @@ -108,7 +107,7 @@ v-if="report.documents?.length" :documents="report.documents" :can-manage="canManage" - @delete="(docId) => removeDocument(docId)" + @delete="(docId) => askDeleteDocument(docId)" /> - + @@ -158,6 +164,11 @@ const confirmOpen = ref(false) const pendingDelete = ref(null) const deleting = ref(false) +// Suppression d'un document joint : passe désormais par une modal de confirmation. +const docConfirmOpen = ref(false) +const pendingDocId = ref(null) +const deletingDoc = ref(false) + // Le plus récent en haut (l'API ne garantit pas l'ordre). const sortedReports = computed(() => [...reports.value].sort((a, b) => b.occurredAt.localeCompare(a.occurredAt)), @@ -222,9 +233,22 @@ async function confirmDelete(): Promise { } } -async function removeDocument(id: number): Promise { - await documentService.remove(id) - await reload() +function askDeleteDocument(id: number): void { + pendingDocId.value = id + docConfirmOpen.value = true +} + +async function confirmDeleteDocument(): Promise { + if (pendingDocId.value === null || deletingDoc.value) return + deletingDoc.value = true + try { + await documentService.remove(pendingDocId.value) + docConfirmOpen.value = false + pendingDocId.value = null + await reload() + } finally { + deletingDoc.value = false + } } async function reload(): Promise { diff --git a/frontend/modules/directory/components/DirectoryAddressBlock.vue b/frontend/modules/directory/components/DirectoryAddressBlock.vue index c5f47dd..8e6fbdb 100644 --- a/frontend/modules/directory/components/DirectoryAddressBlock.vue +++ b/frontend/modules/directory/components/DirectoryAddressBlock.vue @@ -3,7 +3,8 @@ (pas de bordure sous le dernier bloc), comme sur Starseed. -->
-

{{ title }}

+ +

{{ blockTitle }}

-
- +
+ - -
- + + + + -
- + +
+ + +
- - - - - +
@@ -118,6 +123,16 @@ const addressOptions = ref([]) const fetchedCityOptions = ref([]) const addressLoading = ref(false) +// Titre du bloc : le libellé saisi prime ; repli sur « Adresse N » (prop `title`). +const blockTitle = computed(() => (props.modelValue.label ?? '').trim() || props.title) + +// La rue n'est éditable qu'une fois le code postal (5 chiffres) ET la ville +// renseignés — conditionnement métier repris de Starseed. +const canEditStreet = computed(() => { + const digits = (props.modelValue.postalCode ?? '').replace(/\D/g, '') + return digits.length === 5 && !!(props.modelValue.city ?? '').trim() +}) + // Le select Ville n'affiche que les valeurs présentes dans ses options : on // garantit donc que la ville déjà enregistrée (chargement d'une fiche) ou // pré-remplie par l'autocomplétion d'adresse figure toujours dans la liste, @@ -146,6 +161,23 @@ function notifyUnavailable(): void { toast.info({ title: '', message: t('directory.addresses.autocompleteUnavailable') }) } +/** + * Sélection d'une ville → vide rue + complément (devenus incohérents avec la + * nouvelle ville). Ne réagit qu'à un vrai changement de valeur. + */ +function onCityChange(value: string | number | null): void { + const next = value === null ? '' : String(value) + if (next === (props.modelValue.city ?? '')) return + addressOptions.value = [] + lastAddressSuggestions = [] + emit('update:modelValue', { + ...props.modelValue, + city: next === '' ? null : next, + street: null, + streetComplement: null, + }) +} + /** Recherche d'adresse assistée (event de MalioInputAutocomplete). */ async function onAddressSearch(query: string): Promise { if (query.trim().length < 3) { @@ -186,10 +218,30 @@ function onAddressSelect(option: Option | null): void { }) } -/** Saisie du code postal → met à jour le champ + interroge la BAN pour la ville. */ +/** + * Saisie du code postal → réinitialise ville/rue/complément quand le CP est + * complet (5 chiffres) ET réellement modifié, puis interroge la BAN pour les + * villes. Sinon simple mise à jour du champ (correction partielle). + */ async function onPostalCodeInput(value: string): Promise { - update('postalCode', value) const digits = (value ?? '').replace(/\D/g, '') + const previousDigits = (props.modelValue.postalCode ?? '').replace(/\D/g, '') + + if (digits.length === 5 && digits !== previousDigits) { + addressOptions.value = [] + lastAddressSuggestions = [] + emit('update:modelValue', { + ...props.modelValue, + postalCode: value, + city: null, + street: null, + streetComplement: null, + }) + } + else { + update('postalCode', value) + } + if (digits.length < 5) return try { const suggestions = await autocomplete.searchCity(digits) diff --git a/frontend/modules/directory/components/DirectoryContactBlock.vue b/frontend/modules/directory/components/DirectoryContactBlock.vue index 3303fe1..b6b1060 100644 --- a/frontend/modules/directory/components/DirectoryContactBlock.vue +++ b/frontend/modules/directory/components/DirectoryContactBlock.vue @@ -14,7 +14,7 @@ />
-
+
- - - diff --git a/frontend/modules/directory/components/ReportDocumentUpload.vue b/frontend/modules/directory/components/ReportDocumentUpload.vue index 14625bc..1b71361 100644 --- a/frontend/modules/directory/components/ReportDocumentUpload.vue +++ b/frontend/modules/directory/components/ReportDocumentUpload.vue @@ -1,20 +1,14 @@ @@ -25,21 +19,19 @@ const props = defineProps<{ reportId: number }>() const emit = defineEmits<{ uploaded: [] }>() const service = useReportDocumentService() -const fileInput = ref(null) +// Nom du fichier affiché par le champ Malio (v-model) ; réinitialisé après envoi. +const fileName = ref('') const uploading = ref(false) -async function onFileSelected(event: Event): Promise { - const input = event.target as HTMLInputElement - const file = input.files?.[0] - if (!file) return - +// L'upload se déclenche dès la sélection (event natif du composant Malio). +async function onFile(file: File): Promise { uploading.value = true try { await service.upload(props.reportId, file) emit('uploaded') } finally { uploading.value = false - input.value = '' + fileName.value = '' } } diff --git a/frontend/modules/directory/composables/useDirectoryDetail.ts b/frontend/modules/directory/composables/useDirectoryDetail.ts index 764e537..23caa16 100644 --- a/frontend/modules/directory/composables/useDirectoryDetail.ts +++ b/frontend/modules/directory/composables/useDirectoryDetail.ts @@ -14,6 +14,7 @@ type Owner = { client?: string, prospect?: string, prestataire?: string } * tel quel par les deux pages. */ export function useDirectoryDetail(owner: Owner) { + const { t } = useI18n() const contactService = useContactService() const addressService = useAddressService() @@ -59,6 +60,39 @@ export function useDirectoryDetail(owner: Owner) { addresses.value.splice(index, 1) } + // Confirmation de suppression d'un bloc (contact / adresse) : la corbeille du + // bloc ouvre une modal ; la suppression effective n'a lieu qu'à la confirmation. + const removeModalOpen = ref(false) + const pendingRemoval = ref<{ type: 'contact' | 'address', index: number } | null>(null) + + const removeModalTitle = computed(() => + pendingRemoval.value?.type === 'address' + ? t('directory.addresses.deleteConfirmTitle') + : t('directory.contacts.deleteConfirmTitle'), + ) + const removeModalMessage = computed(() => + pendingRemoval.value?.type === 'address' + ? t('directory.addresses.deleteConfirmMessage') + : t('directory.contacts.deleteConfirmMessage'), + ) + + function askRemoveContact(index: number): void { + pendingRemoval.value = { type: 'contact', index } + removeModalOpen.value = true + } + function askRemoveAddress(index: number): void { + pendingRemoval.value = { type: 'address', index } + removeModalOpen.value = true + } + async function confirmRemove(): Promise { + const p = pendingRemoval.value + if (!p) return + if (p.type === 'contact') await removeContact(p.index) + else await removeAddress(p.index) + removeModalOpen.value = false + pendingRemoval.value = null + } + // Persistance au clic : met à jour les blocs existants, crée les nouveaux // blocs renseignés. Les amorces vides (sans contenu) sont ignorées. async function saveContacts(): Promise { @@ -117,5 +151,12 @@ export function useDirectoryDetail(owner: Owner) { removeAddress, saveAddresses, load, + // Suppression de bloc avec confirmation (modal partagée contact/adresse). + removeModalOpen, + removeModalTitle, + removeModalMessage, + askRemoveContact, + askRemoveAddress, + confirmRemove, } } diff --git a/frontend/modules/directory/pages/directory/clients/[id].vue b/frontend/modules/directory/pages/directory/clients/[id].vue index c3f7614..d5a5990 100644 --- a/frontend/modules/directory/pages/directory/clients/[id].vue +++ b/frontend/modules/directory/pages/directory/clients/[id].vue @@ -2,7 +2,14 @@
- + {{ client?.name ?? '…' }} @@ -13,7 +20,7 @@
+ +
@@ -143,13 +154,17 @@ const { savingAddresses, onContactInput, addContact, - removeContact, + askRemoveContact, saveContacts, onAddressInput, addAddress, - removeAddress, + askRemoveAddress, saveAddresses, load, + removeModalOpen, + removeModalTitle, + removeModalMessage, + confirmRemove, } = useDirectoryDetail(owner) const { can } = usePermissions() diff --git a/frontend/modules/directory/pages/directory/prestataires/[id].vue b/frontend/modules/directory/pages/directory/prestataires/[id].vue index 745d940..4319fa6 100644 --- a/frontend/modules/directory/pages/directory/prestataires/[id].vue +++ b/frontend/modules/directory/pages/directory/prestataires/[id].vue @@ -2,7 +2,14 @@
- + {{ prestataire?.name ?? '…' }} @@ -13,7 +20,7 @@
+ +
@@ -143,13 +154,17 @@ const { savingAddresses, onContactInput, addContact, - removeContact, + askRemoveContact, saveContacts, onAddressInput, addAddress, - removeAddress, + askRemoveAddress, saveAddresses, load, + removeModalOpen, + removeModalTitle, + removeModalMessage, + confirmRemove, } = useDirectoryDetail(owner) const { can } = usePermissions() diff --git a/frontend/modules/directory/pages/directory/prospects/[id].vue b/frontend/modules/directory/pages/directory/prospects/[id].vue index 9697253..5fecdf5 100644 --- a/frontend/modules/directory/pages/directory/prospects/[id].vue +++ b/frontend/modules/directory/pages/directory/prospects/[id].vue @@ -2,7 +2,14 @@
- + {{ prospect?.company ?? '…' }} @@ -13,7 +20,7 @@
+ + @@ -158,13 +169,17 @@ const { savingAddresses, onContactInput, addContact, - removeContact, + askRemoveContact, saveContacts, onAddressInput, addAddress, - removeAddress, + askRemoveAddress, saveAddresses, load, + removeModalOpen, + removeModalTitle, + removeModalMessage, + confirmRemove, } = useDirectoryDetail(owner) const { can } = usePermissions()