mirror of
https://github.com/R0m1k3/Regisflow.git
synced 2026-10-21 06:08:18 +02:00
Implement secure user authentication with role-based access control
Integrates user authentication using bcrypt, session management with PostgreSQL, and role-based access control for enhanced security and user 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/ab880d72-1a34-4d36-b5be-9f44df85ed1a.jpg
This commit is contained in:
1 parent
42b249e5b0
commit
5c01234fc4
16 files changed
+2384
-827
No files matched your search
@@ -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<void>;
|
||||
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<Sale | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState<number | null>(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<Sale[]>({
|
||||
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 (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-8">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-2"></div>
|
||||
<p className="text-muted-foreground">Chargement des ventes...</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-8">
|
||||
<div className="text-center">
|
||||
<AlertCircle className="h-8 w-8 text-destructive mx-auto mb-2" />
|
||||
<p className="text-destructive">{error}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-center">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<History className="h-5 w-5" />
|
||||
Historique des Ventes
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Filters */}
|
||||
<div className="flex gap-4 items-end">
|
||||
<div className="grid grid-cols-2 gap-4 flex-1">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<BarChart3 className="h-5 w-5" />
|
||||
Historique des Ventes
|
||||
</CardTitle>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Gestion et export des ventes enregistrées
|
||||
</p>
|
||||
<label className="text-sm font-medium">Date de début</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<Button onClick={handleExportCSV} className="bg-success hover:bg-success/90">
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Exporter CSV
|
||||
</Button>
|
||||
<Button onClick={handleDownloadBackup} variant="outline">
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Backup Auto
|
||||
</Button>
|
||||
<Button onClick={handleDiagnostic} variant="outline">
|
||||
<Search className="h-4 w-4 mr-2" />
|
||||
Debug
|
||||
</Button>
|
||||
<div>
|
||||
<label className="text-sm font-medium">Date de fin</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
{/* Status Information */}
|
||||
<div className="px-6 py-3 bg-muted border-b border-border">
|
||||
<div className="flex items-center text-sm text-muted-foreground">
|
||||
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>
|
||||
Données stockées localement ({sales.length} vente{sales.length !== 1 ? 's' : ''}) • Export CSV compatible Excel • Sauvegarde automatique à chaque ajout
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
{backup ? (
|
||||
<span>
|
||||
Dernier backup: {formatDate(backup.timestamp)} ({backup.recordCount} enreg.)
|
||||
</span>
|
||||
) : (
|
||||
<span>Dernier backup: Aucun</span>
|
||||
)}
|
||||
</div>
|
||||
<Button onClick={exportToCSV} variant="outline">
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Export CSV
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<CardContent className="p-0">
|
||||
{sales.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<svg className="h-12 w-12 text-muted-foreground mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||
</svg>
|
||||
<p className="text-muted-foreground">Aucune vente enregistrée</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Vendeur</TableHead>
|
||||
<TableHead>Client</TableHead>
|
||||
<TableHead>Produit</TableHead>
|
||||
<TableHead>Catégorie</TableHead>
|
||||
<TableHead>Quantité</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sales.map((sale) => (
|
||||
<TableRow key={sale.id}>
|
||||
<TableCell>
|
||||
{formatDate(sale.dateVente)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{sale.vendeur || 'Non spécifié'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<div className="font-medium">{sale.nom} {sale.prenom}</div>
|
||||
{sale.typeIdentite && (
|
||||
<div className="text-xs text-muted-foreground">{sale.typeIdentite}</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<div className="font-medium">{sale.typeArticle}</div>
|
||||
{sale.gencode && (
|
||||
<div className="text-xs text-muted-foreground">{sale.gencode}</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={sale.categorie === 'F2' ? 'secondary' : 'destructive'}>
|
||||
{/* Sales Table */}
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
) : sales.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
Aucune vente trouvée
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Date/Heure</TableHead>
|
||||
<TableHead>Vendeur</TableHead>
|
||||
<TableHead>Client</TableHead>
|
||||
<TableHead>Article</TableHead>
|
||||
<TableHead>Quantité</TableHead>
|
||||
<TableHead>Gencode</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sales.map((sale) => (
|
||||
<TableRow key={sale.id}>
|
||||
<TableCell>{formatDateTime(sale.timestamp!)}</TableCell>
|
||||
<TableCell>{sale.vendeur}</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p className="font-medium">{sale.nom} {sale.prenom}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatDate(sale.dateNaissance)}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p className="font-medium">{sale.typeArticle}</p>
|
||||
<span className={`inline-flex px-2 py-1 text-xs rounded-full ${
|
||||
sale.categorie === 'F3'
|
||||
? 'bg-red-100 text-red-800'
|
||||
: 'bg-green-100 text-green-800'
|
||||
}`}>
|
||||
{sale.categorie}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{sale.quantite}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedSale(sale)}
|
||||
title="Voir les détails"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{sale.quantite}</TableCell>
|
||||
<TableCell>{sale.gencode}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedSale(sale)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Détails de la vente</DialogTitle>
|
||||
</DialogHeader>
|
||||
{selectedSale && (
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Informations générales</h4>
|
||||
<p><strong>Date/Heure :</strong> {formatDateTime(selectedSale.timestamp!)}</p>
|
||||
<p><strong>Vendeur :</strong> {selectedSale.vendeur}</p>
|
||||
<p><strong>Date de vente :</strong> {formatDate(selectedSale.dateVente)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Produit</h4>
|
||||
<p><strong>Type :</strong> {selectedSale.typeArticle}</p>
|
||||
<p><strong>Catégorie :</strong> {selectedSale.categorie}</p>
|
||||
<p><strong>Quantité :</strong> {selectedSale.quantite}</p>
|
||||
<p><strong>Gencode :</strong> {selectedSale.gencode}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Client</h4>
|
||||
<p><strong>Nom :</strong> {selectedSale.nom}</p>
|
||||
<p><strong>Prénom :</strong> {selectedSale.prenom}</p>
|
||||
<p><strong>Date de naissance :</strong> {formatDate(selectedSale.dateNaissance)}</p>
|
||||
{selectedSale.lieuNaissance && (
|
||||
<p><strong>Lieu de naissance :</strong> {selectedSale.lieuNaissance}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Pièce d'identité</h4>
|
||||
<p><strong>Type :</strong> {selectedSale.typeIdentite}</p>
|
||||
<p><strong>Numéro :</strong> {selectedSale.numeroIdentite}</p>
|
||||
<p><strong>Autorité :</strong> {selectedSale.autoriteDelivrance}</p>
|
||||
<p><strong>Date délivrance :</strong> {formatDate(selectedSale.dateDelivrance)}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{canDelete && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteSale(sale)}
|
||||
disabled={isDeleting === sale.id}
|
||||
className="text-destructive hover:text-destructive"
|
||||
title="Supprimer la vente"
|
||||
disabled={deleteSaleMutation.isPending}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<Trash2 className="h-4 w-4 text-red-600" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<SaleDetailsModal
|
||||
isOpen={!!selectedSale}
|
||||
sale={selectedSale}
|
||||
onClose={() => setSelectedSale(null)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user