394 lines
19 KiB
Vue
394 lines
19 KiB
Vue
|
|
<script setup lang="ts">
|
||
|
|
import { computed } from 'vue';
|
||
|
|
import { Head, Link } from '@inertiajs/vue3';
|
||
|
|
import {
|
||
|
|
Briefcase,
|
||
|
|
Building2,
|
||
|
|
Users,
|
||
|
|
FolderOpen,
|
||
|
|
AlertTriangle,
|
||
|
|
Clock,
|
||
|
|
FileCheck,
|
||
|
|
MessageSquareWarning,
|
||
|
|
ArrowRight,
|
||
|
|
Folder,
|
||
|
|
} from 'lucide-vue-next';
|
||
|
|
import AppLayout from '@/layouts/AppLayout.vue';
|
||
|
|
import type { BreadcrumbItem } from '@/types';
|
||
|
|
import { Button } from '@/components/ui/button';
|
||
|
|
import { Badge } from '@/components/ui/badge';
|
||
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||
|
|
import { dashboard } from '@/routes';
|
||
|
|
import PlaceholderPattern from '@/components/PlaceholderPattern.vue';
|
||
|
|
|
||
|
|
type AssignedFolder = {
|
||
|
|
id: number;
|
||
|
|
title: string;
|
||
|
|
type: string;
|
||
|
|
client_name: string;
|
||
|
|
status: string;
|
||
|
|
due_date: string | null;
|
||
|
|
priority: string | null;
|
||
|
|
showUrl: string;
|
||
|
|
};
|
||
|
|
|
||
|
|
type NotificationItem = {
|
||
|
|
id: number;
|
||
|
|
title: string;
|
||
|
|
client_name: string;
|
||
|
|
due_date?: string;
|
||
|
|
showUrl: string;
|
||
|
|
};
|
||
|
|
|
||
|
|
type Props = {
|
||
|
|
assignedFolders: AssignedFolder[];
|
||
|
|
notifications: {
|
||
|
|
overdue: NotificationItem[];
|
||
|
|
due_soon: NotificationItem[];
|
||
|
|
documents_received: NotificationItem[];
|
||
|
|
awaiting_validation: NotificationItem[];
|
||
|
|
};
|
||
|
|
workspaceName: string | null;
|
||
|
|
foldersUrl: string | null;
|
||
|
|
clientsUrl: string | null;
|
||
|
|
};
|
||
|
|
|
||
|
|
const props = defineProps<Props>();
|
||
|
|
|
||
|
|
const breadcrumbs: BreadcrumbItem[] = [
|
||
|
|
{
|
||
|
|
title: 'Dashboard',
|
||
|
|
href: dashboard().url,
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
const hasWorkspace = computed(() => !!props.workspaceName);
|
||
|
|
|
||
|
|
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 statusLabels: Record<string, string> = {
|
||
|
|
draft: 'Brouillon',
|
||
|
|
waiting_documents: 'En attente documents',
|
||
|
|
documents_received: 'Documents reçus',
|
||
|
|
processing: 'En cours',
|
||
|
|
additional_documents_requested: 'Pièces complémentaires',
|
||
|
|
waiting_client_validation: 'En attente validation',
|
||
|
|
validated: 'Validé',
|
||
|
|
closed: 'Clôturé',
|
||
|
|
cancelled: 'Annulé',
|
||
|
|
};
|
||
|
|
|
||
|
|
const statusVariant: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
|
||
|
|
draft: 'secondary',
|
||
|
|
waiting_documents: 'outline',
|
||
|
|
documents_received: 'default',
|
||
|
|
processing: 'default',
|
||
|
|
additional_documents_requested: 'default',
|
||
|
|
waiting_client_validation: 'outline',
|
||
|
|
validated: 'secondary',
|
||
|
|
closed: 'secondary',
|
||
|
|
cancelled: 'secondary',
|
||
|
|
};
|
||
|
|
|
||
|
|
function statusLabel(s: string): string {
|
||
|
|
return statusLabels[s] ?? s;
|
||
|
|
}
|
||
|
|
|
||
|
|
function typeLabel(t: string): string {
|
||
|
|
return typeLabels[t] ?? t;
|
||
|
|
}
|
||
|
|
|
||
|
|
function progressPercent(status: string): number {
|
||
|
|
const steps: Record<string, number> = {
|
||
|
|
draft: 0,
|
||
|
|
waiting_documents: 10,
|
||
|
|
documents_received: 30,
|
||
|
|
processing: 50,
|
||
|
|
additional_documents_requested: 45,
|
||
|
|
waiting_client_validation: 80,
|
||
|
|
validated: 100,
|
||
|
|
closed: 100,
|
||
|
|
cancelled: 0,
|
||
|
|
};
|
||
|
|
return steps[status] ?? 50;
|
||
|
|
}
|
||
|
|
|
||
|
|
const hasAnyNotifications = computed(
|
||
|
|
() =>
|
||
|
|
props.notifications.overdue.length > 0 ||
|
||
|
|
props.notifications.due_soon.length > 0 ||
|
||
|
|
props.notifications.documents_received.length > 0 ||
|
||
|
|
props.notifications.awaiting_validation.length > 0,
|
||
|
|
);
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<template>
|
||
|
|
|
||
|
|
<Head title="Dashboard" />
|
||
|
|
|
||
|
|
<AppLayout :breadcrumbs="breadcrumbs">
|
||
|
|
<div class="flex h-full flex-1 flex-col gap-6 overflow-x-auto rounded-xl p-4">
|
||
|
|
<!-- Quick links when no workspace -->
|
||
|
|
<div v-if="!hasWorkspace" class="grid auto-rows-min gap-4 md:grid-cols-3">
|
||
|
|
<Link href="/users"
|
||
|
|
class="relative flex aspect-video flex-col items-center justify-center gap-2 overflow-hidden rounded-xl border border-sidebar-border/70 transition-colors hover:bg-muted/50 dark:border-sidebar-border">
|
||
|
|
<Users class="h-8 w-8" />
|
||
|
|
<span class="font-medium">Users</span>
|
||
|
|
<span class="text-xs text-muted-foreground">Manage users</span>
|
||
|
|
</Link>
|
||
|
|
<Link href="/workspaces"
|
||
|
|
class="relative flex aspect-video flex-col items-center justify-center gap-2 overflow-hidden rounded-xl border border-sidebar-border/70 transition-colors hover:bg-muted/50 dark:border-sidebar-border">
|
||
|
|
<Building2 class="h-8 w-8" />
|
||
|
|
<span class="font-medium">Workspaces</span>
|
||
|
|
<span class="text-xs text-muted-foreground">Cabinets comptables</span>
|
||
|
|
</Link>
|
||
|
|
<Link v-if="clientsUrl" :href="clientsUrl"
|
||
|
|
class="relative flex aspect-video flex-col items-center justify-center gap-2 overflow-hidden rounded-xl border border-sidebar-border/70 transition-colors hover:bg-muted/50 dark:border-sidebar-border">
|
||
|
|
<Briefcase class="h-8 w-8" />
|
||
|
|
<span class="font-medium">Clients</span>
|
||
|
|
<span class="text-xs text-muted-foreground">Manage clients</span>
|
||
|
|
</Link>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div v-if="hasWorkspace" class="grid auto-rows-min gap-4 md:grid-cols-3">
|
||
|
|
<Link href="/folders"
|
||
|
|
class="relative flex aspect-video flex-col items-center justify-center gap-2 overflow-hidden rounded-xl border border-sidebar-border/70 transition-colors hover:bg-muted/50 dark:border-sidebar-border">
|
||
|
|
<Folder class="h-8 w-8" />
|
||
|
|
<span class="font-medium">Dossiers</span>
|
||
|
|
<span class="text-xs text-muted-foreground">Manage folders</span>
|
||
|
|
</Link>
|
||
|
|
<Link v-if="clientsUrl" :href="clientsUrl"
|
||
|
|
class="relative flex aspect-video flex-col items-center justify-center gap-2 overflow-hidden rounded-xl border border-sidebar-border/70 transition-colors hover:bg-muted/50 dark:border-sidebar-border">
|
||
|
|
<Briefcase class="h-8 w-8" />
|
||
|
|
<span class="font-medium">Clients</span>
|
||
|
|
<span class="text-xs text-muted-foreground">Manage clients</span>
|
||
|
|
</Link>
|
||
|
|
<div
|
||
|
|
class="relative aspect-video overflow-hidden rounded-xl border border-sidebar-border/70 dark:border-sidebar-border"
|
||
|
|
>
|
||
|
|
<PlaceholderPattern />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Workspace dashboard -->
|
||
|
|
<template v-if="hasWorkspace">
|
||
|
|
<!-- Notifications -->
|
||
|
|
<div v-if="hasAnyNotifications" class="space-y-4">
|
||
|
|
<h2 class="text-lg font-semibold">À traiter</h2>
|
||
|
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||
|
|
<Card v-if="notifications.overdue.length > 0" class="border-destructive/50 bg-destructive/5">
|
||
|
|
<CardHeader class="pb-2">
|
||
|
|
<CardTitle class="flex items-center gap-2 text-base">
|
||
|
|
<AlertTriangle class="h-4 w-4 text-destructive" />
|
||
|
|
En retard
|
||
|
|
</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent class="space-y-2">
|
||
|
|
<Link v-for="item in notifications.overdue" :key="item.id" :href="item.showUrl"
|
||
|
|
class="flex items-center justify-between rounded-md p-2 text-sm transition-colors hover:bg-muted/50">
|
||
|
|
<div class="truncate">
|
||
|
|
<span class="font-medium">{{
|
||
|
|
item.title
|
||
|
|
}}</span>
|
||
|
|
<span class="ml-1 text-muted-foreground">
|
||
|
|
{{ item.client_name }}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<ArrowRight class="h-4 w-4 shrink-0" />
|
||
|
|
</Link>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
<Card v-if="notifications.due_soon.length > 0" class="border-amber-500/50 bg-amber-500/5">
|
||
|
|
<CardHeader class="pb-2">
|
||
|
|
<CardTitle class="flex items-center gap-2 text-base">
|
||
|
|
<Clock class="h-4 w-4 text-amber-600" />
|
||
|
|
Échéance proche
|
||
|
|
</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent class="space-y-2">
|
||
|
|
<Link v-for="item in notifications.due_soon" :key="item.id" :href="item.showUrl"
|
||
|
|
class="flex items-center justify-between rounded-md p-2 text-sm transition-colors hover:bg-muted/50">
|
||
|
|
<div class="truncate">
|
||
|
|
<span class="font-medium">{{
|
||
|
|
item.title
|
||
|
|
}}</span>
|
||
|
|
<span class="ml-1 text-muted-foreground">
|
||
|
|
{{ item.client_name }} —
|
||
|
|
{{ item.due_date }}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<ArrowRight class="h-4 w-4 shrink-0" />
|
||
|
|
</Link>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
<Card v-if="notifications.documents_received.length > 0" class="border-primary/50 bg-primary/5">
|
||
|
|
<CardHeader class="pb-2">
|
||
|
|
<CardTitle class="flex items-center gap-2 text-base">
|
||
|
|
<FileCheck class="h-4 w-4 text-primary" />
|
||
|
|
Documents reçus
|
||
|
|
</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent class="space-y-2">
|
||
|
|
<Link v-for="item in notifications.documents_received" :key="item.id"
|
||
|
|
:href="item.showUrl"
|
||
|
|
class="flex items-center justify-between rounded-md p-2 text-sm transition-colors hover:bg-muted/50">
|
||
|
|
<div class="truncate">
|
||
|
|
<span class="font-medium">{{
|
||
|
|
item.title
|
||
|
|
}}</span>
|
||
|
|
<span class="ml-1 text-muted-foreground">
|
||
|
|
{{ item.client_name }}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<ArrowRight class="h-4 w-4 shrink-0" />
|
||
|
|
</Link>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
<Card v-if="notifications.awaiting_validation.length > 0"
|
||
|
|
class="border-blue-500/50 bg-blue-500/5">
|
||
|
|
<CardHeader class="pb-2">
|
||
|
|
<CardTitle class="flex items-center gap-2 text-base">
|
||
|
|
<MessageSquareWarning class="h-4 w-4 text-blue-600" />
|
||
|
|
En attente validation client
|
||
|
|
</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent class="space-y-2">
|
||
|
|
<Link v-for="item in notifications.awaiting_validation" :key="item.id"
|
||
|
|
:href="item.showUrl"
|
||
|
|
class="flex items-center justify-between rounded-md p-2 text-sm transition-colors hover:bg-muted/50">
|
||
|
|
<div class="truncate">
|
||
|
|
<span class="font-medium">{{
|
||
|
|
item.title
|
||
|
|
}}</span>
|
||
|
|
<span class="ml-1 text-muted-foreground">
|
||
|
|
{{ item.client_name }}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<ArrowRight class="h-4 w-4 shrink-0" />
|
||
|
|
</Link>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- My assigned dossiers -->
|
||
|
|
<div class="space-y-4">
|
||
|
|
<div class="flex items-center justify-between">
|
||
|
|
<h2 class="text-lg font-semibold">
|
||
|
|
Mes dossiers — {{ workspaceName }}
|
||
|
|
</h2>
|
||
|
|
<Button v-if="foldersUrl" variant="outline" as-child>
|
||
|
|
<Link :href="foldersUrl">
|
||
|
|
Tous les dossiers
|
||
|
|
<ArrowRight class="ml-1 h-4 w-4" />
|
||
|
|
</Link>
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Card v-if="assignedFolders.length > 0" class="overflow-hidden">
|
||
|
|
<div class="overflow-x-auto">
|
||
|
|
<table class="w-full text-sm">
|
||
|
|
<thead class="border-b border-sidebar-border/70 bg-muted/50">
|
||
|
|
<tr>
|
||
|
|
<th class="h-10 px-4 text-left font-medium">
|
||
|
|
Dossier / Client
|
||
|
|
</th>
|
||
|
|
<th class="h-10 px-4 text-left font-medium">
|
||
|
|
Type
|
||
|
|
</th>
|
||
|
|
<th class="h-10 px-4 text-left font-medium">
|
||
|
|
Statut
|
||
|
|
</th>
|
||
|
|
<th class="h-10 px-4 text-left font-medium">
|
||
|
|
Progression
|
||
|
|
</th>
|
||
|
|
<th class="h-10 px-4 text-left font-medium">
|
||
|
|
Date limite
|
||
|
|
</th>
|
||
|
|
<th class="h-10 w-10 px-4"></th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<tr v-for="folder in assignedFolders" :key="folder.id"
|
||
|
|
class="border-b border-sidebar-border/50 last:border-0 transition-colors hover:bg-muted/30">
|
||
|
|
<td class="px-4 py-3">
|
||
|
|
<Link :href="folder.showUrl" class="block font-medium hover:underline">
|
||
|
|
{{ folder.title }}
|
||
|
|
</Link>
|
||
|
|
<span class="block text-xs text-muted-foreground">
|
||
|
|
{{ folder.client_name }}
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
<td class="px-4 py-3 text-muted-foreground">
|
||
|
|
{{ typeLabel(folder.type) }}
|
||
|
|
</td>
|
||
|
|
<td class="px-4 py-3">
|
||
|
|
<Badge :variant="statusVariant[folder.status] ?? 'secondary'
|
||
|
|
">
|
||
|
|
{{
|
||
|
|
statusLabel(folder.status)
|
||
|
|
}}
|
||
|
|
</Badge>
|
||
|
|
</td>
|
||
|
|
<td class="px-4 py-3">
|
||
|
|
<div class="flex h-2 w-24 overflow-hidden rounded-full bg-muted">
|
||
|
|
<div class="h-full bg-primary transition-all" :style="{
|
||
|
|
width: `${progressPercent(folder.status)}%`,
|
||
|
|
}" />
|
||
|
|
</div>
|
||
|
|
<span class="text-xs text-muted-foreground">
|
||
|
|
{{ progressPercent(folder.status) }}%
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
<td class="px-4 py-3">
|
||
|
|
<span :class="{
|
||
|
|
'text-destructive font-medium':
|
||
|
|
folder.due_date &&
|
||
|
|
folder.due_date <
|
||
|
|
new Date()
|
||
|
|
.toISOString()
|
||
|
|
.slice(0, 10),
|
||
|
|
}">
|
||
|
|
{{ folder.due_date || '—' }}
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
<td class="px-4 py-3">
|
||
|
|
<Button variant="ghost" size="sm" as-child>
|
||
|
|
<Link :href="folder.showUrl">
|
||
|
|
Voir
|
||
|
|
<ArrowRight class="ml-1 h-3 w-3" />
|
||
|
|
</Link>
|
||
|
|
</Button>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Card v-else>
|
||
|
|
<CardContent class="flex flex-col items-center justify-center py-12">
|
||
|
|
<FolderOpen class="mb-3 h-12 w-12 text-muted-foreground" />
|
||
|
|
<p class="mb-2 text-muted-foreground">
|
||
|
|
Aucun dossier ne vous est assigné pour le moment.
|
||
|
|
</p>
|
||
|
|
<Button v-if="foldersUrl" as-child>
|
||
|
|
<Link :href="foldersUrl">Voir tous les dossiers</Link>
|
||
|
|
</Button>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
</div>
|
||
|
|
</AppLayout>
|
||
|
|
</template>
|