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:
@@ -3,7 +3,8 @@
|
||||
(pas de bordure sous le dernier bloc), comme sur Starseed. -->
|
||||
<div class="pb-5" :class="{ 'border-b border-black': !last }">
|
||||
<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
|
||||
v-if="removable && !readonly"
|
||||
icon="mdi:delete-outline"
|
||||
@@ -14,74 +15,78 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid grid-cols-2 gap-x-[44px] gap-y-4">
|
||||
<MalioInputText
|
||||
class="col-span-2"
|
||||
:label="$t('directory.addresses.fields.label')"
|
||||
:model-value="modelValue.label ?? ''"
|
||||
:readonly="readonly"
|
||||
@update:model-value="update('label', $event)"
|
||||
/>
|
||||
<div class="mt-6 grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||
<MalioInputText
|
||||
class="col-span-2"
|
||||
:label="$t('directory.addresses.fields.label')"
|
||||
:model-value="modelValue.label ?? ''"
|
||||
:readonly="readonly"
|
||||
@update:model-value="update('label', $event)"
|
||||
/>
|
||||
|
||||
<!-- Rue : saisie assistée (BAN) en édition, champ texte en lecture seule.
|
||||
allow-create conserve le texte saisi si la BAN ne propose rien
|
||||
(erreur/timeout). Choisir une suggestion remplit rue + CP + ville. -->
|
||||
<div class="col-span-2">
|
||||
<MalioInputAutocomplete
|
||||
v-if="!readonly"
|
||||
:model-value="modelValue.street ?? ''"
|
||||
:options="addressOptions"
|
||||
:loading="addressLoading"
|
||||
:min-search-length="3"
|
||||
:allow-create="true"
|
||||
: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"
|
||||
<!-- On commence par le code postal : il alimente la liste des villes (BAN)
|
||||
et réinitialise ville/rue devenues incohérentes en cas de changement. -->
|
||||
<MalioInputText
|
||||
:label="$t('directory.addresses.fields.postalCode')"
|
||||
:model-value="modelValue.postalCode ?? ''"
|
||||
:readonly="readonly"
|
||||
@update:model-value="onPostalCodeInput"
|
||||
/>
|
||||
|
||||
<!-- 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="onCityChange"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-else
|
||||
:label="$t('directory.addresses.fields.street')"
|
||||
:model-value="modelValue.street ?? ''"
|
||||
:label="$t('directory.addresses.fields.city')"
|
||||
:model-value="modelValue.city ?? ''"
|
||||
:readonly="readonly"
|
||||
@update:model-value="update('street', $event)"
|
||||
@update:model-value="update('city', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MalioInputText
|
||||
class="col-span-2"
|
||||
:label="$t('directory.addresses.fields.streetComplement')"
|
||||
:model-value="modelValue.streetComplement ?? ''"
|
||||
:readonly="readonly"
|
||||
@update:model-value="update('streetComplement', $event)"
|
||||
/>
|
||||
<!-- Rue : conditionnée au code postal + ville (comme Starseed). Saisie
|
||||
assistée (BAN) filtrée par le code postal ; désactivée tant que CP et
|
||||
ville ne sont pas renseignés. Champ texte simple en lecture seule. -->
|
||||
<div class="col-span-2">
|
||||
<MalioInputAutocomplete
|
||||
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
|
||||
:label="$t('directory.addresses.fields.postalCode')"
|
||||
:model-value="modelValue.postalCode ?? ''"
|
||||
:readonly="readonly"
|
||||
@update:model-value="onPostalCodeInput"
|
||||
/>
|
||||
|
||||
<!-- 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)"
|
||||
/>
|
||||
<MalioInputText
|
||||
class="col-span-2"
|
||||
:label="$t('directory.addresses.fields.streetComplement')"
|
||||
:model-value="modelValue.streetComplement ?? ''"
|
||||
:readonly="readonly"
|
||||
@update:model-value="update('streetComplement', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -118,6 +123,16 @@ const addressOptions = ref<Option[]>([])
|
||||
const fetchedCityOptions = ref<Option[]>([])
|
||||
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
|
||||
// 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,
|
||||
@@ -146,6 +161,23 @@ function notifyUnavailable(): void {
|
||||
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). */
|
||||
async function onAddressSearch(query: string): Promise<void> {
|
||||
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> {
|
||||
update('postalCode', value)
|
||||
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
|
||||
try {
|
||||
const suggestions = await autocomplete.searchCity(digits)
|
||||
|
||||
Reference in New Issue
Block a user