diff --git a/app/employe/gains-client/page.tsx b/app/employe/gains-client/page.tsx
index cfdb5a9..280d790 100644
--- a/app/employe/gains-client/page.tsx
+++ b/app/employe/gains-client/page.tsx
@@ -2,8 +2,9 @@
import { useState } from 'react';
import { Card } from '@/components/ui/Card';
+import { EmptyState } from '@/components/ui/EmptyState';
import Button from '@/components/Button';
-import { API_BASE_URL } from '@/utils/constants';
+import { apiFetch } from '@/hooks/useApi';
import toast from 'react-hot-toast';
import {
Search,
@@ -63,26 +64,10 @@ export default function GainsClientPage() {
setLoading(true);
try {
- const token = localStorage.getItem('auth_token') || localStorage.getItem('token');
const queryParam = searchType === 'email' ? `email=${encodeURIComponent(searchValue)}` : `phone=${encodeURIComponent(searchValue)}`;
-
- const response = await fetch(
- `${API_BASE_URL}/employee/client-prizes?${queryParam}`,
- {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- }
- );
-
- if (!response.ok) {
- const error = await response.json();
- throw new Error(error.error || 'Client non trouvé');
- }
-
- const data = await response.json();
+ const data = await apiFetch<{ data: ClientData }>(`/employee/client-prizes?${queryParam}`);
setClientData(data.data);
- toast.success(`✅ Client trouvé: ${data.data.client.firstName} ${data.data.client.lastName}`);
+ toast.success(`Client trouvé: ${data.data.client.firstName} ${data.data.client.lastName}`);
} catch (error: any) {
console.error('Error searching client:', error);
toast.error(error.message || 'Erreur lors de la recherche');
@@ -99,30 +84,11 @@ export default function GainsClientPage() {
setValidatingTicketId(ticketId);
try {
- const token = localStorage.getItem('auth_token') || localStorage.getItem('token');
- const response = await fetch(
- `${API_BASE_URL}/employee/validate-ticket`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify({
- ticketId,
- action: 'APPROVE',
- }),
- }
- );
-
- if (!response.ok) {
- const error = await response.json();
- throw new Error(error.error || 'Erreur lors de la validation');
- }
-
- toast.success('✅ Lot marqué comme remis!');
-
- // Recharger les données du client
+ await apiFetch('/employee/validate-ticket', {
+ method: 'POST',
+ body: JSON.stringify({ ticketId, action: 'APPROVE' }),
+ });
+ toast.success('Lot marqué comme remis!');
handleSearch();
} catch (error: any) {
console.error('Error validating prize:', error);
@@ -282,10 +248,10 @@ export default function GainsClientPage() {
{clientData.prizes.length === 0 ? (
-
-
-
Ce client n'a pas encore gagné de lots
-
+
) : (
{clientData.prizes.map((prize) => (
diff --git a/app/employe/historique/page.tsx b/app/employe/historique/page.tsx
index 9ccce6e..46b5b8d 100644
--- a/app/employe/historique/page.tsx
+++ b/app/employe/historique/page.tsx
@@ -2,9 +2,9 @@
import { useState, useEffect } from 'react';
import { useAuth } from '@/hooks';
-import { Card } from '@/components/ui';
+import { Card, EmptyState } from '@/components/ui';
import { Loading } from '@/components/ui/Loading';
-import { API_BASE_URL } from '@/utils/constants';
+import { apiFetch } from '@/hooks/useApi';
import toast from 'react-hot-toast';
import {
CheckCircle,
@@ -45,23 +45,7 @@ export default function EmployeeHistoryPage() {
const loadHistory = async () => {
try {
setLoading(true);
- const token = localStorage.getItem('auth_token') || localStorage.getItem('token');
-
- // Charger tous les tickets validés par cet employé
- const response = await fetch(
- `${API_BASE_URL}/employee/history`,
- {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- }
- );
-
- if (!response.ok) {
- throw new Error('Erreur lors du chargement de l\'historique');
- }
-
- const data = await response.json();
+ const data = await apiFetch<{ data: HistoryTicket[] }>('/employee/history');
setHistory(data.data || []);
} catch (error: any) {
console.error('Error loading history:', error);
@@ -209,17 +193,13 @@ export default function EmployeeHistoryPage() {
{/* History List */}
{filteredHistory.length === 0 ? (
-
-
📝
-
- Aucun ticket dans l'historique
-
-
- {filter === 'ALL'
- ? 'Vous n\'avez pas encore validé de tickets'
- : `Aucun ticket ${filter === 'CLAIMED' ? 'validé' : 'rejeté'}`}
-
-
+
) : (
{filteredHistory.map((ticket) => (
diff --git a/components/ui/EmptyState.tsx b/components/ui/EmptyState.tsx
new file mode 100644
index 0000000..ab6abb9
--- /dev/null
+++ b/components/ui/EmptyState.tsx
@@ -0,0 +1,41 @@
+'use client';
+
+import React from 'react';
+
+interface EmptyStateProps {
+ icon?: string;
+ title?: string;
+ message: string;
+ action?: {
+ label: string;
+ onClick: () => void;
+ };
+}
+
+/**
+ * Reusable empty state component for lists and tables
+ */
+export const EmptyState: React.FC
= ({
+ icon = '📝',
+ title,
+ message,
+ action,
+}) => {
+ return (
+
+
{icon}
+ {title &&
{title}
}
+
{message}
+ {action && (
+
+ )}
+
+ );
+};
+
+export default EmptyState;
diff --git a/components/ui/Pagination.tsx b/components/ui/Pagination.tsx
new file mode 100644
index 0000000..fdf5d6b
--- /dev/null
+++ b/components/ui/Pagination.tsx
@@ -0,0 +1,121 @@
+'use client';
+
+import React from 'react';
+import { ChevronLeft, ChevronRight } from 'lucide-react';
+
+interface PaginationProps {
+ currentPage: number;
+ totalPages: number;
+ onPageChange: (page: number) => void;
+ showPageNumbers?: boolean;
+ className?: string;
+}
+
+/**
+ * Reusable pagination component
+ */
+export const Pagination: React.FC = ({
+ currentPage,
+ totalPages,
+ onPageChange,
+ showPageNumbers = true,
+ className = '',
+}) => {
+ if (totalPages <= 1) return null;
+
+ const handlePrevious = () => {
+ if (currentPage > 1) {
+ onPageChange(currentPage - 1);
+ }
+ };
+
+ const handleNext = () => {
+ if (currentPage < totalPages) {
+ onPageChange(currentPage + 1);
+ }
+ };
+
+ // Generate page numbers to show
+ const getPageNumbers = () => {
+ const pages: (number | string)[] = [];
+ const maxVisible = 5;
+
+ if (totalPages <= maxVisible) {
+ for (let i = 1; i <= totalPages; i++) {
+ pages.push(i);
+ }
+ } else {
+ if (currentPage <= 3) {
+ for (let i = 1; i <= 4; i++) pages.push(i);
+ pages.push('...');
+ pages.push(totalPages);
+ } else if (currentPage >= totalPages - 2) {
+ pages.push(1);
+ pages.push('...');
+ for (let i = totalPages - 3; i <= totalPages; i++) pages.push(i);
+ } else {
+ pages.push(1);
+ pages.push('...');
+ for (let i = currentPage - 1; i <= currentPage + 1; i++) pages.push(i);
+ pages.push('...');
+ pages.push(totalPages);
+ }
+ }
+
+ return pages;
+ };
+
+ return (
+
+
+
+ {showPageNumbers && (
+
+ {getPageNumbers().map((page, index) =>
+ page === '...' ? (
+
+ ...
+
+ ) : (
+
+ )
+ )}
+
+ )}
+
+ {!showPageNumbers && (
+
+ Page {currentPage} sur {totalPages}
+
+ )}
+
+
+
+ );
+};
+
+export default Pagination;
diff --git a/components/ui/index.ts b/components/ui/index.ts
index 9bdb93c..b9502c8 100644
--- a/components/ui/index.ts
+++ b/components/ui/index.ts
@@ -7,3 +7,5 @@ export { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '.
export { LoadingState } from './LoadingState';
export { ErrorState } from './ErrorState';
export { StatusBadge, getRoleBadgeColor, getTicketStatusColor, getStatusColor } from './StatusBadge';
+export { EmptyState } from './EmptyState';
+export { Pagination } from './Pagination';
diff --git a/utils/export.ts b/utils/export.ts
new file mode 100644
index 0000000..5ba6a99
--- /dev/null
+++ b/utils/export.ts
@@ -0,0 +1,80 @@
+/**
+ * Utility functions for data export
+ */
+
+type CsvRow = (string | number | boolean | null | undefined)[];
+
+/**
+ * Export data to CSV file
+ */
+export const exportToCSV = (
+ data: CsvRow[],
+ filename: string,
+ options?: { separator?: string; includeDate?: boolean }
+): void => {
+ const { separator = ',', includeDate = true } = options || {};
+
+ const csvContent = data
+ .map((row) =>
+ row
+ .map((cell) => {
+ if (cell === null || cell === undefined) return '';
+ const cellStr = String(cell);
+ // Escape quotes and wrap in quotes if contains separator or quotes
+ if (cellStr.includes(separator) || cellStr.includes('"') || cellStr.includes('\n')) {
+ return `"${cellStr.replace(/"/g, '""')}"`;
+ }
+ return cellStr;
+ })
+ .join(separator)
+ )
+ .join('\n');
+
+ const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
+ const link = document.createElement('a');
+ const url = URL.createObjectURL(blob);
+
+ const date = includeDate ? `-${new Date().toISOString().split('T')[0]}` : '';
+ link.href = url;
+ link.download = `${filename}${date}.csv`;
+ link.style.display = 'none';
+
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+};
+
+/**
+ * Export data to JSON file
+ */
+export const exportToJSON = (data: T, filename: string): void => {
+ const jsonContent = JSON.stringify(data, null, 2);
+ const blob = new Blob([jsonContent], { type: 'application/json;charset=utf-8;' });
+ const link = document.createElement('a');
+ const url = URL.createObjectURL(blob);
+
+ link.href = url;
+ link.download = `${filename}-${new Date().toISOString().split('T')[0]}.json`;
+ link.style.display = 'none';
+
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+};
+
+/**
+ * Format date for export
+ */
+export const formatDateForExport = (date: string | Date | null | undefined): string => {
+ if (!date) return '';
+ const d = new Date(date);
+ return d.toLocaleDateString('fr-FR', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+};