feat(directory) : entête Action, hover, modal de conversion et ajustements UI (LST-72)
- 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:
@@ -424,6 +424,7 @@
|
|||||||
"edit": "Modifier",
|
"edit": "Modifier",
|
||||||
"delete": "Supprimer",
|
"delete": "Supprimer",
|
||||||
"add": "Ajouter",
|
"add": "Ajouter",
|
||||||
|
"actions": "Action",
|
||||||
"loading": "Chargement...",
|
"loading": "Chargement...",
|
||||||
"archived": "Archivé",
|
"archived": "Archivé",
|
||||||
"noClient": "Aucun client",
|
"noClient": "Aucun client",
|
||||||
@@ -918,6 +919,9 @@
|
|||||||
"editProspect": "Modifier un prospect",
|
"editProspect": "Modifier un prospect",
|
||||||
"convert": "Convertir en client",
|
"convert": "Convertir en client",
|
||||||
"alreadyConverted": "Déjà converti en client",
|
"alreadyConverted": "Déjà converti en client",
|
||||||
|
"convertConfirmTitle": "Convertir le prospect",
|
||||||
|
"convertConfirmMessage": "Êtes-vous sûr de vouloir convertir le prospect « {name} » en client ? Le prospect deviendra un client.",
|
||||||
|
"convertConfirm": "Convertir",
|
||||||
"deleteConfirmTitle": "Supprimer le prospect",
|
"deleteConfirmTitle": "Supprimer le prospect",
|
||||||
"deleteConfirmMessage": "Êtes-vous sûr de vouloir supprimer le prospect « {name} » ? Cette action est irréversible.",
|
"deleteConfirmMessage": "Êtes-vous sûr de vouloir supprimer le prospect « {name} » ? Cette action est irréversible.",
|
||||||
"fields": {
|
"fields": {
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Teleport v-if="modelValue" to="body">
|
|
||||||
<Transition name="modal" appear>
|
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center">
|
|
||||||
<div class="absolute inset-0 bg-black/30" @click="cancel" />
|
|
||||||
<div class="relative z-10 w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
|
|
||||||
<h3 class="text-lg font-bold text-neutral-900">{{ title }}</h3>
|
|
||||||
<p class="mt-3 text-sm text-neutral-600">
|
|
||||||
{{ message }}
|
|
||||||
</p>
|
|
||||||
<div class="mt-6 flex justify-end gap-3">
|
|
||||||
<MalioButton
|
|
||||||
variant="tertiary"
|
|
||||||
:label="$t('common.cancel')"
|
|
||||||
button-class="w-auto px-4"
|
|
||||||
@click="cancel"
|
|
||||||
/>
|
|
||||||
<MalioButton
|
|
||||||
variant="danger"
|
|
||||||
:label="$t('common.delete')"
|
|
||||||
button-class="w-auto px-4"
|
|
||||||
@click="$emit('confirm')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineProps<{
|
|
||||||
modelValue: boolean
|
|
||||||
title: string
|
|
||||||
message: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: boolean): void
|
|
||||||
(e: 'confirm'): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
function cancel() {
|
|
||||||
emit('update:modelValue', false)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.modal-enter-active,
|
|
||||||
.modal-leave-active {
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-enter-from,
|
|
||||||
.modal-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<MalioModal
|
||||||
|
:model-value="modelValue"
|
||||||
|
modal-class="max-w-md"
|
||||||
|
@update:model-value="$emit('update:modelValue', $event)"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-[24px] font-bold">{{ title }}</h2>
|
||||||
|
</template>
|
||||||
|
<!-- Corps : slot par défaut pour permettre du texte enrichi (nom en gras
|
||||||
|
via <i18n-t>) ; sinon repli sur le message texte simple. -->
|
||||||
|
<slot>
|
||||||
|
<p>{{ message }}</p>
|
||||||
|
</slot>
|
||||||
|
<template #footer>
|
||||||
|
<MalioButton
|
||||||
|
variant="secondary"
|
||||||
|
button-class="flex-1"
|
||||||
|
:label="cancelLabel ?? $t('common.cancel')"
|
||||||
|
@click="$emit('update:modelValue', false)"
|
||||||
|
/>
|
||||||
|
<MalioButton
|
||||||
|
:variant="confirmVariant"
|
||||||
|
button-class="flex-1"
|
||||||
|
:label="confirmLabel ?? $t('common.delete')"
|
||||||
|
@click="$emit('confirm')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</MalioModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
title: string
|
||||||
|
message?: string
|
||||||
|
confirmLabel?: string
|
||||||
|
cancelLabel?: string
|
||||||
|
confirmVariant?: 'primary' | 'secondary' | 'tertiary' | 'danger'
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
message: undefined,
|
||||||
|
confirmLabel: undefined,
|
||||||
|
cancelLabel: undefined,
|
||||||
|
confirmVariant: 'danger',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: boolean): void
|
||||||
|
(e: 'confirm'): void
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
@@ -9,12 +9,11 @@
|
|||||||
<!-- Clients -->
|
<!-- Clients -->
|
||||||
<template #clients>
|
<template #clients>
|
||||||
<div class="flex min-h-[30rem] flex-col gap-4 pt-10">
|
<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
|
<MalioButton
|
||||||
icon-name="mdi:plus"
|
icon-name="mdi:plus"
|
||||||
icon-position="left"
|
icon-position="left"
|
||||||
button-class="w-auto px-4"
|
:label="$t('common.add')"
|
||||||
:label="$t('directory.clients.add')"
|
|
||||||
@click="openCreateClient"
|
@click="openCreateClient"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,6 +25,9 @@
|
|||||||
:empty-message="$t('directory.clients.empty')"
|
:empty-message="$t('directory.clients.empty')"
|
||||||
@row-click="openEditClient"
|
@row-click="openEditClient"
|
||||||
>
|
>
|
||||||
|
<template #header-actions>
|
||||||
|
<span class="block text-right font-semibold text-black">{{ $t('common.actions') }}</span>
|
||||||
|
</template>
|
||||||
<template #cell-email="{ item }">
|
<template #cell-email="{ item }">
|
||||||
{{ (item as Client).email ?? '—' }}
|
{{ (item as Client).email ?? '—' }}
|
||||||
</template>
|
</template>
|
||||||
@@ -37,7 +39,7 @@
|
|||||||
<MalioButtonIcon
|
<MalioButtonIcon
|
||||||
icon="mdi:trash-can-outline"
|
icon="mdi:trash-can-outline"
|
||||||
:aria-label="$t('common.delete')"
|
: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"
|
:icon-size="18"
|
||||||
@click="askDeleteClient(item as Client)"
|
@click="askDeleteClient(item as Client)"
|
||||||
/>
|
/>
|
||||||
@@ -50,7 +52,7 @@
|
|||||||
<!-- Prospects -->
|
<!-- Prospects -->
|
||||||
<template #prospects>
|
<template #prospects>
|
||||||
<div class="flex min-h-[30rem] flex-col gap-4 pt-10">
|
<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
|
<MalioSelect
|
||||||
v-model="statusFilter"
|
v-model="statusFilter"
|
||||||
:label="$t('prospects.fields.status')"
|
:label="$t('prospects.fields.status')"
|
||||||
@@ -61,8 +63,7 @@
|
|||||||
<MalioButton
|
<MalioButton
|
||||||
icon-name="mdi:plus"
|
icon-name="mdi:plus"
|
||||||
icon-position="left"
|
icon-position="left"
|
||||||
button-class="w-auto px-4"
|
:label="$t('common.add')"
|
||||||
:label="$t('directory.prospects.add')"
|
|
||||||
@click="openCreateProspect"
|
@click="openCreateProspect"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,6 +75,9 @@
|
|||||||
:empty-message="$t('directory.prospects.empty')"
|
:empty-message="$t('directory.prospects.empty')"
|
||||||
@row-click="openEditProspect"
|
@row-click="openEditProspect"
|
||||||
>
|
>
|
||||||
|
<template #header-actions>
|
||||||
|
<span class="block text-right font-semibold text-black">{{ $t('common.actions') }}</span>
|
||||||
|
</template>
|
||||||
<template #cell-status="{ item }">
|
<template #cell-status="{ item }">
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
:label="statusLabel((item as ProspectRow).status)"
|
:label="statusLabel((item as ProspectRow).status)"
|
||||||
@@ -92,14 +96,14 @@
|
|||||||
v-if="!(item as ProspectRow).convertedClient"
|
v-if="!(item as ProspectRow).convertedClient"
|
||||||
icon="mdi:account-convert"
|
icon="mdi:account-convert"
|
||||||
:aria-label="$t('prospects.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"
|
:icon-size="18"
|
||||||
@click="convertProspect(item as ProspectRow)"
|
@click="askConvertProspect(item as ProspectRow)"
|
||||||
/>
|
/>
|
||||||
<MalioButtonIcon
|
<MalioButtonIcon
|
||||||
icon="mdi:trash-can-outline"
|
icon="mdi:trash-can-outline"
|
||||||
:aria-label="$t('common.delete')"
|
: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"
|
:icon-size="18"
|
||||||
@click="askDeleteProspect(item as ProspectRow)"
|
@click="askDeleteProspect(item as ProspectRow)"
|
||||||
/>
|
/>
|
||||||
@@ -111,12 +115,11 @@
|
|||||||
<!-- Prestataires -->
|
<!-- Prestataires -->
|
||||||
<template #prestataires>
|
<template #prestataires>
|
||||||
<div class="flex min-h-[30rem] flex-col gap-4 pt-10">
|
<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
|
<MalioButton
|
||||||
icon-name="mdi:plus"
|
icon-name="mdi:plus"
|
||||||
icon-position="left"
|
icon-position="left"
|
||||||
button-class="w-auto px-4"
|
:label="$t('common.add')"
|
||||||
:label="$t('directory.prestataires.add')"
|
|
||||||
@click="openCreatePrestataire"
|
@click="openCreatePrestataire"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,6 +131,9 @@
|
|||||||
:empty-message="$t('directory.prestataires.empty')"
|
:empty-message="$t('directory.prestataires.empty')"
|
||||||
@row-click="openEditPrestataire"
|
@row-click="openEditPrestataire"
|
||||||
>
|
>
|
||||||
|
<template #header-actions>
|
||||||
|
<span class="block text-right font-semibold text-black">{{ $t('common.actions') }}</span>
|
||||||
|
</template>
|
||||||
<template #cell-email="{ item }">
|
<template #cell-email="{ item }">
|
||||||
{{ (item as Prestataire).email ?? '—' }}
|
{{ (item as Prestataire).email ?? '—' }}
|
||||||
</template>
|
</template>
|
||||||
@@ -139,7 +145,7 @@
|
|||||||
<MalioButtonIcon
|
<MalioButtonIcon
|
||||||
icon="mdi:trash-can-outline"
|
icon="mdi:trash-can-outline"
|
||||||
:aria-label="$t('common.delete')"
|
: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"
|
:icon-size="18"
|
||||||
@click="askDeletePrestataire(item as Prestataire)"
|
@click="askDeletePrestataire(item as Prestataire)"
|
||||||
/>
|
/>
|
||||||
@@ -166,12 +172,31 @@
|
|||||||
@saved="loadPrestataires"
|
@saved="loadPrestataires"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConfirmDeleteModal
|
<ConfirmModal
|
||||||
v-model="deleteModalOpen"
|
v-model="deleteModalOpen"
|
||||||
:title="deleteModalTitle"
|
:title="deleteModalTitle"
|
||||||
:message="deleteModalMessage"
|
|
||||||
@confirm="confirmDelete"
|
@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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -220,7 +245,7 @@ const clientColumns = [
|
|||||||
{ key: 'name', label: t('prospects.fields.company') },
|
{ key: 'name', label: t('prospects.fields.company') },
|
||||||
{ key: 'email', label: t('prospects.fields.email') },
|
{ key: 'email', label: t('prospects.fields.email') },
|
||||||
{ key: 'phone', label: t('prospects.fields.phone') },
|
{ key: 'phone', label: t('prospects.fields.phone') },
|
||||||
{ key: 'actions', label: '' },
|
{ key: 'actions', label: t('common.actions') },
|
||||||
]
|
]
|
||||||
|
|
||||||
async function loadClients() {
|
async function loadClients() {
|
||||||
@@ -255,7 +280,7 @@ const prospectColumns = [
|
|||||||
{ key: 'status', label: t('prospects.fields.status') },
|
{ key: 'status', label: t('prospects.fields.status') },
|
||||||
{ key: 'email', label: t('prospects.fields.email') },
|
{ key: 'email', label: t('prospects.fields.email') },
|
||||||
{ key: 'phone', label: t('prospects.fields.phone') },
|
{ key: 'phone', label: t('prospects.fields.phone') },
|
||||||
{ key: 'actions', label: '' },
|
{ key: 'actions', label: t('common.actions') },
|
||||||
]
|
]
|
||||||
|
|
||||||
const prospectRows = computed<ProspectRow[]>(() => prospects.value)
|
const prospectRows = computed<ProspectRow[]>(() => prospects.value)
|
||||||
@@ -294,10 +319,23 @@ function openEditProspect(item: Record<string, unknown>) {
|
|||||||
navigateToDetail(`/directory/prospects/${(item as Prospect).id}`)
|
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)
|
await prospectService.convert(row.id)
|
||||||
// La conversion crée un client et retire le prospect : rafraîchir les deux listes.
|
// La conversion crée un client et retire le prospect : rafraîchir les deux listes.
|
||||||
await Promise.all([loadProspects(), loadClients()])
|
await Promise.all([loadProspects(), loadClients()])
|
||||||
|
convertModalOpen.value = false
|
||||||
|
convertTarget.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Le ProspectDrawer porte aussi le bouton « Convertir » : son event 'saved' peut
|
// 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: 'name', label: t('prospects.fields.company') },
|
||||||
{ key: 'email', label: t('prospects.fields.email') },
|
{ key: 'email', label: t('prospects.fields.email') },
|
||||||
{ key: 'phone', label: t('prospects.fields.phone') },
|
{ key: 'phone', label: t('prospects.fields.phone') },
|
||||||
{ key: 'actions', label: '' },
|
{ key: 'actions', label: t('common.actions') },
|
||||||
]
|
]
|
||||||
|
|
||||||
async function loadPrestataires() {
|
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
|
const target = deleteTarget.value
|
||||||
if (!target) return ''
|
if (!target) return ''
|
||||||
switch (target.type) {
|
return target.type === 'prospect' ? target.item.company : target.item.name
|
||||||
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 })
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function askDeleteClient(item: Client) {
|
function askDeleteClient(item: Client) {
|
||||||
|
|||||||
Reference in New Issue
Block a user