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',

View File

@@ -11,6 +11,7 @@ import {
Users,
} from 'lucide-vue-next';
import { computed } from 'vue';
import PriorityAlertsPanel from '@/components/dashboard/PriorityAlertsPanel.vue';
import StatCard from '@/components/dashboard/StatCard.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@@ -163,6 +164,12 @@ function navigateToDeclaration(declaration: DashboardDeclaration): void {
/>
</div>
<!-- Priority Alerts Panel -->
<PriorityAlertsPanel
:alerts="alerts"
:view-all-url="viewAllAlertsUrl"
/>
<!-- Urgent Declarations Table -->
<div class="space-y-4">
<div class="flex items-center justify-between">

View File

@@ -1,18 +1,10 @@
<script setup lang="ts">
import { Head, Link } from '@inertiajs/vue3';
import {
Building2,
Calendar,
FileCheck,
Mail,
Shield,
Users,
} from 'lucide-vue-next';
import { Calendar, FileCheck, Mail, Shield, Users } from 'lucide-vue-next';
import AppLogoIcon from '@/components/AppLogoIcon.vue';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,

View File

@@ -29,7 +29,7 @@ type Props = {
csrfToken: string;
};
const props = defineProps<Props>();
defineProps<Props>();
const page = usePage<{ flash?: { type?: string; message?: string } }>();
const flash = computed(() => page.props.flash);

View File

@@ -1,7 +1,6 @@
<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 { Badge } from '@/components/ui/badge';

View File

@@ -5,8 +5,6 @@ import { computed, ref, watch, nextTick } from 'vue';
import MessageBubble from '@/components/declarations/MessageBubble.vue';
import Heading from '@/components/Heading.vue';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Spinner } from '@/components/ui/spinner';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';

View File

@@ -170,7 +170,9 @@ watch(
(members) => {
if (!permissionsMember.value || !showPermissionsDialog.value) return;
const updated = members.find(
(m) => m.workspace_user_id === permissionsMember.value!.workspace_user_id,
(m) =>
m.workspace_user_id ===
permissionsMember.value!.workspace_user_id,
);
if (updated) {
permissionsMember.value = updated;
@@ -547,7 +549,7 @@ const breadcrumbs: BreadcrumbItem[] = [
isOwner &&
getMemberData(member)!
.role ===
ROLE_MANAGER
ROLE_MANAGER
"
@click="
openPermissionsDialog(

View File

@@ -4,7 +4,6 @@ import Heading from '@/components/Heading.vue';
import UserForm from '@/components/UserForm.vue';
import type { UserFormData } from '@/components/UserForm.vue';
import AppLayout from '@/layouts/AppLayout.vue';
import type { BreadcrumbItem } from '@/types';
type Props = {
indexUrl: string;

View File

@@ -25,12 +25,25 @@ export type StatCardLink = {
href: string;
};
export type DashboardAlert = {
id: number;
severity: 'critical' | 'warning' | 'info';
clientName: string;
declarationType: string;
typeLabel: string;
daysValue: number;
daysLabel: string;
showUrl: string;
};
export type DashboardProps = {
stats: DashboardStats | null;
statCards: StatCardLink[];
declarations: DashboardDeclaration[];
alerts: DashboardAlert[];
workspaceName: string | null;
roleLabel: string | null;
declarationsUrl: string | null;
clientsUrl: string | null;
viewAllAlertsUrl: string | null;
};