Enhance user experience and interface of sales and dashboard sections

Refactor UI components in NewSaleForm.tsx, SalesHistory.tsx, index.css, validation.ts, and Dashboard.tsx for improved UX.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: dec54137-d5a2-4406-8ad4-5f2f9fb110bf
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0715008c-7244-40f4-befc-26c014852236/c74640af-aff6-4467-98ef-6922e186fed2.jpg
This commit is contained in:
michaelschal committed 2025-07-10 08:33:18 +00:00
1 parent b2ac262871
commit 71db65e5b5
5 files changed
+709 -1045

No files matched your search

+56 -99
View File
@@ -1,7 +1,6 @@
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
@@ -12,7 +11,7 @@ import { useAuth } from '@/hooks/useAuth';
import { useStoreContext } from '@/hooks/useStoreContext';
import { apiRequest } from '@/lib/queryClient';
import { validateRequiredFields, validateEAN13, ARTICLE_CATEGORY_MAPPING, IDENTITY_TYPES, PAYMENT_METHODS } from '@/lib/validation';
import { Package, User, Users, Save, X } from 'lucide-react';
import { Package, User, Users, Save, Calendar, BadgeCheck, Info } from 'lucide-react';
interface FormData {
vendeur: string;
@@ -66,7 +65,6 @@ export default function NewSaleForm() {
mutationFn: async (saleData: any) => {
const dataToSend = {
...saleData,
// Include storeId for admin users
...(user?.role === 'admin' && selectedStoreId && { storeId: selectedStoreId })
};
@@ -77,7 +75,6 @@ export default function NewSaleForm() {
return response.json();
},
onSuccess: () => {
// Invalidate all sales queries to refresh the history
queryClient.invalidateQueries({ queryKey: ['/api/sales'], exact: false });
toast({
title: "Vente enregistrée",
@@ -115,7 +112,6 @@ export default function NewSaleForm() {
try {
setIsSubmitting(true);
// Validate required fields
const missingFields = validateRequiredFields(data);
if (missingFields.length > 0) {
toast({
@@ -126,7 +122,6 @@ export default function NewSaleForm() {
return;
}
// Validate EAN13
if (!validateEAN13(data.gencode)) {
toast({
title: "Code EAN13 invalide",
@@ -162,17 +157,15 @@ export default function NewSaleForm() {
};
return (
<Card className="modern-card">
<CardContent className="pt-6">
<div className="elegant-card animate-slide-in">
<div className="p-6">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* Seller Information */}
<div className="modern-section modern-section-vendor">
<div className="modern-section-header">
<div className="w-10 h-10 bg-purple-500 text-white flex items-center justify-center border-2 border-purple-500 shadow-md">
<User className="h-5 w-5" />
</div>
<span className="truncate">Informations Vendeur</span>
{/* Section Vendeur */}
<div className="section-vendor">
<div className="section-header">
<User className="h-5 w-5" />
<span>Informations Vendeur</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<FormField
@@ -193,7 +186,7 @@ export default function NewSaleForm() {
name="dateVente"
render={({ field }) => (
<FormItem>
<FormLabel>Date de vente</FormLabel>
<FormLabel>Date de vente <span className="text-destructive">*</span></FormLabel>
<FormControl>
<Input type="date" className="modern-input" {...field} />
</FormControl>
@@ -204,13 +197,11 @@ export default function NewSaleForm() {
</div>
</div>
{/* Product Information */}
<div className="modern-section modern-section-product">
<div className="modern-section-header">
<div className="w-10 h-10 bg-blue-500 text-white flex items-center justify-center border-2 border-blue-500 shadow-md">
<Package className="h-5 w-5" />
</div>
<span className="truncate">Informations Produit</span>
{/* Section Produit */}
<div className="section-product">
<div className="section-header">
<Package className="h-5 w-5" />
<span>Informations Produit</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<FormField
@@ -221,10 +212,10 @@ export default function NewSaleForm() {
<FormLabel>Type d'article <span className="text-destructive">*</span></FormLabel>
<Select value={field.value} onValueChange={(value) => {
field.onChange(value);
form.setValue('categorie', ''); // Reset category when article changes
form.setValue('categorie', '');
}}>
<FormControl>
<SelectTrigger className="modern-select-trigger">
<SelectTrigger className="modern-input">
<SelectValue placeholder="Sélectionner un type" />
</SelectTrigger>
</FormControl>
@@ -248,7 +239,7 @@ export default function NewSaleForm() {
<FormLabel>Catégorie réglementaire <span className="text-destructive">*</span></FormLabel>
<Select value={field.value} onValueChange={field.onChange} disabled={!typeArticle}>
<FormControl>
<SelectTrigger className="modern-select-trigger">
<SelectTrigger className="modern-input">
<SelectValue placeholder="Sélectionner une catégorie" />
</SelectTrigger>
</FormControl>
@@ -292,29 +283,21 @@ export default function NewSaleForm() {
/>
</div>
<Alert className="mt-4">
<svg className="h-4 w-4" 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>
<AlertDescription>
<strong>Information réglementaire :</strong>
<ul className="mt-1 space-y-1 text-sm">
<li>• <strong>F1</strong> : petits artifices (clac-doigts, mini-fontaines), accessibles dès 12 ans.</li>
<li>• <strong>F2, F3</strong> : artifices plus puissants, vente et utilisation réservées aux majeurs.</li>
</ul>
<Alert className="mt-4 border-blue-200 bg-blue-50">
<Info className="h-4 w-4 text-blue-600" />
<AlertDescription className="text-blue-800">
<strong>Information réglementaire :</strong> F2/F3 = artifices puissants réservés aux majeurs, vente encadrée
</AlertDescription>
</Alert>
</div>
{/* Customer Information */}
<div className="modern-section modern-section-customer">
<div className="modern-section-header">
<div className="w-10 h-10 bg-emerald-500 text-white flex items-center justify-center border-2 border-emerald-500 shadow-md">
<Users className="h-5 w-5" />
</div>
<span className="truncate">Informations Client</span>
{/* Section Client */}
<div className="section-client">
<div className="section-header">
<Users className="h-5 w-5" />
<span>Informations Client</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 mobile-gap-responsive">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<FormField
control={form.control}
name="nom"
@@ -375,7 +358,7 @@ export default function NewSaleForm() {
<FormLabel>Mode de paiement <span className="text-destructive">*</span></FormLabel>
<Select value={field.value} onValueChange={field.onChange}>
<FormControl>
<SelectTrigger className="modern-select-trigger">
<SelectTrigger className="modern-input">
<SelectValue placeholder="Sélectionner le mode de paiement" />
</SelectTrigger>
</FormControl>
@@ -394,17 +377,13 @@ export default function NewSaleForm() {
</div>
</div>
{/* Identity Information */}
<div className="modern-section modern-section-identity">
<div className="modern-section-header">
<div className="w-10 h-10 bg-amber-500 text-white flex items-center justify-center border-2 border-amber-500 shadow-md">
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2" />
</svg>
</div>
<span className="truncate">Pièce d'Identité</span>
{/* Section Identité */}
<div className="section-identity">
<div className="section-header">
<BadgeCheck className="h-5 w-5" />
<span>Pièce d'Identité</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 mobile-gap-responsive">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<FormField
control={form.control}
name="typeIdentite"
@@ -413,7 +392,7 @@ export default function NewSaleForm() {
<FormLabel>Type de pièce d'identité <span className="text-destructive">*</span></FormLabel>
<Select value={field.value} onValueChange={field.onChange}>
<FormControl>
<SelectTrigger className="modern-select-trigger">
<SelectTrigger className="modern-input">
<SelectValue placeholder="Sélectionner le type" />
</SelectTrigger>
</FormControl>
@@ -436,7 +415,7 @@ export default function NewSaleForm() {
<FormItem>
<FormLabel>Numéro de pièce d'identité <span className="text-destructive">*</span></FormLabel>
<FormControl>
<Input placeholder="Numéro de la pièce d'identité" className="modern-input" {...field} />
<Input placeholder="Numéro de la pièce" className="modern-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -447,9 +426,9 @@ export default function NewSaleForm() {
name="autoriteDelivrance"
render={({ field }) => (
<FormItem>
<FormLabel>Autorité de délivrance <span className="text-destructive">*</span></FormLabel>
<FormLabel>Autorité de délivrance</FormLabel>
<FormControl>
<Input placeholder="Préfecture, Mairie, etc." className="modern-input" {...field} />
<Input placeholder="Autorité ayant délivré la pièce (optionnel)" className="modern-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -471,49 +450,27 @@ export default function NewSaleForm() {
</div>
</div>
{/* Form Actions */}
<div className="flex flex-col sm:flex-row justify-end gap-3 sm:gap-4 pt-6 sm:pt-8 mt-6 sm:mt-8 border-t border-border/50">
<Button
type="button"
variant="outline"
size="lg"
className="border-2 hover:bg-muted smooth-transition touch-friendly-button order-2 sm:order-1"
onClick={() => {
form.reset({
vendeur: '',
dateVente: new Date().toISOString().split('T')[0],
typeArticle: '',
categorie: '',
quantite: '',
gencode: '',
nom: '',
prenom: '',
dateNaissance: '',
lieuNaissance: '',
modePaiement: 'Espèce',
typeIdentite: '',
numeroIdentite: '',
autoriteDelivrance: '',
dateDelivrance: '',
});
}}>
<X className="h-5 w-5 mr-2" />
Réinitialiser
</Button>
<Button
type="submit"
size="lg"
disabled={isSubmitting || createSaleMutation.isPending}
className="bg-primary hover:bg-primary/90 text-primary-foreground border-0 shadow-lg hover:shadow-xl order-1 sm:order-2"
{/* Boutons d'action */}
<div className="flex flex-col sm:flex-row gap-4 pt-6">
<button
type="submit"
disabled={isSubmitting}
className="modern-button modern-button-success flex-1"
>
<Save className="h-5 w-5 mr-2" />
<span className="hidden sm:inline">{isSubmitting || createSaleMutation.isPending ? 'Enregistrement...' : 'Enregistrer la Vente'}</span>
<span className="sm:hidden">{isSubmitting || createSaleMutation.isPending ? 'Enregistrement...' : 'Enregistrer'}</span>
</Button>
<Save className="h-4 w-4" />
{isSubmitting ? 'Enregistrement...' : 'Enregistrer la vente'}
</button>
<button
type="button"
onClick={() => form.reset()}
className="modern-button modern-button-secondary flex-1"
>
Réinitialiser
</button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
</div>
);
}
+275 -288
View File
@@ -1,6 +1,5 @@
import { useState } from 'react';
import { useState, useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
@@ -11,7 +10,7 @@ import { useStoreContext } from '@/hooks/useStoreContext';
import { useAuth } from '@/hooks/useAuth';
import { apiRequest } from '@/lib/queryClient';
import type { Sale } from '@shared/schema';
import { History, Download, Trash2, Eye, Filter, FileText } from 'lucide-react';
import { History, Download, Trash2, Eye, Filter, FileText, Search, X, Calendar, Users } from 'lucide-react';
interface SalesHistoryProps {
canDelete?: boolean;
@@ -25,6 +24,7 @@ export default function SalesHistory({ canDelete = false }: SalesHistoryProps) {
const [selectedSale, setSelectedSale] = useState<Sale | null>(null);
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [searchQuery, setSearchQuery] = useState('');
// Query for sales
const { data: salesData, isLoading } = useQuery<Sale[]>({
@@ -34,7 +34,6 @@ export default function SalesHistory({ canDelete = false }: SalesHistoryProps) {
if (startDate) params.append('startDate', startDate);
if (endDate) params.append('endDate', endDate);
// Always add storeId for queries
if (selectedStoreId) {
params.append('storeId', selectedStoreId.toString());
}
@@ -43,19 +42,35 @@ export default function SalesHistory({ canDelete = false }: SalesHistoryProps) {
const response = await apiRequest(`/api/sales${queryString ? `?${queryString}` : ''}`);
return response.json();
},
enabled: !!selectedStoreId && !!user, // Only run query when store is selected and user is loaded
refetchOnMount: true, // Always refetch when component mounts
refetchOnWindowFocus: false, // Don't refetch on window focus
enabled: !!selectedStoreId && !!user,
refetchOnMount: true,
refetchOnWindowFocus: false,
});
const sales = Array.isArray(salesData) ? salesData : [];
// Filtrer les ventes selon la recherche
const filteredSales = useMemo(() => {
if (!searchQuery.trim()) return sales;
const query = searchQuery.toLowerCase().trim();
return sales.filter(sale =>
sale.nom.toLowerCase().includes(query) ||
sale.prenom.toLowerCase().includes(query) ||
sale.vendeur.toLowerCase().includes(query) ||
sale.typeArticle.toLowerCase().includes(query) ||
sale.gencode.includes(query) ||
sale.numeroIdentite.toLowerCase().includes(query) ||
(sale.lieuNaissance && sale.lieuNaissance.toLowerCase().includes(query)) ||
sale.modePaiement.toLowerCase().includes(query)
);
}, [sales, searchQuery]);
// Delete mutation
const deleteSaleMutation = useMutation({
mutationFn: (saleId: number) =>
apiRequest(`/api/sales/${saleId}`, { method: 'DELETE' }),
onSuccess: () => {
// Invalidate all sales queries to refresh the history
queryClient.invalidateQueries({ queryKey: ['/api/sales'], exact: false });
toast({
title: "Vente supprimée",
@@ -77,7 +92,7 @@ export default function SalesHistory({ canDelete = false }: SalesHistoryProps) {
};
const exportToPDF = async () => {
if (sales.length === 0) {
if (filteredSales.length === 0) {
toast({
title: "Aucune donnée",
description: "Aucune vente à exporter",
@@ -92,84 +107,44 @@ export default function SalesHistory({ canDelete = false }: SalesHistoryProps) {
const doc = new jsPDF();
// Configuration du document
doc.setFont('helvetica');
// Titre du document
doc.setFontSize(16);
doc.text('Historique des Ventes de Feux d\'Artifice', 20, 20);
// Informations générales
doc.setFontSize(10);
doc.text(`Date d'export: ${new Date().toLocaleDateString('fr-FR')}`, 20, 30);
doc.text(`Nombre de ventes: ${sales.length}`, 20, 35);
doc.text(`Généré le ${new Date().toLocaleDateString('fr-FR')} à ${new Date().toLocaleTimeString('fr-FR')}`, 20, 30);
if (startDate || endDate) {
let periodText = 'Période: ';
if (startDate) periodText += `du ${new Date(startDate).toLocaleDateString('fr-FR')} `;
if (endDate) periodText += `au ${new Date(endDate).toLocaleDateString('fr-FR')}`;
doc.text(periodText, 20, 40);
const period = `Période: ${startDate ? new Date(startDate).toLocaleDateString('fr-FR') : 'début'} - ${endDate ? new Date(endDate).toLocaleDateString('fr-FR') : 'fin'}`;
doc.text(period, 20, 35);
}
// Données du tableau
const tableData = sales.map(sale => [
new Date(sale.timestamp!).toLocaleDateString('fr-FR'),
const tableData = filteredSales.map(sale => [
new Date(sale.timestamp).toLocaleDateString('fr-FR'),
sale.vendeur,
`${sale.nom} ${sale.prenom}`,
sale.typeArticle,
sale.categorie,
sale.quantite.toString(),
sale.nom + ' ' + sale.prenom,
sale.modePaiement || 'Espèce',
sale.typeIdentite,
sale.numeroIdentite
sale.modePaiement,
sale.gencode
]);
// Configuration du tableau
autoTable(doc, {
head: [['Date', 'Vendeur', 'Article', 'Cat.', 'Qté', 'Client', 'Paiement', 'ID Type', 'N° ID']],
head: [['Date', 'Vendeur', 'Client', 'Article', 'Cat.', 'Qty', 'Paiement', 'Code']],
body: tableData,
startY: startDate || endDate ? 50 : 45,
styles: {
fontSize: 8,
cellPadding: 2,
},
headStyles: {
fillColor: [59, 130, 246],
textColor: 255,
fontSize: 9,
},
alternateRowStyles: {
fillColor: [248, 250, 252],
},
columnStyles: {
0: { cellWidth: 18 }, // Date
1: { cellWidth: 25 }, // Vendeur
2: { cellWidth: 35 }, // Article
3: { cellWidth: 12 }, // Catégorie
4: { cellWidth: 12 }, // Quantité
5: { cellWidth: 30 }, // Client
6: { cellWidth: 20 }, // Paiement
7: { cellWidth: 15 }, // ID Type
8: { cellWidth: 25 }, // N° ID
},
margin: { left: 10, right: 10 },
startY: startDate || endDate ? 45 : 40,
styles: { fontSize: 8 },
headStyles: { fillColor: [64, 64, 64] },
margin: { top: 40, left: 20, right: 20 },
});
// Pied de page
const pageCount = doc.internal.getNumberOfPages();
for (let i = 1; i <= pageCount; i++) {
doc.setPage(i);
doc.setFontSize(8);
doc.text(`Page ${i} sur ${pageCount}`, doc.internal.pageSize.width - 30, doc.internal.pageSize.height - 10);
doc.text('RegisFlow - Registre des ventes de feux d\'artifice', 20, doc.internal.pageSize.height - 10);
}
// Téléchargement
doc.save(`historique_ventes_${new Date().toISOString().split('T')[0]}.pdf`);
doc.save(`ventes-feux-artifice-${new Date().toISOString().split('T')[0]}.pdf`);
toast({
title: "Export PDF réussi",
description: `${sales.length} ventes exportées en PDF`,
title: "Export réussi",
description: "Le fichier PDF a été téléchargé",
variant: "success",
});
} catch (error) {
@@ -182,254 +157,266 @@ export default function SalesHistory({ canDelete = false }: SalesHistoryProps) {
}
};
const exportToCSV = () => {
if (sales.length === 0) {
toast({
title: "Aucune donnée",
description: "Aucune vente à exporter",
variant: "destructive",
});
return;
}
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',
'Mode de paiement',
'Type identité',
'Numéro identité',
'Autorité délivrance',
'Date délivrance'
].join(',');
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.modePaiement || 'Espèce',
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`,
variant: "success",
});
const clearFilters = () => {
setStartDate('');
setEndDate('');
setSearchQuery('');
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('fr-FR');
};
const formatDateTime = (dateString: string) => {
return new Date(dateString).toLocaleString('fr-FR');
};
return (
<Card className="modern-card">
<CardHeader>
<CardTitle className="flex items-center gap-3">
<div className="modern-icon-container">
<History className="h-6 w-6" />
</div>
<span className="truncate">Historique des Ventes</span>
</CardTitle>
<p className="text-muted-foreground mt-2">
Consultation et gestion des ventes enregistrées
</p>
</CardHeader>
<CardContent>
{/* Filters */}
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-end modern-filter-card">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 flex-1 w-full">
<div>
<label className="font-medium text-sm block mb-2">Date de début</label>
<Input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="modern-input"
/>
</div>
<div>
<label className="font-medium text-sm block mb-2">Date de fin</label>
<Input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="modern-input"
/>
</div>
</div>
<div className="flex gap-2 w-full sm:w-auto">
<Button onClick={exportToCSV} variant="outline" className="modern-button flex-1 sm:flex-none">
<Download className="h-4 w-4 mr-2" />
<span className="hidden sm:inline">Export CSV</span>
<span className="sm:hidden">CSV</span>
</Button>
<Button onClick={exportToPDF} variant="outline" className="modern-button flex-1 sm:flex-none">
<FileText className="h-4 w-4 mr-2" />
<span className="hidden sm:inline">Export PDF</span>
<span className="sm:hidden">PDF</span>
</Button>
if (isLoading) {
return (
<div className="elegant-card">
<div className="p-6">
<div className="animate-pulse space-y-4">
<div className="h-4 bg-gray-200 rounded w-1/4"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
<div className="h-4 bg-gray-200 rounded w-1/3"></div>
</div>
</div>
</div>
);
}
{/* 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>
return (
<div className="space-y-6 animate-slide-in">
{/* Module de recherche */}
<div className="search-container">
<div className="search-header">
<Search className="h-5 w-5" />
<span>Rechercher dans l'historique</span>
</div>
<div className="space-y-4">
<div className="relative">
<Input
placeholder="Rechercher par nom, prénom, vendeur, article, code..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="search-input pr-10"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
<X className="h-4 w-4" />
</button>
)}
</div>
) : sales.length === 0 ? (
<div className="text-center py-8 text-muted-foreground responsive-body">
Aucune vente trouvée
{searchQuery && (
<div className="text-sm text-gray-600">
{filteredSales.length} résultat{filteredSales.length > 1 ? 's' : ''} trouvé{filteredSales.length > 1 ? 's' : ''} sur {sales.length} vente{sales.length > 1 ? 's' : ''}
</div>
)}
</div>
</div>
{/* Filtres */}
<div className="filters-container">
<div className="section-header mb-4">
<Filter className="h-5 w-5" />
<span>Filtres par date</span>
</div>
<div className="filters-grid">
<div>
<label className="block text-sm font-medium mb-2">Date de début</label>
<Input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="modern-input"
/>
</div>
) : (
<div className="rounded-md border overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="responsive-caption">Date/Heure</TableHead>
<TableHead className="responsive-caption">Vendeur</TableHead>
<TableHead className="responsive-caption">Client</TableHead>
<TableHead className="responsive-caption">Article</TableHead>
<TableHead className="responsive-caption">Quantité</TableHead>
<TableHead className="responsive-caption hidden lg:table-cell">Paiement</TableHead>
<TableHead className="responsive-caption hidden sm:table-cell">Gencode</TableHead>
<TableHead className="responsive-caption">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>
<label className="block text-sm font-medium mb-2">Date de fin</label>
<Input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="modern-input"
/>
</div>
<div className="flex items-end">
<button
onClick={clearFilters}
className="modern-button modern-button-secondary"
>
<X className="h-4 w-4" />
Effacer
</button>
</div>
</div>
</div>
{/* Actions d'export */}
<div className="elegant-card">
<div className="p-4">
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 text-gray-600">
<History className="h-5 w-5" />
<span className="font-medium">
{filteredSales.length} vente{filteredSales.length > 1 ? 's' : ''}
</span>
</div>
{searchQuery && (
<div className="text-sm text-blue-600 bg-blue-50 px-2 py-1 rounded">
Recherche active
</div>
)}
</div>
<button
onClick={exportToPDF}
disabled={filteredSales.length === 0}
className="modern-button modern-button-primary"
>
<Download className="h-4 w-4" />
Exporter en PDF
</button>
</div>
</div>
</div>
{/* Table des ventes */}
<div className="elegant-card">
<div className="overflow-x-auto">
<table className="modern-table">
<thead>
<tr>
<th>Date</th>
<th>Vendeur</th>
<th>Client</th>
<th>Article</th>
<th>Cat.</th>
<th>Qté</th>
<th>Paiement</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{filteredSales.length === 0 ? (
<tr>
<td colSpan={8} className="text-center py-8 text-gray-500">
{searchQuery ? 'Aucun résultat pour cette recherche' : 'Aucune vente enregistrée'}
</td>
</tr>
) : (
filteredSales.map((sale) => (
<tr key={sale.id}>
<td className="font-mono text-sm">
{new Date(sale.timestamp).toLocaleDateString('fr-FR')}
</td>
<td className="font-medium">{sale.vendeur}</td>
<td>
<div className="flex items-center gap-2">
<Users className="h-4 w-4 text-gray-400" />
<span>{sale.nom} {sale.prenom}</span>
</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}
</span>
</div>
</TableCell>
<TableCell>{sale.quantite}</TableCell>
<TableCell className="hidden lg:table-cell">
<span className="inline-flex px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-800">
{sale.modePaiement || 'Espèce'}
</td>
<td className="text-sm">{sale.typeArticle}</td>
<td>
<span className={`modern-badge ${
sale.categorie === 'F3' ? 'badge-destructive' : 'badge-secondary'
}`}>
{sale.categorie}
</span>
</TableCell>
<TableCell className="hidden sm:table-cell">{sale.gencode}</TableCell>
<TableCell>
<div className="flex gap-2">
</td>
<td className="text-center">{sale.quantite}</td>
<td className="text-sm">{sale.modePaiement}</td>
<td>
<div className="flex items-center gap-2">
<Dialog>
<DialogTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedSale(sale)}
>
<button className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
<Eye className="h-4 w-4" />
</Button>
</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 className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<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>
<label className="text-sm font-medium text-gray-600">Date de vente</label>
<p className="font-mono">{new Date(sale.timestamp).toLocaleDateString('fr-FR')}</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>
<label className="text-sm font-medium text-gray-600">Vendeur</label>
<p>{sale.vendeur}</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>
)}
<p><strong>Mode de paiement :</strong> {selectedSale.modePaiement || 'Espèce'}</p>
<label className="text-sm font-medium text-gray-600">Client</label>
<p>{sale.nom} {sale.prenom}</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>
<label className="text-sm font-medium text-gray-600">Date de naissance</label>
<p>{new Date(sale.dateNaissance).toLocaleDateString('fr-FR')}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-600">Lieu de naissance</label>
<p>{sale.lieuNaissance || 'Non renseigné'}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-600">Type d'article</label>
<p>{sale.typeArticle}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-600">Catégorie</label>
<p>{sale.categorie}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-600">Quantité</label>
<p>{sale.quantite}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-600">Gencode</label>
<p className="font-mono">{sale.gencode}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-600">Mode de paiement</label>
<p>{sale.modePaiement}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-600">Type d'identité</label>
<p>{sale.typeIdentite}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-600">Numéro d'identité</label>
<p className="font-mono">{sale.numeroIdentite}</p>
</div>
{sale.autoriteDelivrance && (
<div>
<label className="text-sm font-medium text-gray-600">Autorité de délivrance</label>
<p>{sale.autoriteDelivrance}</p>
</div>
)}
<div>
<label className="text-sm font-medium text-gray-600">Date de délivrance</label>
<p>{new Date(sale.dateDelivrance).toLocaleDateString('fr-FR')}</p>
</div>
</div>
)}
</div>
</DialogContent>
</Dialog>
{canDelete && (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="sm"
disabled={deleteSaleMutation.isPending}
>
<Trash2 className="h-4 w-4 text-red-600" />
</Button>
<button className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
<Trash2 className="h-4 w-4" />
</button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirmer la suppression</AlertDialogTitle>
<AlertDialogTitle>Supprimer la vente</AlertDialogTitle>
<AlertDialogDescription>
Êtes-vous sûr de vouloir supprimer cette vente ? Cette action est irréversible.
<br /><br />
<strong>Client :</strong> {sale.nom} {sale.prenom}<br />
<strong>Article :</strong> {sale.typeArticle}<br />
<strong>Date :</strong> {formatDateTime(sale.timestamp!)}
Êtes-vous sûr de vouloir supprimer cette vente ?
<div className="mt-2 p-3 bg-gray-50 rounded-lg">
<p><strong>Client :</strong> {sale.nom} {sale.prenom}</p>
<p><strong>Article :</strong> {sale.typeArticle}</p>
<p><strong>Date :</strong> {new Date(sale.timestamp).toLocaleDateString('fr-FR')}</p>
</div>
Cette action est irréversible.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
@@ -445,14 +432,14 @@ export default function SalesHistory({ canDelete = false }: SalesHistoryProps) {
</AlertDialog>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
);
}
+376 -656
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -27,8 +27,8 @@ export function validateRequiredFields(formData: any): string[] {
{ key: 'modePaiement', label: 'Mode de paiement' },
{ key: 'typeIdentite', label: 'Type de pièce d\'identité' },
{ key: 'numeroIdentite', label: 'Numéro de pièce d\'identité' },
{ key: 'autoriteDelivrance', label: 'Autorité de délivrance' },
{ key: 'dateDelivrance', label: 'Date de délivrance' }
// Note: autoriteDelivrance n'est plus obligatoire
];
return requiredFields
+1 -1
View File
@@ -41,7 +41,7 @@ function DashboardContent() {
<div className="min-h-screen bg-background">
{/* Modern Header */}
<header className="sticky top-0 z-50 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="max-w-7xl mx-auto mobile-padding">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center gap-4">
<div className="relative">