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:
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
154
resources/js/components/dashboard/PriorityAlertsPanel.vue
Normal file
154
resources/js/components/dashboard/PriorityAlertsPanel.vue
Normal 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 — 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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user