diff --git a/.cursor/worktrees.json b/.cursor/worktrees.json new file mode 100644 index 0000000..77e9744 --- /dev/null +++ b/.cursor/worktrees.json @@ -0,0 +1,5 @@ +{ + "setup-worktree": [ + "npm install" + ] +} diff --git a/src/app/api/air-quality/route.ts b/src/app/api/air-quality/route.ts index 8a2dabf..66a6e46 100644 --- a/src/app/api/air-quality/route.ts +++ b/src/app/api/air-quality/route.ts @@ -15,6 +15,7 @@ import { prisma } from "@/lib/prisma"; import { airQualitySchema } from "@/lib/validations"; import { validateRequest } from "@/lib/validation-helpers"; import { loggerHelpers } from "@/lib/logger"; +import { parsePagination, createPaginatedResponse } from "@/lib/pagination"; export async function POST(request: NextRequest) { try { @@ -91,25 +92,20 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); } - const { searchParams } = new URL(request.url); - const limit = parseInt(searchParams.get("limit") || "100"); - const offset = parseInt(searchParams.get("offset") || "0"); + const { page, limit, skip, take } = parsePagination(request); const [airQuality, total] = await Promise.all([ prisma.airQuality.findMany({ - take: limit, - skip: offset, + take, + skip, orderBy: { date: "desc" }, }), prisma.airQuality.count(), ]); - return NextResponse.json({ - data: airQuality, - total, - limit, - offset, - }); + return NextResponse.json( + createPaginatedResponse(airQuality, total, page, limit) + ); } catch (error) { console.error("Error fetching air quality data:", error); return NextResponse.json( diff --git a/src/app/api/climate-data/route.ts b/src/app/api/climate-data/route.ts index 0c0d76c..cc94d83 100644 --- a/src/app/api/climate-data/route.ts +++ b/src/app/api/climate-data/route.ts @@ -15,6 +15,7 @@ import { prisma } from "@/lib/prisma"; import { climateDataSchema } from "@/lib/validations"; import { validateRequest } from "@/lib/validation-helpers"; import { loggerHelpers } from "@/lib/logger"; +import { parsePagination, createPaginatedResponse } from "@/lib/pagination"; export async function POST(request: NextRequest) { try { @@ -97,8 +98,7 @@ export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const stationId = searchParams.get("stationId"); - const limit = parseInt(searchParams.get("limit") || "100"); - const offset = parseInt(searchParams.get("offset") || "0"); + const { page, limit, skip, take } = parsePagination(request); const where: any = {}; if (stationId) { @@ -108,24 +108,22 @@ export async function GET(request: NextRequest) { const [climateData, total] = await Promise.all([ prisma.climateData.findMany({ where, - take: limit, - skip: offset, + take, + skip, orderBy: { date: "desc" }, }), prisma.climateData.count({ where }), ]); // Cache for 5 minutes - return NextResponse.json({ - data: climateData, - total, - limit, - offset, - }, { - headers: { - 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600', - }, - }); + return NextResponse.json( + createPaginatedResponse(climateData, total, page, limit), + { + headers: { + 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600', + }, + } + ); } catch (error) { console.error("Error fetching climate data:", error); return NextResponse.json( diff --git a/src/app/api/employees/route.ts b/src/app/api/employees/route.ts index ceaa354..e8066d1 100644 --- a/src/app/api/employees/route.ts +++ b/src/app/api/employees/route.ts @@ -15,6 +15,7 @@ import { prisma } from "@/lib/prisma"; import { employeeSchema } from "@/lib/validations"; import { validateRequest } from "@/lib/validation-helpers"; import { loggerHelpers } from "@/lib/logger"; +import { parsePagination, createPaginatedResponse } from "@/lib/pagination"; export async function POST(request: NextRequest) { try { @@ -85,26 +86,36 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); } - const employees = await prisma.employee.findMany({ - include: { - user: { - select: { - firstName: true, - lastName: true, - email: true, - role: true, + const { page, limit, skip, take } = parsePagination(request); + + const [employees, total] = await Promise.all([ + prisma.employee.findMany({ + include: { + user: { + select: { + firstName: true, + lastName: true, + email: true, + role: true, + }, }, }, - }, - orderBy: { createdAt: "desc" }, - }); + orderBy: { createdAt: "desc" }, + take, + skip, + }), + prisma.employee.count(), + ]); // Cache for 5 minutes - return NextResponse.json(employees, { - headers: { - 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600', - }, - }); + return NextResponse.json( + createPaginatedResponse(employees, total, page, limit), + { + headers: { + 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600', + }, + } + ); } catch (error) { loggerHelpers.apiError(error as Error, { route: "/api/employees", diff --git a/src/app/api/water-quality/route.ts b/src/app/api/water-quality/route.ts index dc53265..47ea4bf 100644 --- a/src/app/api/water-quality/route.ts +++ b/src/app/api/water-quality/route.ts @@ -15,6 +15,7 @@ import { prisma } from "@/lib/prisma"; import { waterQualitySchema } from "@/lib/validations"; import { validateRequest } from "@/lib/validation-helpers"; import { loggerHelpers } from "@/lib/logger"; +import { parsePagination, createPaginatedResponse } from "@/lib/pagination"; export async function POST(request: NextRequest) { try { @@ -95,8 +96,7 @@ export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const type = searchParams.get("type"); - const limit = parseInt(searchParams.get("limit") || "100"); - const offset = parseInt(searchParams.get("offset") || "0"); + const { page, limit, skip, take } = parsePagination(request); const where: any = {}; if (type) { @@ -106,24 +106,22 @@ export async function GET(request: NextRequest) { const [waterQuality, total] = await Promise.all([ prisma.waterQuality.findMany({ where, - take: limit, - skip: offset, + take, + skip, orderBy: { date: "desc" }, }), prisma.waterQuality.count({ where }), ]); // Cache for 5 minutes - return NextResponse.json({ - data: waterQuality, - total, - limit, - offset, - }, { - headers: { - 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600', - }, - }); + return NextResponse.json( + createPaginatedResponse(waterQuality, total, page, limit), + { + headers: { + 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600', + }, + } + ); } catch (error) { console.error("Error fetching water quality data:", error); return NextResponse.json( diff --git a/src/app/dashboard/documents/client-page.tsx b/src/app/dashboard/documents/client-page.tsx new file mode 100644 index 0000000..c7a250f --- /dev/null +++ b/src/app/dashboard/documents/client-page.tsx @@ -0,0 +1,233 @@ +/** + * @file client-page.tsx + * @description src/app/dashboard/documents/client-page.tsx + * @author 1 + * @created 2026-01-06 + * @updated 2026-01-06 + * @updates 1 + * @lines 220 + * @size 7.5 KB + */ +"use client"; + +import { useState, useEffect } from "react"; +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { EmptyState } from "@/components/ui/empty-state"; +import { ExportButtons } from "@/components/export/export-buttons"; +import { Pagination } from "@/components/ui/pagination"; +import Link from "next/link"; +import { Plus, FileText, Download } from "lucide-react"; +import { formatDate } from "@/lib/utils"; + +const typeLabels: Record = { + RAPPORT_SCIENTIFIQUE: "Rapport Scientifique", + RAPPORT_ADMINISTRATIF: "Rapport Administratif", + DONNEE_BRUTE: "Donnée Brute", + PUBLICATION: "Publication", + AUTRE: "Autre", +}; + +interface Document { + id: string; + title: string; + type: string; + version: number; + createdAt: string; + author: { + firstName: string; + lastName: string; + }; + mission: { + title: string; + } | null; + fileUrl: string; +} + +export default function DocumentsPageClient() { + const [documents, setDocuments] = useState([]); + const [loading, setLoading] = useState(true); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(25); + const [totalItems, setTotalItems] = useState(0); + + useEffect(() => { + async function fetchData() { + setLoading(true); + try { + const response = await fetch(`/api/documents?page=${currentPage}&limit=${pageSize}`); + if (response.ok) { + const data = await response.json(); + setDocuments(data.data || []); + setTotalItems(data.meta?.total || 0); + } + } catch (error) { + console.error("Error fetching documents:", error); + } finally { + setLoading(false); + } + } + fetchData(); + }, [currentPage, pageSize]); + + const totalPages = Math.ceil(totalItems / pageSize); + + if (loading) { + return ( +
+
+
+

+ Gestion Documentaire +

+

+ Rapports, données et publications +

+
+
+ +
Chargement...
+
+
+ ); + } + + return ( +
+
+
+

+ Gestion Documentaire +

+

+ Rapports, données et publications +

+
+
+ + + + +
+
+ + +
+ + + + + + + + + + + + + + {documents.length === 0 ? ( + + + + ) : ( + documents.map((doc) => ( + + + + + + + + + + )) + )} + +
+ Titre + + Type + + Auteur + + Mission + + Version + + Date + + Actions +
+ + + + } + /> +
+
+ +
+ {doc.title} +
+
+
+ + {typeLabels[doc.type] || doc.type} + + +
+ {doc.author.firstName} {doc.author.lastName} +
+
+
+ {doc.mission?.title || "N/A"} +
+
+
v{doc.version}
+
+
+ {formatDate(doc.createdAt)} +
+
+
+ + + + + + +
+
+
+ {totalPages > 1 && ( + + )} +
+
+ ); +} + diff --git a/src/app/dashboard/documents/page.tsx b/src/app/dashboard/documents/page.tsx index 19a8443..aafb034 100644 --- a/src/app/dashboard/documents/page.tsx +++ b/src/app/dashboard/documents/page.tsx @@ -3,172 +3,13 @@ * @description src/app/dashboard/documents/page.tsx * @author 1 * @created 2026-01-01 - * @updated 2026-01-04 - * @updates 3 - * @lines 175 - * @size 6.79 KB + * @updated 2026-01-06 + * @updates 4 + * @lines 5 + * @size 0.20 KB */ -import { prisma } from "@/lib/prisma"; -import { Card } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { EmptyState } from "@/components/ui/empty-state"; -import Link from "next/link"; -import { Plus, FileText, Download } from "lucide-react"; -import { formatDate } from "@/lib/utils"; +import DocumentsPageClient from "./client-page"; -const typeLabels: Record = { - RAPPORT_SCIENTIFIQUE: "Rapport Scientifique", - RAPPORT_ADMINISTRATIF: "Rapport Administratif", - DONNEE_BRUTE: "Donnée Brute", - PUBLICATION: "Publication", - AUTRE: "Autre", -}; - -// Force dynamic rendering to avoid build-time database queries -export const dynamic = 'force-dynamic'; -export const revalidate = 0; - -export default async function DocumentsPage() { - const documents = await prisma.document.findMany({ - include: { - author: { - select: { - firstName: true, - lastName: true, - }, - }, - mission: { - select: { - title: true, - }, - }, - }, - orderBy: { createdAt: "desc" }, - }); - - return ( -
-
-
-

- Gestion Documentaire -

-

- Rapports, données et publications -

-
- - - -
- - -
- - - - - - - - - - - - - - {documents.length === 0 ? ( - - - - ) : ( - documents.map((doc) => ( - - - - - - - - - - )) - )} - -
- Titre - - Type - - Auteur - - Mission - - Version - - Date - - Actions -
- - - - } - /> -
-
- -
- {doc.title} -
-
-
- - {typeLabels[doc.type] || doc.type} - - -
- {doc.author.firstName} {doc.author.lastName} -
-
-
- {doc.mission?.title || "N/A"} -
-
-
v{doc.version}
-
-
- {formatDate(doc.createdAt)} -
-
-
- - - - - - -
-
-
-
-
- ); +export default function DocumentsPage() { + return ; } - diff --git a/src/app/dashboard/environment/climate/client-page.tsx b/src/app/dashboard/environment/climate/client-page.tsx new file mode 100644 index 0000000..9ce067d --- /dev/null +++ b/src/app/dashboard/environment/climate/client-page.tsx @@ -0,0 +1,212 @@ +/** + * @file client-page.tsx + * @description src/app/dashboard/environment/climate/client-page.tsx + * @author 1 + * @created 2026-01-06 + * @updated 2026-01-06 + * @updates 1 + * @lines 200 + * @size 7.0 KB + */ +"use client"; + +import { useState, useEffect } from "react"; +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { EmptyState } from "@/components/ui/empty-state"; +import { ExportButtons } from "@/components/export/export-buttons"; +import { Pagination } from "@/components/ui/pagination"; +import Link from "next/link"; +import { Plus, Thermometer } from "lucide-react"; +import { formatDate } from "@/lib/utils"; + +interface ClimateData { + id: string; + stationId: string | null; + location: string; + latitude: number | null; + longitude: number | null; + date: string; + temperature: number | null; + humidity: number | null; + windSpeed: number | null; + windDirection: number | null; + precipitation: number | null; +} + +export default function ClimateDataPageClient() { + const [climateData, setClimateData] = useState([]); + const [loading, setLoading] = useState(true); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(25); + const [totalItems, setTotalItems] = useState(0); + + useEffect(() => { + async function fetchData() { + setLoading(true); + try { + const response = await fetch(`/api/climate-data?page=${currentPage}&limit=${pageSize}`); + if (response.ok) { + const data = await response.json(); + setClimateData(data.data || []); + setTotalItems(data.meta?.total || 0); + } + } catch (error) { + console.error("Error fetching climate data:", error); + } finally { + setLoading(false); + } + } + fetchData(); + }, [currentPage, pageSize]); + + const totalPages = Math.ceil(totalItems / pageSize); + + if (loading) { + return ( +
+
+
+

+ Données Climatiques +

+

+ Liste des mesures climatiques +

+
+
+ +
Chargement...
+
+
+ ); + } + + return ( +
+
+
+

+ Données Climatiques +

+

+ Liste des mesures climatiques +

+
+
+ + + + +
+
+ + +
+ + + + + + + + + + + + + + {climateData.length === 0 ? ( + + + + ) : ( + climateData.map((climate) => ( + + + + + + + + + + )) + )} + +
+ Station + + Localisation + + Date + + Température + + Humidité + + Vent + + Précipitations +
+ + + + } + /> +
+
+ + + {climate.stationId || "-"} + +
+
+
+ {climate.location} +
+ {climate.latitude && climate.longitude && ( +
+ {climate.latitude.toFixed(4)}, {climate.longitude.toFixed(4)} +
+ )} +
+ {formatDate(climate.date)} + + {climate.temperature ? `${climate.temperature}°C` : "-"} + + {climate.humidity ? `${climate.humidity}%` : "-"} + + {climate.windSpeed + ? `${climate.windSpeed} m/s${climate.windDirection ? ` (${climate.windDirection}°)` : ""}` + : "-"} + + {climate.precipitation ? `${climate.precipitation} mm` : "-"} +
+
+ {totalPages > 1 && ( + + )} +
+
+ ); +} + diff --git a/src/app/dashboard/environment/climate/page.tsx b/src/app/dashboard/environment/climate/page.tsx index 46c93bf..bbebacd 100644 --- a/src/app/dashboard/environment/climate/page.tsx +++ b/src/app/dashboard/environment/climate/page.tsx @@ -8,136 +8,9 @@ * @lines 144 * @size 6.19 KB */ -import { prisma } from "@/lib/prisma"; -import { Card } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { EmptyState } from "@/components/ui/empty-state"; -import Link from "next/link"; -import { Plus, Thermometer } from "lucide-react"; -import { formatDate } from "@/lib/utils"; +import ClimateDataPageClient from "./client-page"; -// Force dynamic rendering to avoid build-time database queries -export const dynamic = 'force-dynamic'; -export const revalidate = 0; - -export default async function ClimateDataPage() { - const climateData = await prisma.climateData.findMany({ - orderBy: { date: "desc" }, - take: 100, - }); - - return ( -
-
-
-

