feat: complete Epic 0 — foundation migration & infrastructure setup

Stories 0.2-0.5: rename folders→declarations (backend+frontend), configure
Redis for cache/queue/sessions, add foundation database migrations
(permissions, archived_at), replace DeclarationStatus enum with architecture
lifecycle values, create DeclarationObserver for status transition validation
and auto-archive, fix controller status transitions to respect observer rules.

93 tests pass (240 assertions).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 18:25:32 +00:00
parent d380df4074
commit fd43a6f429
105 changed files with 3899 additions and 1558 deletions

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import { computed } from 'vue';
import { Head, Link } from '@inertiajs/vue3';
import {
Briefcase,
@@ -11,17 +10,18 @@ import {
FileCheck,
MessageSquareWarning,
ArrowRight,
Folder,
FileStack,
} 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 { computed } from 'vue';
import PlaceholderPattern from '@/components/PlaceholderPattern.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import AppLayout from '@/layouts/AppLayout.vue';
import { dashboard } from '@/routes';
import type { BreadcrumbItem } from '@/types';
type AssignedFolder = {
type AssignedDeclaration = {
id: number;
title: string;
type: string;
@@ -41,7 +41,7 @@ type NotificationItem = {
};
type Props = {
assignedFolders: AssignedFolder[];
assignedDeclarations: AssignedDeclaration[];
notifications: {
overdue: NotificationItem[];
due_soon: NotificationItem[];
@@ -49,7 +49,7 @@ type Props = {
awaiting_validation: NotificationItem[];
};
workspaceName: string | null;
foldersUrl: string | null;
declarationsUrl: string | null;
clientsUrl: string | null;
};
@@ -87,7 +87,10 @@ const statusLabels: Record<string, string> = {
cancelled: 'Annulé',
};
const statusVariant: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
const statusVariant: Record<
string,
'default' | 'secondary' | 'destructive' | 'outline'
> = {
draft: 'secondary',
waiting_documents: 'outline',
documents_received: 'default',
@@ -132,45 +135,75 @@ const hasAnyNotifications = computed(
</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">
<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">
<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>
<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">
<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>
<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">
<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>
<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>
<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">
<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>
<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"
@@ -185,21 +218,34 @@ const hasAnyNotifications = computed(
<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">
<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" />
<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">
<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">
}}</span>
<span
class="ml-1 text-muted-foreground"
>
{{ item.client_name }}
</span>
</div>
@@ -207,21 +253,32 @@ const hasAnyNotifications = computed(
</Link>
</CardContent>
</Card>
<Card v-if="notifications.due_soon.length > 0" class="border-amber-500/50 bg-amber-500/5">
<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">
<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">
<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">
}}</span>
<span
class="ml-1 text-muted-foreground"
>
{{ item.client_name }}
{{ item.due_date }}
</span>
@@ -230,22 +287,32 @@ const hasAnyNotifications = computed(
</Link>
</CardContent>
</Card>
<Card v-if="notifications.documents_received.length > 0" class="border-primary/50 bg-primary/5">
<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">
<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"
<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">
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">
}}</span>
<span
class="ml-1 text-muted-foreground"
>
{{ item.client_name }}
</span>
</div>
@@ -253,23 +320,34 @@ const hasAnyNotifications = computed(
</Link>
</CardContent>
</Card>
<Card v-if="notifications.awaiting_validation.length > 0"
class="border-blue-500/50 bg-blue-500/5">
<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" />
<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"
<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">
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">
}}</span>
<span
class="ml-1 text-muted-foreground"
>
{{ item.client_name }}
</span>
</div>
@@ -280,92 +358,151 @@ const hasAnyNotifications = computed(
</div>
</div>
<!-- My assigned dossiers -->
<!-- My assigned declarations -->
<div class="space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold">
Mes dossiers {{ workspaceName }}
Mes déclarations {{ workspaceName }}
</h2>
<Button v-if="foldersUrl" variant="outline" as-child>
<Link :href="foldersUrl">
Tous les dossiers
<Button
v-if="declarationsUrl"
variant="outline"
as-child
>
<Link :href="declarationsUrl">
Toutes les déclarations
<ArrowRight class="ml-1 h-4 w-4" />
</Link>
</Button>
</div>
<Card v-if="assignedFolders.length > 0" class="overflow-hidden">
<Card
v-if="assignedDeclarations.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">
<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
class="h-10 px-4 text-left font-medium"
>
Déclaration / Client
</th>
<th class="h-10 px-4 text-left font-medium">
<th
class="h-10 px-4 text-left font-medium"
>
Type
</th>
<th class="h-10 px-4 text-left font-medium">
<th
class="h-10 px-4 text-left font-medium"
>
Statut
</th>
<th class="h-10 px-4 text-left font-medium">
<th
class="h-10 px-4 text-left font-medium"
>
Progression
</th>
<th class="h-10 px-4 text-left font-medium">
<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">
<tr
v-for="declaration in assignedDeclarations"
:key="declaration.id"
class="border-b border-sidebar-border/50 transition-colors last:border-0 hover:bg-muted/30"
>
<td class="px-4 py-3">
<Link :href="folder.showUrl" class="block font-medium hover:underline">
{{ folder.title }}
<Link
:href="declaration.showUrl"
class="block font-medium hover:underline"
>
{{ declaration.title }}
</Link>
<span class="block text-xs text-muted-foreground">
{{ folder.client_name }}
<span
class="block text-xs text-muted-foreground"
>
{{ declaration.client_name }}
</span>
</td>
<td class="px-4 py-3 text-muted-foreground">
{{ typeLabel(folder.type) }}
<td
class="px-4 py-3 text-muted-foreground"
>
{{ typeLabel(declaration.type) }}
</td>
<td class="px-4 py-3">
<Badge :variant="statusVariant[folder.status] ?? 'secondary'
">
<Badge
:variant="
statusVariant[
declaration.status
] ?? 'secondary'
"
>
{{
statusLabel(folder.status)
statusLabel(
declaration.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
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(folder.status) }}%
<span
class="text-xs text-muted-foreground"
>
{{
progressPercent(
declaration.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
: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="folder.showUrl">
<Button
variant="ghost"
size="sm"
as-child
>
<Link
:href="declaration.showUrl"
>
Voir
<ArrowRight class="ml-1 h-3 w-3" />
<ArrowRight
class="ml-1 h-3 w-3"
/>
</Link>
</Button>
</td>
@@ -376,13 +513,20 @@ const hasAnyNotifications = computed(
</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" />
<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.
Aucune déclaration ne vous est assignée pour le
moment.
</p>
<Button v-if="foldersUrl" as-child>
<Link :href="foldersUrl">Voir tous les dossiers</Link>
<Button v-if="declarationsUrl" as-child>
<Link :href="declarationsUrl"
>Voir toutes les déclarations</Link
>
</Button>
</CardContent>
</Card>