feat(directory) : add client detail page with contact/address/report tabs
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import type { Contact } from '~/modules/directory/services/dto/contact'
|
||||
import type { Address } from '~/modules/directory/services/dto/address'
|
||||
import { useContactService } from '~/modules/directory/services/contacts'
|
||||
import { useAddressService } from '~/modules/directory/services/addresses'
|
||||
|
||||
type Owner = { client?: string, prospect?: string }
|
||||
|
||||
/**
|
||||
* Logique partagée des fiches détail Client/Prospect : gestion des blocs
|
||||
* répétables Contact et Adresse (chargement, ajout, édition par bloc avec
|
||||
* persistance immédiate, suppression). Paramétré par l'IRI du propriétaire
|
||||
* (`{ client }` ou `{ prospect }`), réutilisé tel quel par les deux pages.
|
||||
*/
|
||||
export function useDirectoryDetail(owner: Owner) {
|
||||
const contactService = useContactService()
|
||||
const addressService = useAddressService()
|
||||
|
||||
const contacts = ref<Contact[]>([])
|
||||
const addresses = ref<Address[]>([])
|
||||
|
||||
function emptyContact(): Contact {
|
||||
return { id: 0, firstName: null, lastName: null, jobTitle: null, email: null, phonePrimary: null, phoneSecondary: null, ...owner }
|
||||
}
|
||||
function emptyAddress(): Address {
|
||||
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> {
|
||||
contacts.value[index] = value
|
||||
await persistContact(index)
|
||||
}
|
||||
async function persistContact(index: number): Promise<void> {
|
||||
const c = contacts.value[index]
|
||||
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 {
|
||||
contacts.value.push(emptyContact())
|
||||
}
|
||||
async function removeContact(index: number): Promise<void> {
|
||||
const c = contacts.value[index]
|
||||
if (c?.id && c.id > 0) await contactService.remove(c.id)
|
||||
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> {
|
||||
const a = addresses.value[index]
|
||||
if (a?.id && a.id > 0) await addressService.remove(a.id)
|
||||
addresses.value.splice(index, 1)
|
||||
}
|
||||
|
||||
async function load(): Promise<void> {
|
||||
contacts.value = await contactService.getByOwner(owner)
|
||||
addresses.value = await addressService.getByOwner(owner)
|
||||
}
|
||||
|
||||
return {
|
||||
contacts,
|
||||
addresses,
|
||||
onContactInput,
|
||||
addContact,
|
||||
removeContact,
|
||||
onAddressInput,
|
||||
addAddress,
|
||||
removeAddress,
|
||||
load,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex items-center gap-3 pt-4">
|
||||
<MalioButtonIcon icon="mdi:arrow-left" :aria-label="$t('common.back')" @click="goBack" />
|
||||
<h1 class="text-2xl font-bold text-neutral-900">{{ client?.name ?? '…' }}</h1>
|
||||
</div>
|
||||
|
||||
<p v-if="loading">{{ $t('common.loading') }}</p>
|
||||
<template v-else-if="client">
|
||||
<MalioTabList v-model="activeTab" :tabs="tabs">
|
||||
<template #contact>
|
||||
<div class="flex flex-col gap-4 pt-6">
|
||||
<DirectoryContactBlock
|
||||
v-for="(contact, i) in contacts"
|
||||
:key="contact.id || `new-${i}`"
|
||||
:model-value="contact"
|
||||
:title="$t('directory.contacts.item', { n: i + 1 })"
|
||||
:removable="contacts.length > 0"
|
||||
@update:model-value="(v) => onContactInput(i, v)"
|
||||
@remove="removeContact(i)"
|
||||
/>
|
||||
<MalioButton
|
||||
icon-name="mdi:plus"
|
||||
icon-position="left"
|
||||
button-class="w-auto px-4"
|
||||
:label="$t('directory.contacts.add')"
|
||||
@click="addContact"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #address>
|
||||
<div class="flex flex-col gap-4 pt-6">
|
||||
<DirectoryAddressBlock
|
||||
v-for="(address, i) in addresses"
|
||||
:key="address.id || `new-${i}`"
|
||||
:model-value="address"
|
||||
:title="$t('directory.addresses.item', { n: i + 1 })"
|
||||
:removable="addresses.length > 0"
|
||||
@update:model-value="(v) => onAddressInput(i, v)"
|
||||
@remove="removeAddress(i)"
|
||||
/>
|
||||
<MalioButton
|
||||
icon-name="mdi:plus"
|
||||
icon-position="left"
|
||||
button-class="w-auto px-4"
|
||||
:label="$t('directory.addresses.add')"
|
||||
@click="addAddress"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #report>
|
||||
<CommercialReportTab :owner="owner" :is-admin="true" />
|
||||
</template>
|
||||
</MalioTabList>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Client } from '~/modules/directory/services/dto/client'
|
||||
import { useClientService } from '~/modules/directory/services/clients'
|
||||
|
||||
definePageMeta({ middleware: ['admin'] })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
const id = Number(route.params.id)
|
||||
const ownerIri = `/api/clients/${id}`
|
||||
const owner = { client: ownerIri }
|
||||
|
||||
const clientService = useClientService()
|
||||
const {
|
||||
contacts,
|
||||
addresses,
|
||||
onContactInput,
|
||||
addContact,
|
||||
removeContact,
|
||||
onAddressInput,
|
||||
addAddress,
|
||||
removeAddress,
|
||||
load,
|
||||
} = useDirectoryDetail(owner)
|
||||
|
||||
const client = ref<Client | null>(null)
|
||||
const loading = ref(true)
|
||||
|
||||
const activeTab = ref('contact')
|
||||
const tabs = [
|
||||
{ key: 'contact', label: t('directory.tabs.contact'), icon: 'mdi:account-outline' },
|
||||
{ key: 'address', label: t('directory.tabs.address'), icon: 'mdi:map-marker-outline' },
|
||||
{ key: 'report', label: t('directory.tabs.report'), icon: 'mdi:file-document-outline' },
|
||||
]
|
||||
|
||||
function goBack(): void {
|
||||
router.push('/directory')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
client.value = await clientService.getById(id)
|
||||
await load()
|
||||
loading.value = false
|
||||
})
|
||||
</script>
|
||||
@@ -10,6 +10,10 @@ export function useClientService() {
|
||||
return extractHydraMembers(data)
|
||||
}
|
||||
|
||||
async function getById(id: number): Promise<Client> {
|
||||
return api.get<Client>(`/clients/${id}`)
|
||||
}
|
||||
|
||||
async function create(payload: ClientWrite): Promise<Client> {
|
||||
return api.post<Client>('/clients', payload as Record<string, unknown>, {
|
||||
toastSuccessKey: 'clients.created',
|
||||
@@ -28,5 +32,5 @@ export function useClientService() {
|
||||
})
|
||||
}
|
||||
|
||||
return { getAll, create, update, remove }
|
||||
return { getAll, getById, create, update, remove }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user