- Données Climatiques -

-

- Liste des mesures climatiques -

-
- - - -
- - -
- - - - - - - - - - - - - - {climateData.length === 0 ? ( - - - - ) : ( - climateData.map((climate) => ( - - - - - - - - - - )) - )} - -
- Station - - Localisation - - Date - - Température - - Humidité - - Vent - - Précipitations -
- - - - } - /> -
-
- - - {climate.stationId || "-"} - -
-
-
- {climate.location} -
- {climate.latitude && climate.longitude && ( -
- {climate.latitude.toFixed(4)}, {climate.longitude.toFixed(4)} -
- )} -
- {formatDate(climate.date)} - - {climate.temperature ? `${climate.temperature}°C` : "-"} - - {climate.humidity ? `${climate.humidity}%` : "-"} - - {climate.windSpeed - ? `${climate.windSpeed} m/s${climate.windDirection ? ` (${climate.windDirection}°)` : ""}` - : "-"} - - {climate.precipitation ? `${climate.precipitation} mm` : "-"} -
-
-
-
- ); +export default function ClimateDataPage() { + return ; } diff --git a/src/app/dashboard/environment/page.tsx b/src/app/dashboard/environment/page.tsx index 06e2f5a..964a915 100644 --- a/src/app/dashboard/environment/page.tsx +++ b/src/app/dashboard/environment/page.tsx @@ -21,9 +21,9 @@ const waterTypeLabels: Record = { BARRAGE: "Barrage", }; -// Force dynamic rendering to avoid build-time database queries +// HTTP caching for environment page - revalidate every 60 seconds export const dynamic = 'force-dynamic'; -export const revalidate = 0; +export const revalidate = 60; export default async function EnvironmentPage() { const [waterQuality, airQuality, climateData, sensorData, counts] = await Promise.all([ diff --git a/src/app/dashboard/publications/client-page.tsx b/src/app/dashboard/publications/client-page.tsx new file mode 100644 index 0000000..16fe6fd --- /dev/null +++ b/src/app/dashboard/publications/client-page.tsx @@ -0,0 +1,261 @@ +/** + * @file client-page.tsx + * @description src/app/dashboard/publications/client-page.tsx + * @author 1 + * @created 2026-01-06 + * @updated 2026-01-06 + * @updates 1 + * @lines 240 + * @size 8.5 KB + */ +"use client"; + +import { useState, useEffect } from "react"; +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { EmptyState } from "@/components/ui/empty-state"; +import { ExportButtons } from "@/components/export/export-buttons"; +import { Pagination } from "@/components/ui/pagination"; +import Link from "next/link"; +import { Plus, BookOpen, FileDown } from "lucide-react"; + +interface Publication { + id: string; + title: string; + year: number; + type: string; + isPublished: boolean; + _count: { + chapters: number; + }; +} + +export default function PublicationsPageClient() { + const [publications, setPublications] = useState([]); + const [loading, setLoading] = useState(true); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(25); + const [totalItems, setTotalItems] = useState(0); + + useEffect(() => { + async function fetchData() { + setLoading(true); + try { + const response = await fetch(`/api/publications?page=${currentPage}&limit=${pageSize}`); + if (response.ok) { + const data = await response.json(); + setPublications(data.data || []); + setTotalItems(data.meta?.total || 0); + } + } catch (error) { + console.error("Error fetching publications:", error); + } finally { + setLoading(false); + } + } + fetchData(); + }, [currentPage, pageSize]); + + const totalPages = Math.ceil(totalItems / pageSize); + const publishedCount = publications.filter((p) => p.isPublished).length; + const draftCount = publications.filter((p) => !p.isPublished).length; + + if (loading) { + return ( +
+
+
+

