Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions src/app/board/notifications/components/notifications.table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"use client";

import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Notification } from "@/types/notification.types";
import { formatDate } from "@/lib/utils";

interface NotificationsTableProps {
notifications: Notification[];
isLoading: boolean;
}

export function NotificationsTable({ notifications, isLoading }: NotificationsTableProps) {
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<p>Chargement ...</p>
</div>
);
}

if (notifications.length === 0) {
return (
<div className="flex flex-col items-center justify-center h-64">
<p className="text-muted-foreground mb-4">Aucune notification trouvée.</p>
</div>
);
}

return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Message</TableHead>
<TableHead>Status</TableHead>
<TableHead>Cours</TableHead>
<TableHead>Date</TableHead>
<TableHead>Destinataire</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{notifications.map((notification) => (
<TableRow key={notification.id}>
<TableCell className="font-medium">{notification.message}</TableCell>
<TableCell>
<StatusBadge status={notification.status} />
</TableCell>
<TableCell>
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="text-left">
{notification.emargement.classSession.course.name}
</TooltipTrigger>
<TooltipContent>
<div className="text-xs space-y-1">
<p>Date: {formatDate(notification.emargement.classSession.date)}</p>
<p>Début: {notification.emargement.classSession.heureDebut}</p>
<p>Fin: {notification.emargement.classSession.heureFin}</p>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</TableCell>
<TableCell>{formatDate(notification.createdAt)}</TableCell>
<TableCell>{notification.recipient.name}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}

function StatusBadge({ status }: { status: Notification["status"] }) {
const variant = {
SENT: "default",
READ: "success",
DELETED: "destructive",
}[status];

const label = {
SENT: "Envoyé",
READ: "Lu",
DELETED: "Supprimé",
}[status];

return <Badge variant={variant as any}>{label}</Badge>;
}
80 changes: 80 additions & 0 deletions src/app/board/notifications/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"use client";

import { AppSidebar } from "@/components/shared/navigation/app.sidebar";
import UserDropdown from "@/components/shared/navigation/user.dropdown";
import FeedbackDialog from "@/components/shared/others/feedback.dialog";
import { ModeToggle } from "@/components/shared/theme/mode-toggle";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Separator } from "@/components/ui/separator";
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { useNotificationsQuery } from "@/hooks/queries/use-notification.query";
import { RiBellLine, RiScanLine } from "@remixicon/react";
import { NotificationsTable } from "./components/notifications.table";
import { PiBellDuotone, PiBuildingsDuotone } from "react-icons/pi";




export default function NotificationsPage() {
const { data: notifications = [], isLoading } = useNotificationsQuery();

return (
<SidebarProvider>
<AppSidebar />
<SidebarInset className="overflow-hidden px-4 md:px-6 lg:px-8">
<header className="flex h-16 shrink-0 items-center gap-2 border-b">
<div className="flex flex-1 items-center gap-2 px-3">
<SidebarTrigger className="-ms-4" />
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="/board">
<RiScanLine size={22} aria-hidden="true" />
<span className="sr-only">Dashboard</span>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Notifications</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="flex gap-3 ml-auto">
<FeedbackDialog />
<ModeToggle />
<UserDropdown />
</div>
</header>
<div className="flex flex-1 flex-col gap-4 lg:gap-6 py-4 lg:py-6">
{/* Page intro */}
<div className="flex items-center justify-between gap-4">
<div className="space-y-1">
<h1 className="text-2xl font-semibold flex items-center gap-2">
<PiBellDuotone className="text-primary" />
Notifications
</h1>
<p className="text-sm text-muted-foreground"> Gérez vos notifications liées aux émargements de cours.</p>
</div>
</div>

{/* Table */}
<div className="flex-1 overflow-auto">
<NotificationsTable notifications={notifications} isLoading={isLoading} />

</div>
</div>
</SidebarInset>


</SidebarProvider>
);
}
1 change: 1 addition & 0 deletions src/config/navigation-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
PiGearDuotone,
PiHouseDuotone,
PiUserDuotone,
PiBellDuotone,
} from "react-icons/pi";
import { routes } from "./routes";

