feat(directory) : revamp commercial report tab and polish info tab
Pull Request — Quality gate / Frontend (build) (pull_request) Successful in 39s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 1m31s

- report tab redesigned as a reverse-chronological timeline with type
  badges/icons, relative dates and author
- add/edit moved to a side drawer; body now uses the rich text editor
  (MalioInputRichText), displayed read-only as inline prose
- delete now asks for confirmation (ConfirmDeleteReportModal)
- empty state with CTA and pluralized count
- info tab: use v-model, neutral i18n validation key, real admin flag
  instead of hardcoded true on CommercialReportTab
This commit is contained in:
Matthieu
2026-06-24 09:34:58 +02:00
parent 3fe108d38a
commit 80b2fa5ce6
6 changed files with 424 additions and 146 deletions
@@ -0,0 +1,144 @@
<template>
<MalioDrawer v-model="isOpen">
<template #header>
<h2 class="text-xl font-bold">
{{ isEditing ? $t('directory.reports.editTitle') : $t('directory.reports.addTitle') }}
</h2>
</template>
<form @submit.prevent="handleSubmit" class="flex flex-col gap-4">
<MalioInputText
v-model="form.subject"
:label="$t('directory.reports.fields.subject')"
input-class="w-full"
:error="touched.subject && !form.subject.trim() ? $t('directory.validation.subjectRequired') : ''"
@blur="touched.subject = true"
/>
<MalioSelect
v-model="form.type"
:label="$t('directory.reports.fields.type')"
:options="typeOptions"
group-class="w-full"
/>
<MalioDate
v-model="form.occurredAt"
:label="$t('directory.reports.fields.occurredAt')"
/>
<MalioInputRichText
v-model="form.body"
:label="$t('directory.reports.fields.body')"
min-height="180px"
/>
<div class="mt-4 flex justify-end gap-3">
<MalioButton
variant="tertiary"
button-class="w-auto px-4"
:label="$t('common.cancel')"
@click="isOpen = false"
/>
<MalioButton
button-class="w-auto px-6"
:label="$t('common.save')"
:disabled="isSubmitting"
@click="handleSubmit"
/>
</div>
</form>
</MalioDrawer>
</template>
<script setup lang="ts">
import type { CommercialReport, CommercialReportWrite, ReportType } from '~/modules/directory/services/dto/commercial-report'
import { useCommercialReportService } from '~/modules/directory/services/commercial-reports'
const props = defineProps<{
modelValue: boolean
report: CommercialReport | null
owner: { client?: string, prospect?: string }
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'saved'): void
}>()
const { t } = useI18n()
const { create, update } = useCommercialReportService()
const isOpen = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v),
})
const isEditing = computed(() => !!props.report)
const isSubmitting = ref(false)
const typeOptions: { label: string, value: ReportType }[] = [
{ label: t('directory.reports.types.call'), value: 'call' },
{ label: t('directory.reports.types.meeting'), value: 'meeting' },
{ label: t('directory.reports.types.email'), value: 'email' },
{ label: t('directory.reports.types.note'), value: 'note' },
]
function today(): string {
return new Date().toISOString().slice(0, 10)
}
// L'éditeur riche émet du HTML : un contenu « vide » vaut `<p></p>`. On le
// normalise en null pour ne pas persister une coquille vide.
function normalizeBody(html: string): string | null {
const stripped = html.replace(/<[^>]*>/g, '').replace(/&nbsp;/g, ' ').trim()
return stripped ? html : null
}
const form = reactive<{ subject: string, type: ReportType, occurredAt: string, body: string }>({
subject: '',
type: 'note',
occurredAt: today(),
body: '',
})
const touched = reactive({ subject: false })
watch(() => props.modelValue, (open) => {
if (!open) return
if (props.report) {
form.subject = props.report.subject
form.type = props.report.type
form.occurredAt = props.report.occurredAt.slice(0, 10)
form.body = props.report.body ?? ''
} else {
form.subject = ''
form.type = 'note'
form.occurredAt = today()
form.body = ''
}
touched.subject = false
})
async function handleSubmit(): Promise<void> {
touched.subject = true
if (!form.subject.trim() || isSubmitting.value) return
isSubmitting.value = true
try {
const payload: CommercialReportWrite = {
subject: form.subject.trim(),
type: form.type,
occurredAt: form.occurredAt,
body: normalizeBody(form.body),
...props.owner,
}
if (isEditing.value && props.report) {
await update(props.report.id, payload)
} else {
await create(payload)
}
emit('saved')
isOpen.value = false
} finally {
isSubmitting.value = false
}
}
</script>