feat: implement Story 2.1 — Owner/Manager Command Center Dashboard

- Rewrite DashboardController with cached role-scoped KPI aggregation
  (Cache::remember, 5-min TTL, Declaration::forUser scope)
- Create StatCard.vue component with CVA status variants and a11y
- Rewrite Dashboard.vue with 4-column KPI grid + urgent declarations table
- Add mise_en_demeure status to DeclarationStatus enum with transitions
- Exclude termine, mise_en_demeure, ferme from dashboard queries
- Set deadline proximity red threshold to ≤5 days
- Add abort(404) for non-member workspace access per architecture
- Fix null-safe client access for soft-deleted clients
- Fix hardcoded routes with Wayfinder type-safe imports
- Fix DashboardProps.stats type to allow null
- Add aria-pressed to StatCard for accessibility
- Install shadcn-vue table component (11 files)
- Add 11 Pest feature tests + 3 mise_en_demeure transition tests
- Fix DeclarationFactory eager workspace creation causing slug collisions
- 196 tests pass, 836 assertions, zero regressions
This commit is contained in:
2026-03-20 12:00:24 +00:00
parent e53b013359
commit a2ab6f365d
23 changed files with 1283 additions and 523 deletions

View File

@@ -0,0 +1,81 @@
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
import { cva } from 'class-variance-authority';
import { computed } from 'vue';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { cn } from '@/lib/utils';
type Props = {
label: string;
count: number;
status: 'danger' | 'warning' | 'info' | 'success';
href: string;
loading?: boolean;
};
const props = withDefaults(defineProps<Props>(), {
loading: false,
});
const cardVariants = cva(
'cursor-pointer transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none',
{
variants: {
status: {
danger: 'border-red-500/50 bg-red-500/5 hover:bg-red-500/10',
warning:
'border-amber-500/50 bg-amber-500/5 hover:bg-amber-500/10',
info: 'border-blue-500/50 bg-blue-500/5 hover:bg-blue-500/10',
success:
'border-green-500/50 bg-green-500/5 hover:bg-green-500/10',
},
},
},
);
const countColorClass = computed(() => {
const map: Record<string, string> = {
danger: 'text-red-600',
warning: 'text-amber-600',
info: 'text-blue-600',
success: 'text-green-600',
};
return map[props.status] ?? '';
});
function navigate(): void {
router.get(props.href);
}
function handleKeydown(event: KeyboardEvent): void {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
navigate();
}
}
</script>
<template>
<Card
:class="cn(cardVariants({ status }))"
role="button"
tabindex="0"
aria-pressed="false"
@click="navigate"
@keydown="handleKeydown"
>
<CardHeader class="pb-2">
<CardTitle class="text-sm font-medium text-muted-foreground">
<Skeleton v-if="loading" class="h-4 w-20" />
<template v-else>{{ label }}</template>
</CardTitle>
</CardHeader>
<CardContent>
<Skeleton v-if="loading" class="h-8 w-16" />
<div v-else :class="cn('text-3xl font-bold', countColorClass)">
{{ count }}
</div>
</CardContent>
</Card>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div data-slot="table-container" class="relative w-full overflow-auto">
<table data-slot="table" :class="cn('w-full caption-bottom text-sm', props.class)">
<slot />
</table>
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<tbody
data-slot="table-body"
:class="cn('[&_tr:last-child]:border-0', props.class)"
>
<slot />
</tbody>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<caption
data-slot="table-caption"
:class="cn('text-muted-foreground mt-4 text-sm', props.class)"
>
<slot />
</caption>
</template>

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<td
data-slot="table-cell"
:class="
cn(
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
props.class,
)
"
>
<slot />
</td>
</template>

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { cn } from "@/lib/utils"
import TableCell from "./TableCell.vue"
import TableRow from "./TableRow.vue"
const props = withDefaults(defineProps<{
class?: HTMLAttributes["class"]
colspan?: number
}>(), {
colspan: 1,
})
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<TableRow>
<TableCell
:class="
cn(
'p-4 whitespace-nowrap align-middle text-sm text-foreground',
props.class,
)
"
v-bind="delegatedProps"
>
<div class="flex items-center justify-center py-10">
<slot />
</div>
</TableCell>
</TableRow>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<tfoot
data-slot="table-footer"
:class="cn('bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', props.class)"
>
<slot />
</tfoot>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<th
data-slot="table-head"
:class="cn('text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]', props.class)"
>
<slot />
</th>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<thead
data-slot="table-header"
:class="cn('[&_tr]:border-b', props.class)"
>
<slot />
</thead>
</template>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<tr
data-slot="table-row"
:class="cn('hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors', props.class)"
>
<slot />
</tr>
</template>

