feat(directory) : add editable Information tab on client/prospect detail

Add an Information tab (first, active by default) to the client and prospect
detail pages so base fields can be edited directly from the record. Client:
name/email/phone. Prospect: name/company/status/email/phone/source/notes.
Fields are edited in memory and persisted only on explicit save (PATCH),
matching the Contact/Address tabs pattern.
This commit is contained in:
Matthieu
2026-06-24 09:07:13 +02:00
parent 903030afbc
commit 3fe108d38a
3 changed files with 174 additions and 3 deletions
@@ -8,6 +8,39 @@
<p v-if="loading">{{ $t('common.loading') }}</p>
<template v-else-if="client">
<MalioTabList v-model="activeTab" :tabs="tabs">
<template #info>
<div class="flex flex-col gap-4 pt-6">
<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)]">
<MalioInputText
class="col-span-2"
:model-value="info.name"
:label="$t('directory.info.fields.name')"
:error="infoTouched.name && !info.name.trim() ? $t('prospects.validation.nameRequired') : ''"
@update:model-value="info.name = $event"
@blur="infoTouched.name = true"
/>
<MalioInputText
:model-value="info.email"
:label="$t('directory.info.fields.email')"
@update:model-value="info.email = $event"
/>
<MalioInputText
:model-value="info.phone"
:label="$t('directory.info.fields.phone')"
@update:model-value="info.phone = $event"
/>
</div>
<div class="flex justify-center pt-2">
<MalioButton
button-class="w-auto px-6"
:label="$t('common.save')"
:disabled="savingInfo"
@click="saveInfo"
/>
</div>
</div>
</template>
<template #contact>
<div class="flex flex-col gap-4 pt-6">
<DirectoryContactBlock
@@ -110,19 +143,44 @@ const {
const client = ref<Client | null>(null)
const loading = ref(true)
const activeTab = ref('contact')
const activeTab = ref('info')
const tabs = [
{ key: 'info', label: t('directory.tabs.info'), icon: 'mdi:information-outline' },
{ 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' },
]
// Champs de base de la fiche, édités en mémoire et persistés au clic sur
// « Enregistrer » (PATCH), comme les onglets Contact/Adresse.
const info = reactive({ name: '', email: '', phone: '' })
const infoTouched = reactive({ name: false })
const savingInfo = ref(false)
async function saveInfo(): Promise<void> {
infoTouched.name = true
if (!info.name.trim() || savingInfo.value) return
savingInfo.value = true
try {
client.value = await clientService.update(id, {
name: info.name.trim(),
email: info.email.trim() || null,
phone: info.phone.trim() || null,
})
} finally {
savingInfo.value = false
}
}
function goBack(): void {
router.push('/directory')
}
onMounted(async () => {
client.value = await clientService.getById(id)
info.name = client.value.name ?? ''
info.email = client.value.email ?? ''
info.phone = client.value.phone ?? ''
await load()
loading.value = false
})
@@ -8,6 +8,62 @@
<p v-if="loading">{{ $t('common.loading') }}</p>
<template v-else-if="prospect">
<MalioTabList v-model="activeTab" :tabs="tabs">
<template #info>
<div class="flex flex-col gap-4 pt-6">
<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)]">
<MalioInputText
class="col-span-2"
:model-value="info.name"
:label="$t('prospects.fields.name')"
:error="infoTouched.name && !info.name.trim() ? $t('prospects.validation.nameRequired') : ''"
@update:model-value="info.name = $event"
@blur="infoTouched.name = true"
/>
<MalioInputText
:model-value="info.company"
:label="$t('prospects.fields.company')"
@update:model-value="info.company = $event"
/>
<MalioSelect
v-model="info.status"
:label="$t('prospects.fields.status')"
:options="statusOptions"
group-class="w-full"
/>
<MalioInputText
:model-value="info.email"
:label="$t('prospects.fields.email')"
@update:model-value="info.email = $event"
/>
<MalioInputText
:model-value="info.phone"
:label="$t('prospects.fields.phone')"
@update:model-value="info.phone = $event"
/>
<MalioInputText
class="col-span-2"
:model-value="info.source"
:label="$t('prospects.fields.source')"
@update:model-value="info.source = $event"
/>
<MalioInputTextArea
class="col-span-2"
:model-value="info.notes"
:label="$t('prospects.fields.notes')"
@update:model-value="info.notes = $event"
/>
</div>
<div class="flex justify-center pt-2">
<MalioButton
button-class="w-auto px-6"
:label="$t('common.save')"
:disabled="savingInfo"
@click="saveInfo"
/>
</div>
</div>
</template>
<template #contact>
<div class="flex flex-col gap-4 pt-6">
<DirectoryContactBlock
@@ -77,7 +133,7 @@
</template>
<script setup lang="ts">
import type { Prospect } from '~/modules/directory/services/dto/prospect'
import type { Prospect, ProspectStatus } from '~/modules/directory/services/dto/prospect'
import { useProspectService } from '~/modules/directory/services/prospects'
definePageMeta({ middleware: ['admin'] })
@@ -110,19 +166,68 @@ const {
const prospect = ref<Prospect | null>(null)
const loading = ref(true)
const activeTab = ref('contact')
const activeTab = ref('info')
const tabs = [
{ key: 'info', label: t('directory.tabs.info'), icon: 'mdi:information-outline' },
{ 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' },
]
const statusOptions = [
{ label: t('prospects.status.new'), value: 'new' },
{ label: t('prospects.status.contacted'), value: 'contacted' },
{ label: t('prospects.status.qualified'), value: 'qualified' },
{ label: t('prospects.status.won'), value: 'won' },
{ label: t('prospects.status.lost'), value: 'lost' },
]
// Champs de base de la fiche, édités en mémoire et persistés au clic sur
// « Enregistrer » (PATCH), comme les onglets Contact/Adresse.
const info = reactive<{
name: string
company: string
email: string
phone: string
status: ProspectStatus
source: string
notes: string
}>({ name: '', company: '', email: '', phone: '', status: 'new', source: '', notes: '' })
const infoTouched = reactive({ name: false })
const savingInfo = ref(false)
async function saveInfo(): Promise<void> {
infoTouched.name = true
if (!info.name.trim() || savingInfo.value) return
savingInfo.value = true
try {
prospect.value = await prospectService.update(id, {
name: info.name.trim(),
company: info.company.trim() || null,
email: info.email.trim() || null,
phone: info.phone.trim() || null,
status: info.status,
source: info.source.trim() || null,
notes: info.notes.trim() || null,
})
} finally {
savingInfo.value = false
}
}
function goBack(): void {
router.push('/directory')
}
onMounted(async () => {
prospect.value = await prospectService.getById(id)
info.name = prospect.value.name ?? ''
info.company = prospect.value.company ?? ''
info.email = prospect.value.email ?? ''
info.phone = prospect.value.phone ?? ''
info.status = prospect.value.status ?? 'new'
info.source = prospect.value.source ?? ''
info.notes = prospect.value.notes ?? ''
await load()
loading.value = false
})