feat(directory) : refonte UI des fiches + onglet rapport (LST-72)
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:
@@ -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>
|
|
||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user