fix(directory) : persist contacts/addresses on explicit save instead of on blur
Hold contact/address block edits in memory and persist them via explicit saveContacts/saveAddresses on click (with saving guards), matching the task forms. Keep immediate deletion. Minor restyle of blocks and action buttons.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-6 pt-6">
|
<div class="flex flex-col gap-6 pt-6">
|
||||||
<!-- Formulaire d'ajout / édition -->
|
<!-- Formulaire d'ajout / édition -->
|
||||||
<div v-if="isAdmin" class="grid grid-cols-2 gap-x-8 gap-y-3 rounded bg-white p-4 shadow">
|
<div v-if="isAdmin" class="grid grid-cols-2 gap-x-[44px] gap-y-4 rounded-lg bg-white px-7 py-5 shadow-[0_4px_4px_0_rgba(0,0,0,0.10)]">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
class="col-span-2"
|
class="col-span-2"
|
||||||
:label="$t('directory.reports.fields.subject')"
|
:label="$t('directory.reports.fields.subject')"
|
||||||
@@ -50,8 +50,8 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isAdmin" class="flex gap-2">
|
<div v-if="isAdmin" class="flex gap-2">
|
||||||
<MalioButtonIcon icon="mdi:pencil-outline" :aria-label="$t('common.edit')" @click="edit(report)" />
|
<MalioButtonIcon icon="mdi:pencil-outline" variant="ghost" :aria-label="$t('common.edit')" @click="edit(report)" />
|
||||||
<MalioButtonIcon icon="mdi:trash-can-outline" button-class="!text-red-600" :aria-label="$t('common.delete')" @click="remove(report.id)" />
|
<MalioButtonIcon icon="mdi:delete-outline" variant="ghost" :aria-label="$t('common.delete')" @click="remove(report.id)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="report.body" class="mt-2 whitespace-pre-wrap text-sm text-neutral-700">{{ report.body }}</p>
|
<p v-if="report.body" class="mt-2 whitespace-pre-wrap text-sm text-neutral-700">{{ report.body }}</p>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative grid grid-cols-2 gap-x-8 gap-y-3 rounded bg-white p-4 shadow">
|
<div class="relative grid grid-cols-2 gap-x-[44px] gap-y-4 rounded-lg bg-white px-7 py-5 shadow-[0_4px_4px_0_rgba(0,0,0,0.10)]">
|
||||||
<h3 class="col-span-2 text-sm font-semibold text-neutral-700">
|
<h3 class="col-span-2 text-sm font-semibold text-neutral-700">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h3>
|
</h3>
|
||||||
<MalioButtonIcon
|
<MalioButtonIcon
|
||||||
v-if="removable && !readonly"
|
v-if="removable && !readonly"
|
||||||
icon="mdi:trash-can-outline"
|
icon="mdi:delete-outline"
|
||||||
class="absolute right-2 top-2"
|
variant="ghost"
|
||||||
button-class="!text-red-600"
|
class="absolute right-3 top-3"
|
||||||
:aria-label="$t('common.delete')"
|
:aria-label="$t('common.delete')"
|
||||||
@click="$emit('remove')"
|
@click="$emit('remove')"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative grid grid-cols-2 gap-x-8 gap-y-3 rounded bg-white p-4 shadow">
|
<div class="relative grid grid-cols-2 gap-x-[44px] gap-y-4 rounded-lg bg-white px-7 py-5 shadow-[0_4px_4px_0_rgba(0,0,0,0.10)]">
|
||||||
<h3 class="col-span-2 text-sm font-semibold text-neutral-700">
|
<h3 class="col-span-2 text-sm font-semibold text-neutral-700">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h3>
|
</h3>
|
||||||
<MalioButtonIcon
|
<MalioButtonIcon
|
||||||
v-if="removable && !readonly"
|
v-if="removable && !readonly"
|
||||||
icon="mdi:trash-can-outline"
|
icon="mdi:delete-outline"
|
||||||
class="absolute right-2 top-2"
|
variant="ghost"
|
||||||
button-class="!text-red-600"
|
class="absolute right-3 top-3"
|
||||||
:aria-label="$t('common.delete')"
|
:aria-label="$t('common.delete')"
|
||||||
@click="$emit('remove')"
|
@click="$emit('remove')"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import { useAddressService } from '~/modules/directory/services/addresses'
|
|||||||
type Owner = { client?: string, prospect?: string }
|
type Owner = { client?: string, prospect?: string }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logique partagée des fiches détail Client/Prospect : gestion des blocs
|
* Logique partagée des fiches détail Client/Prospect : blocs répétables Contact
|
||||||
* répétables Contact et Adresse (chargement, ajout, édition par bloc avec
|
* et Adresse (chargement, ajout, suppression). L'édition est tenue en mémoire
|
||||||
* persistance immédiate, suppression). Paramétré par l'IRI du propriétaire
|
* localement ; la persistance se fait au clic sur « Enregistrer » (saveContacts/
|
||||||
* (`{ client }` ou `{ prospect }`), réutilisé tel quel par les deux pages.
|
* saveAddresses), comme les formulaires de tâche — pas d'enregistrement au blur.
|
||||||
|
* Paramétré par l'IRI du propriétaire (`{ client }` ou `{ prospect }`), réutilisé
|
||||||
|
* tel quel par les deux pages.
|
||||||
*/
|
*/
|
||||||
export function useDirectoryDetail(owner: Owner) {
|
export function useDirectoryDetail(owner: Owner) {
|
||||||
const contactService = useContactService()
|
const contactService = useContactService()
|
||||||
@@ -17,6 +19,8 @@ export function useDirectoryDetail(owner: Owner) {
|
|||||||
|
|
||||||
const contacts = ref<Contact[]>([])
|
const contacts = ref<Contact[]>([])
|
||||||
const addresses = ref<Address[]>([])
|
const addresses = ref<Address[]>([])
|
||||||
|
const savingContacts = ref(false)
|
||||||
|
const savingAddresses = ref(false)
|
||||||
|
|
||||||
function emptyContact(): Contact {
|
function emptyContact(): Contact {
|
||||||
return { id: 0, firstName: null, lastName: null, jobTitle: null, email: null, phonePrimary: null, phoneSecondary: null, ...owner }
|
return { id: 0, firstName: null, lastName: null, jobTitle: null, email: null, phonePrimary: null, phoneSecondary: null, ...owner }
|
||||||
@@ -25,54 +29,75 @@ export function useDirectoryDetail(owner: Owner) {
|
|||||||
return { id: 0, label: null, street: null, streetComplement: null, postalCode: null, city: null, country: 'FR', ...owner }
|
return { id: 0, label: null, street: null, streetComplement: null, postalCode: null, city: null, country: 'FR', ...owner }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onContactInput(index: number, value: Contact): Promise<void> {
|
// Édition locale uniquement : on remplace le bloc en mémoire, rien n'est
|
||||||
|
// persisté tant que l'utilisateur n'a pas cliqué sur « Enregistrer ».
|
||||||
|
function onContactInput(index: number, value: Contact): void {
|
||||||
contacts.value[index] = value
|
contacts.value[index] = value
|
||||||
await persistContact(index)
|
|
||||||
}
|
}
|
||||||
async function persistContact(index: number): Promise<void> {
|
function onAddressInput(index: number, value: Address): void {
|
||||||
const c = contacts.value[index]
|
addresses.value[index] = value
|
||||||
if (!c) return
|
|
||||||
const payload = { firstName: c.firstName, lastName: c.lastName, jobTitle: c.jobTitle, email: c.email, phonePrimary: c.phonePrimary, phoneSecondary: c.phoneSecondary, ...owner }
|
|
||||||
if (c.id && c.id > 0) {
|
|
||||||
await contactService.update(c.id, payload)
|
|
||||||
} else if (c.lastName || c.firstName) {
|
|
||||||
const created = await contactService.create(payload)
|
|
||||||
contacts.value[index] = created
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addContact(): void {
|
function addContact(): void {
|
||||||
contacts.value.push(emptyContact())
|
contacts.value.push(emptyContact())
|
||||||
}
|
}
|
||||||
|
function addAddress(): void {
|
||||||
|
addresses.value.push(emptyAddress())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suppression immédiate (comme la corbeille du formulaire de tâche) : un bloc
|
||||||
|
// déjà enregistré est supprimé côté serveur, une amorce non enregistrée est
|
||||||
|
// simplement retirée de la liste.
|
||||||
async function removeContact(index: number): Promise<void> {
|
async function removeContact(index: number): Promise<void> {
|
||||||
const c = contacts.value[index]
|
const c = contacts.value[index]
|
||||||
if (c?.id && c.id > 0) await contactService.remove(c.id)
|
if (c?.id && c.id > 0) await contactService.remove(c.id)
|
||||||
contacts.value.splice(index, 1)
|
contacts.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onAddressInput(index: number, value: Address): Promise<void> {
|
|
||||||
addresses.value[index] = value
|
|
||||||
await persistAddress(index)
|
|
||||||
}
|
|
||||||
async function persistAddress(index: number): Promise<void> {
|
|
||||||
const a = addresses.value[index]
|
|
||||||
if (!a) return
|
|
||||||
const payload = { label: a.label, street: a.street, streetComplement: a.streetComplement, postalCode: a.postalCode, city: a.city, country: a.country, ...owner }
|
|
||||||
if (a.id && a.id > 0) {
|
|
||||||
await addressService.update(a.id, payload)
|
|
||||||
} else if (a.street || a.city || a.postalCode) {
|
|
||||||
const created = await addressService.create(payload)
|
|
||||||
addresses.value[index] = created
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function addAddress(): void {
|
|
||||||
addresses.value.push(emptyAddress())
|
|
||||||
}
|
|
||||||
async function removeAddress(index: number): Promise<void> {
|
async function removeAddress(index: number): Promise<void> {
|
||||||
const a = addresses.value[index]
|
const a = addresses.value[index]
|
||||||
if (a?.id && a.id > 0) await addressService.remove(a.id)
|
if (a?.id && a.id > 0) await addressService.remove(a.id)
|
||||||
addresses.value.splice(index, 1)
|
addresses.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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<void> {
|
||||||
|
if (savingContacts.value) return
|
||||||
|
savingContacts.value = true
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < contacts.value.length; i++) {
|
||||||
|
const c = contacts.value[i]
|
||||||
|
if (!c) continue
|
||||||
|
const payload = { firstName: c.firstName, lastName: c.lastName, jobTitle: c.jobTitle, email: c.email, phonePrimary: c.phonePrimary, phoneSecondary: c.phoneSecondary, ...owner }
|
||||||
|
if (c.id && c.id > 0) {
|
||||||
|
contacts.value[i] = await contactService.update(c.id, payload)
|
||||||
|
} else if (c.lastName || c.firstName) {
|
||||||
|
contacts.value[i] = await contactService.create(payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
savingContacts.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function saveAddresses(): Promise<void> {
|
||||||
|
if (savingAddresses.value) return
|
||||||
|
savingAddresses.value = true
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < addresses.value.length; i++) {
|
||||||
|
const a = addresses.value[i]
|
||||||
|
if (!a) continue
|
||||||
|
const payload = { label: a.label, street: a.street, streetComplement: a.streetComplement, postalCode: a.postalCode, city: a.city, country: a.country, ...owner }
|
||||||
|
if (a.id && a.id > 0) {
|
||||||
|
addresses.value[i] = await addressService.update(a.id, payload)
|
||||||
|
} else if (a.street || a.city || a.postalCode) {
|
||||||
|
addresses.value[i] = await addressService.create(payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
savingAddresses.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function load(): Promise<void> {
|
async function load(): Promise<void> {
|
||||||
contacts.value = await contactService.getByOwner(owner)
|
contacts.value = await contactService.getByOwner(owner)
|
||||||
addresses.value = await addressService.getByOwner(owner)
|
addresses.value = await addressService.getByOwner(owner)
|
||||||
@@ -81,12 +106,16 @@ export function useDirectoryDetail(owner: Owner) {
|
|||||||
return {
|
return {
|
||||||
contacts,
|
contacts,
|
||||||
addresses,
|
addresses,
|
||||||
|
savingContacts,
|
||||||
|
savingAddresses,
|
||||||
onContactInput,
|
onContactInput,
|
||||||
addContact,
|
addContact,
|
||||||
removeContact,
|
removeContact,
|
||||||
|
saveContacts,
|
||||||
onAddressInput,
|
onAddressInput,
|
||||||
addAddress,
|
addAddress,
|
||||||
removeAddress,
|
removeAddress,
|
||||||
|
saveAddresses,
|
||||||
load,
|
load,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,22 @@
|
|||||||
@update:model-value="(v) => onContactInput(i, v)"
|
@update:model-value="(v) => onContactInput(i, v)"
|
||||||
@remove="removeContact(i)"
|
@remove="removeContact(i)"
|
||||||
/>
|
/>
|
||||||
<MalioButton
|
<div class="flex justify-center gap-3 pt-2">
|
||||||
icon-name="mdi:plus"
|
<MalioButton
|
||||||
icon-position="left"
|
variant="tertiary"
|
||||||
button-class="w-auto px-4"
|
icon-name="mdi:plus"
|
||||||
:label="$t('directory.contacts.add')"
|
icon-position="left"
|
||||||
@click="addContact"
|
button-class="w-auto px-4"
|
||||||
/>
|
:label="$t('directory.contacts.add')"
|
||||||
|
@click="addContact"
|
||||||
|
/>
|
||||||
|
<MalioButton
|
||||||
|
button-class="w-auto px-6"
|
||||||
|
:label="$t('common.save')"
|
||||||
|
:disabled="savingContacts"
|
||||||
|
@click="saveContacts"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -40,13 +49,22 @@
|
|||||||
@update:model-value="(v) => onAddressInput(i, v)"
|
@update:model-value="(v) => onAddressInput(i, v)"
|
||||||
@remove="removeAddress(i)"
|
@remove="removeAddress(i)"
|
||||||
/>
|
/>
|
||||||
<MalioButton
|
<div class="flex justify-center gap-3 pt-2">
|
||||||
icon-name="mdi:plus"
|
<MalioButton
|
||||||
icon-position="left"
|
variant="tertiary"
|
||||||
button-class="w-auto px-4"
|
icon-name="mdi:plus"
|
||||||
:label="$t('directory.addresses.add')"
|
icon-position="left"
|
||||||
@click="addAddress"
|
button-class="w-auto px-4"
|
||||||
/>
|
:label="$t('directory.addresses.add')"
|
||||||
|
@click="addAddress"
|
||||||
|
/>
|
||||||
|
<MalioButton
|
||||||
|
button-class="w-auto px-6"
|
||||||
|
:label="$t('common.save')"
|
||||||
|
:disabled="savingAddresses"
|
||||||
|
@click="saveAddresses"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -76,12 +94,16 @@ const clientService = useClientService()
|
|||||||
const {
|
const {
|
||||||
contacts,
|
contacts,
|
||||||
addresses,
|
addresses,
|
||||||
|
savingContacts,
|
||||||
|
savingAddresses,
|
||||||
onContactInput,
|
onContactInput,
|
||||||
addContact,
|
addContact,
|
||||||
removeContact,
|
removeContact,
|
||||||
|
saveContacts,
|
||||||
onAddressInput,
|
onAddressInput,
|
||||||
addAddress,
|
addAddress,
|
||||||
removeAddress,
|
removeAddress,
|
||||||
|
saveAddresses,
|
||||||
load,
|
load,
|
||||||
} = useDirectoryDetail(owner)
|
} = useDirectoryDetail(owner)
|
||||||
|
|
||||||
|
|||||||
@@ -19,13 +19,22 @@
|
|||||||
@update:model-value="(v) => onContactInput(i, v)"
|
@update:model-value="(v) => onContactInput(i, v)"
|
||||||
@remove="removeContact(i)"
|
@remove="removeContact(i)"
|
||||||
/>
|
/>
|
||||||
<MalioButton
|
<div class="flex justify-center gap-3 pt-2">
|
||||||
icon-name="mdi:plus"
|
<MalioButton
|
||||||
icon-position="left"
|
variant="tertiary"
|
||||||
button-class="w-auto px-4"
|
icon-name="mdi:plus"
|
||||||
:label="$t('directory.contacts.add')"
|
icon-position="left"
|
||||||
@click="addContact"
|
button-class="w-auto px-4"
|
||||||
/>
|
:label="$t('directory.contacts.add')"
|
||||||
|
@click="addContact"
|
||||||
|
/>
|
||||||
|
<MalioButton
|
||||||
|
button-class="w-auto px-6"
|
||||||
|
:label="$t('common.save')"
|
||||||
|
:disabled="savingContacts"
|
||||||
|
@click="saveContacts"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -40,13 +49,22 @@
|
|||||||
@update:model-value="(v) => onAddressInput(i, v)"
|
@update:model-value="(v) => onAddressInput(i, v)"
|
||||||
@remove="removeAddress(i)"
|
@remove="removeAddress(i)"
|
||||||
/>
|
/>
|
||||||
<MalioButton
|
<div class="flex justify-center gap-3 pt-2">
|
||||||
icon-name="mdi:plus"
|
<MalioButton
|
||||||
icon-position="left"
|
variant="tertiary"
|
||||||
button-class="w-auto px-4"
|
icon-name="mdi:plus"
|
||||||
:label="$t('directory.addresses.add')"
|
icon-position="left"
|
||||||
@click="addAddress"
|
button-class="w-auto px-4"
|
||||||
/>
|
:label="$t('directory.addresses.add')"
|
||||||
|
@click="addAddress"
|
||||||
|
/>
|
||||||
|
<MalioButton
|
||||||
|
button-class="w-auto px-6"
|
||||||
|
:label="$t('common.save')"
|
||||||
|
:disabled="savingAddresses"
|
||||||
|
@click="saveAddresses"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -76,12 +94,16 @@ const prospectService = useProspectService()
|
|||||||
const {
|
const {
|
||||||
contacts,
|
contacts,
|
||||||
addresses,
|
addresses,
|
||||||
|
savingContacts,
|
||||||
|
savingAddresses,
|
||||||
onContactInput,
|
onContactInput,
|
||||||
addContact,
|
addContact,
|
||||||
removeContact,
|
removeContact,
|
||||||
|
saveContacts,
|
||||||
onAddressInput,
|
onAddressInput,
|
||||||
addAddress,
|
addAddress,
|
||||||
removeAddress,
|
removeAddress,
|
||||||
|
saveAddresses,
|
||||||
load,
|
load,
|
||||||
} = useDirectoryDetail(owner)
|
} = useDirectoryDetail(owner)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user