feat: implement Story 2.2 — Priority Alerts Panel with UI fixes

Add PriorityAlertsPanel component to the dashboard, update DashboardController
with alert logic, and apply misc UI fixes across sidebar, forms, and pages.
Includes epic-1 retrospective and sprint status update.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 12:33:27 +00:00
parent a2ab6f365d
commit 4807376c49
1196 changed files with 170844 additions and 32 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Link, usePage } from '@inertiajs/vue3';
import { usePage } from '@inertiajs/vue3';
import {
BookOpen,
Briefcase,
@@ -19,16 +19,12 @@ import {
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/ui/sidebar';
import { dashboard } from '@/routes';
import { index as clientsIndex } from '@/routes/clients';
import { index as declarationsIndex } from '@/routes/declarations';
import { index as teamIndex } from '@/routes/team';
import type { NavItem } from '@/types';
import AppLogo from './AppLogo.vue';
import WorkspaceSwitcher from './WorkspaceSwitcher.vue';
const page = usePage();

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
/* eslint-disable vue/no-mutating-props -- Inertia useForm objects are reactive and designed to be mutated via props */
import { Building2, Plus, Settings, Trash2, User } from 'lucide-vue-next';
import { computed } from 'vue';
import InputError from '@/components/InputError.vue';

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
/* eslint-disable vue/no-mutating-props -- Inertia useForm objects are reactive and designed to be mutated via props */
import type { Form } from '@inertiajs/vue3';
import { watch } from 'vue';
import InputError from '@/components/InputError.vue';

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
/* eslint-disable vue/no-mutating-props -- Inertia useForm objects are reactive and designed to be mutated via props */
import type { Form } from '@inertiajs/vue3';
import InputError from '@/components/InputError.vue';
import { Button } from '@/components/ui/button';

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
/* eslint-disable vue/no-mutating-props -- Inertia useForm objects are reactive and designed to be mutated via props */
import type { Form } from '@inertiajs/vue3';
import InputError from '@/components/InputError.vue';
import { Button } from '@/components/ui/button';

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { router, usePage } from '@inertiajs/vue3';
import { BoxSelect, Check, ChevronsUpDown } from 'lucide-vue-next';
import { computed, ref } from 'vue';
import {
DropdownMenu,
DropdownMenuContent,

View File

@@ -37,14 +37,6 @@ const monthLabel = computed(
`${monthNames[current.value.getMonth()]} ${current.value.getFullYear()}`,
);
const datesWithDeclarations = computed(() => {
const set = new Set<string>();
props.declarations.forEach((f) => {
if (f.due_date) set.add(f.due_date);
});
return set;
});
const declarationsByDate = computed(() => {
const map = new Map<string, number>();
props.declarations.forEach((f) => {

View File

@@ -0,0 +1,154 @@
<script setup lang="ts">
import { Link, router } from '@inertiajs/vue3';
import {
AlertCircle,
AlertTriangle,
CheckCircle,
ChevronRight,
Info,
} from 'lucide-vue-next';
import { computed } from 'vue';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import type { DashboardAlert } from '@/types';
type Props = {
alerts: DashboardAlert[];
viewAllUrl: string | null;
};
const props = defineProps<Props>();
const alertCount = computed(() => props.alerts.length);
const severityConfig = {
critical: {
icon: AlertCircle,
colorClass: 'text-red-600',
bgClass: 'bg-red-500/10',
pulseClass: 'animate-pulse',
},
warning: {
icon: AlertTriangle,
colorClass: 'text-amber-600',
bgClass: 'bg-amber-500/10',
pulseClass: '',
},
info: {
icon: Info,
colorClass: 'text-blue-600',
bgClass: 'bg-blue-500/10',
pulseClass: '',
},
} as const;
function daysText(alert: DashboardAlert): string {
return `${alert.daysValue} ${alert.daysLabel}`;
}
function navigateToAlert(alert: DashboardAlert): void {
router.get(alert.showUrl);
}
</script>
<template>
<div class="space-y-4">
<div class="flex items-center gap-2">
<h2 class="text-lg font-semibold">Alertes prioritaires</h2>
<Badge v-if="alertCount > 0" variant="secondary">
{{ alertCount }}
</Badge>
</div>
<!-- Empty state -->
<Card v-if="alertCount === 0">
<CardContent
class="flex flex-col items-center justify-center py-12"
>
<CheckCircle class="mb-3 h-12 w-12 text-green-500" />
<p class="text-muted-foreground">
Aucune alerte &mdash; tout est en ordre
</p>
</CardContent>
</Card>
<!-- Alerts list -->
<Card v-else class="overflow-hidden">
<CardContent class="p-0">
<ul class="divide-y">
<li
v-for="alert in alerts"
:key="alert.id"
role="link"
tabindex="0"
class="flex cursor-pointer items-center gap-3 px-4 py-3 transition-colors hover:bg-muted/50 focus-visible:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
@click="navigateToAlert(alert)"
@keydown.enter="navigateToAlert(alert)"
>
<!-- Severity icon -->
<div
:class="[
'flex h-8 w-8 shrink-0 items-center justify-center rounded-full',
severityConfig[alert.severity].bgClass,
]"
>
<component
:is="severityConfig[alert.severity].icon"
:class="[
'h-4 w-4',
severityConfig[alert.severity].colorClass,
alert.severity === 'critical'
? severityConfig[alert.severity]
.pulseClass
: '',
]"
/>
</div>
<!-- Alert content -->
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<span class="truncate font-medium">
{{ alert.clientName }}
</span>
<span class="text-sm text-muted-foreground">
{{ alert.typeLabel }}
</span>
</div>
<p
:class="[
'text-sm',
severityConfig[alert.severity].colorClass,
alert.severity === 'critical'
? severityConfig[alert.severity]
.pulseClass
: '',
]"
>
{{ daysText(alert) }}
</p>
</div>
<!-- Arrow -->
<ChevronRight
class="h-4 w-4 shrink-0 text-muted-foreground"
/>
</li>
</ul>
<!-- View all link -->
<div
v-if="alertCount === 20 && viewAllUrl"
class="border-t px-4 py-3 text-center"
>
<Link
:href="viewAllUrl"
class="text-sm font-medium text-primary hover:underline"
>
Voir tout
</Link>
</div>
</CardContent>
</Card>
</div>
</template>

View File

@@ -23,7 +23,7 @@ type Props = {
messageTypeLabels: Record<string, string>;
};
const props = defineProps<Props>();
defineProps<Props>();
const typeColors: Record<string, string> = {
invite: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',