- {/* Product Information */}
-
-
-
-
- Informations Client
-
-
-
(
-
- Nom de famille *
+ {/* Identity Information */}
+
+
- {/* Identity Document */}
-
-
- {/* Identity Photos */}
-
-
-
- Photos de la Pièce d'Identité
-
-
- {/* Photo Recto */}
-
-
Photo Recto
-
- {photos.recto ? (
-
-
-
deletePhoto('recto')}
- className="mt-2 text-destructive hover:text-destructive"
- >
-
- Supprimer
-
-
- ) : (
-
-
-
Aucune photo
-
handlePhotoCapture('recto')}
- className="bg-primary text-white hover:bg-primary/90"
- >
-
- Prendre photo
-
-
- )}
-
-
-
- {/* Photo Verso */}
-
-
Photo Verso
-
- {photos.verso ? (
-
-
-
deletePhoto('verso')}
- className="mt-2 text-destructive hover:text-destructive"
- >
-
- Supprimer
-
-
- ) : (
-
-
-
Aucune photo
-
handlePhotoCapture('verso')}
- className="bg-primary text-white hover:bg-primary/90"
- >
-
- Prendre photo
-
-
- )}
-
-
-
-
-
- {/* Form Actions - Always visible */}
-
- {
- form.reset({
- vendeur: '',
- dateVente: new Date().toISOString().split('T')[0],
- typeArticle: '',
- categorie: '',
- quantite: '',
- gencode: '',
- nom: '',
- prenom: '',
- dateNaissance: '',
- lieuNaissance: '',
- typeIdentite: '',
- numeroIdentite: '',
- autoriteDelivrance: '',
- dateDelivrance: '',
- });
- setPhotos({});
- }}>
-
- Annuler
-
-
-
- {isSubmitting ? 'Enregistrement...' : 'Enregistrer la Vente'}
-
-
-
-
-
-
-
-
{
- 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",
- });
- }
- }}
- />
- >
+ {/* Form Actions - Always visible */}
+
+ {
+ form.reset({
+ vendeur: '',
+ dateVente: new Date().toISOString().split('T')[0],
+ typeArticle: '',
+ categorie: '',
+ quantite: '',
+ gencode: '',
+ nom: '',
+ prenom: '',
+ dateNaissance: '',
+ lieuNaissance: '',
+ typeIdentite: '',
+ numeroIdentite: '',
+ autoriteDelivrance: '',
+ dateDelivrance: '',
+ });
+ }}>
+
+ Annuler
+
+
+
+ {isSubmitting || createSaleMutation.isPending ? 'Enregistrement...' : 'Enregistrer la Vente'}
+
+
+
+
+
+
);
-}
+}
\ No newline at end of file
diff --git a/client/src/components/SalesHistory.tsx b/client/src/components/SalesHistory.tsx
index 2644eb4..a341d55 100644
--- a/client/src/components/SalesHistory.tsx
+++ b/client/src/components/SalesHistory.tsx
@@ -1,284 +1,286 @@
import { useState } from 'react';
-import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
-import { Badge } from '@/components/ui/badge';
+import { Input } from '@/components/ui/input';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
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';
+import { apiRequest } from '@/lib/queryClient';
+import type { Sale } from '@shared/schema';
+import { History, Download, Trash2, Eye, Filter } from 'lucide-react';
interface SalesHistoryProps {
- sales: Sale[];
- isLoading: boolean;
- error: string | null;
- onDeleteSale: (id: number) => Promise;
+ canDelete?: boolean;
}
-export default function SalesHistory({ sales, isLoading, error, onDeleteSale }: SalesHistoryProps) {
+export default function SalesHistory({ canDelete = false }: SalesHistoryProps) {
const { toast } = useToast();
+ const queryClient = useQueryClient();
const [selectedSale, setSelectedSale] = useState(null);
- const [isDeleting, setIsDeleting] = useState(null);
+ const [startDate, setStartDate] = useState('');
+ const [endDate, setEndDate] = useState('');
- const handleExportCSV = () => {
- if (sales.length === 0) {
- toast({
- title: "Aucune vente à exporter",
- description: "Il n'y a aucune vente enregistrée",
- });
- return;
- }
+ // Query for sales
+ const { data: sales = [], isLoading } = useQuery({
+ queryKey: ['/api/sales', startDate, endDate],
+ queryFn: () => {
+ const params = new URLSearchParams();
+ if (startDate) params.append('startDate', startDate);
+ if (endDate) params.append('endDate', endDate);
+ const queryString = params.toString();
+ return apiRequest(`/api/sales${queryString ? `?${queryString}` : ''}`);
+ },
+ });
- 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);
+ // Delete mutation
+ const deleteSaleMutation = useMutation({
+ mutationFn: (saleId: number) =>
+ apiRequest(`/api/sales/${saleId}`, { method: 'DELETE' }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['/api/sales'] });
toast({
title: "Vente supprimée",
- description: `Vente de ${sale.nom} ${sale.prenom} supprimée`,
+ description: "La vente a été supprimée avec succès",
});
- } catch (error) {
+ },
+ onError: () => {
toast({
title: "Erreur",
description: "Impossible de supprimer la vente",
variant: "destructive",
});
- } finally {
- setIsDeleting(null);
+ },
+ });
+
+ const handleDeleteSale = async (sale: Sale) => {
+ if (confirm(`Êtes-vous sûr de vouloir supprimer cette vente ?`)) {
+ deleteSaleMutation.mutate(sale.id);
}
};
- const backup = getAutoBackup();
+ const exportToCSV = () => {
+ if (sales.length === 0) {
+ toast({
+ title: "Aucune donnée",
+ description: "Aucune vente à exporter",
+ variant: "destructive",
+ });
+ return;
+ }
- if (isLoading) {
- return (
-
-
-
-
-
Chargement des ventes...
-
-
-
- );
- }
+ const csvHeader = [
+ 'Date/Heure',
+ 'Vendeur',
+ 'Date de vente',
+ 'Type d\'article',
+ 'Catégorie',
+ 'Quantité',
+ 'Gencode',
+ 'Nom client',
+ 'Prénom client',
+ 'Date de naissance',
+ 'Lieu de naissance',
+ 'Type identité',
+ 'Numéro identité',
+ 'Autorité délivrance',
+ 'Date délivrance'
+ ].join(',');
- if (error) {
- return (
-
-
-
-
-
- );
- }
+ const csvData = sales.map(sale => [
+ new Date(sale.timestamp!).toLocaleString('fr-FR'),
+ sale.vendeur,
+ sale.dateVente,
+ sale.typeArticle,
+ sale.categorie,
+ sale.quantite,
+ sale.gencode,
+ sale.nom,
+ sale.prenom,
+ sale.dateNaissance,
+ sale.lieuNaissance || '',
+ sale.typeIdentite,
+ sale.numeroIdentite,
+ sale.autoriteDelivrance,
+ sale.dateDelivrance
+ ].map(field => `"${field}"`).join(',')).join('\n');
+
+ const csvContent = csvHeader + '\n' + csvData;
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ link.download = `ventes_fireworks_${new Date().toISOString().split('T')[0]}.csv`;
+ link.click();
+
+ toast({
+ title: "Export réussi",
+ description: `${sales.length} ventes exportées`,
+ });
+ };
+
+ const formatDate = (dateString: string) => {
+ return new Date(dateString).toLocaleDateString('fr-FR');
+ };
+
+ const formatDateTime = (dateString: string) => {
+ return new Date(dateString).toLocaleString('fr-FR');
+ };
return (
- <>
-
-
-
+
+
+
+
+ Historique des Ventes
+
+
+
+ {/* Filters */}
+
+
-
-
- Historique des Ventes
-
-
- Gestion et export des ventes enregistrées
-
+
Date de début
+
setStartDate(e.target.value)}
+ />
-
-
-
- Exporter CSV
-
-
-
- Backup Auto
-
-
-
- Debug
-
+
+ Date de fin
+ setEndDate(e.target.value)}
+ />
-
-
- {/* 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
- )}
-
+
+
+ Export CSV
+
-
- {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}
- )}
-
-
-
-
+ {/* Sales Table */}
+ {isLoading ? (
+
+ ) : sales.length === 0 ? (
+
+ Aucune vente trouvée
+
+ ) : (
+
+
+
+
+ Date/Heure
+ Vendeur
+ Client
+ Article
+ Quantité
+ Gencode
+ Actions
+
+
+
+ {sales.map((sale) => (
+
+ {formatDateTime(sale.timestamp!)}
+ {sale.vendeur}
+
+
+
{sale.nom} {sale.prenom}
+
+ {formatDate(sale.dateNaissance)}
+
+
+
+
+
+
{sale.typeArticle}
+
{sale.categorie}
-
-
-
- {sale.quantite}
-
-
-
- setSelectedSale(sale)}
- title="Voir les détails"
- >
-
-
+
+
+
+ {sale.quantite}
+ {sale.gencode}
+
+
+
+
+ setSelectedSale(sale)}
+ >
+
+
+
+
+
+ Détails de la vente
+
+ {selectedSale && (
+
+
+
Informations générales
+
Date/Heure : {formatDateTime(selectedSale.timestamp!)}
+
Vendeur : {selectedSale.vendeur}
+
Date de vente : {formatDate(selectedSale.dateVente)}
+
+
+
Produit
+
Type : {selectedSale.typeArticle}
+
Catégorie : {selectedSale.categorie}
+
Quantité : {selectedSale.quantite}
+
Gencode : {selectedSale.gencode}
+
+
+
Client
+
Nom : {selectedSale.nom}
+
Prénom : {selectedSale.prenom}
+
Date de naissance : {formatDate(selectedSale.dateNaissance)}
+ {selectedSale.lieuNaissance && (
+
Lieu de naissance : {selectedSale.lieuNaissance}
+ )}
+
+
+
Pièce d'identité
+
Type : {selectedSale.typeIdentite}
+
Numéro : {selectedSale.numeroIdentite}
+
Autorité : {selectedSale.autoriteDelivrance}
+
Date délivrance : {formatDate(selectedSale.dateDelivrance)}
+
+
+ )}
+
+
+ {canDelete && (
handleDeleteSale(sale)}
- disabled={isDeleting === sale.id}
- className="text-destructive hover:text-destructive"
- title="Supprimer la vente"
+ disabled={deleteSaleMutation.isPending}
>
-
+
-
-
-
- ))}
-
-
-
- )}
-
-
-
- setSelectedSale(null)}
- />
- >
+ )}
+
+
+
+ ))}
+
+
+
+ )}
+
+
);
-}
+}
\ No newline at end of file
diff --git a/client/src/hooks/useAuth.ts b/client/src/hooks/useAuth.ts
new file mode 100644
index 0000000..c2d6027
--- /dev/null
+++ b/client/src/hooks/useAuth.ts
@@ -0,0 +1,67 @@
+import { useState, useEffect } from "react";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { apiRequest } from "@/lib/queryClient";
+import type { User, LoginData } from "@shared/schema";
+
+export function useAuth() {
+ const queryClient = useQueryClient();
+ const [user, setUser] = useState
(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const checkAuth = async () => {
+ try {
+ const response = await fetch('/api/auth/me', {
+ credentials: 'include',
+ });
+
+ if (response.ok) {
+ const userData = await response.json();
+ setUser(userData);
+ } else {
+ setUser(null);
+ }
+ } catch (error) {
+ setUser(null);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ checkAuth();
+ }, []);
+
+ const loginMutation = useMutation({
+ mutationFn: async (credentials: LoginData) => {
+ const response = await apiRequest('/api/auth/login', {
+ method: 'POST',
+ body: JSON.stringify(credentials)
+ });
+ return response.json();
+ },
+ onSuccess: (data) => {
+ setUser(data.user);
+ queryClient.clear();
+ },
+ });
+
+ const logoutMutation = useMutation({
+ mutationFn: async () => {
+ await apiRequest('/api/auth/logout', { method: 'POST' });
+ },
+ onSuccess: () => {
+ setUser(null);
+ queryClient.clear();
+ },
+ });
+
+ return {
+ user,
+ isLoading,
+ isAuthenticated: !!user,
+ login: loginMutation.mutateAsync,
+ logout: logoutMutation.mutateAsync,
+ isLoggingIn: loginMutation.isPending,
+ isLoggingOut: logoutMutation.isPending,
+ };
+}
\ No newline at end of file
diff --git a/client/src/lib/queryClient.ts b/client/src/lib/queryClient.ts
index f315a14..faaed00 100644
--- a/client/src/lib/queryClient.ts
+++ b/client/src/lib/queryClient.ts
@@ -8,14 +8,22 @@ async function throwIfResNotOk(res: Response) {
}
export async function apiRequest(
- method: string,
url: string,
- data?: unknown | undefined,
+ options: {
+ method?: string;
+ headers?: Record;
+ body?: string;
+ } = {}
): Promise {
+ const { method = 'GET', headers = {}, body } = options;
+
const res = await fetch(url, {
method,
- headers: data ? { "Content-Type": "application/json" } : {},
- body: data ? JSON.stringify(data) : undefined,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...headers,
+ },
+ body,
credentials: "include",
});
@@ -44,7 +52,7 @@ export const getQueryFn: (options: {
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
- queryFn: getQueryFn({ on401: "throw" }),
+ queryFn: getQueryFn({ on401: "returnNull" }),
refetchInterval: false,
refetchOnWindowFocus: false,
staleTime: Infinity,
diff --git a/client/src/pages/Dashboard.tsx b/client/src/pages/Dashboard.tsx
new file mode 100644
index 0000000..4c644df
--- /dev/null
+++ b/client/src/pages/Dashboard.tsx
@@ -0,0 +1,105 @@
+import { useState } from "react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { useAuth } from "@/hooks/useAuth";
+import NewSaleForm from "@/components/NewSaleForm";
+import SalesHistory from "@/components/SalesHistory";
+import Administration from "@/components/Administration";
+import { Package, History, Settings, LogOut, User } from "lucide-react";
+import { useToast } from "@/hooks/use-toast";
+
+export default function Dashboard() {
+ const { user, logout } = useAuth();
+ const { toast } = useToast();
+ const [activeTab, setActiveTab] = useState("new-sale");
+
+ const handleLogout = async () => {
+ try {
+ await logout();
+ toast({
+ title: "Déconnexion réussie",
+ description: "À bientôt !",
+ });
+ } catch (error) {
+ toast({
+ title: "Erreur",
+ description: "Erreur lors de la déconnexion",
+ variant: "destructive",
+ });
+ }
+ };
+
+ const canAccessAdmin = user?.role === "admin";
+ const canDeleteSales = user?.role === "admin" || user?.role === "manager";
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+
Registre des Ventes
+
Gestion des feux d'artifice
+
+
+
+
+
+
+ {user?.firstName} {user?.lastName}
+
+
{user?.role}
+
+
+
+ Déconnexion
+
+
+
+
+
+
+ {/* Main Content */}
+
+
+
+
+
+ Nouvelle Vente
+
+
+
+ Historique
+
+ {canAccessAdmin && (
+
+
+ Administration
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {canAccessAdmin && (
+
+
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/pages/Login.tsx b/client/src/pages/Login.tsx
new file mode 100644
index 0000000..6edecb6
--- /dev/null
+++ b/client/src/pages/Login.tsx
@@ -0,0 +1,100 @@
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
+import { useToast } from "@/hooks/use-toast";
+import { useAuth } from "@/hooks/useAuth";
+import { loginSchema, type LoginData } from "@shared/schema";
+import { LogIn, Sparkles } from "lucide-react";
+
+export default function Login() {
+ const { login, isLoggingIn } = useAuth();
+ const { toast } = useToast();
+
+ const form = useForm({
+ resolver: zodResolver(loginSchema),
+ defaultValues: {
+ username: "",
+ password: "",
+ },
+ });
+
+ const onSubmit = async (data: LoginData) => {
+ try {
+ await login(data);
+ toast({
+ title: "Connexion réussie",
+ description: "Bienvenue dans l'application !",
+ });
+ } catch (error) {
+ toast({
+ title: "Erreur de connexion",
+ description: "Nom d'utilisateur ou mot de passe incorrect",
+ variant: "destructive",
+ });
+ }
+ };
+
+ return (
+
+
+
+
+ Registre des Ventes
+ Connectez-vous pour accéder à l'application
+
+
+
+
+
+
+
Compte par défaut :
+
+
Utilisateur : admin
+
Mot de passe : admin123
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 65dfa0c..2685a9d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -40,6 +40,9 @@
"@radix-ui/react-toggle-group": "^1.1.3",
"@radix-ui/react-tooltip": "^1.2.0",
"@tanstack/react-query": "^5.60.5",
+ "@types/bcryptjs": "^2.4.6",
+ "@types/memoizee": "^0.4.12",
+ "bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -53,9 +56,11 @@
"framer-motion": "^11.13.1",
"input-otp": "^1.4.2",
"lucide-react": "^0.453.0",
+ "memoizee": "^0.4.17",
"memorystore": "^1.6.7",
"nanoid": "^5.1.5",
"next-themes": "^0.4.6",
+ "openid-client": "^6.6.2",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
"react": "^18.3.1",
@@ -3319,6 +3324,12 @@
"@babel/types": "^7.20.7"
}
},
+ "node_modules/@types/bcryptjs": {
+ "version": "2.4.6",
+ "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
+ "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
+ "license": "MIT"
+ },
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@@ -3465,6 +3476,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/memoizee": {
+ "version": "0.4.12",
+ "resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.12.tgz",
+ "integrity": "sha512-EdtpwNYNhe3kZ+4TlXj/++pvBoU0KdrAICMzgI7vjWgu9sIvvUhu9XR8Ks4L6Wh3sxpZ22wkZR7yCLAqUjnZuQ==",
+ "license": "MIT"
+ },
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@@ -3744,6 +3761,15 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
+ "node_modules/bcryptjs": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz",
+ "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==",
+ "license": "BSD-3-Clause",
+ "bin": {
+ "bcrypt": "bin/bcrypt"
+ }
+ },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -4112,6 +4138,19 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
+ "node_modules/d": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
+ "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
+ "license": "ISC",
+ "dependencies": {
+ "es5-ext": "^0.10.64",
+ "type": "^2.7.2"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
@@ -5010,6 +5049,58 @@
"node": ">= 0.4"
}
},
+ "node_modules/es5-ext": {
+ "version": "0.10.64",
+ "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
+ "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
+ "hasInstallScript": true,
+ "license": "ISC",
+ "dependencies": {
+ "es6-iterator": "^2.0.3",
+ "es6-symbol": "^3.1.3",
+ "esniff": "^2.0.1",
+ "next-tick": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/es6-iterator": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+ "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
+ "license": "MIT",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "^0.10.35",
+ "es6-symbol": "^3.1.1"
+ }
+ },
+ "node_modules/es6-symbol": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
+ "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
+ "license": "ISC",
+ "dependencies": {
+ "d": "^1.0.2",
+ "ext": "^1.7.0"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/es6-weak-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz",
+ "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==",
+ "license": "ISC",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "^0.10.46",
+ "es6-iterator": "^2.0.3",
+ "es6-symbol": "^3.1.1"
+ }
+ },
"node_modules/esbuild": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
@@ -5080,6 +5171,21 @@
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
+ "node_modules/esniff": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
+ "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
+ "license": "ISC",
+ "dependencies": {
+ "d": "^1.0.1",
+ "es5-ext": "^0.10.62",
+ "event-emitter": "^0.3.5",
+ "type": "^2.7.2"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -5089,6 +5195,16 @@
"node": ">= 0.6"
}
},
+ "node_modules/event-emitter": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+ "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
+ "license": "MIT",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "~0.10.14"
+ }
+ },
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@@ -5205,6 +5321,15 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
+ "node_modules/ext": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
+ "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
+ "license": "ISC",
+ "dependencies": {
+ "type": "^2.7.2"
+ }
+ },
"node_modules/fast-equals": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz",
@@ -5678,6 +5803,12 @@
"node": ">=0.12.0"
}
},
+ "node_modules/is-promise": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
+ "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
+ "license": "MIT"
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -5708,6 +5839,15 @@
"jiti": "bin/jiti.js"
}
},
+ "node_modules/jose": {
+ "version": "6.0.11",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz",
+ "integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -6034,6 +6174,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lru-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz",
+ "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es5-ext": "~0.10.2"
+ }
+ },
"node_modules/lucide-react": {
"version": "0.453.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.453.0.tgz",
@@ -6060,6 +6209,25 @@
"node": ">= 0.6"
}
},
+ "node_modules/memoizee": {
+ "version": "0.4.17",
+ "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz",
+ "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==",
+ "license": "ISC",
+ "dependencies": {
+ "d": "^1.0.2",
+ "es5-ext": "^0.10.64",
+ "es6-weak-map": "^2.0.3",
+ "event-emitter": "^0.3.5",
+ "is-promise": "^2.2.2",
+ "lru-queue": "^0.1.0",
+ "next-tick": "^1.1.0",
+ "timers-ext": "^0.1.7"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
"node_modules/memorystore": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/memorystore/-/memorystore-1.6.7.tgz",
@@ -6261,6 +6429,12 @@
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
+ "node_modules/next-tick": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
+ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
+ "license": "ISC"
+ },
"node_modules/node-gyp-build": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz",
@@ -6299,6 +6473,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/oauth4webapi": {
+ "version": "3.5.5",
+ "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.5.5.tgz",
+ "integrity": "sha512-1K88D2GiAydGblHo39NBro5TebGXa+7tYoyIbxvqv3+haDDry7CBE1eSYuNbOSsYCCU6y0gdynVZAkm4YPw4hg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -6356,6 +6539,19 @@
"node": ">= 0.8"
}
},
+ "node_modules/openid-client": {
+ "version": "6.6.2",
+ "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.6.2.tgz",
+ "integrity": "sha512-Xya5TNMnnZuTM6DbHdB4q0S3ig2NTAELnii/ASie1xDEr8iiB8zZbO871OWBdrw++sd3hW6bqWjgcmSy1RTWHA==",
+ "license": "MIT",
+ "dependencies": {
+ "jose": "^6.0.11",
+ "oauth4webapi": "^3.5.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -7744,6 +7940,19 @@
"node": ">=0.8"
}
},
+ "node_modules/timers-ext": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz",
+ "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==",
+ "license": "ISC",
+ "dependencies": {
+ "es5-ext": "^0.10.64",
+ "next-tick": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@@ -8259,6 +8468,12 @@
"url": "https://github.com/sponsors/Wombosvideo"
}
},
+ "node_modules/type": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
+ "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
+ "license": "ISC"
+ },
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
diff --git a/package.json b/package.json
index 6e825ae..6ddd279 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,9 @@
"@radix-ui/react-toggle-group": "^1.1.3",
"@radix-ui/react-tooltip": "^1.2.0",
"@tanstack/react-query": "^5.60.5",
+ "@types/bcryptjs": "^2.4.6",
+ "@types/memoizee": "^0.4.12",
+ "bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -55,9 +58,11 @@
"framer-motion": "^11.13.1",
"input-otp": "^1.4.2",
"lucide-react": "^0.453.0",
+ "memoizee": "^0.4.17",
"memorystore": "^1.6.7",
"nanoid": "^5.1.5",
"next-themes": "^0.4.6",
+ "openid-client": "^6.6.2",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
"react": "^18.3.1",
diff --git a/replit.md b/replit.md
index c30bd1a..5c142b3 100644
--- a/replit.md
+++ b/replit.md
@@ -2,7 +2,7 @@
## Overview
-This is a React-based web application for managing and tracking fireworks sales in compliance with French regulations. The application provides a complete sales registration system with photo capture capabilities, data export features, and regulatory compliance tracking. It's built as a single-page application using React with TypeScript, TailwindCSS for styling, and IndexedDB for local data storage.
+Application web complète de gestion et suivi des ventes de feux d'artifice en conformité avec la réglementation française. L'application fournit un système d'enregistrement des ventes avec authentification multi-utilisateur, contrôle d'accès basé sur les rôles, gestion multi-magasins, et stockage sécurisé en base de données PostgreSQL.
## User Preferences
@@ -16,50 +16,68 @@ Preferred communication style: Simple, everyday language.
- **Styling**: TailwindCSS with shadcn/ui component library
- **State Management**: React Hook Form for form handling, TanStack Query for server state
- **UI Components**: Radix UI primitives with custom shadcn/ui components
+- **Authentication**: Session-based authentication with role-based access control
### Backend Architecture
- **Server**: Express.js with TypeScript
- **Database ORM**: Drizzle ORM configured for PostgreSQL
-- **Database**: Neon Database (PostgreSQL) for production
-- **Development**: In-memory storage for development/testing
+- **Database**: PostgreSQL via environment variable DATABASE_URL
+- **Authentication**: bcrypt password hashing with Express sessions
+- **Session Storage**: PostgreSQL session store for scalability
### Data Storage
-- **Primary Storage**: PostgreSQL database via Neon Database service
-- **Local Storage**: IndexedDB for offline capabilities and local data persistence
+- **Primary Storage**: PostgreSQL database with complete schema
+- **Session Storage**: PostgreSQL sessions table for user authentication
- **Schema Management**: Drizzle migrations for database versioning
+- **Multi-tenant**: Each store has isolated sales data
## Key Components
### Core Application Features
-1. **Sales Form Management**: Comprehensive form for recording fireworks sales with validation
-2. **Customer Identity Verification**: Photo capture for ID documents (front/back)
-3. **Product Classification**: Categorization system for different fireworks types (F2/F3)
-4. **Sales History**: Complete transaction history with search and filtering
-5. **Data Export**: CSV export functionality for regulatory reporting
-6. **Backup System**: Automatic local backups using IndexedDB
+1. **Multi-User Authentication**: Secure login system with role-based access control
+2. **User Management**: Administrative interface for creating and managing users
+3. **Store Management**: Multi-tenant architecture with store isolation
+4. **Sales Form Management**: Comprehensive form for recording fireworks sales with validation
+5. **Sales History**: Complete transaction history with date filtering and search
+6. **Data Export**: CSV export functionality for regulatory reporting
+7. **Role-Based Permissions**: Different access levels for Admin, Manager, and Employee roles
+
+### Authentication & Authorization
+- **Administrators**: Full access to all features including user/store management
+- **Managers**: Sales management and history access, no administration features
+- **Employees**: Sales creation and history viewing only
+- **Default Account**: admin/admin123 (auto-created on first database setup)
### Technical Components
-- **Camera Integration**: Native device camera access for ID photo capture
+- **Session Management**: Secure session handling with PostgreSQL storage
- **Form Validation**: Multi-step validation including EAN-13 barcode validation
- **Responsive Design**: Mobile-first approach with tablet and desktop support
- **Toast Notifications**: User feedback system for actions and errors
-- **Modal System**: Dialog-based UI for camera capture and sale details
+- **Modal System**: Dialog-based UI for detailed views and confirmations
## Data Flow
+### Authentication Flow
+1. User accesses application and is redirected to login page
+2. Credentials verified against PostgreSQL users table with bcrypt
+3. Successful login creates secure session stored in PostgreSQL
+4. User role determines available features and access levels
+5. Session automatically managed with configurable expiration
+
### Sales Recording Process
-1. User fills out comprehensive sales form (vendor, product, customer details)
-2. Photos captured using device camera for customer ID verification
-3. Form validation ensures regulatory compliance
-4. Data saved to IndexedDB for immediate access
-5. Automatic backup created for data persistence
-6. Optional sync to PostgreSQL database for permanent storage
+1. Authenticated user fills out comprehensive sales form
+2. Form validation ensures regulatory compliance (EAN-13, required fields)
+3. Data submitted to backend with user and store association
+4. Sales record created in PostgreSQL with complete audit trail
+5. Real-time updates to sales history via TanStack Query
+6. Export capability for regulatory compliance reporting
### Data Management
-- **Local-First**: All data operations work offline using IndexedDB
-- **Progressive Enhancement**: Database integration available for multi-device sync
-- **Export Capability**: Data can be exported as CSV for regulatory compliance
-- **Backup/Restore**: Built-in backup system for data protection
+- **Database-First**: All data operations use PostgreSQL for consistency
+- **Multi-Tenant**: Store-based data isolation for security
+- **Role-Based Access**: Different permissions based on user role
+- **Session Security**: Secure session management with PostgreSQL storage
+- **Audit Trail**: Complete transaction history with timestamps
## External Dependencies
@@ -70,6 +88,12 @@ Preferred communication style: Simple, everyday language.
- **react-hook-form**: Form handling and validation
- **@hookform/resolvers**: Form validation resolvers
+### Authentication & Security
+- **bcryptjs**: Password hashing for secure authentication
+- **express-session**: Session management middleware
+- **connect-pg-simple**: PostgreSQL session store
+- **drizzle-zod**: Schema validation with Zod integration
+
### UI and Styling
- **@radix-ui/***: Headless UI primitives for accessibility
- **tailwindcss**: Utility-first CSS framework
@@ -85,23 +109,46 @@ Preferred communication style: Simple, everyday language.
### Development Environment
- **Local Development**: Vite dev server with hot module replacement
-- **Database**: In-memory storage for rapid development
+- **Database**: PostgreSQL with auto-initialization of default admin account
+- **Authentication**: Session-based with development-friendly settings
- **Build Process**: TypeScript compilation with Vite bundling
### Production Environment
- **Frontend**: Static assets served via Vite build output
- **Backend**: Express.js server bundled with esbuild
-- **Database**: PostgreSQL via Neon Database service
-- **Environment Variables**: DATABASE_URL for production database connection
+- **Database**: PostgreSQL via DATABASE_URL environment variable
+- **Sessions**: Secure session storage in PostgreSQL sessions table
+- **Environment Variables**: DATABASE_URL, SESSION_SECRET (optional)
+
+### Security Configuration
+- **Password Hashing**: bcrypt with appropriate salt rounds
+- **Session Security**: HTTP-only cookies with configurable expiration
+- **Role-Based Access**: Middleware-enforced permission system
+- **CSRF Protection**: Session-based protection against cross-site attacks
### Build Configuration
- **Client Build**: `vite build` generates optimized static assets
- **Server Build**: `esbuild` bundles Express server for deployment
- **Type Checking**: TypeScript compilation ensures type safety
-- **Database Migrations**: `drizzle-kit push` for schema updates
+- **Database Setup**: `npm run db:push` for schema deployment
+- **Default Account**: Automatic creation of admin/admin123 on first run
### Deployment Features
- **Environment Detection**: Automatic development/production mode switching
- **Asset Optimization**: Bundled and minified production builds
- **Hot Reloading**: Development-only feature for rapid iteration
-- **Error Handling**: Comprehensive error boundaries and logging
\ No newline at end of file
+- **Error Handling**: Comprehensive error boundaries and logging
+- **Multi-Tenant Ready**: Store-based data isolation for scalability
+
+## Recent Changes (January 2025)
+
+### Migration to Full-Stack Architecture
+- ✅ Migrated from IndexedDB to PostgreSQL database with Drizzle ORM
+- ✅ Implemented complete authentication system with bcrypt password hashing
+- ✅ Added role-based access control (Administrator, Manager, Employee)
+- ✅ Created multi-tenant architecture with store-based data isolation
+- ✅ Built administration panel for user and store management
+- ✅ Generated default admin account (admin/admin123) for initial setup
+- ✅ Updated all components to work with database instead of local storage
+- ✅ Added session-based authentication with PostgreSQL session store
+- ✅ Implemented comprehensive API with proper validation and error handling
\ No newline at end of file
diff --git a/server/db.ts b/server/db.ts
new file mode 100644
index 0000000..7ca3d56
--- /dev/null
+++ b/server/db.ts
@@ -0,0 +1,15 @@
+import { Pool, neonConfig } from '@neondatabase/serverless';
+import { drizzle } from 'drizzle-orm/neon-serverless';
+import ws from "ws";
+import * as schema from "@shared/schema";
+
+neonConfig.webSocketConstructor = ws;
+
+if (!process.env.DATABASE_URL) {
+ throw new Error(
+ "DATABASE_URL must be set. Did you forget to provision a database?",
+ );
+}
+
+export const pool = new Pool({ connectionString: process.env.DATABASE_URL });
+export const db = drizzle({ client: pool, schema });
\ No newline at end of file
diff --git a/server/routes.ts b/server/routes.ts
index 9da3a81..fde706e 100644
--- a/server/routes.ts
+++ b/server/routes.ts
@@ -1,15 +1,265 @@
-import type { Express } from "express";
+import type { Express, Request, Response, NextFunction } from "express";
import { createServer, type Server } from "http";
import { storage } from "./storage";
+import session from "express-session";
+import connectPg from "connect-pg-simple";
+import { loginSchema, insertUserSchema, insertStoreSchema, insertSaleSchema } from "@shared/schema";
+import { z } from "zod";
+
+declare module 'express-session' {
+ interface SessionData {
+ userId?: number;
+ }
+}
+
+// Authentication middleware
+const requireAuth = (req: Request, res: Response, next: NextFunction) => {
+ if (!req.session?.userId) {
+ return res.status(401).json({ error: "Authentication required" });
+ }
+ next();
+};
+
+// Role-based authorization middleware
+const requireRole = (roles: string[]) => {
+ return async (req: Request, res: Response, next: NextFunction) => {
+ if (!req.session?.userId) {
+ return res.status(401).json({ error: "Authentication required" });
+ }
+
+ const user = await storage.getUser(req.session.userId);
+ if (!user || !roles.includes(user.role)) {
+ return res.status(403).json({ error: "Insufficient permissions" });
+ }
+
+ req.user = user;
+ next();
+ };
+};
+
+declare global {
+ namespace Express {
+ interface Request {
+ user?: any;
+ }
+ }
+}
export async function registerRoutes(app: Express): Promise {
- // put application routes here
- // prefix all routes with /api
+ // Session configuration with PostgreSQL store
+ const pgStore = connectPg(session);
+ app.use(session({
+ store: new pgStore({
+ conString: process.env.DATABASE_URL,
+ createTableIfMissing: false,
+ tableName: 'sessions',
+ }),
+ secret: process.env.SESSION_SECRET || 'fireworks-secret-key-2024',
+ resave: false,
+ saveUninitialized: false,
+ cookie: {
+ secure: false, // Set to true in production with HTTPS
+ maxAge: 24 * 60 * 60 * 1000 // 24 hours
+ }
+ }));
- // use storage to perform CRUD operations on the storage interface
- // e.g. storage.insertUser(user) or storage.getUserByUsername(username)
+ // Initialize default admin if needed
+ const initResult = await storage.initializeDefaults();
+ if (initResult.defaultAdminCredentials) {
+ console.log('\n=== DEFAULT ADMIN CREATED ===');
+ console.log('Username:', initResult.defaultAdminCredentials.username);
+ console.log('Password:', initResult.defaultAdminCredentials.password);
+ console.log('Please change this password after first login!');
+ console.log('==============================\n');
+ }
+
+ // Auth routes
+ app.post('/api/auth/login', async (req, res) => {
+ try {
+ const { username, password } = loginSchema.parse(req.body);
+
+ const user = await storage.authenticateUser(username, password);
+ if (!user) {
+ return res.status(401).json({ error: "Invalid credentials" });
+ }
+
+ req.session.userId = user.id;
+ res.json({ user: { ...user, password: undefined } });
+ } catch (error) {
+ console.error('Login error:', error);
+ if (error instanceof z.ZodError) {
+ return res.status(400).json({ error: "Invalid input", details: error.errors });
+ }
+ res.status(500).json({ error: "Login failed" });
+ }
+ });
+
+ app.post('/api/auth/logout', (req, res) => {
+ req.session.destroy((err) => {
+ if (err) {
+ console.error('Logout error:', err);
+ return res.status(500).json({ error: "Logout failed" });
+ }
+ res.json({ message: "Logged out successfully" });
+ });
+ });
+
+ app.get('/api/auth/me', requireAuth, async (req, res) => {
+ try {
+ const user = await storage.getUser(req.session.userId!);
+ if (!user) {
+ return res.status(404).json({ error: "User not found" });
+ }
+ res.json({ ...user, password: undefined });
+ } catch (error) {
+ console.error('Get user error:', error);
+ res.status(500).json({ error: "Failed to get user info" });
+ }
+ });
+
+ // Sales routes
+ app.get('/api/sales', requireAuth, async (req, res) => {
+ try {
+ const user = await storage.getUser(req.session.userId!);
+ if (!user) {
+ return res.status(404).json({ error: "User not found" });
+ }
+
+ const { startDate, endDate } = req.query;
+ const sales = await storage.getSalesByStore(
+ user.storeId!,
+ startDate as string,
+ endDate as string
+ );
+ res.json(sales);
+ } catch (error) {
+ console.error('Get sales error:', error);
+ res.status(500).json({ error: "Failed to get sales" });
+ }
+ });
+
+ app.post('/api/sales', requireAuth, async (req, res) => {
+ try {
+ const user = await storage.getUser(req.session.userId!);
+ if (!user) {
+ return res.status(404).json({ error: "User not found" });
+ }
+
+ const saleData = insertSaleSchema.parse({
+ ...req.body,
+ storeId: user.storeId,
+ userId: user.id
+ });
+
+ const sale = await storage.createSale(saleData);
+ res.status(201).json(sale);
+ } catch (error) {
+ console.error('Create sale error:', error);
+ if (error instanceof z.ZodError) {
+ return res.status(400).json({ error: "Invalid input", details: error.errors });
+ }
+ res.status(500).json({ error: "Failed to create sale" });
+ }
+ });
+
+ app.delete('/api/sales/:id', requireRole(['admin', 'manager']), async (req, res) => {
+ try {
+ const saleId = parseInt(req.params.id);
+ await storage.deleteSale(saleId);
+ res.json({ message: "Sale deleted successfully" });
+ } catch (error) {
+ console.error('Delete sale error:', error);
+ res.status(500).json({ error: "Failed to delete sale" });
+ }
+ });
+
+ // Admin routes - Users
+ app.get('/api/admin/users', requireRole(['admin']), async (req, res) => {
+ try {
+ const users = await storage.getAllUsers();
+ const safeUsers = users.map(user => ({ ...user, password: undefined }));
+ res.json(safeUsers);
+ } catch (error) {
+ console.error('Get users error:', error);
+ res.status(500).json({ error: "Failed to get users" });
+ }
+ });
+
+ app.post('/api/admin/users', requireRole(['admin']), async (req, res) => {
+ try {
+ const userData = insertUserSchema.parse(req.body);
+ const user = await storage.createUser(userData);
+ res.status(201).json({ ...user, password: undefined });
+ } catch (error) {
+ console.error('Create user error:', error);
+ if (error instanceof z.ZodError) {
+ return res.status(400).json({ error: "Invalid input", details: error.errors });
+ }
+ res.status(500).json({ error: "Failed to create user" });
+ }
+ });
+
+ app.put('/api/admin/users/:id', requireRole(['admin']), async (req, res) => {
+ try {
+ const userId = parseInt(req.params.id);
+ const userData = insertUserSchema.partial().parse(req.body);
+ const user = await storage.updateUser(userId, userData);
+ if (!user) {
+ return res.status(404).json({ error: "User not found" });
+ }
+ res.json({ ...user, password: undefined });
+ } catch (error) {
+ console.error('Update user error:', error);
+ if (error instanceof z.ZodError) {
+ return res.status(400).json({ error: "Invalid input", details: error.errors });
+ }
+ res.status(500).json({ error: "Failed to update user" });
+ }
+ });
+
+ // Admin routes - Stores
+ app.get('/api/admin/stores', requireRole(['admin']), async (req, res) => {
+ try {
+ const stores = await storage.getAllStores();
+ res.json(stores);
+ } catch (error) {
+ console.error('Get stores error:', error);
+ res.status(500).json({ error: "Failed to get stores" });
+ }
+ });
+
+ app.post('/api/admin/stores', requireRole(['admin']), async (req, res) => {
+ try {
+ const storeData = insertStoreSchema.parse(req.body);
+ const store = await storage.createStore(storeData);
+ res.status(201).json(store);
+ } catch (error) {
+ console.error('Create store error:', error);
+ if (error instanceof z.ZodError) {
+ return res.status(400).json({ error: "Invalid input", details: error.errors });
+ }
+ res.status(500).json({ error: "Failed to create store" });
+ }
+ });
+
+ app.put('/api/admin/stores/:id', requireRole(['admin']), async (req, res) => {
+ try {
+ const storeId = parseInt(req.params.id);
+ const storeData = insertStoreSchema.partial().parse(req.body);
+ const store = await storage.updateStore(storeId, storeData);
+ if (!store) {
+ return res.status(404).json({ error: "Store not found" });
+ }
+ res.json(store);
+ } catch (error) {
+ console.error('Update store error:', error);
+ if (error instanceof z.ZodError) {
+ return res.status(400).json({ error: "Invalid input", details: error.errors });
+ }
+ res.status(500).json({ error: "Failed to update store" });
+ }
+ });
const httpServer = createServer(app);
-
return httpServer;
}
diff --git a/server/storage.ts b/server/storage.ts
index e01abec..d40c9fb 100644
--- a/server/storage.ts
+++ b/server/storage.ts
@@ -1,39 +1,198 @@
-import { users, type User, type InsertUser } from "@shared/schema";
-
-// modify the interface with any CRUD methods
-// you might need
+import {
+ users,
+ stores,
+ sales,
+ type User,
+ type InsertUser,
+ type Store,
+ type InsertStore,
+ type Sale,
+ type InsertSale,
+} from "@shared/schema";
+import { db } from "./db";
+import { eq, and, gte, lte, desc } from "drizzle-orm";
+import bcrypt from "bcryptjs";
export interface IStorage {
+ // User operations
getUser(id: number): Promise;
getUserByUsername(username: string): Promise;
createUser(user: InsertUser): Promise;
+ updateUser(id: number, user: Partial): Promise;
+ getAllUsers(): Promise;
+
+ // Store operations
+ getStore(id: number): Promise;
+ createStore(store: InsertStore): Promise;
+ getAllStores(): Promise;
+ updateStore(id: number, store: Partial): Promise;
+
+ // Sale operations
+ getSale(id: number): Promise;
+ createSale(sale: InsertSale): Promise;
+ getSalesByStore(storeId: number, startDate?: string, endDate?: string): Promise;
+ deleteSale(id: number): Promise;
+
+ // Auth operations
+ authenticateUser(username: string, password: string): Promise;
+ hashPassword(password: string): Promise;
+ initializeDefaults(): Promise<{ defaultAdminCredentials?: { username: string; password: string } }>;
}
-export class MemStorage implements IStorage {
- private users: Map;
- currentId: number;
-
- constructor() {
- this.users = new Map();
- this.currentId = 1;
- }
-
+export class DatabaseStorage implements IStorage {
+ // User operations
async getUser(id: number): Promise {
- return this.users.get(id);
+ const [user] = await db.select().from(users).where(eq(users.id, id));
+ return user;
}
async getUserByUsername(username: string): Promise {
- return Array.from(this.users.values()).find(
- (user) => user.username === username,
- );
+ const [user] = await db.select().from(users).where(eq(users.username, username));
+ return user;
}
- async createUser(insertUser: InsertUser): Promise {
- const id = this.currentId++;
- const user: User = { ...insertUser, id };
- this.users.set(id, user);
+ async createUser(userData: InsertUser): Promise {
+ // Hash password if provided
+ if (userData.password) {
+ userData.password = await this.hashPassword(userData.password);
+ }
+
+ const [user] = await db
+ .insert(users)
+ .values(userData)
+ .returning();
return user;
}
+
+ async updateUser(id: number, userData: Partial): Promise {
+ // Hash password if provided
+ if (userData.password) {
+ userData.password = await this.hashPassword(userData.password);
+ }
+
+ const [user] = await db
+ .update(users)
+ .set(userData)
+ .where(eq(users.id, id))
+ .returning();
+ return user;
+ }
+
+ async getAllUsers(): Promise {
+ return await db.select().from(users).orderBy(desc(users.createdAt));
+ }
+
+ // Store operations
+ async getStore(id: number): Promise {
+ const [store] = await db.select().from(stores).where(eq(stores.id, id));
+ return store;
+ }
+
+ async createStore(storeData: InsertStore): Promise {
+ const [store] = await db
+ .insert(stores)
+ .values(storeData)
+ .returning();
+ return store;
+ }
+
+ async getAllStores(): Promise {
+ return await db.select().from(stores).orderBy(desc(stores.createdAt));
+ }
+
+ async updateStore(id: number, storeData: Partial): Promise {
+ const [store] = await db
+ .update(stores)
+ .set(storeData)
+ .where(eq(stores.id, id))
+ .returning();
+ return store;
+ }
+
+ // Sale operations
+ async getSale(id: number): Promise {
+ const [sale] = await db.select().from(sales).where(eq(sales.id, id));
+ return sale;
+ }
+
+ async createSale(saleData: InsertSale): Promise {
+ const [sale] = await db
+ .insert(sales)
+ .values(saleData)
+ .returning();
+ return sale;
+ }
+
+ async getSalesByStore(storeId: number, startDate?: string, endDate?: string): Promise {
+ let query = db.select().from(sales).where(eq(sales.storeId, storeId));
+
+ if (startDate && endDate) {
+ query = db.select().from(sales).where(
+ and(
+ eq(sales.storeId, storeId),
+ gte(sales.dateVente, startDate),
+ lte(sales.dateVente, endDate)
+ )
+ );
+ }
+
+ return await query.orderBy(desc(sales.timestamp));
+ }
+
+ async deleteSale(id: number): Promise {
+ await db.delete(sales).where(eq(sales.id, id));
+ }
+
+ // Auth operations
+ async authenticateUser(username: string, password: string): Promise {
+ const user = await this.getUserByUsername(username);
+ if (!user || !user.isActive) {
+ return null;
+ }
+
+ const isValid = await bcrypt.compare(password, user.password);
+ return isValid ? user : null;
+ }
+
+ async hashPassword(password: string): Promise {
+ return await bcrypt.hash(password, 12);
+ }
+
+ // Initialize default admin user and store if none exist
+ async initializeDefaults(): Promise<{ defaultAdminCredentials?: { username: string; password: string } }> {
+ const existingUsers = await this.getAllUsers();
+
+ if (existingUsers.length === 0) {
+ // Create default store
+ const defaultStore = await this.createStore({
+ name: "Magasin Principal",
+ address: "À définir",
+ isActive: true,
+ });
+
+ // Create default admin user
+ const defaultPassword = "admin123";
+ const adminUser = await this.createUser({
+ username: "admin",
+ password: defaultPassword,
+ email: "admin@example.com",
+ firstName: "Administrateur",
+ lastName: "Principal",
+ role: "admin",
+ storeId: defaultStore.id,
+ isActive: true,
+ });
+
+ return {
+ defaultAdminCredentials: {
+ username: "admin",
+ password: defaultPassword
+ }
+ };
+ }
+
+ return {};
+ }
}
-export const storage = new MemStorage();
+export const storage = new DatabaseStorage();
diff --git a/shared/schema.ts b/shared/schema.ts
index e58e0af..ecb7d7d 100644
--- a/shared/schema.ts
+++ b/shared/schema.ts
@@ -1,17 +1,144 @@
-import { pgTable, text, serial, integer, boolean } from "drizzle-orm/pg-core";
+import {
+ pgTable,
+ text,
+ varchar,
+ timestamp,
+ jsonb,
+ index,
+ serial,
+ integer,
+ boolean,
+} from "drizzle-orm/pg-core";
+import { relations } from "drizzle-orm";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod";
+// Session storage table
+export const sessions = pgTable(
+ "sessions",
+ {
+ sid: varchar("sid").primaryKey(),
+ sess: jsonb("sess").notNull(),
+ expire: timestamp("expire").notNull(),
+ },
+ (table) => [index("IDX_session_expire").on(table.expire)],
+);
+
+// Magasins table
+export const stores = pgTable("stores", {
+ id: serial("id").primaryKey(),
+ name: varchar("name", { length: 255 }).notNull(),
+ address: text("address"),
+ phone: varchar("phone", { length: 20 }),
+ email: varchar("email", { length: 255 }),
+ isActive: boolean("is_active").default(true),
+ createdAt: timestamp("created_at").defaultNow(),
+ updatedAt: timestamp("updated_at").defaultNow(),
+});
+
+// Users table with authentication
export const users = pgTable("users", {
id: serial("id").primaryKey(),
- username: text("username").notNull().unique(),
- password: text("password").notNull(),
+ username: varchar("username", { length: 50 }).unique().notNull(),
+ password: varchar("password", { length: 255 }).notNull(), // Will be hashed
+ email: varchar("email", { length: 255 }).unique(),
+ firstName: varchar("first_name", { length: 100 }),
+ lastName: varchar("last_name", { length: 100 }),
+ role: varchar("role", { length: 20 }).notNull().default("employee"), // admin, manager, employee
+ storeId: integer("store_id").references(() => stores.id),
+ isActive: boolean("is_active").default(true),
+ createdAt: timestamp("created_at").defaultNow(),
+ updatedAt: timestamp("updated_at").defaultNow(),
});
-export const insertUserSchema = createInsertSchema(users).pick({
- username: true,
- password: true,
+// Sales table
+export const sales = pgTable("sales", {
+ id: serial("id").primaryKey(),
+ storeId: integer("store_id").references(() => stores.id).notNull(),
+ userId: integer("user_id").references(() => users.id).notNull(),
+ timestamp: timestamp("timestamp").defaultNow(),
+
+ // Vendeur
+ vendeur: varchar("vendeur", { length: 255 }).notNull(),
+ dateVente: varchar("date_vente", { length: 10 }).notNull(),
+
+ // Produit
+ typeArticle: varchar("type_article", { length: 255 }).notNull(),
+ categorie: varchar("categorie", { length: 2 }).notNull(),
+ quantite: integer("quantite").notNull(),
+ gencode: varchar("gencode", { length: 13 }).notNull(),
+
+ // Client
+ nom: varchar("nom", { length: 255 }).notNull(),
+ prenom: varchar("prenom", { length: 255 }).notNull(),
+ dateNaissance: varchar("date_naissance", { length: 10 }).notNull(),
+ lieuNaissance: varchar("lieu_naissance", { length: 255 }),
+
+ // Identité
+ typeIdentite: varchar("type_identite", { length: 50 }).notNull(),
+ numeroIdentite: varchar("numero_identite", { length: 100 }).notNull(),
+ autoriteDelivrance: varchar("autorite_delivrance", { length: 255 }).notNull(),
+ dateDelivrance: varchar("date_delivrance", { length: 10 }).notNull(),
+
+ // Photos
+ photoRecto: text("photo_recto"),
+ photoVerso: text("photo_verso"),
});
-export type InsertUser = z.infer;
+// Relations
+export const storesRelations = relations(stores, ({ many }) => ({
+ users: many(users),
+ sales: many(sales),
+}));
+
+export const usersRelations = relations(users, ({ one, many }) => ({
+ store: one(stores, {
+ fields: [users.storeId],
+ references: [stores.id],
+ }),
+ sales: many(sales),
+}));
+
+export const salesRelations = relations(sales, ({ one }) => ({
+ store: one(stores, {
+ fields: [sales.storeId],
+ references: [stores.id],
+ }),
+ user: one(users, {
+ fields: [sales.userId],
+ references: [users.id],
+ }),
+}));
+
+// Insert schemas
+export const insertUserSchema = createInsertSchema(users).omit({
+ id: true,
+ createdAt: true,
+ updatedAt: true,
+});
+
+export const insertStoreSchema = createInsertSchema(stores).omit({
+ id: true,
+ createdAt: true,
+ updatedAt: true,
+});
+
+export const insertSaleSchema = createInsertSchema(sales).omit({
+ id: true,
+ timestamp: true,
+});
+
+// Login schema
+export const loginSchema = z.object({
+ username: z.string().min(1, "Nom d'utilisateur requis"),
+ password: z.string().min(1, "Mot de passe requis"),
+});
+
+// Types
export type User = typeof users.$inferSelect;
+export type InsertUser = typeof users.$inferInsert;
+export type Store = typeof stores.$inferSelect;
+export type InsertStore = typeof stores.$inferInsert;
+export type Sale = typeof sales.$inferSelect;
+export type InsertSale = typeof sales.$inferInsert;
+export type LoginData = z.infer;