Expand Down
4 changes: 2 additions & 2 deletions src/config/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ export const routes = {
home: "/board",
users: "/board/users",
profile: "/board/profile",
settings: "/board/settings",
academicYears: "/board/academic-years",
settings: "/board/settings", academicYears: "/board/academic-years",
organizations: "/board/organizations",
attendance: "/board/attendance",
attendanceAdmin: "/board/attendance-admin",
Expand All @@ -19,5 +18,6 @@ export const routes = {
programs: "/board/programs",
universities: "/board/universities",
classSessions: "/board/class-sessions",
notifications: "/board/notifications",
},
};
78 changes: 1 addition & 77 deletions src/server/services/notification.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,80 +49,4 @@ export class NotificationService {
return data;
} catch (error) {
console.error(`Erreur lors de la récupération de la notification ${id}:`, error);
throw new Error("Impossible de récupérer les détails de la notification");
}
}

/**
* Créer une nouvelle notification
*/
static async createNotification(input: CreateNotificationDto): Promise<Notification> {
try {
const token = getAuthToken();
if (!token) {
throw new Error("Vous devez être connecté pour accéder à cette ressource");
}

const { data } = await api.post(`/api/v1/notifications`, input, {
headers: {
Authorization: `Bearer ${token}`,
},
});

return data;
} catch (error) {
console.error("Erreur lors de la création de la notification:", error);
throw new Error("Impossible de créer la notification");
}
}

/**
* Mettre à jour le statut d'une notification
*/
static async updateNotificationStatus(id: string, status: string): Promise<boolean> {
try {
const token = getAuthToken();
if (!token) {
throw new Error("Vous devez être connecté pour accéder à cette ressource");
}

await api.put(
`/api/v1/notifications/${id}`,
{ status },
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);

return true;
} catch (error) {
console.error(`Erreur lors de la mise à jour du statut de la notification ${id}:`, error);
throw new Error("Impossible de mettre à jour le statut de la notification");
}
}

/**
* Récupérer les notifications non lues pour un utilisateur
*/
static async getUnreadNotifications(): Promise<Notification[]> {
try {
const token = getAuthToken();
if (!token) {
throw new Error("Vous devez être connecté pour accéder à cette ressource");
}

const { data } = await api.get(`/api/v1/notifications?status=SENT`, {
headers: {
Authorization: `Bearer ${token}`,
},
});

return data.items || data;
} catch (error) {
console.error("Erreur lors de la récupération des notifications non lues:", error);
throw new Error("Impossible de récupérer les notifications non lues");
}
}
}
throw new Error("Impossible de récupérer les détails d
28 changes: 20 additions & 8 deletions src/types/attendance.types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
import { AcademicYear } from "./academic-year.types";
import { User } from "./user.types";

import { Course } from "./course.types"
import { AcademicYear } from "./academic-year.types"
import { User } from "./user.types"

export interface ClassSession {
id: string;
date: string;
heureDebut: string;
heureFin: string;
academicYear: AcademicYear;
course: Course;
professor: User;
classRepresentative: User;
createdAt: string;
updatedAt: string;
}

export interface Attendance {
id: string;
professorId: string;
courseId: string;
date: string;
status: "PRESENT" | "ABSENT" | "LATE";
classSession: ClassSession;
professor: User;
comments?: string;
createdAt: string;
updatedAt: string;
Expand All @@ -31,14 +50,6 @@ export interface UpdateAttendanceInput {
comments?: string;
}

export interface Course {
id: string;
title: string;
startTime: string;
endTime: string;
location: string;
hasAttendance: boolean;
}

export interface ClassSession {
id: string;
Expand Down Expand Up @@ -101,3 +112,4 @@ export interface UpdateEmargementInput {
id: string;
status?: "PENDING" | "PRESENT" | "ABSENT" | "SUPERVISOR_CONFIRMED" | "CLASS_HEADER_CONFIRMED";
}

8 changes: 5 additions & 3 deletions src/types/notification.types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Emargement } from "./attendance.types";
import { User } from "./user.types";

export type NotificationStatus = "SENT" | "CONFIRMED" | "RECEIVED" | "READ" | "DELETED";

export interface Notification {
id: string;
message: string;
status: "SENT" | "CONFIRMED" | "RECEIVED" | "READ";
status: NotificationStatus;
emargement?: Emargement;
recipient: User;
createdAt: string;
Expand All @@ -13,13 +15,13 @@ export interface Notification {

export interface CreateNotificationDto {
message: string;
status: "SENT" | "CONFIRMED" | "RECEIVED" | "READ";
status: NotificationStatus;
emargementId: string;
recipientId: string;
}

export interface UpdateNotificationDto {
id: string;
status?: "SENT" | "CONFIRMED" | "RECEIVED" | "READ";
status?: NotificationStatus;
message?: string;
}