+ Édition & Publication +

+

+ Livre annuel et publications scientifiques +

+
+
+ +
Chargement...
+
+
+ ); + } + + return ( +
+
+
+

+ Édition & Publication +

+

+ Livre annuel et publications scientifiques +

+
+
+ + + + +
+
+ +
+ +
+
+

+ Total publications +

+

+ {totalItems} +

+
+
+ +
+
+
+ + +
+
+

+ Publiées +

+

+ {publishedCount} +

+
+
+ +
+
+
+ + +
+
+

+ En préparation +

+

+ {draftCount} +

+
+
+ +
+
+
+
+ + +
+ + + + + + + + + + + + + {publications.length === 0 ? ( + + + + ) : ( + publications.map((pub) => ( + + + + + + + + + )) + )} + +
+ Titre + + Année + + Type + + Chapitres + + Statut + + Actions +
+ + + + } + /> +
+
+ {pub.title} +
+
+
{pub.year}
+
+
{pub.type}
+
+
+ {pub._count.chapters} chapitre(s) +
+
+ + {pub.isPublished ? "Publié" : "En préparation"} + + +
+ + + + {pub.isPublished && ( + + )} +
+
+
+ {totalPages > 1 && ( + + )} +
+
+ ); +} + diff --git a/src/app/dashboard/publications/page.tsx b/src/app/dashboard/publications/page.tsx index 1b142f9..27041d6 100644 --- a/src/app/dashboard/publications/page.tsx +++ b/src/app/dashboard/publications/page.tsx @@ -3,198 +3,13 @@ * @description src/app/dashboard/publications/page.tsx * @author 1 * @created 2026-01-01 - * @updated 2026-01-04 - * @updates 3 - * @lines 201 - * @size 8.24 KB + * @updated 2026-01-06 + * @updates 4 + * @lines 5 + * @size 0.20 KB */ -import { prisma } from "@/lib/prisma"; -import { Card } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { EmptyState } from "@/components/ui/empty-state"; -import Link from "next/link"; -import { Plus, BookOpen, FileDown } from "lucide-react"; -import { formatDate } from "@/lib/utils"; +import PublicationsPageClient from "./client-page"; -// Force dynamic rendering to avoid build-time database queries -export const dynamic = 'force-dynamic'; -export const revalidate = 0; - -export default async function PublicationsPage() { - const publications = await prisma.publication.findMany({ - include: { - _count: { - select: { - chapters: true, - }, - }, - }, - orderBy: { year: "desc" }, - }); - - return ( -
-
-
-

