514 lines
21 KiB
Vue
514 lines
21 KiB
Vue
|
|
<script setup lang="ts">
|
||
|
|
import { Head, Link } from '@inertiajs/vue3';
|
||
|
|
import { computed } from 'vue';
|
||
|
|
import Heading from '@/components/Heading.vue';
|
||
|
|
import AppLayout from '@/layouts/AppLayout.vue';
|
||
|
|
import { Button } from '@/components/ui/button';
|
||
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||
|
|
import { Badge } from '@/components/ui/badge';
|
||
|
|
import FolderCalendar from '@/components/clients/FolderCalendar.vue';
|
||
|
|
import { Building2, FileText, FolderOpen } from 'lucide-vue-next';
|
||
|
|
|
||
|
|
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 Folder = {
|
||
|
|
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;
|
||
|
|
folders: Folder[];
|
||
|
|
stats: Stats;
|
||
|
|
indexUrl: string;
|
||
|
|
editUrl: string;
|
||
|
|
createFolderUrl: 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 folderStatusLabels: 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="createFolderUrl">Nouveau dossier</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 dossiers</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="whitespace-pre-wrap text-sm">
|
||
|
|
{{ 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">
|
||
|
|
Dossiers par date limite
|
||
|
|
</p>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<FolderCalendar :folders="folders" />
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Folders history -->
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>Historique des dossiers</CardTitle>
|
||
|
|
<p class="text-sm text-muted-foreground">
|
||
|
|
Derniers dossiers du client
|
||
|
|
</p>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div
|
||
|
|
v-if="folders.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"
|
||
|
|
>
|
||
|
|
Dossier
|
||
|
|
</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="folder in folders"
|
||
|
|
:key="folder.id"
|
||
|
|
class="hover:bg-muted/30"
|
||
|
|
>
|
||
|
|
<td class="px-4 py-3 font-medium">
|
||
|
|
{{ folder.title }}
|
||
|
|
</td>
|
||
|
|
<td class="px-4 py-3">
|
||
|
|
{{
|
||
|
|
typeLabels[folder.type] ??
|
||
|
|
folder.type
|
||
|
|
}}
|
||
|
|
</td>
|
||
|
|
<td class="px-4 py-3">
|
||
|
|
{{
|
||
|
|
folderStatusLabels[
|
||
|
|
folder.status
|
||
|
|
] ?? folder.status
|
||
|
|
}}
|
||
|
|
</td>
|
||
|
|
<td class="px-4 py-3">
|
||
|
|
{{ folder.due_date ?? '—' }}
|
||
|
|
</td>
|
||
|
|
<td class="px-4 py-3">
|
||
|
|
{{ folder.created_at }}
|
||
|
|
</td>
|
||
|
|
<td class="px-4 py-3">
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="sm"
|
||
|
|
as-child
|
||
|
|
>
|
||
|
|
<Link :href="folder.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"
|
||
|
|
>
|
||
|
|
Aucun dossier.
|
||
|
|
<Link
|
||
|
|
:href="createFolderUrl"
|
||
|
|
class="text-primary underline"
|
||
|
|
>Créer un dossier</Link
|
||
|
|
>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
</AppLayout>
|
||
|
|
</template>
|