feat(directory) : entête Action, hover, modal de conversion et ajustements UI (LST-72)
Pull Request — Quality gate / Frontend (build) (pull_request) Successful in 40s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 1m9s

- Colonne « Action » avec entête (alignée à droite) sur les 3 tableaux
- Feedback hover sur les boutons d'action (poubelle / convertir)
- Conversion prospect → client passe par une modal de confirmation
- ConfirmModal basé sur MalioModal (design Starseed), remplace ConfirmDeleteModal
- Nom (client/prospect/prestataire) en gras dans les modals via <i18n-t>
- Boutons « Ajouter » : label raccourci + taille standard Malio (180px)
- Barres d'outils à hauteur homogène (48px) : le bouton ne saute plus entre onglets

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 15:43:49 +02:00
parent be079cdbe2
commit 6b65839061
4 changed files with 131 additions and 88 deletions
@@ -9,12 +9,11 @@
<!-- Clients -->
<template #clients>
<div class="flex min-h-[30rem] flex-col gap-4 pt-10">
<div class="flex items-center justify-end">
<div class="flex min-h-[48px] items-center justify-end">
<MalioButton
icon-name="mdi:plus"
icon-position="left"
button-class="w-auto px-4"
:label="$t('directory.clients.add')"
:label="$t('common.add')"
@click="openCreateClient"
/>
</div>
@@ -26,6 +25,9 @@
:empty-message="$t('directory.clients.empty')"
@row-click="openEditClient"
>
<template #header-actions>
<span class="block text-right font-semibold text-black">{{ $t('common.actions') }}</span>
</template>
<template #cell-email="{ item }">
{{ (item as Client).email ?? '—' }}
</template>
@@ -37,7 +39,7 @@
<MalioButtonIcon
icon="mdi:trash-can-outline"
:aria-label="$t('common.delete')"
button-class="!bg-red-100 !text-red-700"
button-class="!bg-red-100 !text-red-700 hover:!bg-red-200"
:icon-size="18"
@click="askDeleteClient(item as Client)"
/>
@@ -50,7 +52,7 @@
<!-- Prospects -->
<template #prospects>
<div class="flex min-h-[30rem] flex-col gap-4 pt-10">
<div class="flex flex-wrap items-end justify-between gap-3">
<div class="flex min-h-[48px] flex-wrap items-center justify-between gap-3">
<MalioSelect
v-model="statusFilter"
:label="$t('prospects.fields.status')"
@@ -61,8 +63,7 @@
<MalioButton
icon-name="mdi:plus"
icon-position="left"
button-class="w-auto px-4"
:label="$t('directory.prospects.add')"
:label="$t('common.add')"
@click="openCreateProspect"
/>
</div>
@@ -74,6 +75,9 @@
:empty-message="$t('directory.prospects.empty')"
@row-click="openEditProspect"
>
<template #header-actions>
<span class="block text-right font-semibold text-black">{{ $t('common.actions') }}</span>
</template>
<template #cell-status="{ item }">
<StatusBadge
:label="statusLabel((item as ProspectRow).status)"
@@ -92,14 +96,14 @@
v-if="!(item as ProspectRow).convertedClient"
icon="mdi:account-convert"
:aria-label="$t('prospects.convert')"
button-class="!bg-green-100 !text-green-700"
button-class="!bg-green-100 !text-green-700 hover:!bg-green-200"
:icon-size="18"
@click="convertProspect(item as ProspectRow)"
@click="askConvertProspect(item as ProspectRow)"
/>
<MalioButtonIcon
icon="mdi:trash-can-outline"
:aria-label="$t('common.delete')"
button-class="!bg-red-100 !text-red-700"
button-class="!bg-red-100 !text-red-700 hover:!bg-red-200"
:icon-size="18"
@click="askDeleteProspect(item as ProspectRow)"
/>
@@ -111,12 +115,11 @@
<!-- Prestataires -->
<template #prestataires>
<div class="flex min-h-[30rem] flex-col gap-4 pt-10">
<div class="flex items-center justify-end">
<div class="flex min-h-[48px] items-center justify-end">
<MalioButton
icon-name="mdi:plus"
icon-position="left"
button-class="w-auto px-4"
:label="$t('directory.prestataires.add')"
:label="$t('common.add')"
@click="openCreatePrestataire"
/>
</div>
@@ -128,6 +131,9 @@
:empty-message="$t('directory.prestataires.empty')"
@row-click="openEditPrestataire"
>
<template #header-actions>
<span class="block text-right font-semibold text-black">{{ $t('common.actions') }}</span>
</template>
<template #cell-email="{ item }">
{{ (item as Prestataire).email ?? '—' }}
</template>
@@ -139,7 +145,7 @@
<MalioButtonIcon
icon="mdi:trash-can-outline"
:aria-label="$t('common.delete')"
button-class="!bg-red-100 !text-red-700"
button-class="!bg-red-100 !text-red-700 hover:!bg-red-200"
:icon-size="18"
@click="askDeletePrestataire(item as Prestataire)"
/>
@@ -166,12 +172,31 @@
@saved="loadPrestataires"
/>
<ConfirmDeleteModal
<ConfirmModal
v-model="deleteModalOpen"
:title="deleteModalTitle"
:message="deleteModalMessage"
@confirm="confirmDelete"
/>
>
<i18n-t :keypath="deleteModalKeypath" tag="p" scope="global">
<template #name>
<strong class="font-semibold">{{ deleteTargetName }}</strong>
</template>
</i18n-t>
</ConfirmModal>
<ConfirmModal
v-model="convertModalOpen"
:title="$t('prospects.convertConfirmTitle')"
:confirm-label="$t('prospects.convertConfirm')"
confirm-variant="primary"
@confirm="confirmConvert"
>
<i18n-t keypath="prospects.convertConfirmMessage" tag="p" scope="global">
<template #name>
<strong class="font-semibold">{{ convertTarget?.company }}</strong>
</template>
</i18n-t>
</ConfirmModal>
</div>
</div>
</template>
@@ -220,7 +245,7 @@ const clientColumns = [
{ key: 'name', label: t('prospects.fields.company') },
{ key: 'email', label: t('prospects.fields.email') },
{ key: 'phone', label: t('prospects.fields.phone') },
{ key: 'actions', label: '' },
{ key: 'actions', label: t('common.actions') },
]
async function loadClients() {
@@ -255,7 +280,7 @@ const prospectColumns = [
{ key: 'status', label: t('prospects.fields.status') },
{ key: 'email', label: t('prospects.fields.email') },
{ key: 'phone', label: t('prospects.fields.phone') },
{ key: 'actions', label: '' },
{ key: 'actions', label: t('common.actions') },
]
const prospectRows = computed<ProspectRow[]>(() => prospects.value)
@@ -294,10 +319,23 @@ function openEditProspect(item: Record<string, unknown>) {
navigateToDetail(`/directory/prospects/${(item as Prospect).id}`)
}
async function convertProspect(row: ProspectRow) {
// La conversion passe par une modal de confirmation (le prospect devient client).
const convertModalOpen = ref(false)
const convertTarget = ref<ProspectRow | null>(null)
function askConvertProspect(row: ProspectRow) {
convertTarget.value = row
convertModalOpen.value = true
}
async function confirmConvert() {
const row = convertTarget.value
if (!row) return
await prospectService.convert(row.id)
// La conversion crée un client et retire le prospect : rafraîchir les deux listes.
await Promise.all([loadProspects(), loadClients()])
convertModalOpen.value = false
convertTarget.value = null
}
// Le ProspectDrawer porte aussi le bouton « Convertir » : son event 'saved' peut
@@ -315,7 +353,7 @@ const prestataireColumns = [
{ key: 'name', label: t('prospects.fields.company') },
{ key: 'email', label: t('prospects.fields.email') },
{ key: 'phone', label: t('prospects.fields.phone') },
{ key: 'actions', label: '' },
{ key: 'actions', label: t('common.actions') },
]
async function loadPrestataires() {
@@ -351,17 +389,22 @@ const deleteModalTitle = computed(() => {
}
})
const deleteModalMessage = computed(() => {
// Clé i18n du message (le nom y est injecté en gras via <i18n-t> côté template).
const deleteModalKeypath = computed(() => {
switch (deleteTarget.value?.type) {
case 'prospect':
return 'prospects.deleteConfirmMessage'
case 'prestataire':
return 'prestataires.deleteConfirmMessage'
default:
return 'clients.deleteConfirmMessage'
}
})
const deleteTargetName = computed(() => {
const target = deleteTarget.value
if (!target) return ''
switch (target.type) {
case 'prospect':
return t('prospects.deleteConfirmMessage', { name: target.item.company })
case 'prestataire':
return t('prestataires.deleteConfirmMessage', { name: target.item.name })
default:
return t('clients.deleteConfirmMessage', { name: target.item.name })
}
return target.type === 'prospect' ? target.item.company : target.item.name
})
function askDeleteClient(item: Client) {