- Édition & Publication -

-

- Livre annuel et publications scientifiques -

-
- - - -
- -
- -
-
-

- Total publications -

-

- {publications.length} -

-
-
- -
-
-
- - -
-
-

- Publiées -

-

- {publications.filter((p) => p.isPublished).length} -

-
-
- -
-
-
- - -
-
-

- En préparation -

-

- {publications.filter((p) => !p.isPublished).length} -

-
-
- -
-
-
-
- - -
- - - - - - - - - - - - - {publications.length === 0 ? ( - - - - ) : ( - publications.map((pub) => ( - - - - - - - - - )) - )} - -
- Titre - - Année - - Type - - Chapitres - - Statut - - Actions -
- - - - } - /> -
-
- {pub.title} -
-
-
{pub.year}
-
-
{pub.type}
-
-
- {pub._count.chapters} chapitre(s) -
-
- - {pub.isPublished ? "Publié" : "En préparation"} - - -
- - - - {pub.isPublished && ( - - )} -
-
-
-
-
- ); +export default function PublicationsPage() { + return ; } - diff --git a/src/app/dashboard/rh/employees/client-page.tsx b/src/app/dashboard/rh/employees/client-page.tsx new file mode 100644 index 0000000..06ba6e8 --- /dev/null +++ b/src/app/dashboard/rh/employees/client-page.tsx @@ -0,0 +1,239 @@ +/** + * @file client-page.tsx + * @description src/app/dashboard/rh/employees/client-page.tsx + * @author 1 + * @created 2026-01-06 + * @updated 2026-01-06 + * @updates 1 + * @lines 220 + * @size 7.5 KB + */ +"use client"; + +import { useState, useEffect } from "react"; +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { EmptyState } from "@/components/ui/empty-state"; +import { ExportButtons } from "@/components/export/export-buttons"; +import { Pagination } from "@/components/ui/pagination"; +import Link from "next/link"; +import { Plus, Edit, Eye, Users } from "lucide-react"; +import { formatDate, formatCurrency } from "@/lib/utils"; + +interface Employee { + id: string; + employeeNumber: string; + contractType: string; + contractEnd: string | null; + baseSalary: number; + hireDate: string; + isActive: boolean; + user: { + firstName: string; + lastName: string; + email: string; + role: string; + } | null; +} + +export default function EmployeesPageClient() { + const [employees, setEmployees] = useState([]); + const [loading, setLoading] = useState(true); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(25); + const [totalItems, setTotalItems] = useState(0); + + useEffect(() => { + async function fetchData() { + setLoading(true); + try { + const response = await fetch(`/api/employees?page=${currentPage}&limit=${pageSize}`); + if (response.ok) { + const data = await response.json(); + setEmployees(data.data || []); + setTotalItems(data.meta?.total || 0); + } + } catch (error) { + console.error("Error fetching employees:", error); + } finally { + setLoading(false); + } + } + fetchData(); + }, [currentPage, pageSize]); + + const totalPages = Math.ceil(totalItems / pageSize); + + if (loading) { + return ( +
+
+
+

+ Employés +

+

+ Liste complète des employés +

+
+
+ +
Chargement...
+
+
+ ); + } + + return ( +
+
+
+

+ Employés +

+

+ Liste complète des employés +

+
+
+ + + + +
+
+ + +
+ + + + + + + + + + + + + + {employees.length === 0 ? ( + + + + ) : ( + employees.map((employee) => ( + + + + + + + + + + )) + )} + +
+ Nom + + Numéro + + Contrat + + Salaire de base + + Date d'embauche + + Statut + + Actions +
+ + + + } + /> +
+
+ {employee.user + ? `${employee.user.firstName} ${employee.user.lastName}` + : `Employé #${employee.employeeNumber}`} +
+ {employee.user && ( +
+ {employee.user.email} +
+ )} +
+
+ {employee.employeeNumber} +
+
+
+ {employee.contractType} +
+ {employee.contractEnd && ( +
+ Jusqu'au {formatDate(employee.contractEnd)} +
+ )} +
+
+ {formatCurrency(Number(employee.baseSalary))} +
+
+
+ {formatDate(employee.hireDate)} +
+
+ + {employee.isActive ? "Actif" : "Inactif"} + + +
+ + + + + + +
+
+
+ {totalPages > 1 && ( + + )} +
+
+ ); +} + diff --git a/src/app/dashboard/rh/employees/page.tsx b/src/app/dashboard/rh/employees/page.tsx index 1d372ba..81236cc 100644 --- a/src/app/dashboard/rh/employees/page.tsx +++ b/src/app/dashboard/rh/employees/page.tsx @@ -8,170 +8,9 @@ * @lines 178 * @size 7.25 KB */ -import { prisma } from "@/lib/prisma"; -import { Card } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { EmptyState } from "@/components/ui/empty-state"; -import Link from "next/link"; -import { Plus, Edit, Eye, Users } from "lucide-react"; -import { formatDate, formatCurrency } from "@/lib/utils"; +import EmployeesPageClient from "./client-page"; -// Force dynamic rendering to avoid build-time database queries -export const dynamic = 'force-dynamic'; -export const revalidate = 0; - -export default async function EmployeesPage() { - const employees = await prisma.employee.findMany({ - include: { - user: { - select: { - firstName: true, - lastName: true, - email: true, - role: true, - }, - }, - }, - orderBy: { createdAt: "desc" }, - }); - - return ( -
-
-
-