View File

@@ -0,0 +1,9 @@
export { default as Table } from "./Table.vue"
export { default as TableBody } from "./TableBody.vue"
export { default as TableCaption } from "./TableCaption.vue"
export { default as TableCell } from "./TableCell.vue"
export { default as TableEmpty } from "./TableEmpty.vue"
export { default as TableFooter } from "./TableFooter.vue"
export { default as TableHead } from "./TableHead.vue"
export { default as TableHeader } from "./TableHeader.vue"
export { default as TableRow } from "./TableRow.vue"

View File

@@ -0,0 +1,10 @@
import type { Updater } from "@tanstack/vue-table"
import type { Ref } from "vue"
import { isFunction } from "@tanstack/vue-table"
export function valueUpdater<T>(updaterOrValue: Updater<T>, ref: Ref<T>) {
ref.value = isFunction(updaterOrValue)
? updaterOrValue(ref.value)
: updaterOrValue
}

View File

@@ -1,57 +1,46 @@
<script setup lang="ts">
import { Head, Link } from '@inertiajs/vue3';
import { Head, Link, router } from '@inertiajs/vue3';
import {
Briefcase,
Building2,
Users,
EllipsisVertical,
Eye,
FolderOpen,
AlertTriangle,
Clock,
FileCheck,
MessageSquareWarning,
ArrowRight,
FileStack,
Send,
UserRoundCog,
Users,
} from 'lucide-vue-next';
import { computed } from 'vue';
import PlaceholderPattern from '@/components/PlaceholderPattern.vue';
import StatCard from '@/components/dashboard/StatCard.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
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 type { BreadcrumbItem } from '@/types';
import { index as usersIndex } from '@/routes/users';
import { index as workspacesIndex } from '@/routes/workspaces';
import type {
BreadcrumbItem,
DashboardDeclaration,
DashboardProps,
StatCardLink,
} from '@/types';
type AssignedDeclaration = {
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 = {
assignedDeclarations: AssignedDeclaration[];
notifications: {
overdue: NotificationItem[];
due_soon: NotificationItem[];
documents_received: NotificationItem[];
awaiting_validation: NotificationItem[];
};
workspaceName: string | null;
declarationsUrl: string | null;
clientsUrl: string | null;
};
type Props = DashboardProps;
const props = defineProps<Props>();
@@ -64,74 +53,53 @@ const breadcrumbs: BreadcrumbItem[] = [
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',
};
type DeadlineProximity = 'safe' | 'approaching' | 'urgent' | 'overdue' | 'none';
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 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 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,
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 steps[status] ?? 50;
return map[proximity];
}
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,
);
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>
@@ -141,13 +109,13 @@ const hasAnyNotifications = computed(
<div
class="flex h-full flex-1 flex-col gap-6 overflow-x-auto rounded-xl p-4"
>
<!-- Quick links when no workspace -->
<!-- Quick links when no workspace (admin view) -->
<div
v-if="!hasWorkspace"
class="grid auto-rows-min gap-4 md:grid-cols-3"
>
<Link
href="/users"
: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" />
@@ -157,7 +125,7 @@ const hasAnyNotifications = computed(
>
</Link>
<Link
href="/workspaces"
: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" />
@@ -179,190 +147,27 @@ const hasAnyNotifications = computed(
</Link>
</div>
<div
v-if="hasWorkspace"
class="grid auto-rows-min gap-4 md:grid-cols-3"
>
<Link
v-if="declarationsUrl"
:href="declarationsUrl"
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"
>
<FileStack class="h-8 w-8" />
<span class="font-medium">Déclarations</span>
<span class="text-xs text-muted-foreground"
>Gérer les déclarations</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>
<!-- 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>
<!-- My assigned declarations -->
<!-- Urgent Declarations Table -->
<div class="space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold">
Mes déclarations {{ workspaceName }}
Déclarations urgentes
</h2>
<Button
v-if="declarationsUrl"
@@ -371,144 +176,117 @@ const hasAnyNotifications = computed(
>
<Link :href="declarationsUrl">
Toutes les déclarations
<ArrowRight class="ml-1 h-4 w-4" />
</Link>
</Button>
</div>
<Card
v-if="assignedDeclarations.length > 0"
v-if="declarations.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"
>
Déclaration / 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="declaration in assignedDeclarations"
<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="border-b border-sidebar-border/50 transition-colors last:border-0 hover:bg-muted/30"
class="cursor-pointer"
@click="
navigateToDeclaration(declaration)
"
>
<td class="px-4 py-3">
<Link
:href="declaration.showUrl"
class="block font-medium hover:underline"
>
{{ declaration.title }}
</Link>
<TableCell class="font-medium">
{{ declaration.clientName }}
</TableCell>
<TableCell>
{{ declaration.typeLabel }}
</TableCell>
<TableCell>
<span
class="block text-xs text-muted-foreground"
>
{{ declaration.client_name }}
</span>
</td>
<td
class="px-4 py-3 text-muted-foreground"
>
{{ typeLabel(declaration.type) }}
</td>
<td class="px-4 py-3">
<Badge
:variant="
statusVariant[
declaration.status
] ?? 'secondary'
:class="
deadlineClass(
declaration.dueDate,
)
"
>
{{
statusLabel(
{{ declaration.dueDate ?? '—' }}
</span>
</TableCell>
<TableCell>
{{
declaration.assigneeName ?? '—'
}}
</TableCell>
<TableCell>
<Badge
:variant="
statusBadgeVariant(
declaration.status,
)
}}
"
>
{{ declaration.statusLabel }}
</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(declaration.status)}%`,
}"
/>
</div>
<span
class="text-xs text-muted-foreground"
>
{{
progressPercent(
declaration.status,
)
}}%
</span>
</td>
<td class="px-4 py-3">
<span
:class="{
'font-medium text-destructive':
declaration.due_date &&
declaration.due_date <
new Date()
.toISOString()
.slice(0, 10),
}"
>
{{
declaration.due_date || '—'
}}
</span>
</td>
<td class="px-4 py-3">
<Button
variant="ghost"
size="sm"
as-child
>
<Link
:href="declaration.showUrl"
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger
as-child
@click.stop
>
Voir
<ArrowRight
class="ml-1 h-3 w-3"
/>
</Link>
</Button>
</td>
</tr>
</tbody>
</table>
<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>
@@ -519,15 +297,9 @@ const hasAnyNotifications = computed(
<FolderOpen
class="mb-3 h-12 w-12 text-muted-foreground"
/>
<p class="mb-2 text-muted-foreground">
Aucune déclaration ne vous est assignée pour le
moment.
<p class="text-muted-foreground">
Aucune déclaration urgente pour le moment.
</p>
<Button v-if="declarationsUrl" as-child>
<Link :href="declarationsUrl"
>Voir toutes les déclarations</Link
>
</Button>
</CardContent>
</Card>
</div>

View File

@@ -0,0 +1,36 @@
export type DashboardStats = {
overdue: number;
dueThisWeek: number;
enAttenteClient: number;
enCours: number;
};
export type DashboardDeclaration = {
id: number;
title: string;
type: string;
typeLabel: string;
clientName: string;
assigneeName: string | null;
status: string;
statusLabel: string;
dueDate: string | null;
showUrl: string;
};
export type StatCardLink = {
label: string;
count: number;
status: 'danger' | 'warning' | 'info' | 'success';
href: string;
};
export type DashboardProps = {
stats: DashboardStats | null;
statCards: StatCardLink[];
declarations: DashboardDeclaration[];
workspaceName: string | null;
roleLabel: string | null;
declarationsUrl: string | null;
clientsUrl: string | null;
};

View File

@@ -1,4 +1,5 @@
export * from './auth';
export * from './dashboard';
export * from './navigation';
export * from './team';
export * from './ui';