'use client'; export const dynamic = 'force-dynamic'; import React, { useState, useEffect, useRef } from 'react'; import { useRouter } from 'next/navigation'; import { Card } from 'primereact/card'; import { Button } from 'primereact/button'; import { InputText } from 'primereact/inputtext'; import { InputTextarea } from 'primereact/inputtextarea'; import { InputNumber } from 'primereact/inputnumber'; import { Calendar } from 'primereact/calendar'; import { Dropdown } from 'primereact/dropdown'; import { Toast } from 'primereact/toast'; import { Divider } from 'primereact/divider'; import { DataTable } from 'primereact/datatable'; import { Column } from 'primereact/column'; import { Dialog } from 'primereact/dialog'; import { Steps } from 'primereact/steps'; import { RadioButton } from 'primereact/radiobutton'; import { clientService, chantierService, devisService } from '../../../../services/api'; import { formatCurrency } from '../../../../utils/formatters'; import type { Facture, LigneFacture, Client, Chantier, Devis } from '../../../../types/btp'; import { StatutFacture, TypeFacture } from '../../../../types/btp'; interface LigneFactureFormData { designation: string; description: string; quantite: number; unite: string; prixUnitaire: number; montantLigne: number; ordre: number; } const NouvelleFacturePage = () => { const router = useRouter(); const toast = useRef(null); const [loading, setLoading] = useState(false); const [submitted, setSubmitted] = useState(false); const [activeIndex, setActiveIndex] = useState(0); const [clients, setClients] = useState([]); const [chantiers, setChantiers] = useState([]); const [devisList, setDevisList] = useState([]); const [ligneDialog, setLigneDialog] = useState(false); const [creationMode, setCreationMode] = useState<'manual' | 'from_devis'>('manual'); const [facture, setFacture] = useState>({ id: '', numero: '', objet: '', description: '', dateEmission: new Date().toISOString(), dateEcheance: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // +30 jours datePaiement: null, statut: StatutFacture.BROUILLON, montantHT: 0, tauxTVA: 20, montantTVA: 0, montantTTC: 0, montantPaye: 0, conditionsPaiement: 'Paiement à 30 jours fin de mois', typeFacture: TypeFacture.FACTURE, actif: true, client: null, chantier: null, devis: null, lignes: [] }); const [selectedDevis, setSelectedDevis] = useState(null); const [currentLigne, setCurrentLigne] = useState({ designation: '', description: '', quantite: 1, unite: 'h', prixUnitaire: 0, montantLigne: 0, ordre: 1 }); const [editingLigne, setEditingLigne] = useState(null); const [errors, setErrors] = useState>({}); const unites = [ { label: 'Heures', value: 'h' }, { label: 'Jours', value: 'j' }, { label: 'Mètres', value: 'm' }, { label: 'Mètres carrés', value: 'm²' }, { label: 'Mètres cubes', value: 'm³' }, { label: 'Unités', value: 'u' }, { label: 'Forfait', value: 'forfait' }, { label: 'Lots', value: 'lot' } ]; const statuts = [ { label: 'Brouillon', value: 'BROUILLON' }, { label: 'Émise', value: 'ENVOYEE' }, { label: 'Payée', value: 'PAYEE' } ]; const typesFacture = [ { label: 'Facture complète', value: 'FACTURE' }, { label: 'Facture d\'acompte', value: 'ACOMPTE' }, { label: 'Avoir', value: 'AVOIR' } ]; const steps = [ { label: 'Mode de création' }, { label: 'Informations générales' }, { label: 'Client et références' }, { label: 'Prestations' }, { label: 'Conditions' }, { label: 'Validation' } ]; useEffect(() => { loadClients(); generateNumero(); }, []); useEffect(() => { if (facture.client) { const clientId = typeof facture.client === 'string' ? facture.client : facture.client.id; loadChantiersByClient(clientId); loadDevisByClient(clientId); } else { setChantiers([]); setDevisList([]); } }, [facture.client]); useEffect(() => { calculateTotals(); }, [facture.lignes, facture.tauxTVA]); useEffect(() => { if (selectedDevis && creationMode === 'from_devis') { importFromDevis(); } }, [selectedDevis]); const generateNumero = () => { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const time = String(now.getHours()).padStart(2, '0') + String(now.getMinutes()).padStart(2, '0'); const numero = `FACT-${year}${month}${day}-${time}`; setFacture(prev => ({ ...prev, numero })); }; const loadClients = async () => { try { const data = await clientService.getAll(); setClients(data.map(client => ({ label: `${client.prenom} ${client.nom}${client.entreprise ? ' - ' + client.entreprise : ''}`, value: client.id, client: client }))); } catch (error) { console.error('Erreur lors du chargement des clients:', error); } }; const loadChantiersByClient = async (clientId: string) => { try { const data = await chantierService.getByClient(clientId); setChantiers(data.map(chantier => ({ label: chantier.nom, value: chantier.id, chantier: chantier }))); } catch (error) { console.error('Erreur lors du chargement des chantiers:', error); setChantiers([]); } }; const loadDevisByClient = async (clientId: string) => { try { const data = await devisService.getAll(); // Filtrer les devis acceptés du client const clientDevis = data.filter(devis => devis.client?.id === clientId && devis.statut === 'ACCEPTE' ); setDevisList(clientDevis.map(devis => ({ label: `${devis.numero} - ${devis.objet} (${formatCurrency(devis.montantTTC || 0)})`, value: devis.id, devis: devis }))); } catch (error) { console.error('Erreur lors du chargement des devis:', error); setDevisList([]); } }; const importFromDevis = () => { if (!selectedDevis) return; setFacture(prev => ({ ...prev, objet: `Facture - ${selectedDevis.objet}`, description: selectedDevis.description || '', montantHT: selectedDevis.montantHT || 0, tauxTVA: selectedDevis.tauxTVA || 20, montantTVA: selectedDevis.montantTVA || 0, montantTTC: selectedDevis.montantTTC || 0, client: selectedDevis.client || null, devis: selectedDevis || null, lignes: selectedDevis.lignes?.map(ligneDevis => ({ id: '', designation: ligneDevis.designation, description: ligneDevis.description || '', quantite: ligneDevis.quantite, unite: ligneDevis.unite, prixUnitaire: ligneDevis.prixUnitaire, montantLigne: ligneDevis.montantLigne || 0, ordre: ligneDevis.ordre, dateCreation: new Date().toISOString(), dateModification: new Date().toISOString(), facture: {} as Facture })) || [] })); }; const calculateTotals = () => { const montantHT = facture.lignes?.reduce((sum, ligne) => sum + (ligne.montantLigne || 0), 0) || 0; const montantTVA = montantHT * (facture.tauxTVA / 100); const montantTTC = montantHT + montantTVA; setFacture(prev => ({ ...prev, montantHT, montantTVA, montantTTC })); }; const validateStep = (step: number) => { const newErrors: Record = {}; switch (step) { case 1: // Informations générales if (!facture.objet.trim()) { newErrors.objet = 'L\'objet de la facture est obligatoire'; } break; case 2: // Client et références if (!facture.client) { newErrors.client = 'Le client est obligatoire'; } break; case 3: // Prestations if (creationMode === 'manual' && (!facture.lignes || facture.lignes.length === 0)) { newErrors.lignes = 'Au moins une prestation est obligatoire'; } if (creationMode === 'from_devis' && !selectedDevis) { newErrors.devis = 'Veuillez sélectionner un devis'; } break; case 4: // Conditions if (!facture.conditionsPaiement?.trim()) { newErrors.conditionsPaiement = 'Les conditions de paiement sont obligatoires'; } break; } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const nextStep = () => { if (validateStep(activeIndex)) { setActiveIndex(prev => Math.min(prev + 1, steps.length - 1)); } }; const prevStep = () => { setActiveIndex(prev => Math.max(prev - 1, 0)); }; const openLigneDialog = () => { setCurrentLigne({ designation: '', description: '', quantite: 1, unite: 'h', prixUnitaire: 0, montantLigne: 0, ordre: (facture.lignes?.length || 0) + 1 }); setEditingLigne(null); setLigneDialog(true); }; const editLigne = (index: number) => { const ligne = facture.lignes?.[index]; if (ligne) { setCurrentLigne({ designation: ligne.designation, description: ligne.description || '', quantite: ligne.quantite, unite: ligne.unite, prixUnitaire: ligne.prixUnitaire, montantLigne: ligne.montantLigne || 0, ordre: ligne.ordre }); setEditingLigne(index); setLigneDialog(true); } }; const saveLigne = () => { const montantLigne = currentLigne.quantite * currentLigne.prixUnitaire; const newLigne = { ...currentLigne, montantLigne, id: '', dateCreation: new Date().toISOString(), dateModification: new Date().toISOString(), facture: {} as Facture }; if (editingLigne !== null) { // Modification const updatedLignes = [...(facture.lignes || [])]; updatedLignes[editingLigne] = newLigne as LigneFacture; setFacture(prev => ({ ...prev, lignes: updatedLignes })); } else { // Ajout setFacture(prev => ({ ...prev, lignes: [...(prev.lignes || []), newLigne as LigneFacture] })); } setLigneDialog(false); }; const deleteLigne = (index: number) => { const updatedLignes = facture.lignes?.filter((_, i) => i !== index) || []; setFacture(prev => ({ ...prev, lignes: updatedLignes })); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setSubmitted(true); if (!validateStep(activeIndex)) { toast.current?.show({ severity: 'error', summary: 'Erreur', detail: 'Veuillez corriger les erreurs du formulaire', life: 3000 }); return; } setLoading(true); try { // Simulation de création (l'API n'est pas encore implémentée) console.log('Données de la facture:', facture); toast.current?.show({ severity: 'info', summary: 'Information', detail: 'Fonctionnalité en cours d\'implémentation côté serveur', life: 3000 }); setTimeout(() => { router.push('/factures'); }, 1000); } catch (error: any) { console.error('Erreur lors de la création:', error); toast.current?.show({ severity: 'error', summary: 'Erreur', detail: 'Impossible de créer la facture', life: 5000 }); } finally { setLoading(false); } }; const handleCancel = () => { router.push('/factures'); }; const onInputChange = (e: React.ChangeEvent, name: string) => { const val = (e.target && e.target.value) || ''; let _facture = { ...facture }; (_facture as any)[name] = val; setFacture(_facture); if (errors[name]) { const newErrors = { ...errors }; delete newErrors[name]; setErrors(newErrors); } }; const onDateChange = (e: any, name: string) => { let _facture = { ...facture }; (_facture as any)[name] = e.value?.toISOString() || new Date().toISOString(); setFacture(_facture); if (errors[name]) { const newErrors = { ...errors }; delete newErrors[name]; setErrors(newErrors); } }; const onNumberChange = (e: any, name: string) => { let _facture = { ...facture }; (_facture as any)[name] = e.value; setFacture(_facture); if (errors[name]) { const newErrors = { ...errors }; delete newErrors[name]; setErrors(newErrors); } }; const onDropdownChange = (e: any, name: string) => { let _facture = { ...facture }; (_facture as any)[name] = e.value; setFacture(_facture); if (errors[name]) { const newErrors = { ...errors }; delete newErrors[name]; setErrors(newErrors); } }; const onLigneInputChange = (e: React.ChangeEvent, name: string) => { const val = (e.target && e.target.value) || ''; let _ligne = { ...currentLigne }; (_ligne as any)[name] = val; setCurrentLigne(_ligne); }; const onLigneNumberChange = (e: any, name: string) => { let _ligne = { ...currentLigne }; (_ligne as any)[name] = e.value || 0; setCurrentLigne(_ligne); }; const onLigneDropdownChange = (e: any, name: string) => { let _ligne = { ...currentLigne }; (_ligne as any)[name] = e.value; setCurrentLigne(_ligne); }; const ligneActionBodyTemplate = (rowData: LigneFacture, options: any) => { return (
); }; const ligneDialogFooter = ( <>