App Staffs
Guide de Développement Staffs
Guide pratique pour développer sur l'application Staffs
Guide de Développement
Ce guide vous accompagne dans le développement de nouvelles fonctionnalités pour l'application Staffs.
Configuration de l'Environnement
Installation
cd apps/staffs
pnpm install
cp .env.example .env
pnpm devL'application sera disponible sur http://localhost:3001
Variables d'Environnement
# API Configuration
NEXT_PUBLIC_API_URL=https://api.2krikaservices.cloud
# Admin Panel
NEXT_PUBLIC_APP_URL=http://localhost:3001Structure des Pages Admin
Créer une Nouvelle Page Admin
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Nouvelle Section - Admin 2Krika',
description: 'Description de la section',
};
export default function NouvelleSectionPage() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-6">Nouvelle Section</h1>
{/* Contenu */}
</div>
);
}Layout Admin
Le layout admin contient:
- Sidebar de navigation
- Header avec profil admin
- Notifications
- Breadcrumbs
import { Sidebar } from '@/components/layout/Sidebar';
import { Header } from '@/components/layout/Header';
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex h-screen">
<Sidebar />
<div className="flex-1 flex flex-col overflow-hidden">
<Header />
<main className="flex-1 overflow-y-auto bg-gray-50">
{children}
</main>
</div>
</div>
);
}Composants Admin Courants
DataTable
Composant de tableau avec pagination et filtres:
'use client';
import { useState } from 'react';
import { Table, Pagination, TextInput } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
interface Column<T> {
key: keyof T;
header: string;
render?: (value: T[keyof T], row: T) => React.ReactNode;
}
interface DataTableProps<T> {
data: T[];
columns: Column<T>[];
searchable?: boolean;
pageSize?: number;
}
export function DataTable<T extends { id: string }>({
data,
columns,
searchable = true,
pageSize = 10,
}: DataTableProps<T>) {
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const filteredData = search
? data.filter((item) =>
Object.values(item).some((val) =>
String(val).toLowerCase().includes(search.toLowerCase())
)
)
: data;
const paginatedData = filteredData.slice(
(page - 1) * pageSize,
page * pageSize
);
return (
<div>
{searchable && (
<TextInput
placeholder="Rechercher..."
leftSection={<IconSearch size={16} />}
value={search}
onChange={(e) => setSearch(e.target.value)}
className="mb-4"
/>
)}
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr>
{columns.map((column) => (
<Table.Th key={String(column.key)}>{column.header}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{paginatedData.map((row) => (
<Table.Tr key={row.id}>
{columns.map((column) => (
<Table.Td key={String(column.key)}>
{column.render
? column.render(row[column.key], row)
: String(row[column.key])}
</Table.Td>
))}
</Table.Tr>
))}
</Table.Tbody>
</Table>
<Pagination
total={Math.ceil(filteredData.length / pageSize)}
value={page}
onChange={setPage}
className="mt-4"
/>
</div>
);
}StatCard
Carte pour afficher des statistiques:
import { Card, Text, Group } from '@mantine/core';
import { IconTrendingUp, IconTrendingDown } from '@tabler/icons-react';
interface StatCardProps {
title: string;
value: string | number;
change?: number;
icon?: React.ReactNode;
}
export function StatCard({ title, value, change, icon }: StatCardProps) {
const isPositive = change && change > 0;
return (
<Card shadow="sm" padding="lg">
<Group justify="space-between" mb="xs">
<Text size="sm" c="dimmed" fw={500}>
{title}
</Text>
{icon}
</Group>
<Text size="xl" fw={700}>
{value}
</Text>
{change !== undefined && (
<Group gap="xs" mt="xs">
{isPositive ? (
<IconTrendingUp size={16} color="green" />
) : (
<IconTrendingDown size={16} color="red" />
)}
<Text
size="sm"
c={isPositive ? 'green' : 'red'}
fw={500}
>
{Math.abs(change)}%
</Text>
</Group>
)}
</Card>
);
}ActionButton
Boutons d'action avec confirmation:
'use client';
import { Button } from '@mantine/core';
import { modals } from '@mantine/modals';
interface ActionButtonProps {
label: string;
onConfirm: () => void;
confirmTitle?: string;
confirmMessage?: string;
color?: string;
variant?: string;
}
export function ActionButton({
label,
onConfirm,
confirmTitle = 'Confirmer l\'action',
confirmMessage = 'Êtes-vous sûr de vouloir effectuer cette action ?',
color = 'blue',
variant = 'filled',
}: ActionButtonProps) {
const openModal = () =>
modals.openConfirmModal({
title: confirmTitle,
children: <Text size="sm">{confirmMessage}</Text>,
labels: { confirm: 'Confirmer', cancel: 'Annuler' },
confirmProps: { color },
onConfirm,
});
return (
<Button color={color} variant={variant} onClick={openModal}>
{label}
</Button>
);
}Hooks Admin Personnalisés
useAdminStats
Hook pour les statistiques du dashboard:
import { useQuery } from '@tanstack/react-query';
import { statsService } from '@/services/statsService';
export function useAdminStats() {
return useQuery({
queryKey: ['admin-stats'],
queryFn: statsService.getAdminStats,
refetchInterval: 5 * 60 * 1000, // Refresh every 5 minutes
});
}
export function useRevenueStats(period: 'day' | 'week' | 'month' | 'year') {
return useQuery({
queryKey: ['revenue-stats', period],
queryFn: () => statsService.getRevenueStats(period),
});
}useUserManagement
Hook pour la gestion des utilisateurs:
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { userService } from '@/services/userService';
import { notifications } from '@mantine/notifications';
export function useSuspendUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (userId: string) => userService.suspend(userId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
notifications.show({
title: 'Succès',
message: 'Utilisateur suspendu',
color: 'green',
});
},
onError: () => {
notifications.show({
title: 'Erreur',
message: 'Échec de la suspension',
color: 'red',
});
},
});
}
export function useBanUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ userId, reason }: { userId: string; reason: string }) =>
userService.ban(userId, reason),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
notifications.show({
title: 'Succès',
message: 'Utilisateur banni',
color: 'green',
});
},
});
}Graphiques et Visualisations
Graphique de Revenus
Utilisation de Recharts:
'use client';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from 'recharts';
interface RevenueChartProps {
data: Array<{
date: string;
revenue: number;
commissions: number;
}>;
}
export function RevenueChart({ data }: RevenueChartProps) {
return (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Legend />
<Line
type="monotone"
dataKey="revenue"
stroke="#8884d8"
name="Revenus"
/>
<Line
type="monotone"
dataKey="commissions"
stroke="#82ca9d"
name="Commissions"
/>
</LineChart>
</ResponsiveContainer>
);
}Modération de Contenu
Composant de Modération de Service
'use client';
import { Card, Image, Text, Group, Button, Textarea } from '@mantine/core';
import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { serviceService } from '@/services/serviceService';
interface ServiceModerationProps {
service: Service;
}
export function ServiceModeration({ service }: ServiceModerationProps) {
const [rejectReason, setRejectReason] = useState('');
const queryClient = useQueryClient();
const approveMutation = useMutation({
mutationFn: () => serviceService.approve(service.id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['services-pending'] });
},
});
const rejectMutation = useMutation({
mutationFn: (reason: string) =>
serviceService.reject(service.id, reason),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['services-pending'] });
},
});
return (
<Card shadow="sm" padding="lg">
<Image
src={service.images[0]}
height={200}
alt={service.title}
/>
<Text size="lg" fw={700} mt="md">
{service.title}
</Text>
<Text size="sm" c="dimmed" mt="xs">
{service.description}
</Text>
<Group mt="md" gap="xs">
<Button
color="green"
onClick={() => approveMutation.mutate()}
loading={approveMutation.isPending}
>
Approuver
</Button>
<Button
color="red"
variant="outline"
onClick={() => {
if (rejectReason) {
rejectMutation.mutate(rejectReason);
}
}}
loading={rejectMutation.isPending}
>
Rejeter
</Button>
</Group>
<Textarea
label="Raison du rejet"
placeholder="Expliquez pourquoi ce service est rejeté..."
value={rejectReason}
onChange={(e) => setRejectReason(e.target.value)}
mt="md"
/>
</Card>
);
}Protection des Routes
Middleware de Protection
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from 'jose';
export async function middleware(request: NextRequest) {
const token = request.cookies.get('admin_token')?.value;
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
try {
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const { payload } = await jwtVerify(token, secret);
// Vérifier que c'est bien un admin
if (payload.role !== 'admin' && payload.role !== 'superadmin') {
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
return NextResponse.next();
} catch (error) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
export const config = {
matcher: [
'/((?!login|api|_next/static|_next/image|favicon.ico).*)',
],
};Services API Admin
Service de Statistiques
import api from './api';
export interface AdminStats {
totalUsers: number;
totalSellers: number;
totalServices: number;
ordersThisMonth: number;
revenueThisMonth: number;
commissionsThisMonth: number;
}
export const statsService = {
getAdminStats: async (): Promise<AdminStats> => {
const { data } = await api.get('/admin/stats');
return data;
},
getRevenueStats: async (period: string) => {
const { data } = await api.get(`/admin/stats/revenue?period=${period}`);
return data;
},
getUserGrowth: async () => {
const { data } = await api.get('/admin/stats/users/growth');
return data;
},
};Export de Données
Fonction d'Export CSV
export function exportToCSV<T>(data: T[], filename: string) {
if (data.length === 0) return;
const headers = Object.keys(data[0] as object);
const csv = [
headers.join(','),
...data.map((row) =>
headers
.map((header) => {
const value = (row as any)[header];
return typeof value === 'string' && value.includes(',')
? `"${value}"`
: value;
})
.join(',')
),
].join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${filename}.csv`;
link.click();
URL.revokeObjectURL(url);
}Composant d'Export
'use client';
import { Button } from '@mantine/core';
import { IconDownload } from '@tabler/icons-react';
import { exportToCSV } from '@/utils/export';
interface ExportButtonProps<T> {
data: T[];
filename: string;
label?: string;
}
export function ExportButton<T>({
data,
filename,
label = 'Exporter CSV',
}: ExportButtonProps<T>) {
return (
<Button
leftSection={<IconDownload size={16} />}
onClick={() => exportToCSV(data, filename)}
>
{label}
</Button>
);
}Bonnes Pratiques
Sécurité
- Toujours vérifier les permissions côté serveur
- Ne jamais exposer de données sensibles
- Logger toutes les actions admin importantes
- Implémenter un système d'audit trail
Performance
- Paginer les listes de données
- Utiliser le cache de React Query
- Lazy load les graphiques lourds
- Optimiser les requêtes API
UX Admin
- Confirmations pour les actions destructrices
- Feedback immédiat (toasts, notifications)
- États de chargement clairs
- Messages d'erreur descriptifs
Code Quality
- Typer toutes les props et retours
- Créer des composants réutilisables
- Documenter les fonctions complexes
- Tester les fonctionnalités critiques