Files
L-Ami-Fiduciaire/resources/js/pages/Dashboard.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

317 lines
13 KiB
Vue

<script setup lang="ts">
import { Head, Link, router } from '@inertiajs/vue3';
import {
Briefcase,
Building2,
EllipsisVertical,
Eye,
FolderOpen,
Send,
UserRoundCog,
Users,
} from 'lucide-vue-next';
import { computed } from 'vue';
import PriorityAlertsPanel from '@/components/dashboard/PriorityAlertsPanel.vue';
import StatCard from '@/components/dashboard/StatCard.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import AppLayout from '@/layouts/AppLayout.vue';
import { dashboard } from '@/routes';
import { index as usersIndex } from '@/routes/users';
import { index as workspacesIndex } from '@/routes/workspaces';
import type {
BreadcrumbItem,
DashboardDeclaration,
DashboardProps,
StatCardLink,
} from '@/types';
type Props = DashboardProps;
const props = defineProps<Props>();
const breadcrumbs: BreadcrumbItem[] = [
{
title: 'Dashboard',
href: dashboard().url,
},
];
const hasWorkspace = computed(() => !!props.workspaceName);
type DeadlineProximity = 'safe' | 'approaching' | 'urgent' | 'overdue' | 'none';
function deadlineProximity(dueDate: string | null): DeadlineProximity {
if (!dueDate) return 'none';
const now = new Date();
const deadline = new Date(dueDate);
const diffDays = Math.ceil(
(deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),
);
if (diffDays < 0) return 'overdue';
if (diffDays <= 5) return 'urgent';
if (diffDays <= 7) return 'approaching';
return 'safe';
}
function deadlineClass(dueDate: string | null): string {
const proximity = deadlineProximity(dueDate);
const map: Record<DeadlineProximity, string> = {
safe: 'text-green-600',
approaching: 'text-amber-600',
urgent: 'text-red-600',
overdue: 'text-red-600 animate-pulse',
none: 'text-muted-foreground',
};
return map[proximity];
}
function statusBadgeVariant(
status: string,
): 'default' | 'secondary' | 'destructive' | 'outline' {
const map: Record<
string,
'default' | 'secondary' | 'destructive' | 'outline'
> = {
created: 'secondary',
en_cours: 'default',
en_attente_client: 'outline',
termine: 'secondary',
mise_en_demeure: 'destructive',
ferme: 'secondary',
};
return map[status] ?? 'secondary';
}
function navigateToDeclaration(declaration: DashboardDeclaration): void {
router.get(declaration.showUrl);
}
</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 (admin view) -->
<div
v-if="!hasWorkspace"
class="grid auto-rows-min gap-4 md:grid-cols-3"
>
<Link
:href="usersIndex().url"
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="workspacesIndex().url"
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>
<!-- Workspace dashboard -->
<template v-if="hasWorkspace">
<!-- KPI StatCards -->
<div
class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4"
>
<StatCard
v-for="card in statCards"
:key="card.label"
:label="card.label"
:count="card.count"
:status="card.status as StatCardLink['status']"
:href="card.href"
/>
</div>
<!-- Priority Alerts Panel -->
<PriorityAlertsPanel
:alerts="alerts"
:view-all-url="viewAllAlertsUrl"
/>
<!-- Urgent Declarations Table -->
<div class="space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold">
Déclarations urgentes
</h2>
<Button
v-if="declarationsUrl"
variant="outline"
as-child
>
<Link :href="declarationsUrl">
Toutes les déclarations
</Link>
</Button>
</div>
<Card
v-if="declarations.length > 0"
class="overflow-hidden"
>
<div class="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Client</TableHead>
<TableHead>Type</TableHead>
<TableHead>Date limite</TableHead>
<TableHead>Assigné à</TableHead>
<TableHead>Statut</TableHead>
<TableHead class="w-10" />
</TableRow>
</TableHeader>
<TableBody>
<TableRow
v-for="declaration in declarations"
:key="declaration.id"
class="cursor-pointer"
@click="
navigateToDeclaration(declaration)
"
>
<TableCell class="font-medium">
{{ declaration.clientName }}
</TableCell>
<TableCell>
{{ declaration.typeLabel }}
</TableCell>
<TableCell>
<span
:class="
deadlineClass(
declaration.dueDate,
)
"
>
{{ declaration.dueDate ?? '—' }}
</span>
</TableCell>
<TableCell>
{{
declaration.assigneeName ?? '—'
}}
</TableCell>
<TableCell>
<Badge
:variant="
statusBadgeVariant(
declaration.status,
)
"
>
{{ declaration.statusLabel }}
</Badge>
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger
as-child
@click.stop
>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
>
<EllipsisVertical
class="h-4 w-4"
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
>
<DropdownMenuItem
@click.stop="
navigateToDeclaration(
declaration,
)
"
>
<Eye
class="mr-2 h-4 w-4"
/>
Voir
</DropdownMenuItem>
<DropdownMenuItem disabled>
<Send
class="mr-2 h-4 w-4"
/>
Relancer
</DropdownMenuItem>
<DropdownMenuItem disabled>
<UserRoundCog
class="mr-2 h-4 w-4"
/>
Réassigner
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
</TableBody>
</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="text-muted-foreground">
Aucune déclaration urgente pour le moment.
</p>
</CardContent>
</Card>
</div>
</template>
</div>
</AppLayout>
</template>