b8dc3cb696
Auto Tag Develop / tag (push) Successful in 7s
Lot de correctifs sur l'écran Client (M1), + un retrait de règle métier et une petite fonctionnalité. ## Formulaire client (création / édition) - Boutons « ajouter un bloc » (Adresse, RIB) désactivés tant que le dernier bloc n'est pas valide. - Onglet Information : bouton Valider désactivé si aucun champ rempli (création) ; onglet Contact accessible dès la création (Information facultatif). - Champs « Relation » (Distributeur/Courtier) et « Prestation de triage » masqués par défaut, révélés seulement si une catégorie ordinaire (≠ Distributeur/Courtier) est sélectionnée. - Bloc RIB affiché uniquement si le type de règlement est LCR (création, édition, consultation) ; plus de RIB fantôme soumis. - Alignement du bas du textarea « Description » sur les autres champs. ## Recherche d'adresse (BAN) - Une erreur de l'API ne bloque plus définitivement la recherche : chaque frappe réessaie (le mode dégradé restait verrouillé). - Garde minimum 3 caractères avant l'appel à l'API. ## Répertoire client - Titres de colonne en noir 16px, corps + tags de site en 14px. ## Navigation - L'onglet actif est conservé au passage consultation ↔ édition (via history.state, hors URL). ## Règle métier - Retrait de RG-1.04 : l'onglet Information n'est plus obligatoire pour le rôle Commerciale — facultatif pour tous (back + tests + docs). Tests : suites front (Vitest) et back (PHPUnit) vertes hormis flakes d'infra connus. Reviewed-on: #76 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
101 lines
4.7 KiB
Vue
101 lines
4.7 KiB
Vue
<template>
|
||
<!--
|
||
Vue de detail d'une ligne d'audit : tableau field/old/new pour une
|
||
update, sinon snapshot complet sous forme de liste { cle: valeur }.
|
||
-->
|
||
<div class="text-sm">
|
||
<p class="text-xs text-gray-500 mb-2">
|
||
<span v-if="entry.ipAddress">IP: {{ entry.ipAddress }}</span>
|
||
<span v-if="entry.requestId" class="ml-3">Req: {{ entry.requestId }}</span>
|
||
</p>
|
||
|
||
<div v-if="entry.action === 'update'">
|
||
<!-- Tableau de comparaison field/old/new. MalioDataTable n'est
|
||
pas adapte ici : cas presentationnel non-paginable (cf.
|
||
exception documentee dans CLAUDE.md). -->
|
||
<table class="min-w-full border border-gray-200 text-xs">
|
||
<thead class="bg-gray-100">
|
||
<tr>
|
||
<th class="px-2 py-1 text-left font-medium">{{ t('audit.detail.field') }}</th>
|
||
<th class="px-2 py-1 text-left font-medium">{{ t('audit.detail.old_value') }}</th>
|
||
<th class="px-2 py-1 text-left font-medium">{{ t('audit.detail.new_value') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(diff, field) in updateDiff" :key="field" class="border-t border-gray-200">
|
||
<td class="px-2 py-1">{{ field }}</td>
|
||
<td class="px-2 py-1 text-red-700">{{ formatValue(diff.old) }}</td>
|
||
<td class="px-2 py-1 text-green-700">{{ formatValue(diff.new) }}</td>
|
||
</tr>
|
||
<!-- Modifications de collections to-many : shape different
|
||
{ added: [ids], removed: [ids] } → affiche + et - sur
|
||
la meme ligne pour garder une colonne field unique. -->
|
||
<tr v-for="(diff, field) in collectionDiff" :key="`col-${field}`" class="border-t border-gray-200">
|
||
<td class="px-2 py-1">{{ field }}</td>
|
||
<td class="px-2 py-1 text-red-700">
|
||
<span v-if="diff.removed.length">− {{ diff.removed.join(', ') }}</span>
|
||
<span v-else class="text-gray-400">∅</span>
|
||
</td>
|
||
<td class="px-2 py-1 text-green-700">
|
||
<span v-if="diff.added.length">+ {{ diff.added.join(', ') }}</span>
|
||
<span v-else class="text-gray-400">∅</span>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div v-else class="space-y-1">
|
||
<div v-for="(value, key) in entry.changes" :key="key" class="flex gap-2">
|
||
<span class="text-xs text-gray-600">{{ key }}:</span>
|
||
<span class="text-xs">{{ formatValue(value) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed } from 'vue'
|
||
import type { AuditLogEntry } from '~/shared/types'
|
||
|
||
const props = defineProps<{ entry: AuditLogEntry }>()
|
||
|
||
const { t } = useI18n()
|
||
|
||
// Extrait les entrees au shape { old, new } pour les updates scalaires.
|
||
const updateDiff = computed<Record<string, { old: unknown; new: unknown }>>(() => {
|
||
const out: Record<string, { old: unknown; new: unknown }> = {}
|
||
for (const [key, value] of Object.entries(props.entry.changes)) {
|
||
if (value && typeof value === 'object' && 'old' in value && 'new' in value) {
|
||
out[key] = value as { old: unknown; new: unknown }
|
||
}
|
||
}
|
||
return out
|
||
})
|
||
|
||
// Extrait les entrees au shape { added, removed } pour les modifications
|
||
// de collections to-many (cf. AuditListener::captureCollectionChange).
|
||
const collectionDiff = computed<Record<string, { added: unknown[]; removed: unknown[] }>>(() => {
|
||
const out: Record<string, { added: unknown[]; removed: unknown[] }> = {}
|
||
for (const [key, value] of Object.entries(props.entry.changes)) {
|
||
if (value && typeof value === 'object' && 'added' in value && 'removed' in value) {
|
||
const diff = value as { added: unknown; removed: unknown }
|
||
out[key] = {
|
||
added: Array.isArray(diff.added) ? diff.added : [],
|
||
removed: Array.isArray(diff.removed) ? diff.removed : [],
|
||
}
|
||
}
|
||
}
|
||
return out
|
||
})
|
||
|
||
function formatValue(value: unknown): string {
|
||
if (value === null || value === undefined) return '∅'
|
||
// Passe par i18n plutot qu'un hardcode FR : si une autre locale est
|
||
// ajoutee, le rendu s'adapte sans nouvelle passe sur ce composant.
|
||
if (typeof value === 'boolean') return value ? t('common.yes') : t('common.no')
|
||
if (typeof value === 'object') return JSON.stringify(value)
|
||
return String(value)
|
||
}
|
||
</script>
|