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,13 +1,13 @@
<script setup lang="ts">
import { Head, Link } from '@inertiajs/vue3';
import { Building2, FileText, FolderOpen } from 'lucide-vue-next';
import { computed } from 'vue';
import DeclarationCalendar from '@/components/clients/DeclarationCalendar.vue';
import Heading from '@/components/Heading.vue';
import AppLayout from '@/layouts/AppLayout.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import FolderCalendar from '@/components/clients/FolderCalendar.vue';
import { Building2, FileText, FolderOpen } from 'lucide-vue-next';
import AppLayout from '@/layouts/AppLayout.vue';
type ClientContact = {
id: number;
@@ -34,7 +34,7 @@ type Client = {
internal_notes: string | null;
};
type Folder = {
type Declaration = {
id: number;
title: string;
type: string;
@@ -52,11 +52,11 @@ type Stats = {
type Props = {
client: Client;
folders: Folder[];
declarations: Declaration[];
stats: Stats;
indexUrl: string;
editUrl: string;
createFolderUrl: string;
createDeclarationUrl: string;
};
const props = defineProps<Props>();
@@ -78,7 +78,7 @@ const typeLabels: Record<string, string> = {
other: 'Autre',
};
const folderStatusLabels: Record<string, string> = {
const declarationStatusLabels: Record<string, string> = {
draft: 'Brouillon',
waiting_documents: 'En attente documents',
documents_received: 'Documents reçus',
@@ -139,7 +139,9 @@ function getFieldValue(fieldKey: string): string {
/>
<div class="flex gap-2">
<Button variant="outline" as-child>
<Link :href="createFolderUrl">Nouveau dossier</Link>
<Link :href="createDeclarationUrl"
>Nouvelle déclaration</Link
>
</Button>
<Button variant="outline" as-child>
<Link :href="editUrl">Modifier le client</Link>
@@ -154,7 +156,7 @@ function getFieldValue(fieldKey: string): string {
class="flex flex-row items-center justify-between space-y-0 pb-2"
>
<CardTitle class="text-sm font-medium"
>Total dossiers</CardTitle
>Total déclarations</CardTitle
>
<FolderOpen class="size-4 text-muted-foreground" />
</CardHeader>
@@ -181,8 +183,8 @@ function getFieldValue(fieldKey: string): string {
?.additional_documents_requested ?? 0) +
(stats.by_status?.waiting_documents ?? 0) +
(stats.by_status?.documents_received ?? 0) +
(stats.by_status
?.waiting_client_validation ?? 0)
(stats.by_status?.waiting_client_validation ??
0)
}}
</div>
</CardContent>
@@ -384,7 +386,7 @@ function getFieldValue(fieldKey: string): string {
>
Notes
</p>
<p class="whitespace-pre-wrap text-sm">
<p class="text-sm whitespace-pre-wrap">
{{ client.internal_notes }}
</p>
</div>
@@ -398,55 +400,45 @@ function getFieldValue(fieldKey: string): string {
<CardHeader>
<CardTitle>Calendrier des échéances</CardTitle>
<p class="text-sm text-muted-foreground">
Dossiers par date limite
Déclarations par date limite
</p>
</CardHeader>
<CardContent>
<FolderCalendar :folders="folders" />
<DeclarationCalendar :declarations="declarations" />
</CardContent>
</Card>
</div>
</div>
<!-- Folders history -->
<!-- Declarations history -->
<Card>
<CardHeader>
<CardTitle>Historique des dossiers</CardTitle>
<CardTitle>Historique des déclarations</CardTitle>
<p class="text-sm text-muted-foreground">
Derniers dossiers du client
Dernières déclarations du client
</p>
</CardHeader>
<CardContent>
<div
v-if="folders.length"
v-if="declarations.length"
class="overflow-x-auto rounded-xl border border-sidebar-border/70"
>
<table class="w-full text-sm">
<thead class="bg-muted/50">
<tr>
<th
class="px-4 py-3 text-left font-medium"
>
Dossier
<th class="px-4 py-3 text-left font-medium">
claration
</th>
<th
class="px-4 py-3 text-left font-medium"
>
<th class="px-4 py-3 text-left font-medium">
Type
</th>
<th
class="px-4 py-3 text-left font-medium"
>
<th class="px-4 py-3 text-left font-medium">
Statut
</th>
<th
class="px-4 py-3 text-left font-medium"
>
<th class="px-4 py-3 text-left font-medium">
Échéance
</th>
<th
class="px-4 py-3 text-left font-medium"
>
<th class="px-4 py-3 text-left font-medium">
Créé le
</th>
<th class="px-4 py-3"></th>
@@ -454,31 +446,31 @@ function getFieldValue(fieldKey: string): string {
</thead>
<tbody class="divide-y divide-sidebar-border/70">
<tr
v-for="folder in folders"
:key="folder.id"
v-for="declaration in declarations"
:key="declaration.id"
class="hover:bg-muted/30"
>
<td class="px-4 py-3 font-medium">
{{ folder.title }}
{{ declaration.title }}
</td>
<td class="px-4 py-3">
{{
typeLabels[folder.type] ??
folder.type
typeLabels[declaration.type] ??
declaration.type
}}
</td>
<td class="px-4 py-3">
{{
folderStatusLabels[
folder.status
] ?? folder.status
declarationStatusLabels[
declaration.status
] ?? declaration.status
}}
</td>
<td class="px-4 py-3">
{{ folder.due_date ?? '—' }}
{{ declaration.due_date ?? '—' }}
</td>
<td class="px-4 py-3">
{{ folder.created_at }}
{{ declaration.created_at }}
</td>
<td class="px-4 py-3">
<Button
@@ -486,7 +478,7 @@ function getFieldValue(fieldKey: string): string {
size="sm"
as-child
>
<Link :href="folder.showUrl"
<Link :href="declaration.showUrl"
>Voir</Link
>
</Button>
@@ -499,11 +491,11 @@ function getFieldValue(fieldKey: string): string {
v-else
class="rounded-xl border border-sidebar-border/70 p-8 text-center text-muted-foreground"
>
Aucun dossier.
Aucune déclaration.
<Link
:href="createFolderUrl"
:href="createDeclarationUrl"
class="text-primary underline"
>Créer un dossier</Link
>Créer une déclaration</Link
>
</div>
</CardContent>