2krika
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 dev

L'application sera disponible sur http://localhost:3001

Variables d'Environnement

.env
# API Configuration
NEXT_PUBLIC_API_URL=https://api.2krikaservices.cloud

# Admin Panel
NEXT_PUBLIC_APP_URL=http://localhost:3001

Structure des Pages Admin

Créer une Nouvelle Page Admin

src/app/(dashboard)/nouvelle-section/page.tsx
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
src/app/(dashboard)/layout.tsx
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:

src/components/ui/DataTable.tsx
'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:

src/components/ui/StatCard.tsx
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:

src/components/ui/ActionButton.tsx
'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:

src/hooks/useAdminStats.ts
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:

src/hooks/useUserManagement.ts
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:

src/components/charts/RevenueChart.tsx
'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

src/components/features/moderation/ServiceModeration.tsx
'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

src/middleware.ts
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

src/services/statsService.ts
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

src/utils/export.ts
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

src/components/ui/ExportButton.tsx
'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

Prochaines Étapes

On this page