feat(directory) : refonte UI des fiches + onglet rapport (LST-72)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 1m17s
Pull Request — Quality gate / Frontend (build) (pull_request) Successful in 1m25s

Fiches Client / Prospect / Prestataire (onglet Rapport mis à part) :
- Champs email/téléphone : composants MalioInputEmail / MalioInputPhone
- Grilles en 4 colonnes (Info + blocs Contact/Adresse)
- Boutons « Nouveau contact/adresse » en secondary ; « Enregistrer » en
  taille Malio standard ; marge form↔bouton homogène entre onglets
- Bouton retour ghost (mdi:arrow-left-bold) comme Starseed
- Adresse : flux CP → ville → rue (rue conditionnée au CP+ville, cascade
  de reset), titre du bloc = libellé saisi
- Suppression d'un bloc Contact/Adresse : modal de confirmation (logique
  centralisée dans useDirectoryDetail)

Onglet Rapport :
- Bouton d'ajout en taille Malio standard, label « Ajouter »
- Suppression compte-rendu : passe à la ConfirmModal partagée (remplace
  l'ancienne ConfirmDeleteReportModal, supprimée)
- Suppression d'un document joint : ajout d'une modal de confirmation
- Upload via MalioInputUpload ; bouton supprimer document aligné
  (mdi:delete-outline ghost)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 17:09:23 +02:00
parent 81069915c1
commit ba462a091b
11 changed files with 299 additions and 199 deletions
@@ -1,61 +0,0 @@
<template>
<Teleport v-if="modelValue" to="body">
<Transition name="modal" appear>
<div class="fixed inset-0 z-[70] flex items-center justify-center">
<div class="absolute inset-0 bg-black/30" @click.stop="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">{{ $t('directory.reports.confirmDeleteTitle') }}</h3>
<p class="mt-3 text-sm text-neutral-600">
{{ $t('directory.reports.confirmDeleteMessage') }}
</p>
<div class="mt-6 flex justify-end gap-3">
<MalioButton
variant="tertiary"
:label="$t('common.cancel')"
button-class="w-auto px-4"
:disabled="busy"
@click="cancel"
/>
<MalioButton
variant="danger"
:label="$t('common.delete')"
button-class="w-auto px-4"
:disabled="busy"
@click="$emit('confirm')"
/>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
const props = defineProps<{
modelValue: boolean
// Suppression en cours : on désactive les actions pour éviter un double envoi.
busy?: boolean
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'confirm'): void
}>()
function cancel() {
if (props.busy) return
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>
+9 -2
View File
@@ -1004,10 +1004,12 @@
"empty": "Aucun prestataire trouvé." "empty": "Aucun prestataire trouvé."
}, },
"contacts": { "contacts": {
"add": "Ajouter un contact", "add": "Nouveau contact",
"item": "Contact {n}", "item": "Contact {n}",
"saved": "Contact enregistré.", "saved": "Contact enregistré.",
"deleted": "Contact supprimé.", "deleted": "Contact supprimé.",
"deleteConfirmTitle": "Supprimer le contact",
"deleteConfirmMessage": "Êtes-vous sûr de vouloir supprimer ce contact ? Cette action est irréversible.",
"fields": { "fields": {
"lastName": "Nom", "lastName": "Nom",
"firstName": "Prénom", "firstName": "Prénom",
@@ -1018,11 +1020,14 @@
} }
}, },
"addresses": { "addresses": {
"add": "Ajouter une adresse", "add": "Nouvelle adresse",
"item": "Adresse {n}", "item": "Adresse {n}",
"saved": "Adresse enregistrée.", "saved": "Adresse enregistrée.",
"deleted": "Adresse supprimée.", "deleted": "Adresse supprimée.",
"deleteConfirmTitle": "Supprimer l'adresse",
"deleteConfirmMessage": "Êtes-vous sûr de vouloir supprimer cette adresse ? Cette action est irréversible.",
"streetNotFound": "Aucune adresse trouvée — saisie libre possible.", "streetNotFound": "Aucune adresse trouvée — saisie libre possible.",
"streetHint": "Renseignez d'abord le code postal et la ville.",
"autocompleteUnavailable": "Recherche d'adresse indisponible : saisissez l'adresse manuellement.", "autocompleteUnavailable": "Recherche d'adresse indisponible : saisissez l'adresse manuellement.",
"fields": { "fields": {
"label": "Libellé", "label": "Libellé",
@@ -1044,6 +1049,8 @@
"deleted": "Compte-rendu supprimé.", "deleted": "Compte-rendu supprimé.",
"confirmDeleteTitle": "Supprimer ce compte-rendu ?", "confirmDeleteTitle": "Supprimer ce compte-rendu ?",
"confirmDeleteMessage": "Cette action est irréversible. Les documents joints seront également supprimés.", "confirmDeleteMessage": "Cette action est irréversible. Les documents joints seront également supprimés.",
"documentDeleteTitle": "Supprimer le document",
"documentDeleteMessage": "Êtes-vous sûr de vouloir supprimer ce document ? Cette action est irréversible.",
"fields": { "fields": {
"subject": "Objet", "subject": "Objet",
"type": "Type d'échange", "type": "Type d'échange",
@@ -9,8 +9,7 @@
v-if="canManage" v-if="canManage"
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.reports.add')"
@click="openCreate" @click="openCreate"
/> />
</div> </div>
@@ -108,7 +107,7 @@
v-if="report.documents?.length" v-if="report.documents?.length"
:documents="report.documents" :documents="report.documents"
:can-manage="canManage" :can-manage="canManage"
@delete="(docId) => removeDocument(docId)" @delete="(docId) => askDeleteDocument(docId)"
/> />
<ReportDocumentUpload <ReportDocumentUpload
v-if="canManage" v-if="canManage"
@@ -127,11 +126,18 @@
:owner="owner" :owner="owner"
@saved="reload" @saved="reload"
/> />
<ConfirmDeleteReportModal <ConfirmModal
v-model="confirmOpen" v-model="confirmOpen"
:busy="deleting" :title="$t('directory.reports.confirmDeleteTitle')"
:message="$t('directory.reports.confirmDeleteMessage')"
@confirm="confirmDelete" @confirm="confirmDelete"
/> />
<ConfirmModal
v-model="docConfirmOpen"
:title="$t('directory.reports.documentDeleteTitle')"
:message="$t('directory.reports.documentDeleteMessage')"
@confirm="confirmDeleteDocument"
/>
</div> </div>
</template> </template>
@@ -158,6 +164,11 @@ const confirmOpen = ref(false)
const pendingDelete = ref<CommercialReport | null>(null) const pendingDelete = ref<CommercialReport | null>(null)
const deleting = ref(false) const deleting = ref(false)
// Suppression d'un document joint : passe désormais par une modal de confirmation.
const docConfirmOpen = ref(false)
const pendingDocId = ref<number | null>(null)
const deletingDoc = ref(false)
// Le plus récent en haut (l'API ne garantit pas l'ordre). // Le plus récent en haut (l'API ne garantit pas l'ordre).
const sortedReports = computed(() => const sortedReports = computed(() =>
[...reports.value].sort((a, b) => b.occurredAt.localeCompare(a.occurredAt)), [...reports.value].sort((a, b) => b.occurredAt.localeCompare(a.occurredAt)),
@@ -222,9 +233,22 @@ async function confirmDelete(): Promise<void> {
} }
} }
async function removeDocument(id: number): Promise<void> { function askDeleteDocument(id: number): void {
await documentService.remove(id) pendingDocId.value = id
await reload() docConfirmOpen.value = true
}
async function confirmDeleteDocument(): Promise<void> {
if (pendingDocId.value === null || deletingDoc.value) return
deletingDoc.value = true
try {
await documentService.remove(pendingDocId.value)
docConfirmOpen.value = false
pendingDocId.value = null
await reload()
} finally {
deletingDoc.value = false
}
} }
async function reload(): Promise<void> { async function reload(): Promise<void> {
@@ -3,7 +3,8 @@
(pas de bordure sous le dernier bloc), comme sur Starseed. --> (pas de bordure sous le dernier bloc), comme sur Starseed. -->
<div class="pb-5" :class="{ 'border-b border-black': !last }"> <div class="pb-5" :class="{ 'border-b border-black': !last }">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h3 class="text-[20px] font-semibold text-black">{{ title }}</h3> <!-- Titre = libellé saisi ; repli sur « Adresse N » tant qu'il est vide. -->
<h3 class="text-[20px] font-semibold text-black">{{ blockTitle }}</h3>
<MalioButtonIcon <MalioButtonIcon
v-if="removable && !readonly" v-if="removable && !readonly"
icon="mdi:delete-outline" icon="mdi:delete-outline"
@@ -14,74 +15,78 @@
/> />
</div> </div>
<div class="mt-6 grid grid-cols-2 gap-x-[44px] gap-y-4"> <div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
class="col-span-2" class="col-span-2"
:label="$t('directory.addresses.fields.label')" :label="$t('directory.addresses.fields.label')"
:model-value="modelValue.label ?? ''" :model-value="modelValue.label ?? ''"
:readonly="readonly" :readonly="readonly"
@update:model-value="update('label', $event)" @update:model-value="update('label', $event)"
/> />
<!-- Rue : saisie assistée (BAN) en édition, champ texte en lecture seule. <!-- On commence par le code postal : il alimente la liste des villes (BAN)
allow-create conserve le texte saisi si la BAN ne propose rien et réinitialise ville/rue devenues incohérentes en cas de changement. -->
(erreur/timeout). Choisir une suggestion remplit rue + CP + ville. --> <MalioInputText
<div class="col-span-2"> :label="$t('directory.addresses.fields.postalCode')"
<MalioInputAutocomplete :model-value="modelValue.postalCode ?? ''"
v-if="!readonly" :readonly="readonly"
:model-value="modelValue.street ?? ''" @update:model-value="onPostalCodeInput"
:options="addressOptions" />
:loading="addressLoading"
:min-search-length="3" <!-- Ville : select alimenté par le code postal (BAN). En mode dégradé
:allow-create="true" (BAN indispo) ou lecture seule, on bascule en saisie libre. -->
:label="$t('directory.addresses.fields.street')" <MalioSelect
:no-results-text="$t('directory.addresses.streetNotFound')" v-if="!readonly && !degraded"
@update:model-value="(v) => update('street', v === null ? '' : String(v))" :model-value="modelValue.city ?? ''"
@search="onAddressSearch" :options="cityOptions"
@select="onAddressSelect" :label="$t('directory.addresses.fields.city')"
empty-option-label=""
group-class="w-full"
@update:model-value="onCityChange"
/> />
<MalioInputText <MalioInputText
v-else v-else
:label="$t('directory.addresses.fields.street')" :label="$t('directory.addresses.fields.city')"
:model-value="modelValue.street ?? ''" :model-value="modelValue.city ?? ''"
:readonly="readonly" :readonly="readonly"
@update:model-value="update('street', $event)" @update:model-value="update('city', $event)"
/> />
</div>
<MalioInputText <!-- Rue : conditionnée au code postal + ville (comme Starseed). Saisie
class="col-span-2" assistée (BAN) filtrée par le code postal ; désactivée tant que CP et
:label="$t('directory.addresses.fields.streetComplement')" ville ne sont pas renseignés. Champ texte simple en lecture seule. -->
:model-value="modelValue.streetComplement ?? ''" <div class="col-span-2">
:readonly="readonly" <MalioInputAutocomplete
@update:model-value="update('streetComplement', $event)" v-if="!readonly"
/> :model-value="modelValue.street ?? ''"
:options="addressOptions"
:loading="addressLoading"
:min-search-length="3"
:allow-create="true"
:disabled="!canEditStreet"
:hint="canEditStreet ? '' : $t('directory.addresses.streetHint')"
:label="$t('directory.addresses.fields.street')"
:no-results-text="$t('directory.addresses.streetNotFound')"
@update:model-value="(v) => update('street', v === null ? '' : String(v))"
@search="onAddressSearch"
@select="onAddressSelect"
/>
<MalioInputText
v-else
:label="$t('directory.addresses.fields.street')"
:model-value="modelValue.street ?? ''"
:readonly="readonly"
@update:model-value="update('street', $event)"
/>
</div>
<MalioInputText <MalioInputText
:label="$t('directory.addresses.fields.postalCode')" class="col-span-2"
:model-value="modelValue.postalCode ?? ''" :label="$t('directory.addresses.fields.streetComplement')"
:readonly="readonly" :model-value="modelValue.streetComplement ?? ''"
@update:model-value="onPostalCodeInput" :readonly="readonly"
/> @update:model-value="update('streetComplement', $event)"
/>
<!-- Ville : select alimenté par le code postal (BAN). En mode dégradé
(BAN indispo) ou lecture seule, on bascule en saisie libre. -->
<MalioSelect
v-if="!readonly && !degraded"
:model-value="modelValue.city ?? ''"
:options="cityOptions"
:label="$t('directory.addresses.fields.city')"
empty-option-label=""
group-class="w-full"
@update:model-value="(v) => update('city', v === null ? '' : String(v))"
/>
<MalioInputText
v-else
:label="$t('directory.addresses.fields.city')"
:model-value="modelValue.city ?? ''"
:readonly="readonly"
@update:model-value="update('city', $event)"
/>
</div> </div>
</div> </div>
</template> </template>
@@ -118,6 +123,16 @@ const addressOptions = ref<Option[]>([])
const fetchedCityOptions = ref<Option[]>([]) const fetchedCityOptions = ref<Option[]>([])
const addressLoading = ref(false) const addressLoading = ref(false)
// Titre du bloc : le libellé saisi prime ; repli sur « Adresse N » (prop `title`).
const blockTitle = computed(() => (props.modelValue.label ?? '').trim() || props.title)
// La rue n'est éditable qu'une fois le code postal (5 chiffres) ET la ville
// renseignés — conditionnement métier repris de Starseed.
const canEditStreet = computed(() => {
const digits = (props.modelValue.postalCode ?? '').replace(/\D/g, '')
return digits.length === 5 && !!(props.modelValue.city ?? '').trim()
})
// Le select Ville n'affiche que les valeurs présentes dans ses options : on // Le select Ville n'affiche que les valeurs présentes dans ses options : on
// garantit donc que la ville déjà enregistrée (chargement d'une fiche) ou // garantit donc que la ville déjà enregistrée (chargement d'une fiche) ou
// pré-remplie par l'autocomplétion d'adresse figure toujours dans la liste, // pré-remplie par l'autocomplétion d'adresse figure toujours dans la liste,
@@ -146,6 +161,23 @@ function notifyUnavailable(): void {
toast.info({ title: '', message: t('directory.addresses.autocompleteUnavailable') }) toast.info({ title: '', message: t('directory.addresses.autocompleteUnavailable') })
} }
/**
* Sélection d'une ville → vide rue + complément (devenus incohérents avec la
* nouvelle ville). Ne réagit qu'à un vrai changement de valeur.
*/
function onCityChange(value: string | number | null): void {
const next = value === null ? '' : String(value)
if (next === (props.modelValue.city ?? '')) return
addressOptions.value = []
lastAddressSuggestions = []
emit('update:modelValue', {
...props.modelValue,
city: next === '' ? null : next,
street: null,
streetComplement: null,
})
}
/** Recherche d'adresse assistée (event de MalioInputAutocomplete). */ /** Recherche d'adresse assistée (event de MalioInputAutocomplete). */
async function onAddressSearch(query: string): Promise<void> { async function onAddressSearch(query: string): Promise<void> {
if (query.trim().length < 3) { if (query.trim().length < 3) {
@@ -186,10 +218,30 @@ function onAddressSelect(option: Option | null): void {
}) })
} }
/** Saisie du code postal → met à jour le champ + interroge la BAN pour la ville. */ /**
* Saisie du code postal → réinitialise ville/rue/complément quand le CP est
* complet (5 chiffres) ET réellement modifié, puis interroge la BAN pour les
* villes. Sinon simple mise à jour du champ (correction partielle).
*/
async function onPostalCodeInput(value: string): Promise<void> { async function onPostalCodeInput(value: string): Promise<void> {
update('postalCode', value)
const digits = (value ?? '').replace(/\D/g, '') const digits = (value ?? '').replace(/\D/g, '')
const previousDigits = (props.modelValue.postalCode ?? '').replace(/\D/g, '')
if (digits.length === 5 && digits !== previousDigits) {
addressOptions.value = []
lastAddressSuggestions = []
emit('update:modelValue', {
...props.modelValue,
postalCode: value,
city: null,
street: null,
streetComplement: null,
})
}
else {
update('postalCode', value)
}
if (digits.length < 5) return if (digits.length < 5) return
try { try {
const suggestions = await autocomplete.searchCity(digits) const suggestions = await autocomplete.searchCity(digits)
@@ -14,7 +14,7 @@
/> />
</div> </div>
<div class="mt-6 grid grid-cols-2 gap-x-[44px] gap-y-4"> <div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
<MalioInputText <MalioInputText
:label="$t('directory.contacts.fields.lastName')" :label="$t('directory.contacts.fields.lastName')"
:model-value="modelValue.lastName ?? ''" :model-value="modelValue.lastName ?? ''"
@@ -34,21 +34,21 @@
:readonly="readonly" :readonly="readonly"
@update:model-value="update('jobTitle', $event)" @update:model-value="update('jobTitle', $event)"
/> />
<MalioInputText <MalioInputEmail
:label="$t('directory.contacts.fields.email')" :label="$t('directory.contacts.fields.email')"
:model-value="modelValue.email ?? ''" :model-value="modelValue.email ?? ''"
:readonly="readonly" :readonly="readonly"
:error="emailError" :error="emailError"
@update:model-value="update('email', $event)" @update:model-value="update('email', $event)"
/> />
<MalioInputText <MalioInputPhone
:label="$t('directory.contacts.fields.phonePrimary')" :label="$t('directory.contacts.fields.phonePrimary')"
:model-value="modelValue.phonePrimary ?? ''" :model-value="modelValue.phonePrimary ?? ''"
:readonly="readonly" :readonly="readonly"
:error="phonePrimaryError" :error="phonePrimaryError"
@update:model-value="update('phonePrimary', $event)" @update:model-value="update('phonePrimary', $event)"
/> />
<MalioInputText <MalioInputPhone
:label="$t('directory.contacts.fields.phoneSecondary')" :label="$t('directory.contacts.fields.phoneSecondary')"
:model-value="modelValue.phoneSecondary ?? ''" :model-value="modelValue.phoneSecondary ?? ''"
:readonly="readonly" :readonly="readonly"
@@ -16,8 +16,8 @@
</a> </a>
<MalioButtonIcon <MalioButtonIcon
v-if="canManage" v-if="canManage"
icon="mdi:trash-can-outline" icon="mdi:delete-outline"
button-class="!text-red-600" variant="ghost"
:aria-label="$t('common.delete')" :aria-label="$t('common.delete')"
@click="$emit('delete', doc.id)" @click="$emit('delete', doc.id)"
/> />
@@ -1,20 +1,14 @@
<template> <template>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<input <MalioInputUpload
ref="fileInput" v-model="fileName"
type="file" class="flex-1"
class="hidden"
@change="onFileSelected"
>
<MalioButton
icon-name="mdi:paperclip"
icon-position="left"
button-class="w-auto px-4"
:label="$t('directory.documents.add')" :label="$t('directory.documents.add')"
:disabled="uploading" :disabled="uploading"
@click="fileInput?.click()" :reserve-message-space="false"
@file-selected="onFile"
/> />
<span v-if="uploading" class="text-sm text-neutral-500">{{ $t('directory.documents.uploading') }}</span> <span v-if="uploading" class="shrink-0 text-sm text-neutral-500">{{ $t('directory.documents.uploading') }}</span>
</div> </div>
</template> </template>
@@ -25,21 +19,19 @@ const props = defineProps<{ reportId: number }>()
const emit = defineEmits<{ uploaded: [] }>() const emit = defineEmits<{ uploaded: [] }>()
const service = useReportDocumentService() const service = useReportDocumentService()
const fileInput = ref<HTMLInputElement | null>(null) // Nom du fichier affiché par le champ Malio (v-model) ; réinitialisé après envoi.
const fileName = ref('')
const uploading = ref(false) const uploading = ref(false)
async function onFileSelected(event: Event): Promise<void> { // L'upload se déclenche dès la sélection (event natif du composant Malio).
const input = event.target as HTMLInputElement async function onFile(file: File): Promise<void> {
const file = input.files?.[0]
if (!file) return
uploading.value = true uploading.value = true
try { try {
await service.upload(props.reportId, file) await service.upload(props.reportId, file)
emit('uploaded') emit('uploaded')
} finally { } finally {
uploading.value = false uploading.value = false
input.value = '' fileName.value = ''
} }
} }
</script> </script>
@@ -14,6 +14,7 @@ type Owner = { client?: string, prospect?: string, prestataire?: string }
* tel quel par les deux pages. * tel quel par les deux pages.
*/ */
export function useDirectoryDetail(owner: Owner) { export function useDirectoryDetail(owner: Owner) {
const { t } = useI18n()
const contactService = useContactService() const contactService = useContactService()
const addressService = useAddressService() const addressService = useAddressService()
@@ -59,6 +60,39 @@ export function useDirectoryDetail(owner: Owner) {
addresses.value.splice(index, 1) addresses.value.splice(index, 1)
} }
// Confirmation de suppression d'un bloc (contact / adresse) : la corbeille du
// bloc ouvre une modal ; la suppression effective n'a lieu qu'à la confirmation.
const removeModalOpen = ref(false)
const pendingRemoval = ref<{ type: 'contact' | 'address', index: number } | null>(null)
const removeModalTitle = computed(() =>
pendingRemoval.value?.type === 'address'
? t('directory.addresses.deleteConfirmTitle')
: t('directory.contacts.deleteConfirmTitle'),
)
const removeModalMessage = computed(() =>
pendingRemoval.value?.type === 'address'
? t('directory.addresses.deleteConfirmMessage')
: t('directory.contacts.deleteConfirmMessage'),
)
function askRemoveContact(index: number): void {
pendingRemoval.value = { type: 'contact', index }
removeModalOpen.value = true
}
function askRemoveAddress(index: number): void {
pendingRemoval.value = { type: 'address', index }
removeModalOpen.value = true
}
async function confirmRemove(): Promise<void> {
const p = pendingRemoval.value
if (!p) return
if (p.type === 'contact') await removeContact(p.index)
else await removeAddress(p.index)
removeModalOpen.value = false
pendingRemoval.value = null
}
// Persistance au clic : met à jour les blocs existants, crée les nouveaux // Persistance au clic : met à jour les blocs existants, crée les nouveaux
// blocs renseignés. Les amorces vides (sans contenu) sont ignorées. // blocs renseignés. Les amorces vides (sans contenu) sont ignorées.
async function saveContacts(): Promise<void> { async function saveContacts(): Promise<void> {
@@ -117,5 +151,12 @@ export function useDirectoryDetail(owner: Owner) {
removeAddress, removeAddress,
saveAddresses, saveAddresses,
load, load,
// Suppression de bloc avec confirmation (modal partagée contact/adresse).
removeModalOpen,
removeModalTitle,
removeModalMessage,
askRemoveContact,
askRemoveAddress,
confirmRemove,
} }
} }
@@ -2,7 +2,14 @@
<div> <div>
<PageHeader> <PageHeader>
<span class="inline-flex items-center gap-3"> <span class="inline-flex items-center gap-3">
<MalioButtonIcon icon="mdi:arrow-left" :aria-label="$t('common.back')" @click="goBack" /> <MalioButtonIcon
icon="mdi:arrow-left-bold"
icon-size="24"
variant="ghost"
:title="$t('common.back')"
:aria-label="$t('common.back')"
@click="goBack"
/>
{{ client?.name ?? '…' }} {{ client?.name ?? '…' }}
</span> </span>
</PageHeader> </PageHeader>
@@ -13,7 +20,7 @@
<MalioTabList v-model="activeTab" :tabs="tabs"> <MalioTabList v-model="activeTab" :tabs="tabs">
<template #info> <template #info>
<div class="flex flex-col gap-4 pt-6"> <div class="flex flex-col gap-4 pt-6">
<div class="grid grid-cols-2 gap-x-[44px] gap-y-4"> <div class="grid grid-cols-4 gap-x-[44px] gap-y-4 pb-5">
<MalioInputText <MalioInputText
v-model="info.name" v-model="info.name"
class="col-span-2" class="col-span-2"
@@ -21,12 +28,12 @@
:error="infoTouched.name && !info.name.trim() ? $t('directory.validation.nameRequired') : ''" :error="infoTouched.name && !info.name.trim() ? $t('directory.validation.nameRequired') : ''"
@blur="infoTouched.name = true" @blur="infoTouched.name = true"
/> />
<MalioInputText <MalioInputEmail
v-model="info.email" v-model="info.email"
:label="$t('directory.info.fields.email')" :label="$t('directory.info.fields.email')"
:error="emailError" :error="emailError"
/> />
<MalioInputText <MalioInputPhone
v-model="info.phone" v-model="info.phone"
:label="$t('directory.info.fields.phone')" :label="$t('directory.info.fields.phone')"
:error="phoneError" :error="phoneError"
@@ -40,7 +47,6 @@
</div> </div>
<div class="flex justify-center pt-2"> <div class="flex justify-center pt-2">
<MalioButton <MalioButton
button-class="w-auto px-6"
:label="$t('common.save')" :label="$t('common.save')"
:disabled="savingInfo || !infoValid" :disabled="savingInfo || !infoValid"
@click="saveInfo" @click="saveInfo"
@@ -59,11 +65,11 @@
:removable="contacts.length > 0" :removable="contacts.length > 0"
:last="i === contacts.length - 1" :last="i === contacts.length - 1"
@update:model-value="(v) => onContactInput(i, v)" @update:model-value="(v) => onContactInput(i, v)"
@remove="removeContact(i)" @remove="askRemoveContact(i)"
/> />
<div class="flex justify-center gap-3 pt-2"> <div class="flex justify-center gap-3 pt-2">
<MalioButton <MalioButton
variant="tertiary" variant="secondary"
icon-name="mdi:plus" icon-name="mdi:plus"
icon-position="left" icon-position="left"
button-class="w-auto px-4" button-class="w-auto px-4"
@@ -71,7 +77,6 @@
@click="addContact" @click="addContact"
/> />
<MalioButton <MalioButton
button-class="w-auto px-6"
:label="$t('common.save')" :label="$t('common.save')"
:disabled="savingContacts" :disabled="savingContacts"
@click="saveContacts" @click="saveContacts"
@@ -90,11 +95,11 @@
:removable="addresses.length > 0" :removable="addresses.length > 0"
:last="i === addresses.length - 1" :last="i === addresses.length - 1"
@update:model-value="(v) => onAddressInput(i, v)" @update:model-value="(v) => onAddressInput(i, v)"
@remove="removeAddress(i)" @remove="askRemoveAddress(i)"
/> />
<div class="flex justify-center gap-3 pt-2"> <div class="flex justify-center gap-3 pt-2">
<MalioButton <MalioButton
variant="tertiary" variant="secondary"
icon-name="mdi:plus" icon-name="mdi:plus"
icon-position="left" icon-position="left"
button-class="w-auto px-4" button-class="w-auto px-4"
@@ -102,7 +107,6 @@
@click="addAddress" @click="addAddress"
/> />
<MalioButton <MalioButton
button-class="w-auto px-6"
:label="$t('common.save')" :label="$t('common.save')"
:disabled="savingAddresses" :disabled="savingAddresses"
@click="saveAddresses" @click="saveAddresses"
@@ -117,6 +121,13 @@
</MalioTabList> </MalioTabList>
</template> </template>
</div> </div>
<ConfirmModal
v-model="removeModalOpen"
:title="removeModalTitle"
:message="removeModalMessage"
@confirm="confirmRemove"
/>
</div> </div>
</template> </template>
@@ -143,13 +154,17 @@ const {
savingAddresses, savingAddresses,
onContactInput, onContactInput,
addContact, addContact,
removeContact, askRemoveContact,
saveContacts, saveContacts,
onAddressInput, onAddressInput,
addAddress, addAddress,
removeAddress, askRemoveAddress,
saveAddresses, saveAddresses,
load, load,
removeModalOpen,
removeModalTitle,
removeModalMessage,
confirmRemove,
} = useDirectoryDetail(owner) } = useDirectoryDetail(owner)
const { can } = usePermissions() const { can } = usePermissions()
@@ -2,7 +2,14 @@
<div> <div>
<PageHeader> <PageHeader>
<span class="inline-flex items-center gap-3"> <span class="inline-flex items-center gap-3">
<MalioButtonIcon icon="mdi:arrow-left" :aria-label="$t('common.back')" @click="goBack" /> <MalioButtonIcon
icon="mdi:arrow-left-bold"
icon-size="24"
variant="ghost"
:title="$t('common.back')"
:aria-label="$t('common.back')"
@click="goBack"
/>
{{ prestataire?.name ?? '…' }} {{ prestataire?.name ?? '…' }}
</span> </span>
</PageHeader> </PageHeader>
@@ -13,7 +20,7 @@
<MalioTabList v-model="activeTab" :tabs="tabs"> <MalioTabList v-model="activeTab" :tabs="tabs">
<template #info> <template #info>
<div class="flex flex-col gap-4 pt-6"> <div class="flex flex-col gap-4 pt-6">
<div class="grid grid-cols-2 gap-x-[44px] gap-y-4"> <div class="grid grid-cols-4 gap-x-[44px] gap-y-4 pb-5">
<MalioInputText <MalioInputText
v-model="info.name" v-model="info.name"
class="col-span-2" class="col-span-2"
@@ -21,12 +28,12 @@
:error="infoTouched.name && !info.name.trim() ? $t('directory.validation.nameRequired') : ''" :error="infoTouched.name && !info.name.trim() ? $t('directory.validation.nameRequired') : ''"
@blur="infoTouched.name = true" @blur="infoTouched.name = true"
/> />
<MalioInputText <MalioInputEmail
v-model="info.email" v-model="info.email"
:label="$t('directory.info.fields.email')" :label="$t('directory.info.fields.email')"
:error="emailError" :error="emailError"
/> />
<MalioInputText <MalioInputPhone
v-model="info.phone" v-model="info.phone"
:label="$t('directory.info.fields.phone')" :label="$t('directory.info.fields.phone')"
:error="phoneError" :error="phoneError"
@@ -40,7 +47,6 @@
</div> </div>
<div class="flex justify-center pt-2"> <div class="flex justify-center pt-2">
<MalioButton <MalioButton
button-class="w-auto px-6"
:label="$t('common.save')" :label="$t('common.save')"
:disabled="savingInfo || !infoValid" :disabled="savingInfo || !infoValid"
@click="saveInfo" @click="saveInfo"
@@ -59,11 +65,11 @@
:removable="contacts.length > 0" :removable="contacts.length > 0"
:last="i === contacts.length - 1" :last="i === contacts.length - 1"
@update:model-value="(v) => onContactInput(i, v)" @update:model-value="(v) => onContactInput(i, v)"
@remove="removeContact(i)" @remove="askRemoveContact(i)"
/> />
<div class="flex justify-center gap-3 pt-2"> <div class="flex justify-center gap-3 pt-2">
<MalioButton <MalioButton
variant="tertiary" variant="secondary"
icon-name="mdi:plus" icon-name="mdi:plus"
icon-position="left" icon-position="left"
button-class="w-auto px-4" button-class="w-auto px-4"
@@ -71,7 +77,6 @@
@click="addContact" @click="addContact"
/> />
<MalioButton <MalioButton
button-class="w-auto px-6"
:label="$t('common.save')" :label="$t('common.save')"
:disabled="savingContacts" :disabled="savingContacts"
@click="saveContacts" @click="saveContacts"
@@ -90,11 +95,11 @@
:removable="addresses.length > 0" :removable="addresses.length > 0"
:last="i === addresses.length - 1" :last="i === addresses.length - 1"
@update:model-value="(v) => onAddressInput(i, v)" @update:model-value="(v) => onAddressInput(i, v)"
@remove="removeAddress(i)" @remove="askRemoveAddress(i)"
/> />
<div class="flex justify-center gap-3 pt-2"> <div class="flex justify-center gap-3 pt-2">
<MalioButton <MalioButton
variant="tertiary" variant="secondary"
icon-name="mdi:plus" icon-name="mdi:plus"
icon-position="left" icon-position="left"
button-class="w-auto px-4" button-class="w-auto px-4"
@@ -102,7 +107,6 @@
@click="addAddress" @click="addAddress"
/> />
<MalioButton <MalioButton
button-class="w-auto px-6"
:label="$t('common.save')" :label="$t('common.save')"
:disabled="savingAddresses" :disabled="savingAddresses"
@click="saveAddresses" @click="saveAddresses"
@@ -117,6 +121,13 @@
</MalioTabList> </MalioTabList>
</template> </template>
</div> </div>
<ConfirmModal
v-model="removeModalOpen"
:title="removeModalTitle"
:message="removeModalMessage"
@confirm="confirmRemove"
/>
</div> </div>
</template> </template>
@@ -143,13 +154,17 @@ const {
savingAddresses, savingAddresses,
onContactInput, onContactInput,
addContact, addContact,
removeContact, askRemoveContact,
saveContacts, saveContacts,
onAddressInput, onAddressInput,
addAddress, addAddress,
removeAddress, askRemoveAddress,
saveAddresses, saveAddresses,
load, load,
removeModalOpen,
removeModalTitle,
removeModalMessage,
confirmRemove,
} = useDirectoryDetail(owner) } = useDirectoryDetail(owner)
const { can } = usePermissions() const { can } = usePermissions()
@@ -2,7 +2,14 @@
<div> <div>
<PageHeader> <PageHeader>
<span class="inline-flex items-center gap-3"> <span class="inline-flex items-center gap-3">
<MalioButtonIcon icon="mdi:arrow-left" :aria-label="$t('common.back')" @click="goBack" /> <MalioButtonIcon
icon="mdi:arrow-left-bold"
icon-size="24"
variant="ghost"
:title="$t('common.back')"
:aria-label="$t('common.back')"
@click="goBack"
/>
{{ prospect?.company ?? '…' }} {{ prospect?.company ?? '…' }}
</span> </span>
</PageHeader> </PageHeader>
@@ -13,7 +20,7 @@
<MalioTabList v-model="activeTab" :tabs="tabs"> <MalioTabList v-model="activeTab" :tabs="tabs">
<template #info> <template #info>
<div class="flex flex-col gap-4 pt-6"> <div class="flex flex-col gap-4 pt-6">
<div class="grid grid-cols-2 gap-x-[44px] gap-y-4"> <div class="grid grid-cols-4 gap-x-[44px] gap-y-4 pb-5">
<MalioInputText <MalioInputText
v-model="info.company" v-model="info.company"
class="col-span-2" class="col-span-2"
@@ -32,12 +39,12 @@
:label="$t('prospects.fields.website')" :label="$t('prospects.fields.website')"
:error="websiteError" :error="websiteError"
/> />
<MalioInputText <MalioInputEmail
v-model="info.email" v-model="info.email"
:label="$t('prospects.fields.email')" :label="$t('prospects.fields.email')"
:error="emailError" :error="emailError"
/> />
<MalioInputText <MalioInputPhone
v-model="info.phone" v-model="info.phone"
:label="$t('prospects.fields.phone')" :label="$t('prospects.fields.phone')"
:error="phoneError" :error="phoneError"
@@ -49,13 +56,12 @@
/> />
<MalioInputTextArea <MalioInputTextArea
v-model="info.notes" v-model="info.notes"
class="col-span-2" class="col-span-4"
:label="$t('prospects.fields.notes')" :label="$t('prospects.fields.notes')"
/> />
</div> </div>
<div class="flex justify-center pt-2"> <div class="flex justify-center pt-2">
<MalioButton <MalioButton
button-class="w-auto px-6"
:label="$t('common.save')" :label="$t('common.save')"
:disabled="savingInfo || !infoValid" :disabled="savingInfo || !infoValid"
@click="saveInfo" @click="saveInfo"
@@ -74,11 +80,11 @@
:removable="contacts.length > 0" :removable="contacts.length > 0"
:last="i === contacts.length - 1" :last="i === contacts.length - 1"
@update:model-value="(v) => onContactInput(i, v)" @update:model-value="(v) => onContactInput(i, v)"
@remove="removeContact(i)" @remove="askRemoveContact(i)"
/> />
<div class="flex justify-center gap-3 pt-2"> <div class="flex justify-center gap-3 pt-2">
<MalioButton <MalioButton
variant="tertiary" variant="secondary"
icon-name="mdi:plus" icon-name="mdi:plus"
icon-position="left" icon-position="left"
button-class="w-auto px-4" button-class="w-auto px-4"
@@ -86,7 +92,6 @@
@click="addContact" @click="addContact"
/> />
<MalioButton <MalioButton
button-class="w-auto px-6"
:label="$t('common.save')" :label="$t('common.save')"
:disabled="savingContacts" :disabled="savingContacts"
@click="saveContacts" @click="saveContacts"
@@ -105,11 +110,11 @@
:removable="addresses.length > 0" :removable="addresses.length > 0"
:last="i === addresses.length - 1" :last="i === addresses.length - 1"
@update:model-value="(v) => onAddressInput(i, v)" @update:model-value="(v) => onAddressInput(i, v)"
@remove="removeAddress(i)" @remove="askRemoveAddress(i)"
/> />
<div class="flex justify-center gap-3 pt-2"> <div class="flex justify-center gap-3 pt-2">
<MalioButton <MalioButton
variant="tertiary" variant="secondary"
icon-name="mdi:plus" icon-name="mdi:plus"
icon-position="left" icon-position="left"
button-class="w-auto px-4" button-class="w-auto px-4"
@@ -117,7 +122,6 @@
@click="addAddress" @click="addAddress"
/> />
<MalioButton <MalioButton
button-class="w-auto px-6"
:label="$t('common.save')" :label="$t('common.save')"
:disabled="savingAddresses" :disabled="savingAddresses"
@click="saveAddresses" @click="saveAddresses"
@@ -132,6 +136,13 @@
</MalioTabList> </MalioTabList>
</template> </template>
</div> </div>
<ConfirmModal
v-model="removeModalOpen"
:title="removeModalTitle"
:message="removeModalMessage"
@confirm="confirmRemove"
/>
</div> </div>
</template> </template>
@@ -158,13 +169,17 @@ const {
savingAddresses, savingAddresses,
onContactInput, onContactInput,
addContact, addContact,
removeContact, askRemoveContact,
saveContacts, saveContacts,
onAddressInput, onAddressInput,
addAddress, addAddress,
removeAddress, askRemoveAddress,
saveAddresses, saveAddresses,
load, load,
removeModalOpen,
removeModalTitle,
removeModalMessage,
confirmRemove,
} = useDirectoryDetail(owner) } = useDirectoryDetail(owner)
const { can } = usePermissions() const { can } = usePermissions()