Initial commit of the L'Ami Fiduciaire SaaS platform built on Laravel 12, Vue 3, Inertia.js 2, and Tailwind CSS 4. Story 0.1 (rename folders to declarations in database) is implemented and code-reviewed: migration, rollback, and 6 Pest tests all passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
|