Files
L-Ami-Fiduciaire/resources/js/pages/clients/Show.vue
Saad Ibn-Ezzoubayr 4807376c49 feat: implement Story 2.2 — Priority Alerts Panel with UI fixes
Add PriorityAlertsPanel component to the dashboard, update DashboardController
with alert logic, and apply misc UI fixes across sidebar, forms, and pages.
Includes epic-1 retrospective and sprint status update.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 12:33:27 +00:00

505 lines
21 KiB
Vue

<script setup lang="ts">
import { Head, Link } from '@inertiajs/vue3';
import { Building2, FileText, FolderOpen } from 'lucide-vue-next';
import DeclarationCalendar from '@/components/clients/DeclarationCalendar.vue';
import Heading from '@/components/Heading.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import AppLayout from '@/layouts/AppLayout.vue';
type ClientContact = {
id: number;
full_name: string;
job_title: string | null;
email: string | null;
phone: string | null;
is_principal: boolean;
};
type Client = {
id: number;
company_name: string;
legal_form: string;
ice: string | null;
fiscal_id: string | null;
rc: string | null;
cnss: string | null;
patente: string | null;
contacts: ClientContact[];
internal_responsible_id: number | null;
internal_responsible_name: string | null;
status: string | null;
internal_notes: string | null;
};
type Declaration = {
id: number;
title: string;
type: string;
status: string;
due_date: string | null;
created_at: string;
showUrl: string;
};
type Stats = {
total: number;
by_status: Record<string, number>;
by_type: Record<string, number>;
};
type Props = {
client: Client;
declarations: Declaration[];
stats: Stats;
indexUrl: string;
editUrl: string;
createDeclarationUrl: string;
};
const props = defineProps<Props>();
const statusLabels: Record<string, string> = {
actif: 'Actif',
inactif: 'Inactif',
suspendu: 'Suspendu',
};
const typeLabels: Record<string, string> = {
vat: 'TVA',
vat_monthly: 'TVA mensuelle',
vat_quarterly: 'TVA trimestrielle',
corporate_tax: 'IS',
income_tax: 'IR',
cnss: 'CNSS',
annual_balance: 'Bilan',
other: 'Autre',
};
const declarationStatusLabels: Record<string, string> = {
draft: 'Brouillon',
waiting_documents: 'En attente documents',
documents_received: 'Documents reçus',
processing: 'En cours de traitement',
additional_documents_requested: 'Pièces complémentaires demandées',
waiting_client_validation: 'En attente validation client',
validated: 'Validé',
closed: 'Clôturé',
cancelled: 'Annulé',
};
function getLegalFormLabel(legalForm: string): string {
const labels: Record<string, string> = {
sarl: 'SARL',
sa: 'SA',
snc: 'SNC',
scs: 'SCS',
eurl: 'EURL',
sel: 'SEL',
auto_entrepreneur: 'Auto-entrepreneur',
entreprise_individuelle: 'Entreprise individuelle',
other: 'Autre',
};
return labels[legalForm] ?? legalForm;
}
function getFieldValue(fieldKey: string): string {
const client = props.client as Record<string, unknown>;
if (fieldKey === 'legal_form') {
return getLegalFormLabel((client.legal_form as string) ?? '');
}
if (fieldKey === 'status') {
const status = client.status as string | null;
return status ? (statusLabels[status] ?? status) : '—';
}
if (fieldKey === 'internal_responsible_name') {
return (client.internal_responsible_name as string) ?? '—';
}
const val = client[fieldKey];
return val != null && val !== '' ? String(val) : '—';
}
</script>
<template>
<AppLayout
:breadcrumbs="[
{ title: 'Clients', href: props.indexUrl },
{ title: props.client.company_name },
]"
>
<Head :title="props.client.company_name" />
<div class="flex flex-col space-y-6 p-4">
<div class="flex items-center justify-between">
<Heading
:title="props.client.company_name"
:description="getLegalFormLabel(props.client.legal_form)"
/>
<div class="flex gap-2">
<Button variant="outline" as-child>
<Link :href="createDeclarationUrl"
>Nouvelle déclaration</Link
>
</Button>
<Button variant="outline" as-child>
<Link :href="editUrl">Modifier le client</Link>
</Button>
</div>
</div>
<!-- Stats -->
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader
class="flex flex-row items-center justify-between space-y-0 pb-2"
>
<CardTitle class="text-sm font-medium"
>Total déclarations</CardTitle
>
<FolderOpen class="size-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">
{{ stats.total }}
</div>
</CardContent>
</Card>
<Card>
<CardHeader
class="flex flex-row items-center justify-between space-y-0 pb-2"
>
<CardTitle class="text-sm font-medium"
>En cours</CardTitle
>
<FileText class="size-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">
{{
(stats.by_status?.processing ?? 0) +
(stats.by_status
?.additional_documents_requested ?? 0) +
(stats.by_status?.waiting_documents ?? 0) +
(stats.by_status?.documents_received ?? 0) +
(stats.by_status?.waiting_client_validation ??
0)
}}
</div>
</CardContent>
</Card>
<Card>
<CardHeader
class="flex flex-row items-center justify-between space-y-0 pb-2"
>
<CardTitle class="text-sm font-medium"
>Validés</CardTitle
>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">
{{ stats.by_status?.validated ?? 0 }}
</div>
</CardContent>
</Card>
<Card>
<CardHeader
class="flex flex-row items-center justify-between space-y-0 pb-2"
>
<CardTitle class="text-sm font-medium"
>Clôturés</CardTitle
>
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">
{{ stats.by_status?.closed ?? 0 }}
</div>
</CardContent>
</Card>
</div>
<div class="grid gap-6 lg:grid-cols-3">
<!-- Client info -->
<div class="space-y-6 lg:col-span-2">
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Building2 class="size-4" />
Informations société
</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<div class="grid gap-4 sm:grid-cols-2">
<div>
<p
class="text-sm font-medium text-muted-foreground"
>
ICE
</p>
<p class="text-sm">
{{ getFieldValue('ice') }}
</p>
</div>
<div>
<p
class="text-sm font-medium text-muted-foreground"
>
IF
</p>
<p class="text-sm">
{{ getFieldValue('fiscal_id') }}
</p>
</div>
<div>
<p
class="text-sm font-medium text-muted-foreground"
>
RC
</p>
<p class="text-sm">
{{ getFieldValue('rc') }}
</p>
</div>
<div>
<p
class="text-sm font-medium text-muted-foreground"
>
CNSS
</p>
<p class="text-sm">
{{ getFieldValue('cnss') }}
</p>
</div>
<div>
<p
class="text-sm font-medium text-muted-foreground"
>
Patente
</p>
<p class="text-sm">
{{ getFieldValue('patente') }}
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Responsables</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<div
v-for="contact in client.contacts"
:key="contact.id"
class="rounded-lg border border-sidebar-border/70 p-4"
>
<div class="mb-2 flex items-center gap-2">
<p class="text-sm font-medium">
{{ contact.full_name }}
</p>
<Badge
v-if="
contact.is_principal &&
client.contacts.length > 1
"
variant="secondary"
>
Principal
</Badge>
</div>
<div class="grid gap-2 sm:grid-cols-2">
<div v-if="contact.job_title">
<p
class="text-sm text-muted-foreground"
>
Fonction
</p>
<p class="text-sm">
{{ contact.job_title }}
</p>
</div>
<div v-if="contact.email">
<p
class="text-sm text-muted-foreground"
>
Email
</p>
<p class="text-sm">
{{ contact.email }}
</p>
</div>
<div v-if="contact.phone">
<p
class="text-sm text-muted-foreground"
>
Téléphone
</p>
<p class="text-sm">
{{ contact.phone }}
</p>
</div>
</div>
</div>
<p
v-if="!client.contacts.length"
class="text-sm text-muted-foreground"
>
Aucun responsable enregistré.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Suivi interne</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<div>
<p
class="text-sm font-medium text-muted-foreground"
>
Responsable
</p>
<p class="text-sm">
{{
getFieldValue(
'internal_responsible_name',
)
}}
</p>
</div>
<div>
<p
class="text-sm font-medium text-muted-foreground"
>
Statut
</p>
<p class="text-sm">
{{ getFieldValue('status') }}
</p>
</div>
<div v-if="client.internal_notes">
<p
class="text-sm font-medium text-muted-foreground"
>
Notes
</p>
<p class="text-sm whitespace-pre-wrap">
{{ client.internal_notes }}
</p>
</div>
</CardContent>
</Card>
</div>
<!-- Calendar -->
<div>
<Card>
<CardHeader>
<CardTitle>Calendrier des échéances</CardTitle>
<p class="text-sm text-muted-foreground">
Déclarations par date limite
</p>
</CardHeader>
<CardContent>
<DeclarationCalendar :declarations="declarations" />
</CardContent>
</Card>
</div>
</div>
<!-- Declarations history -->
<Card>
<CardHeader>
<CardTitle>Historique des déclarations</CardTitle>
<p class="text-sm text-muted-foreground">
Dernières déclarations du client
</p>
</CardHeader>
<CardContent>
<div
v-if="declarations.length"
class="overflow-x-auto rounded-xl border border-sidebar-border/70"
>
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr>
<th class="px-4 py-3 text-left font-medium">
Déclaration
</th>
<th class="px-4 py-3 text-left font-medium">
Type
</th>
<th class="px-4 py-3 text-left font-medium">
Statut
</th>
<th class="px-4 py-3 text-left font-medium">
Échéance
</th>
<th class="px-4 py-3 text-left font-medium">
Créé le
</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-sidebar-border/70">
<tr
v-for="declaration in declarations"
:key="declaration.id"
class="hover:bg-muted/30"
>
<td class="px-4 py-3 font-medium">
{{ declaration.title }}
</td>
<td class="px-4 py-3">
{{
typeLabels[declaration.type] ??
declaration.type
}}
</td>
<td class="px-4 py-3">
{{
declarationStatusLabels[
declaration.status
] ?? declaration.status
}}
</td>
<td class="px-4 py-3">
{{ declaration.due_date ?? '—' }}
</td>
<td class="px-4 py-3">
{{ declaration.created_at }}
</td>
<td class="px-4 py-3">
<Button
variant="ghost"
size="sm"
as-child
>
<Link :href="declaration.showUrl"
>Voir</Link
>
</Button>
</td>
</tr>
</tbody>
</table>
</div>
<div
v-else
class="rounded-xl border border-sidebar-border/70 p-8 text-center text-muted-foreground"
>
Aucune déclaration.
<Link
:href="createDeclarationUrl"
class="text-primary underline"
>Créer une déclaration</Link
>
</div>
</CardContent>
</Card>
</div>
</AppLayout>
</template>