Files
btpxpress-frontend/app/(main)/factures/nouvelle/page.tsx

1002 lines
42 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
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<Toast>(null);
const [loading, setLoading] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [activeIndex, setActiveIndex] = useState(0);
const [clients, setClients] = useState<any[]>([]);
const [chantiers, setChantiers] = useState<any[]>([]);
const [devisList, setDevisList] = useState<any[]>([]);
const [ligneDialog, setLigneDialog] = useState(false);
const [creationMode, setCreationMode] = useState<'manual' | 'from_devis'>('manual');
const [facture, setFacture] = useState<Partial<Facture>>({
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<Devis | null>(null);
const [currentLigne, setCurrentLigne] = useState<LigneFactureFormData>({
designation: '',
description: '',
quantite: 1,
unite: 'h',
prixUnitaire: 0,
montantLigne: 0,
ordre: 1
});
const [editingLigne, setEditingLigne] = useState<number | null>(null);
const [errors, setErrors] = useState<Record<string, string>>({});
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<string, string> = {};
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<HTMLInputElement | HTMLTextAreaElement>, 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<HTMLInputElement | HTMLTextAreaElement>, 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 (
<div className="flex gap-2">
<Button
icon="pi pi-pencil"
rounded
severity="success"
size="small"
onClick={() => editLigne(options.rowIndex)}
/>
<Button
icon="pi pi-trash"
rounded
severity="danger"
size="small"
onClick={() => deleteLigne(options.rowIndex)}
/>
</div>
);
};
const ligneDialogFooter = (
<>
<Button
label="Annuler"
icon="pi pi-times"
text
onClick={() => setLigneDialog(false)}
/>
<Button
label="Enregistrer"
icon="pi pi-check"
text
onClick={saveLigne}
disabled={!currentLigne.designation || currentLigne.quantite <= 0 || currentLigne.prixUnitaire <= 0}
/>
</>
);
const renderStepContent = () => {
switch (activeIndex) {
case 0:
return (
<div className="formgrid grid">
<div className="field col-12">
<h4 className="text-primary">Mode de création de la facture</h4>
<p className="text-600">Choisissez comment vous souhaitez créer cette facture :</p>
</div>
<div className="field col-12">
<div className="flex flex-column gap-3">
<div className="flex align-items-center">
<RadioButton
inputId="manual"
name="creationMode"
value="manual"
onChange={(e) => setCreationMode(e.value)}
checked={creationMode === 'manual'}
/>
<label htmlFor="manual" className="ml-2">
<strong>Création manuelle</strong>
<div className="text-600 text-sm">Saisir les informations et prestations manuellement</div>
</label>
</div>
<div className="flex align-items-center">
<RadioButton
inputId="from_devis"
name="creationMode"
value="from_devis"
onChange={(e) => setCreationMode(e.value)}
checked={creationMode === 'from_devis'}
/>
<label htmlFor="from_devis" className="ml-2">
<strong>À partir d'un devis accepté</strong>
<div className="text-600 text-sm">Importer les informations d'un devis existant</div>
</label>
</div>
</div>
</div>
</div>
);
case 1:
return (
<div className="formgrid grid">
<div className="field col-12 md:col-6">
<label htmlFor="numero" className="font-bold">Numéro de facture</label>
<InputText
id="numero"
value={facture.numero}
disabled
placeholder="Généré automatiquement"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="typeFacture" className="font-bold">Type de facture</label>
<Dropdown
id="typeFacture"
value={facture.typeFacture}
options={typesFacture}
onChange={(e) => onDropdownChange(e, 'typeFacture')}
placeholder="Sélectionnez un type"
/>
</div>
<div className="field col-12">
<label htmlFor="objet" className="font-bold">
Objet de la facture <span className="text-red-500">*</span>
</label>
<InputText
id="objet"
value={facture.objet}
onChange={(e) => onInputChange(e, 'objet')}
className={errors.objet ? 'p-invalid' : ''}
placeholder="Objet de la facture"
/>
{errors.objet && <small className="p-error">{errors.objet}</small>}
</div>
<div className="field col-12">
<label htmlFor="description" className="font-bold">Description</label>
<InputTextarea
id="description"
value={facture.description}
onChange={(e) => onInputChange(e, 'description')}
rows={4}
placeholder="Description détaillée des travaux facturés"
/>
</div>
</div>
);
case 2:
return (
<div className="formgrid grid">
<div className="field col-12">
<label htmlFor="client" className="font-bold">
Client <span className="text-red-500">*</span>
</label>
<Dropdown
id="client"
value={facture.client}
options={clients}
onChange={(e) => onDropdownChange(e, 'client')}
placeholder="Sélectionnez un client"
className={errors.client ? 'p-invalid' : ''}
filter
/>
{errors.client && <small className="p-error">{errors.client}</small>}
</div>
<div className="field col-12">
<label htmlFor="chantier" className="font-bold">Chantier (optionnel)</label>
<Dropdown
id="chantier"
value={facture.chantier}
options={chantiers}
onChange={(e) => onDropdownChange(e, 'chantier')}
placeholder="Sélectionnez un chantier"
disabled={!facture.client}
/>
</div>
{creationMode === 'from_devis' && (
<div className="field col-12">
<label htmlFor="devis" className="font-bold">
Devis de référence <span className="text-red-500">*</span>
</label>
<Dropdown
id="devis"
value={selectedDevis?.id}
options={devisList}
onChange={(e) => {
const devis = devisList.find(d => d.value === e.value)?.devis;
setSelectedDevis(devis || null);
}}
placeholder="Sélectionnez un devis accepté"
className={errors.devis ? 'p-invalid' : ''}
disabled={!facture.client}
/>
{errors.devis && <small className="p-error">{errors.devis}</small>}
</div>
)}
<div className="field col-12 md:col-6">
<label htmlFor="dateEmission" className="font-bold">Date d'émission</label>
<Calendar
id="dateEmission"
value={facture.dateEmission ? new Date(facture.dateEmission) : null}
onChange={(e) => onDateChange(e, 'dateEmission')}
dateFormat="dd/mm/yy"
showIcon
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="dateEcheance" className="font-bold">Date d'échéance</label>
<Calendar
id="dateEcheance"
value={facture.dateEcheance ? new Date(facture.dateEcheance) : null}
onChange={(e) => onDateChange(e, 'dateEcheance')}
dateFormat="dd/mm/yy"
showIcon
minDate={facture.dateEmission ? new Date(facture.dateEmission) : undefined}
/>
</div>
</div>
);
case 3:
return (
<div>
{creationMode === 'manual' ? (
<>
<div className="flex justify-content-between align-items-center mb-3">
<h3>Prestations facturées</h3>
<Button
label="Ajouter une prestation"
icon="pi pi-plus"
onClick={openLigneDialog}
/>
</div>
{errors.lignes && <small className="p-error block mb-3">{errors.lignes}</small>}
<DataTable
value={facture.lignes || []}
responsiveLayout="scroll"
emptyMessage="Aucune prestation ajoutée."
>
<Column field="designation" header="Désignation" />
<Column field="quantite" header="Quantité" />
<Column field="unite" header="Unité" />
<Column
field="prixUnitaire"
header="Prix unitaire"
body={(rowData) => formatCurrency(rowData.prixUnitaire)}
/>
<Column
field="montantLigne"
header="Montant"
body={(rowData) => formatCurrency(rowData.montantLigne || 0)}
/>
<Column body={ligneActionBodyTemplate} headerStyle={{ width: '8rem' }} />
</DataTable>
</>
) : (
<div>
<h3>Prestations importées du devis</h3>
{selectedDevis ? (
<>
<p className="text-600 mb-3">
Devis: <strong>{selectedDevis.numero} - {selectedDevis.objet}</strong>
</p>
<DataTable
value={facture.lignes || []}
responsiveLayout="scroll"
emptyMessage="Aucune prestation dans le devis."
>
<Column field="designation" header="Désignation" />
<Column field="quantite" header="Quantité" />
<Column field="unite" header="Unité" />
<Column
field="prixUnitaire"
header="Prix unitaire"
body={(rowData) => formatCurrency(rowData.prixUnitaire)}
/>
<Column
field="montantLigne"
header="Montant"
body={(rowData) => formatCurrency(rowData.montantLigne || 0)}
/>
</DataTable>
</>
) : (
<p className="text-600">Veuillez d'abord sélectionner un devis à l'étape précédente.</p>
)}
</div>
)}
<div className="mt-3 text-right">
<p><strong>Montant HT: {formatCurrency(facture.montantHT || 0)}</strong></p>
<p>TVA ({facture.tauxTVA}%): {formatCurrency(facture.montantTVA || 0)}</p>
<p className="text-xl"><strong>Montant TTC: {formatCurrency(facture.montantTTC || 0)}</strong></p>
</div>
</div>
);
case 4:
return (
<div className="formgrid grid">
<div className="field col-12 md:col-6">
<label htmlFor="tauxTVA" className="font-bold">Taux TVA (%)</label>
<InputNumber
id="tauxTVA"
value={facture.tauxTVA}
onValueChange={(e) => onNumberChange(e, 'tauxTVA')}
suffix="%"
min={0}
max={100}
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="statut" className="font-bold">Statut initial</label>
<Dropdown
id="statut"
value={facture.statut}
options={statuts}
onChange={(e) => onDropdownChange(e, 'statut')}
placeholder="Sélectionnez un statut"
/>
</div>
<div className="field col-12">
<label htmlFor="conditionsPaiement" className="font-bold">
Conditions de paiement <span className="text-red-500">*</span>
</label>
<InputTextarea
id="conditionsPaiement"
value={facture.conditionsPaiement}
onChange={(e) => onInputChange(e, 'conditionsPaiement')}
rows={3}
className={errors.conditionsPaiement ? 'p-invalid' : ''}
placeholder="Conditions de paiement"
/>
{errors.conditionsPaiement && <small className="p-error">{errors.conditionsPaiement}</small>}
</div>
</div>
);
case 5:
return (
<div className="formgrid grid">
<div className="field col-12">
<h3 className="text-primary">Récapitulatif de la facture</h3>
<Divider />
</div>
<div className="field col-12 md:col-6">
<label className="font-bold">Numéro:</label>
<p>{facture.numero}</p>
</div>
<div className="field col-12 md:col-6">
<label className="font-bold">Type:</label>
<p>{typesFacture.find(t => t.value === facture.typeFacture)?.label}</p>
</div>
<div className="field col-12 md:col-6">
<label className="font-bold">Client:</label>
<p>{clients.find(c => c.value === facture.client)?.label}</p>
</div>
<div className="field col-12 md:col-6">
<label className="font-bold">Mode de création:</label>
<p>{creationMode === 'manual' ? 'Création manuelle' : 'À partir du devis'}</p>
</div>
<div className="field col-12">
<label className="font-bold">Objet:</label>
<p>{facture.objet}</p>
</div>
<div className="field col-12">
<label className="font-bold">Prestations ({facture.lignes?.length || 0}):</label>
<ul>
{facture.lignes?.map((ligne, index) => (
<li key={index}>
{ligne.designation} - {ligne.quantite} {ligne.unite} × {formatCurrency(ligne.prixUnitaire)} = {formatCurrency(ligne.montantLigne || 0)}
</li>
))}
</ul>
</div>
<div className="field col-12 text-right">
<p>Montant HT: {formatCurrency(facture.montantHT || 0)}</p>
<p>TVA ({facture.tauxTVA}%): {formatCurrency(facture.montantTVA || 0)}</p>
<p className="text-xl"><strong>Total TTC: {formatCurrency(facture.montantTTC || 0)}</strong></p>
</div>
</div>
);
default:
return null;
}
};
return (
<div className="grid">
<div className="col-12">
<Card>
<Toast ref={toast} />
<div className="flex justify-content-between align-items-center mb-4">
<h2>Nouvelle Facture</h2>
<Button
icon="pi pi-arrow-left"
label="Retour"
className="p-button-text"
onClick={handleCancel}
/>
</div>
<Steps model={steps} activeIndex={activeIndex} className="mb-4" />
<form onSubmit={handleSubmit} className="p-fluid">
{renderStepContent()}
<Divider />
<div className="flex justify-content-between">
<Button
type="button"
label="Précédent"
icon="pi pi-chevron-left"
className="p-button-text"
onClick={prevStep}
disabled={activeIndex === 0}
/>
<div className="flex gap-2">
<Button
type="button"
label="Annuler"
icon="pi pi-times"
className="p-button-text"
onClick={handleCancel}
disabled={loading}
/>
{activeIndex < steps.length - 1 ? (
<Button
type="button"
label="Suivant"
icon="pi pi-chevron-right"
iconPos="right"
onClick={nextStep}
/>
) : (
<Button
type="submit"
label="Créer la facture"
icon="pi pi-check"
loading={loading}
disabled={loading}
/>
)}
</div>
</div>
</form>
<Dialog
visible={ligneDialog}
style={{ width: '600px' }}
header={editingLigne !== null ? 'Modifier la prestation' : 'Ajouter une prestation'}
modal
className="p-fluid"
footer={ligneDialogFooter}
onHide={() => setLigneDialog(false)}
>
<div className="formgrid grid">
<div className="field col-12">
<label htmlFor="designation" className="font-bold">Désignation</label>
<InputText
id="designation"
value={currentLigne.designation}
onChange={(e) => onLigneInputChange(e, 'designation')}
placeholder="Désignation de la prestation"
/>
</div>
<div className="field col-12">
<label htmlFor="ligneDescription" className="font-bold">Description</label>
<InputTextarea
id="ligneDescription"
value={currentLigne.description}
onChange={(e) => onLigneInputChange(e, 'description')}
rows={2}
placeholder="Description détaillée"
/>
</div>
<div className="field col-12 md:col-4">
<label htmlFor="quantite" className="font-bold">Quantité</label>
<InputNumber
id="quantite"
value={currentLigne.quantite}
onValueChange={(e) => onLigneNumberChange(e, 'quantite')}
min={0.01}
minFractionDigits={0}
maxFractionDigits={2}
/>
</div>
<div className="field col-12 md:col-4">
<label htmlFor="unite" className="font-bold">Unité</label>
<Dropdown
id="unite"
value={currentLigne.unite}
options={unites}
onChange={(e) => onLigneDropdownChange(e, 'unite')}
placeholder="Sélectionnez une unité"
/>
</div>
<div className="field col-12 md:col-4">
<label htmlFor="prixUnitaire" className="font-bold">Prix unitaire ()</label>
<InputNumber
id="prixUnitaire"
value={currentLigne.prixUnitaire}
onValueChange={(e) => onLigneNumberChange(e, 'prixUnitaire')}
mode="currency"
currency="EUR"
locale="fr-FR"
min={0}
/>
</div>
<div className="field col-12">
<label className="font-bold">Montant de la ligne:</label>
<p className="text-primary text-xl">
{formatCurrency(currentLigne.quantite * currentLigne.prixUnitaire)}
</p>
</div>
</div>
</Dialog>
</Card>
</div>
</div>
);
};
export default NouvelleFacturePage;