- Employés -

-

- Liste complète des employés -

-
- - - -
- - -
- - - - - - - - - - - - - - {employees.length === 0 ? ( - - - - ) : ( - employees.map((employee) => ( - - - - - - - - - - )) - )} - -
- Nom - - Numéro - - Contrat - - Salaire de base - - Date d'embauche - - Statut - - Actions -
- - - - } - /> -
-
- {employee.user - ? `${employee.user.firstName} ${employee.user.lastName}` - : `Employé #${employee.employeeNumber}`} -
- {employee.user && ( -
- {employee.user.email} -
- )} -
-
- {employee.employeeNumber} -
-
-
- {employee.contractType} -
- {employee.contractEnd && ( -
- Jusqu'au {formatDate(employee.contractEnd)} -
- )} -
-
- {formatCurrency(Number(employee.baseSalary))} -
-
-
- {formatDate(employee.hireDate)} -
-
- - {employee.isActive ? "Actif" : "Inactif"} - - -
- - - - - - -
-
-
-
-
- ); +export default function EmployeesPage() { + return ; } diff --git a/src/app/dashboard/rh/page.tsx b/src/app/dashboard/rh/page.tsx index 4ea2dfb..975e061 100644 --- a/src/app/dashboard/rh/page.tsx +++ b/src/app/dashboard/rh/page.tsx @@ -17,9 +17,9 @@ import { Plus, Users, Calendar, DollarSign } from "lucide-react"; import { formatCurrency, formatDate } from "@/lib/utils"; import { ExportButtons } from "@/components/export/export-buttons"; -// Force dynamic rendering to avoid build-time database queries +// HTTP caching for RH page - revalidate every 60 seconds export const dynamic = 'force-dynamic'; -export const revalidate = 0; +export const revalidate = 60; export default async function RHPage() { const [employees, activeLeaves, recentSalaries] = await Promise.all([ diff --git a/src/app/dashboard/users/client-page.tsx b/src/app/dashboard/users/client-page.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/export-utils.test.ts b/src/lib/export-utils.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/pagination.test.ts b/src/lib/pagination.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/validation-helpers.test.ts b/src/lib/validation-helpers.test.ts new file mode 100644 index 0000000..e69de29