Stories 0.2-0.5: rename folders→declarations (backend+frontend), configure Redis for cache/queue/sessions, add foundation database migrations (permissions, archived_at), replace DeclarationStatus enum with architecture lifecycle values, create DeclarationObserver for status transition validation and auto-archive, fix controller status transitions to respect observer rules. 93 tests pass (240 assertions). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
506 lines
21 KiB
Vue
506 lines
21 KiB
Vue
<script setup lang="ts">
|
|
import { Head, Link } from '@inertiajs/vue3';
|
|
import { Building2, FileText, FolderOpen } from 'lucide-vue-next';
|
|
import { computed } from 'vue';
|
|
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>
|