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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user