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>
This commit is contained in:
2026-03-22 17:31:23 +01:00
parent 4807376c49
commit 3baf456640
7 changed files with 818 additions and 161 deletions

View File

@@ -3,6 +3,7 @@ import { Head, Link, router } from '@inertiajs/vue3';
import {
Briefcase,
Building2,
ClipboardList,
EllipsisVertical,
Eye,
FolderOpen,
@@ -54,6 +55,13 @@ const breadcrumbs: BreadcrumbItem[] = [
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 {
@@ -150,166 +158,205 @@ function navigateToDeclaration(declaration: DashboardDeclaration): void {
<!-- 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>
<!-- Worker subtitle -->
<p v-if="isWorker" class="text-sm text-muted-foreground">
Mes déclarations
</p>
<!-- Priority Alerts Panel -->
<PriorityAlertsPanel
:alerts="alerts"
:view-all-url="viewAllAlertsUrl"
/>
<!-- 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>
<!-- 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>
<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>
<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>
<!-- Priority Alerts Panel -->
<PriorityAlertsPanel
:alerts="alerts"
:view-all-url="viewAllAlertsUrl"
/>
<Card v-else>
<CardContent
class="flex flex-col items-center justify-center py-12"
<!-- 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"
>
<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>
<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>

View File

@@ -43,6 +43,7 @@ export type DashboardProps = {
alerts: DashboardAlert[];
workspaceName: string | null;
roleLabel: string | null;
isWorker: boolean;
declarationsUrl: string | null;
clientsUrl: string | null;
viewAllAlertsUrl: string | null;