feat(directory) : revamp commercial report tab and polish info tab
- 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:
@@ -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(/ /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>
|
||||
Reference in New Issue
Block a user