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:
@@ -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
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user