From 4fac1078a1541169d07cd3e981250cb39fc421f5 Mon Sep 17 00:00:00 2001
From: michaelschal <35957947-michaelschal@users.noreply.replit.com>
Date: Wed, 9 Jul 2025 18:05:04 +0000
Subject: [PATCH] Create a new sales registry app with identity verification
features
Initial commit of the React application with UI components, form, camera, and indexedDB integration for sale data management.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 91318273-c764-4fd4-be04-bdc12c38af32
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0715008c-7244-40f4-befc-26c014852236/02867739-9ff7-4441-aca2-12e0153376c5.jpg
---
.gitignore | 6 +
.replit | 36 +
client/index.html | 15 +
client/src/App.tsx | 18 +
client/src/components/CameraModal.tsx | 72 +
client/src/components/FireworksApp.tsx | 66 +
client/src/components/NewSaleForm.tsx | 555 ++
client/src/components/SaleDetailsModal.tsx | 119 +
client/src/components/SalesHistory.tsx | 284 +
client/src/components/ui/accordion.tsx | 56 +
client/src/components/ui/alert-dialog.tsx | 139 +
client/src/components/ui/alert.tsx | 59 +
client/src/components/ui/aspect-ratio.tsx | 5 +
client/src/components/ui/avatar.tsx | 50 +
client/src/components/ui/badge.tsx | 36 +
client/src/components/ui/breadcrumb.tsx | 115 +
client/src/components/ui/button.tsx | 56 +
client/src/components/ui/calendar.tsx | 68 +
client/src/components/ui/card.tsx | 79 +
client/src/components/ui/carousel.tsx | 260 +
client/src/components/ui/chart.tsx | 365 +
client/src/components/ui/checkbox.tsx | 28 +
client/src/components/ui/collapsible.tsx | 11 +
client/src/components/ui/command.tsx | 151 +
client/src/components/ui/context-menu.tsx | 198 +
client/src/components/ui/dialog.tsx | 122 +
client/src/components/ui/drawer.tsx | 118 +
client/src/components/ui/dropdown-menu.tsx | 198 +
client/src/components/ui/form.tsx | 178 +
client/src/components/ui/hover-card.tsx | 29 +
client/src/components/ui/input-otp.tsx | 69 +
client/src/components/ui/input.tsx | 22 +
client/src/components/ui/label.tsx | 24 +
client/src/components/ui/menubar.tsx | 256 +
client/src/components/ui/navigation-menu.tsx | 128 +
client/src/components/ui/pagination.tsx | 117 +
client/src/components/ui/popover.tsx | 29 +
client/src/components/ui/progress.tsx | 28 +
client/src/components/ui/radio-group.tsx | 42 +
client/src/components/ui/resizable.tsx | 45 +
client/src/components/ui/scroll-area.tsx | 46 +
client/src/components/ui/select.tsx | 160 +
client/src/components/ui/separator.tsx | 29 +
client/src/components/ui/sheet.tsx | 140 +
client/src/components/ui/sidebar.tsx | 771 ++
client/src/components/ui/skeleton.tsx | 15 +
client/src/components/ui/slider.tsx | 26 +
client/src/components/ui/switch.tsx | 27 +
client/src/components/ui/table.tsx | 117 +
client/src/components/ui/tabs.tsx | 53 +
client/src/components/ui/textarea.tsx | 22 +
client/src/components/ui/toast.tsx | 127 +
client/src/components/ui/toaster.tsx | 33 +
client/src/components/ui/toggle-group.tsx | 61 +
client/src/components/ui/toggle.tsx | 43 +
client/src/components/ui/tooltip.tsx | 30 +
client/src/hooks/use-mobile.tsx | 19 +
client/src/hooks/use-toast.ts | 191 +
client/src/hooks/useCamera.tsx | 102 +
client/src/hooks/useIndexedDB.tsx | 83 +
client/src/index.css | 95 +
client/src/lib/export.ts | 68 +
client/src/lib/queryClient.ts | 57 +
client/src/lib/storage.ts | 119 +
client/src/lib/utils.ts | 6 +
client/src/lib/validation.ts | 48 +
client/src/main.tsx | 5 +
client/src/pages/not-found.tsx | 21 +
client/src/types/sale.ts | 55 +
components.json | 20 +
drizzle.config.ts | 14 +
package-lock.json | 9135 ++++++++++++++++++
package.json | 106 +
postcss.config.js | 6 +
replit.md | 107 +
server/index.ts | 70 +
server/routes.ts | 15 +
server/storage.ts | 39 +
server/vite.ts | 85 +
shared/schema.ts | 17 +
tailwind.config.ts | 90 +
tsconfig.json | 23 +
vite.config.ts | 37 +
83 files changed, 16585 insertions(+)
create mode 100644 .gitignore
create mode 100644 client/index.html
create mode 100644 client/src/App.tsx
create mode 100644 client/src/components/CameraModal.tsx
create mode 100644 client/src/components/FireworksApp.tsx
create mode 100644 client/src/components/NewSaleForm.tsx
create mode 100644 client/src/components/SaleDetailsModal.tsx
create mode 100644 client/src/components/SalesHistory.tsx
create mode 100644 client/src/components/ui/accordion.tsx
create mode 100644 client/src/components/ui/alert-dialog.tsx
create mode 100644 client/src/components/ui/alert.tsx
create mode 100644 client/src/components/ui/aspect-ratio.tsx
create mode 100644 client/src/components/ui/avatar.tsx
create mode 100644 client/src/components/ui/badge.tsx
create mode 100644 client/src/components/ui/breadcrumb.tsx
create mode 100644 client/src/components/ui/button.tsx
create mode 100644 client/src/components/ui/calendar.tsx
create mode 100644 client/src/components/ui/card.tsx
create mode 100644 client/src/components/ui/carousel.tsx
create mode 100644 client/src/components/ui/chart.tsx
create mode 100644 client/src/components/ui/checkbox.tsx
create mode 100644 client/src/components/ui/collapsible.tsx
create mode 100644 client/src/components/ui/command.tsx
create mode 100644 client/src/components/ui/context-menu.tsx
create mode 100644 client/src/components/ui/dialog.tsx
create mode 100644 client/src/components/ui/drawer.tsx
create mode 100644 client/src/components/ui/dropdown-menu.tsx
create mode 100644 client/src/components/ui/form.tsx
create mode 100644 client/src/components/ui/hover-card.tsx
create mode 100644 client/src/components/ui/input-otp.tsx
create mode 100644 client/src/components/ui/input.tsx
create mode 100644 client/src/components/ui/label.tsx
create mode 100644 client/src/components/ui/menubar.tsx
create mode 100644 client/src/components/ui/navigation-menu.tsx
create mode 100644 client/src/components/ui/pagination.tsx
create mode 100644 client/src/components/ui/popover.tsx
create mode 100644 client/src/components/ui/progress.tsx
create mode 100644 client/src/components/ui/radio-group.tsx
create mode 100644 client/src/components/ui/resizable.tsx
create mode 100644 client/src/components/ui/scroll-area.tsx
create mode 100644 client/src/components/ui/select.tsx
create mode 100644 client/src/components/ui/separator.tsx
create mode 100644 client/src/components/ui/sheet.tsx
create mode 100644 client/src/components/ui/sidebar.tsx
create mode 100644 client/src/components/ui/skeleton.tsx
create mode 100644 client/src/components/ui/slider.tsx
create mode 100644 client/src/components/ui/switch.tsx
create mode 100644 client/src/components/ui/table.tsx
create mode 100644 client/src/components/ui/tabs.tsx
create mode 100644 client/src/components/ui/textarea.tsx
create mode 100644 client/src/components/ui/toast.tsx
create mode 100644 client/src/components/ui/toaster.tsx
create mode 100644 client/src/components/ui/toggle-group.tsx
create mode 100644 client/src/components/ui/toggle.tsx
create mode 100644 client/src/components/ui/tooltip.tsx
create mode 100644 client/src/hooks/use-mobile.tsx
create mode 100644 client/src/hooks/use-toast.ts
create mode 100644 client/src/hooks/useCamera.tsx
create mode 100644 client/src/hooks/useIndexedDB.tsx
create mode 100644 client/src/index.css
create mode 100644 client/src/lib/export.ts
create mode 100644 client/src/lib/queryClient.ts
create mode 100644 client/src/lib/storage.ts
create mode 100644 client/src/lib/utils.ts
create mode 100644 client/src/lib/validation.ts
create mode 100644 client/src/main.tsx
create mode 100644 client/src/pages/not-found.tsx
create mode 100644 client/src/types/sale.ts
create mode 100644 components.json
create mode 100644 drizzle.config.ts
create mode 100644 package-lock.json
create mode 100644 package.json
create mode 100644 postcss.config.js
create mode 100644 replit.md
create mode 100644 server/index.ts
create mode 100644 server/routes.ts
create mode 100644 server/storage.ts
create mode 100644 server/vite.ts
create mode 100644 shared/schema.ts
create mode 100644 tailwind.config.ts
create mode 100644 tsconfig.json
create mode 100644 vite.config.ts
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f9ba7f8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+dist
+.DS_Store
+server/public
+vite.config.ts.*
+*.tar.gz
\ No newline at end of file
diff --git a/.replit b/.replit
index e69de29..51f344f 100644
--- a/.replit
+++ b/.replit
@@ -0,0 +1,36 @@
+modules = ["nodejs-20", "web", "postgresql-16"]
+run = "npm run dev"
+hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"]
+
+[nix]
+channel = "stable-24_05"
+
+[deployment]
+deploymentTarget = "autoscale"
+build = ["npm", "run", "build"]
+run = ["npm", "run", "start"]
+
+[[ports]]
+localPort = 5000
+externalPort = 80
+
+[workflows]
+runButton = "Project"
+
+[[workflows.workflow]]
+name = "Project"
+mode = "parallel"
+author = "agent"
+
+[[workflows.workflow.tasks]]
+task = "workflow.run"
+args = "Start application"
+
+[[workflows.workflow]]
+name = "Start application"
+author = "agent"
+
+[[workflows.workflow.tasks]]
+task = "shell.exec"
+args = "npm run dev"
+waitForPort = 5000
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000..c347196
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ Registre des Ventes de Pétards
+
+
+
+
+
+
+
+
+
diff --git a/client/src/App.tsx b/client/src/App.tsx
new file mode 100644
index 0000000..e2a6fc6
--- /dev/null
+++ b/client/src/App.tsx
@@ -0,0 +1,18 @@
+import { QueryClientProvider } from "@tanstack/react-query";
+import { queryClient } from "./lib/queryClient";
+import { Toaster } from "@/components/ui/toaster";
+import { TooltipProvider } from "@/components/ui/tooltip";
+import FireworksApp from "./components/FireworksApp";
+
+function App() {
+ return (
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/client/src/components/CameraModal.tsx b/client/src/components/CameraModal.tsx
new file mode 100644
index 0000000..9c5f29a
--- /dev/null
+++ b/client/src/components/CameraModal.tsx
@@ -0,0 +1,72 @@
+import { useEffect } from 'react';
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Camera, X } from 'lucide-react';
+import { PhotoType } from '@/types/sale';
+
+interface CameraModalProps {
+ isOpen: boolean;
+ photoType: PhotoType | null;
+ videoRef: React.RefObject;
+ canvasRef: React.RefObject;
+ isCapturing: boolean;
+ onClose: () => void;
+ onCapture: () => void;
+}
+
+export default function CameraModal({
+ isOpen,
+ photoType,
+ videoRef,
+ canvasRef,
+ isCapturing,
+ onClose,
+ onCapture
+}: CameraModalProps) {
+ useEffect(() => {
+ if (isOpen && videoRef.current) {
+ // Ensure video starts playing when modal opens
+ videoRef.current.play().catch(console.error);
+ }
+ }, [isOpen, videoRef]);
+
+ return (
+
+ );
+}
diff --git a/client/src/components/FireworksApp.tsx b/client/src/components/FireworksApp.tsx
new file mode 100644
index 0000000..f6fd2f9
--- /dev/null
+++ b/client/src/components/FireworksApp.tsx
@@ -0,0 +1,66 @@
+import { useState } from 'react';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { Shield, Flame } from 'lucide-react';
+import NewSaleForm from './NewSaleForm';
+import SalesHistory from './SalesHistory';
+import { useIndexedDB } from '@/hooks/useIndexedDB';
+
+export default function FireworksApp() {
+ const [activeTab, setActiveTab] = useState('nouvelle-vente');
+ const { sales, isLoading, error, saveSale, deleteSale } = useIndexedDB();
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+ Registre des Ventes de Pétards
+
+
+
+
+ Conforme à la réglementation française
+
+
+
+
+
+ {/* Main Content */}
+
+
+
+
+
+ Nouvelle Vente
+
+
+
+ Historique ({sales.length} vente{sales.length !== 1 ? 's' : ''})
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/NewSaleForm.tsx b/client/src/components/NewSaleForm.tsx
new file mode 100644
index 0000000..0995957
--- /dev/null
+++ b/client/src/components/NewSaleForm.tsx
@@ -0,0 +1,555 @@
+import { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
+import { Input } from '@/components/ui/input';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { Button } from '@/components/ui/button';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { useToast } from '@/hooks/use-toast';
+import { Camera, User, Package, Users, IdCard, Save, X, Trash2 } from 'lucide-react';
+import { Sale, FormData, PhotoType } from '@/types/sale';
+import { validateRequiredFields, validateEAN13, ARTICLE_CATEGORY_MAPPING, IDENTITY_TYPES } from '@/lib/validation';
+import { useCamera } from '@/hooks/useCamera';
+import CameraModal from './CameraModal';
+
+interface NewSaleFormProps {
+ onSaveSale: (sale: Omit) => Promise;
+}
+
+export default function NewSaleForm({ onSaveSale }: NewSaleFormProps) {
+ const { toast } = useToast();
+ const [photos, setPhotos] = useState<{ recto?: string; verso?: string }>({});
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const camera = useCamera();
+
+ const form = useForm({
+ defaultValues: {
+ vendeur: '',
+ dateVente: new Date().toISOString().split('T')[0],
+ typeArticle: '',
+ categorie: '',
+ quantite: '',
+ gencode: '',
+ nom: '',
+ prenom: '',
+ dateNaissance: '',
+ lieuNaissance: '',
+ typeIdentite: '',
+ autoriteDelivrance: '',
+ dateDelivrance: '',
+ }
+ });
+
+ const typeArticle = form.watch('typeArticle');
+ const availableCategories = typeArticle ? ARTICLE_CATEGORY_MAPPING[typeArticle as keyof typeof ARTICLE_CATEGORY_MAPPING] || [] : [];
+
+ const handlePhotoCapture = async (photoType: PhotoType) => {
+ try {
+ await camera.startCamera(photoType);
+ toast({
+ title: "Caméra activée",
+ description: "Positionnez la carte d'identité",
+ });
+ } catch (error) {
+ toast({
+ title: "Erreur caméra",
+ description: error instanceof Error ? error.message : "Impossible d'accéder à la caméra",
+ variant: "destructive",
+ });
+ }
+ };
+
+ const handlePhotoTaken = (photoType: PhotoType, photoData: string) => {
+ setPhotos(prev => ({ ...prev, [photoType]: photoData }));
+ toast({
+ title: "Photo capturée",
+ description: `Photo ${photoType} sauvegardée avec succès`,
+ });
+ };
+
+ const deletePhoto = (photoType: PhotoType) => {
+ setPhotos(prev => {
+ const newPhotos = { ...prev };
+ delete newPhotos[photoType];
+ return newPhotos;
+ });
+ toast({
+ title: "Photo supprimée",
+ description: `Photo ${photoType} supprimée`,
+ });
+ };
+
+ const onSubmit = async (data: FormData) => {
+ try {
+ setIsSubmitting(true);
+
+ // Validate required fields
+ const missingFields = validateRequiredFields(data);
+ if (missingFields.length > 0) {
+ toast({
+ title: "Champs manquants",
+ description: `Veuillez remplir: ${missingFields.join(', ')}`,
+ variant: "destructive",
+ });
+ return;
+ }
+
+ // Validate EAN13 if provided
+ if (data.gencode && !validateEAN13(data.gencode)) {
+ toast({
+ title: "Code EAN13 invalide",
+ description: "Le code produit doit être un code EAN13 valide à 13 chiffres",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ const saleData: Omit = {
+ vendeur: data.vendeur,
+ dateVente: data.dateVente,
+ typeArticle: data.typeArticle,
+ categorie: data.categorie as 'F2' | 'F3',
+ quantite: parseInt(data.quantite),
+ gencode: data.gencode || undefined,
+ nom: data.nom,
+ prenom: data.prenom,
+ dateNaissance: data.dateNaissance || undefined,
+ lieuNaissance: data.lieuNaissance || undefined,
+ typeIdentite: data.typeIdentite || undefined,
+ autoriteDelivrance: data.autoriteDelivrance || undefined,
+ dateDelivrance: data.dateDelivrance || undefined,
+ photoRecto: photos.recto,
+ photoVerso: photos.verso,
+ };
+
+ await onSaveSale(saleData);
+
+ // Reset form
+ form.reset({
+ vendeur: '',
+ dateVente: new Date().toISOString().split('T')[0],
+ typeArticle: '',
+ categorie: '',
+ quantite: '',
+ gencode: '',
+ nom: '',
+ prenom: '',
+ dateNaissance: '',
+ lieuNaissance: '',
+ typeIdentite: '',
+ autoriteDelivrance: '',
+ dateDelivrance: '',
+ });
+ setPhotos({});
+
+ toast({
+ title: "Vente enregistrée",
+ description: "La vente a été enregistrée avec succès",
+ });
+ } catch (error) {
+ toast({
+ title: "Erreur",
+ description: "Erreur lors de l'enregistrement de la vente",
+ variant: "destructive",
+ });
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+ Nouvelle Vente
+
+
+ Enregistrement d'une vente de feux d'artifice
+
+
+
+
+
+
+
+
+ {
+ try {
+ const photoData = await camera.capturePhoto();
+ if (camera.currentPhotoType) {
+ handlePhotoTaken(camera.currentPhotoType, photoData);
+ }
+ camera.stopCamera();
+ } catch (error) {
+ toast({
+ title: "Erreur",
+ description: "Erreur lors de la capture de la photo",
+ variant: "destructive",
+ });
+ }
+ }}
+ />
+ >
+ );
+}
diff --git a/client/src/components/SaleDetailsModal.tsx b/client/src/components/SaleDetailsModal.tsx
new file mode 100644
index 0000000..af5b5cb
--- /dev/null
+++ b/client/src/components/SaleDetailsModal.tsx
@@ -0,0 +1,119 @@
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { X } from 'lucide-react';
+import { Sale } from '@/types/sale';
+import { formatDate, formatDateTime } from '@/lib/export';
+
+interface SaleDetailsModalProps {
+ isOpen: boolean;
+ sale: Sale | null;
+ onClose: () => void;
+}
+
+export default function SaleDetailsModal({ isOpen, sale, onClose }: SaleDetailsModalProps) {
+ if (!sale) return null;
+
+ return (
+
+ );
+}
diff --git a/client/src/components/SalesHistory.tsx b/client/src/components/SalesHistory.tsx
new file mode 100644
index 0000000..2644eb4
--- /dev/null
+++ b/client/src/components/SalesHistory.tsx
@@ -0,0 +1,284 @@
+import { useState } from 'react';
+import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
+import { useToast } from '@/hooks/use-toast';
+import { Download, Save, Search, Eye, Trash2, BarChart3, AlertCircle } from 'lucide-react';
+import { Sale } from '@/types/sale';
+import { exportToCSV, formatDate } from '@/lib/export';
+import { downloadBackup, getAutoBackup } from '@/lib/storage';
+import SaleDetailsModal from './SaleDetailsModal';
+
+interface SalesHistoryProps {
+ sales: Sale[];
+ isLoading: boolean;
+ error: string | null;
+ onDeleteSale: (id: number) => Promise;
+}
+
+export default function SalesHistory({ sales, isLoading, error, onDeleteSale }: SalesHistoryProps) {
+ const { toast } = useToast();
+ const [selectedSale, setSelectedSale] = useState(null);
+ const [isDeleting, setIsDeleting] = useState(null);
+
+ const handleExportCSV = () => {
+ if (sales.length === 0) {
+ toast({
+ title: "Aucune vente à exporter",
+ description: "Il n'y a aucune vente enregistrée",
+ });
+ return;
+ }
+
+ try {
+ exportToCSV(sales);
+ toast({
+ title: "Export réussi !",
+ description: `${sales.length} vente(s) exportée(s)`,
+ });
+ } catch (error) {
+ toast({
+ title: "Erreur d'export",
+ description: "Impossible d'exporter les données",
+ variant: "destructive",
+ });
+ }
+ };
+
+ const handleDownloadBackup = () => {
+ const backup = getAutoBackup();
+ if (!backup) {
+ toast({
+ title: "Aucune sauvegarde disponible",
+ description: "Aucun backup automatique trouvé",
+ });
+ return;
+ }
+
+ try {
+ downloadBackup();
+ toast({
+ title: "Backup téléchargé",
+ description: "Sauvegarde téléchargée avec succès",
+ });
+ } catch (error) {
+ toast({
+ title: "Erreur",
+ description: "Impossible de télécharger la sauvegarde",
+ variant: "destructive",
+ });
+ }
+ };
+
+ const handleDiagnostic = () => {
+ console.log('=== DIAGNOSTIC COMPLET ===');
+ console.log('Nombre de ventes:', sales.length);
+ console.log('Données:', sales);
+ console.log('LocalStorage backup:', getAutoBackup());
+
+ toast({
+ title: "Diagnostic affiché",
+ description: "Vérifiez la console pour les détails",
+ });
+ };
+
+ const handleDeleteSale = async (sale: Sale) => {
+ const confirmed = window.confirm(
+ `Supprimer définitivement la vente de ${sale.nom} ${sale.prenom} ?\n\n⚠️ Cette action est irréversible !\n\nID: ${sale.id}`
+ );
+
+ if (!confirmed) return;
+
+ try {
+ setIsDeleting(sale.id);
+ await onDeleteSale(sale.id);
+ toast({
+ title: "Vente supprimée",
+ description: `Vente de ${sale.nom} ${sale.prenom} supprimée`,
+ });
+ } catch (error) {
+ toast({
+ title: "Erreur",
+ description: "Impossible de supprimer la vente",
+ variant: "destructive",
+ });
+ } finally {
+ setIsDeleting(null);
+ }
+ };
+
+ const backup = getAutoBackup();
+
+ if (isLoading) {
+ return (
+
+
+
+
+
Chargement des ventes...
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+ Historique des Ventes
+
+
+ Gestion et export des ventes enregistrées
+
+
+
+
+
+
+
+
+
+
+ {/* Status Information */}
+
+
+
+
+ Données stockées localement ({sales.length} vente{sales.length !== 1 ? 's' : ''}) • Export CSV compatible Excel • Sauvegarde automatique à chaque ajout
+
+
+
+ {backup ? (
+
+ Dernier backup: {formatDate(backup.timestamp)} ({backup.recordCount} enreg.)
+
+ ) : (
+ Dernier backup: Aucun
+ )}
+
+
+
+
+ {sales.length === 0 ? (
+
+
+
Aucune vente enregistrée
+
+ ) : (
+
+
+
+
+ Date
+ Vendeur
+ Client
+ Produit
+ Catégorie
+ Quantité
+ Actions
+
+
+
+ {sales.map((sale) => (
+
+
+ {formatDate(sale.dateVente)}
+
+
+ {sale.vendeur || 'Non spécifié'}
+
+
+
+
{sale.nom} {sale.prenom}
+ {sale.typeIdentite && (
+
{sale.typeIdentite}
+ )}
+
+
+
+
+
{sale.typeArticle}
+ {sale.gencode && (
+
{sale.gencode}
+ )}
+
+
+
+
+ {sale.categorie}
+
+
+
+ {sale.quantite}
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+ )}
+
+
+
+ setSelectedSale(null)}
+ />
+ >
+ );
+}
diff --git a/client/src/components/ui/accordion.tsx b/client/src/components/ui/accordion.tsx
new file mode 100644
index 0000000..e6a723d
--- /dev/null
+++ b/client/src/components/ui/accordion.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDown } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Accordion = AccordionPrimitive.Root
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = "AccordionItem"
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/client/src/components/ui/alert-dialog.tsx b/client/src/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..8722561
--- /dev/null
+++ b/client/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,139 @@
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/client/src/components/ui/alert.tsx b/client/src/components/ui/alert.tsx
new file mode 100644
index 0000000..41fa7e0
--- /dev/null
+++ b/client/src/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/client/src/components/ui/aspect-ratio.tsx b/client/src/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..c4abbf3
--- /dev/null
+++ b/client/src/components/ui/aspect-ratio.tsx
@@ -0,0 +1,5 @@
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/client/src/components/ui/avatar.tsx b/client/src/components/ui/avatar.tsx
new file mode 100644
index 0000000..51e507b
--- /dev/null
+++ b/client/src/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/client/src/components/ui/badge.tsx b/client/src/components/ui/badge.tsx
new file mode 100644
index 0000000..f000e3e
--- /dev/null
+++ b/client/src/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/client/src/components/ui/breadcrumb.tsx b/client/src/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..60e6c96
--- /dev/null
+++ b/client/src/components/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode
+ }
+>(({ ...props }, ref) => )
+Breadcrumb.displayName = "Breadcrumb"
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbList.displayName = "BreadcrumbList"
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbItem.displayName = "BreadcrumbItem"
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+})
+BreadcrumbLink.displayName = "BreadcrumbLink"
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbPage.displayName = "BreadcrumbPage"
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+ svg]:w-3.5 [&>svg]:h-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+)
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+)
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx
new file mode 100644
index 0000000..36496a2
--- /dev/null
+++ b/client/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/client/src/components/ui/calendar.tsx b/client/src/components/ui/calendar.tsx
new file mode 100644
index 0000000..2174f71
--- /dev/null
+++ b/client/src/components/ui/calendar.tsx
@@ -0,0 +1,68 @@
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ (
+
+ ),
+ IconRight: ({ className, ...props }) => (
+
+ ),
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/client/src/components/ui/card.tsx b/client/src/components/ui/card.tsx
new file mode 100644
index 0000000..f62edea
--- /dev/null
+++ b/client/src/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/client/src/components/ui/carousel.tsx b/client/src/components/ui/carousel.tsx
new file mode 100644
index 0000000..9c2b9bf
--- /dev/null
+++ b/client/src/components/ui/carousel.tsx
@@ -0,0 +1,260 @@
+import * as React from "react"
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react"
+import { ArrowLeft, ArrowRight } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: "horizontal" | "vertical"
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ return context
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(
+ (
+ {
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return
+ }
+
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return
+ }
+
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) {
+ return
+ }
+
+ onSelect(api)
+ api.on("reInit", onSelect)
+ api.on("select", onSelect)
+
+ return () => {
+ api?.off("select", onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
+Carousel.displayName = "Carousel"
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselContent.displayName = "CarouselContent"
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselItem.displayName = "CarouselItem"
+
+const CarouselPrevious = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselPrevious.displayName = "CarouselPrevious"
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselNext.displayName = "CarouselNext"
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}
diff --git a/client/src/components/ui/chart.tsx b/client/src/components/ui/chart.tsx
new file mode 100644
index 0000000..39fba6d
--- /dev/null
+++ b/client/src/components/ui/chart.tsx
@@ -0,0 +1,365 @@
+"use client"
+
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+
+import { cn } from "@/lib/utils"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+})
+ChartContainer.displayName = "Chart"
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+