Files
L-Ami-Fiduciaire/resources/js/pages/Dashboard.vue
Saad Ibn-Ezzoubayr 3baf456640 feat: implement Story 2.3 — Worker-Scoped Dashboard
Scope stat cards and urgent declarations table to the authenticated
worker's own assignments. Add empty state when no declarations are
assigned, hide the "Assigné à" column for worker role, and expose
isWorker flag through DashboardController and dashboard types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 17:31:23 +01:00

364 lines
16 KiB
Vue

<script setup lang="ts">
import { Head, Link, router } from '@inertiajs/vue3';
import {
Briefcase,
Building2,
ClipboardList,
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);
const isWorkerEmpty = computed(
() =>
props.isWorker &&
props.declarations.length === 0 &&
props.statCards.every((c) => c.count === 0),
);
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">
<!-- Worker subtitle -->
<p v-if="isWorker" class="text-sm text-muted-foreground">
Mes déclarations
</p>
<!-- Worker empty state -->
<Card v-if="isWorkerEmpty">
<CardContent
class="flex flex-col items-center justify-center py-16"
>
<ClipboardList
class="mb-3 h-12 w-12 text-muted-foreground"
/>
<p class="text-lg font-medium">
Aucune déclaration assignée
</p>
<p class="text-sm text-muted-foreground">
Contactez votre responsable pour recevoir des
déclarations
</p>
</CardContent>
</Card>
<template v-if="!isWorkerEmpty">
<!-- 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 v-if="!isWorker"
>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 v-if="!isWorker">
{{
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>
</template>
</div>
</AppLayout>
</template>