Files
Regisflow/client/src/components/NewSaleForm.tsx
T
michaelschal 4333a9ae12 Enhance the look and feel of forms and data display for improved UI
Updates the styles of input fields, vendor sections, and filter cards to provide better visual consistency.

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/19d76f99-63e3-4717-987b-4fafe960e234.jpg
2025-07-10 00:07:10 +00:00

518 lines
21 KiB
TypeScript

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';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { useToast } from '@/hooks/use-toast';
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';
interface FormData {
vendeur: string;
dateVente: string;
typeArticle: string;
categorie: string;
quantite: string;
gencode: string;
nom: string;
prenom: string;
dateNaissance: string;
lieuNaissance: string;
modePaiement: string;
typeIdentite: string;
numeroIdentite: string;
autoriteDelivrance: string;
dateDelivrance: string;
}
export default function NewSaleForm() {
const { toast } = useToast();
const queryClient = useQueryClient();
const { user } = useAuth();
const { selectedStoreId } = useStoreContext();
const [isSubmitting, setIsSubmitting] = useState(false);
const form = useForm<FormData>({
defaultValues: {
vendeur: '',
dateVente: new Date().toISOString().split('T')[0],
typeArticle: '',
categorie: '',
quantite: '',
gencode: '',
nom: '',
prenom: '',
dateNaissance: '',
lieuNaissance: '',
modePaiement: 'Espèce',
typeIdentite: '',
numeroIdentite: '',
autoriteDelivrance: '',
dateDelivrance: '',
}
});
const typeArticle = form.watch('typeArticle');
const availableCategories = typeArticle ? ARTICLE_CATEGORY_MAPPING[typeArticle as keyof typeof ARTICLE_CATEGORY_MAPPING] || [] : [];
const createSaleMutation = useMutation({
mutationFn: async (saleData: any) => {
const dataToSend = {
...saleData,
// Include storeId for admin users
...(user?.role === 'admin' && selectedStoreId && { storeId: selectedStoreId })
};
const response = await apiRequest('/api/sales', {
method: 'POST',
body: JSON.stringify(dataToSend),
});
return response.json();
},
onSuccess: () => {
// Invalidate all sales queries to refresh the history
queryClient.invalidateQueries({ queryKey: ['/api/sales'], exact: false });
toast({
title: "Vente enregistrée",
description: "La vente a été enregistrée avec succès",
});
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: '',
});
},
onError: (error: any) => {
toast({
title: "Erreur",
description: error.message || "Erreur lors de l'enregistrement de la vente",
variant: "destructive",
});
},
});
const onSubmit = async (data: FormData) => {
try {
setIsSubmitting(true);
// Validate required fields
const missingFields = validateRequiredFields(data);
if (missingFields.length > 0) {
toast({
title: "Champs manquants",
description: `Veuillez remplir: ${missingFields.join(', ')}`,
variant: "destructive",
});
return;
}
// Validate EAN13
if (!validateEAN13(data.gencode)) {
toast({
title: "Code EAN13 invalide",
description: "Le gencode doit être un code EAN13 valide à 13 chiffres",
variant: "destructive",
});
return;
}
const saleData = {
vendeur: data.vendeur,
dateVente: data.dateVente,
typeArticle: data.typeArticle,
categorie: data.categorie as 'F2' | 'F3',
quantite: parseInt(data.quantite),
gencode: data.gencode,
nom: data.nom,
prenom: data.prenom,
dateNaissance: data.dateNaissance,
lieuNaissance: data.lieuNaissance,
typeIdentite: data.typeIdentite,
numeroIdentite: data.numeroIdentite,
autoriteDelivrance: data.autoriteDelivrance,
dateDelivrance: data.dateDelivrance,
};
await createSaleMutation.mutateAsync(saleData);
} catch (error) {
console.error('Sale creation error:', error);
} finally {
setIsSubmitting(false);
}
};
return (
<Card className="modern-card">
<CardContent className="pt-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>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<FormField
control={form.control}
name="vendeur"
render={({ field }) => (
<FormItem>
<FormLabel>Vendeur <span className="text-destructive">*</span></FormLabel>
<FormControl>
<Input placeholder="Nom du vendeur" className="modern-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="dateVente"
render={({ field }) => (
<FormItem>
<FormLabel>Date de vente</FormLabel>
<FormControl>
<Input type="date" className="modern-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</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>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<FormField
control={form.control}
name="typeArticle"
render={({ field }) => (
<FormItem>
<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
}}>
<FormControl>
<SelectTrigger className="modern-select-trigger">
<SelectValue placeholder="Sélectionner un type" />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.keys(ARTICLE_CATEGORY_MAPPING).map((type) => (
<SelectItem key={type} value={type}>
{type}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="categorie"
render={({ field }) => (
<FormItem>
<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">
<SelectValue placeholder="Sélectionner une catégorie" />
</SelectTrigger>
</FormControl>
<SelectContent>
{availableCategories.map((category) => (
<SelectItem key={category} value={category}>
{category}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="quantite"
render={({ field }) => (
<FormItem>
<FormLabel>Quantité <span className="text-destructive">*</span></FormLabel>
<FormControl>
<Input type="number" min="1" placeholder="Entrez la quantité" className="modern-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="gencode"
render={({ field }) => (
<FormItem>
<FormLabel>Gencode (EAN13) <span className="text-destructive">*</span></FormLabel>
<FormControl>
<Input placeholder="Code EAN13 à 13 chiffres" className="modern-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</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>
</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>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 mobile-gap-responsive">
<FormField
control={form.control}
name="nom"
render={({ field }) => (
<FormItem>
<FormLabel>Nom de famille <span className="text-destructive">*</span></FormLabel>
<FormControl>
<Input placeholder="Nom de famille" className="modern-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="prenom"
render={({ field }) => (
<FormItem>
<FormLabel>Prénom <span className="text-destructive">*</span></FormLabel>
<FormControl>
<Input placeholder="Prénom" className="modern-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="dateNaissance"
render={({ field }) => (
<FormItem>
<FormLabel>Date de naissance <span className="text-destructive">*</span></FormLabel>
<FormControl>
<Input type="date" className="modern-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lieuNaissance"
render={({ field }) => (
<FormItem>
<FormLabel>Lieu de naissance <span className="text-destructive">*</span></FormLabel>
<FormControl>
<Input placeholder="Ville de naissance" className="modern-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="modePaiement"
render={({ field }) => (
<FormItem>
<FormLabel>Mode de paiement <span className="text-destructive">*</span></FormLabel>
<Select value={field.value} onValueChange={field.onChange}>
<FormControl>
<SelectTrigger className="modern-select-trigger">
<SelectValue placeholder="Sélectionner le mode de paiement" />
</SelectTrigger>
</FormControl>
<SelectContent>
{PAYMENT_METHODS.map((method) => (
<SelectItem key={method.value} value={method.value}>
{method.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</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>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 mobile-gap-responsive">
<FormField
control={form.control}
name="typeIdentite"
render={({ field }) => (
<FormItem>
<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">
<SelectValue placeholder="Sélectionner le type" />
</SelectTrigger>
</FormControl>
<SelectContent>
{IDENTITY_TYPES.map((type) => (
<SelectItem key={type.value} value={type.value}>
{type.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="numeroIdentite"
render={({ field }) => (
<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} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="autoriteDelivrance"
render={({ field }) => (
<FormItem>
<FormLabel>Autorité de délivrance <span className="text-destructive">*</span></FormLabel>
<FormControl>
<Input placeholder="Préfecture, Mairie, etc." className="modern-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="dateDelivrance"
render={({ field }) => (
<FormItem>
<FormLabel>Date de délivrance <span className="text-destructive">*</span></FormLabel>
<FormControl>
<Input type="date" className="modern-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</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"
>
<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>
</div>
</form>
</Form>
</CardContent>
</Card>
);
}