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,7 +1,7 @@
<script setup lang="ts">
import { computed } from 'vue';
import { router, usePage } from '@inertiajs/vue3';
import { Bell } from 'lucide-vue-next';
import { computed } from 'vue';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
@@ -16,8 +16,8 @@ type NotificationItem = {
id: string;
type: string;
data: {
folder_id?: number;
folder_title?: string;
declaration_id?: number;
declaration_title?: string;
mentioned_by_name?: string;
message?: string;
url?: string;
@@ -36,7 +36,8 @@ type UserNotifications = {
const page = usePage();
const userNotifications = computed<UserNotifications>(() => {
return (page.props as Record<string, unknown>).userNotifications as UserNotifications;
return (page.props as Record<string, unknown>)
.userNotifications as UserNotifications;
});
const unreadCount = computed(() => userNotifications.value?.unread_count ?? 0);
@@ -63,7 +64,7 @@ function navigateToNotification(notification: NotificationItem) {
router.visit(targetUrl, {
onError: () => {
// Folder may have been deleted — mark as read anyway
// Declaration may have been deleted — mark as read anyway
if (!notification.read_at) {
markAsRead(notification);
}
@@ -86,7 +87,7 @@ function markAllAsRead() {
<Bell class="size-4" />
<span
v-if="unreadCount > 0"
class="absolute -right-0.5 -top-0.5 flex size-4 items-center justify-center rounded-full bg-destructive text-[10px] font-bold text-destructive-foreground"
class="absolute -top-0.5 -right-0.5 flex size-4 items-center justify-center rounded-full bg-destructive text-[10px] font-bold text-destructive-foreground"
>
{{ unreadCount > 9 ? '9+' : unreadCount }}
</span>
@@ -96,11 +97,17 @@ function markAllAsRead() {
<DropdownMenuLabel>Notifications</DropdownMenuLabel>
<DropdownMenuSeparator />
<div v-if="isLoading" class="px-2 py-4 text-center text-sm text-muted-foreground">
<div
v-if="isLoading"
class="px-2 py-4 text-center text-sm text-muted-foreground"
>
Chargement...
</div>
<div v-else-if="!items.length" class="px-2 py-4 text-center text-sm text-muted-foreground">
<div
v-else-if="!items.length"
class="px-2 py-4 text-center text-sm text-muted-foreground"
>
Aucune notification.
</div>
@@ -114,17 +121,27 @@ function markAllAsRead() {
>
<div class="flex w-full items-center justify-between gap-2">
<span class="text-xs font-medium">
{{ notification.data?.mentioned_by_name ?? 'Système' }}
{{
notification.data?.mentioned_by_name ??
'Système'
}}
</span>
<span class="text-xs text-muted-foreground">
{{ notification.created_at }}
</span>
</div>
<p class="text-xs text-muted-foreground">
<span v-if="notification.data?.folder_title" class="font-medium text-foreground">
{{ notification.data.folder_title }}
<span
v-if="notification.data?.declaration_title"
class="font-medium text-foreground"
>
{{ notification.data.declaration_title }}
</span>
{{ notification.data?.message ? ` ${notification.data.message}` : '' }}
{{
notification.data?.message
? `${notification.data.message}`
: ''
}}
</p>
<span
v-if="!notification.read_at"