1002 lines
42 KiB
TypeScript
1002 lines
42 KiB
TypeScript
'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;
|
||
|