- Correction des erreurs TypeScript dans userService.ts et workflowTester.ts - Ajout des propriétés manquantes aux objets User mockés - Conversion des dates de string vers objets Date - Correction des appels asynchrones et des types incompatibles - Ajout de dynamic rendering pour résoudre les erreurs useSearchParams - Enveloppement de useSearchParams dans Suspense boundary - Configuration de force-dynamic au niveau du layout principal Build réussi: 126 pages générées avec succès 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
688 lines
28 KiB
TypeScript
688 lines
28 KiB
TypeScript
'use client';
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import { Card } from 'primereact/card';
|
|
import { DataTable } from 'primereact/datatable';
|
|
import { Column } from 'primereact/column';
|
|
import { Button } from 'primereact/button';
|
|
import { Tag } from 'primereact/tag';
|
|
import { Dialog } from 'primereact/dialog';
|
|
import { InputTextarea } from 'primereact/inputtextarea';
|
|
import { Toast } from 'primereact/toast';
|
|
import { Divider } from 'primereact/divider';
|
|
import { Badge } from 'primereact/badge';
|
|
import { Timeline } from 'primereact/timeline';
|
|
import { Dropdown } from 'primereact/dropdown';
|
|
import { Calendar } from 'primereact/calendar';
|
|
import { Checkbox } from 'primereact/checkbox';
|
|
import { ProgressBar } from 'primereact/progressbar';
|
|
import { Knob } from 'primereact/knob';
|
|
import {
|
|
ActionButtonGroup,
|
|
ViewButton,
|
|
EditButton,
|
|
DeleteButton,
|
|
ActionButton
|
|
} from '../../../../components/ui/ActionButton';
|
|
|
|
/**
|
|
* Page Relances Automatiques Factures BTP Express
|
|
* Gestion complète des relances clients avec workflows automatisés
|
|
*/
|
|
const RelancesFactures = () => {
|
|
const [factures, setFactures] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [relanceDialog, setRelanceDialog] = useState(false);
|
|
const [configDialog, setConfigDialog] = useState(false);
|
|
const [selectedFactures, setSelectedFactures] = useState<any[]>([]);
|
|
const [selectedFacture, setSelectedFacture] = useState<any>(null);
|
|
const [messageRelance, setMessageRelance] = useState('');
|
|
const [typeRelance, setTypeRelance] = useState('');
|
|
const [dateRelance, setDateRelance] = useState<Date | null>(null);
|
|
const [configRelances, setConfigRelances] = useState<any>({});
|
|
const [metriques, setMetriques] = useState<any>({});
|
|
const toast = useRef<Toast>(null);
|
|
|
|
const typesRelance = [
|
|
{ label: 'Relance amiable 1', value: 'AMIABLE_1', delai: 30, ferme: false },
|
|
{ label: 'Relance amiable 2', value: 'AMIABLE_2', delai: 45, ferme: false },
|
|
{ label: 'Mise en demeure', value: 'MISE_EN_DEMEURE', delai: 60, ferme: true },
|
|
{ label: 'Procédure contentieux', value: 'CONTENTIEUX', delai: 90, ferme: true }
|
|
];
|
|
|
|
const modeleMessages = {
|
|
'AMIABLE_1': `Madame, Monsieur,
|
|
|
|
Nous vous informons que votre facture n°[NUMERO] d'un montant de [MONTANT]€ émise le [DATE_EMISSION] reste impayée à ce jour.
|
|
|
|
Le délai de paiement étant dépassé, nous vous remercions de bien vouloir procéder au règlement dans les plus brefs délais.
|
|
|
|
En cas d'oubli de votre part, vous pouvez procéder au paiement en ligne ou nous contacter.
|
|
|
|
Cordialement,
|
|
L'équipe BTP Express`,
|
|
|
|
'AMIABLE_2': `Madame, Monsieur,
|
|
|
|
SECONDE RELANCE - Facture n°[NUMERO]
|
|
|
|
Malgré notre précédent courrier, votre facture n°[NUMERO] d'un montant de [MONTANT]€ demeure impayée.
|
|
|
|
Nous vous rappelons que le délai de paiement est largement dépassé. Pour éviter tout désagrément, nous vous demandons de régulariser votre situation sous 8 jours.
|
|
|
|
En l'absence de règlement, nous nous verrons contraints d'engager une procédure de recouvrement.
|
|
|
|
Cordialement,
|
|
L'équipe BTP Express`,
|
|
|
|
'MISE_EN_DEMEURE': `MISE EN DEMEURE DE PAYER
|
|
Facture n°[NUMERO]
|
|
|
|
Madame, Monsieur,
|
|
|
|
Nos précédentes relances étant restées sans effet, nous vous mettons en demeure de procéder au règlement de votre facture n°[NUMERO] d'un montant de [MONTANT]€.
|
|
|
|
VOUS DISPOSEZ DE 8 JOURS pour régulariser votre situation.
|
|
|
|
À défaut, nous engagerons contre vous une procédure de recouvrement contentieux qui entraînera des frais supplémentaires à votre charge.
|
|
|
|
BTP Express`
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadFactures();
|
|
loadConfiguration();
|
|
loadMetriques();
|
|
}, []);
|
|
|
|
const loadFactures = async () => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
// Simulation factures avec relances
|
|
const mockFactures = [
|
|
{
|
|
id: 'FAC-2025-001',
|
|
numero: 'FAC-2025-001',
|
|
client: 'Claire Rousseau',
|
|
montantTTC: 18000,
|
|
dateEmission: '2024-12-15',
|
|
dateEcheance: '2025-01-15',
|
|
joursRetard: 15,
|
|
statut: 'IMPAYEE',
|
|
nbRelances: 1,
|
|
derniereRelance: '2025-01-20',
|
|
prochainNiveau: 'AMIABLE_2',
|
|
risqueClient: 'FAIBLE',
|
|
historique: [
|
|
{ date: '2025-01-20', type: 'AMIABLE_1', canal: 'EMAIL', statut: 'ENVOYE' }
|
|
]
|
|
},
|
|
{
|
|
id: 'FAC-2025-002',
|
|
numero: 'FAC-2025-002',
|
|
client: 'Jean Dupont',
|
|
montantTTC: 32000,
|
|
dateEmission: '2024-11-30',
|
|
dateEcheance: '2024-12-30',
|
|
joursRetard: 31,
|
|
statut: 'IMPAYEE',
|
|
nbRelances: 2,
|
|
derniereRelance: '2025-01-25',
|
|
prochainNiveau: 'MISE_EN_DEMEURE',
|
|
risqueClient: 'MOYEN',
|
|
historique: [
|
|
{ date: '2025-01-10', type: 'AMIABLE_1', canal: 'EMAIL', statut: 'ENVOYE' },
|
|
{ date: '2025-01-25', type: 'AMIABLE_2', canal: 'EMAIL', statut: 'ENVOYE' }
|
|
]
|
|
},
|
|
{
|
|
id: 'FAC-2025-003',
|
|
numero: 'FAC-2025-003',
|
|
client: 'Sophie Martin',
|
|
montantTTC: 8500,
|
|
dateEmission: '2025-01-10',
|
|
dateEcheance: '2025-02-10',
|
|
joursRetard: -10, // Pas encore échue
|
|
statut: 'EN_ATTENTE',
|
|
nbRelances: 0,
|
|
derniereRelance: null,
|
|
prochainNiveau: 'AMIABLE_1',
|
|
risqueClient: 'FAIBLE',
|
|
historique: []
|
|
},
|
|
{
|
|
id: 'FAC-2024-089',
|
|
numero: 'FAC-2024-089',
|
|
client: 'Michel Bernard',
|
|
montantTTC: 45000,
|
|
dateEmission: '2024-10-15',
|
|
dateEcheance: '2024-11-15',
|
|
joursRetard: 77,
|
|
statut: 'CONTENTIEUX',
|
|
nbRelances: 3,
|
|
derniereRelance: '2025-01-15',
|
|
prochainNiveau: 'CONTENTIEUX',
|
|
risqueClient: 'FORT',
|
|
historique: [
|
|
{ date: '2024-12-01', type: 'AMIABLE_1', canal: 'EMAIL', statut: 'ENVOYE' },
|
|
{ date: '2024-12-20', type: 'AMIABLE_2', canal: 'EMAIL', statut: 'ENVOYE' },
|
|
{ date: '2025-01-15', type: 'MISE_EN_DEMEURE', canal: 'COURRIER', statut: 'ENVOYE' }
|
|
]
|
|
}
|
|
];
|
|
|
|
setFactures(mockFactures);
|
|
} catch (error) {
|
|
console.error('Erreur chargement factures:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const loadConfiguration = () => {
|
|
setConfigRelances({
|
|
delaiAmiable1: 30,
|
|
delaiAmiable2: 45,
|
|
delaiMiseEnDemeure: 60,
|
|
automatique: true,
|
|
canalPrioritaire: 'EMAIL',
|
|
avecCopie: true,
|
|
fraisRecouvrement: 40
|
|
});
|
|
};
|
|
|
|
const loadMetriques = () => {
|
|
setMetriques({
|
|
montantEnAttente: 103500,
|
|
nombreFacturesImpayees: 4,
|
|
tauxRecouvrement: 87.5,
|
|
delaiMoyenPaiement: 38,
|
|
relancesEnvoyees: 12,
|
|
montantRecouvre: 285000
|
|
});
|
|
};
|
|
|
|
const ouvrirRelance = (facture: any) => {
|
|
setSelectedFacture(facture);
|
|
setTypeRelance(facture.prochainNiveau);
|
|
setMessageRelance(modeleMessages[facture.prochainNiveau as keyof typeof modeleMessages] || '');
|
|
setDateRelance(new Date());
|
|
setRelanceDialog(true);
|
|
};
|
|
|
|
const envoyerRelance = async () => {
|
|
if (!selectedFacture || !typeRelance) return;
|
|
|
|
try {
|
|
// Simulation envoi relance
|
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
|
|
const factureUpdated = {
|
|
...selectedFacture,
|
|
nbRelances: selectedFacture.nbRelances + 1,
|
|
derniereRelance: new Date().toISOString().split('T')[0],
|
|
historique: [
|
|
...selectedFacture.historique,
|
|
{
|
|
date: new Date().toISOString().split('T')[0],
|
|
type: typeRelance,
|
|
canal: 'EMAIL',
|
|
statut: 'ENVOYE'
|
|
}
|
|
]
|
|
};
|
|
|
|
// Déterminer prochain niveau
|
|
const indexActuel = typesRelance.findIndex(t => t.value === typeRelance);
|
|
if (indexActuel < typesRelance.length - 1) {
|
|
factureUpdated.prochainNiveau = typesRelance[indexActuel + 1].value;
|
|
}
|
|
|
|
// Mettre à jour la liste
|
|
const facturesUpdated = factures.map(f =>
|
|
f.id === selectedFacture.id ? factureUpdated : f
|
|
);
|
|
setFactures(facturesUpdated);
|
|
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Relance envoyée',
|
|
detail: `Relance ${typeRelance} envoyée à ${selectedFacture.client}`,
|
|
life: 4000
|
|
});
|
|
|
|
setRelanceDialog(false);
|
|
|
|
} catch (error) {
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Impossible d\'envoyer la relance',
|
|
life: 3000
|
|
});
|
|
}
|
|
};
|
|
|
|
const envoyerRelancesGroupees = async () => {
|
|
if (selectedFactures.length === 0) return;
|
|
|
|
try {
|
|
toast.current?.show({
|
|
severity: 'info',
|
|
summary: 'Traitement en cours',
|
|
detail: `Envoi de ${selectedFactures.length} relances...`,
|
|
life: 3000
|
|
});
|
|
|
|
// Simulation envoi groupé
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
const facturesUpdated = factures.map(f => {
|
|
if (selectedFactures.find(sf => sf.id === f.id)) {
|
|
return {
|
|
...f,
|
|
nbRelances: f.nbRelances + 1,
|
|
derniereRelance: new Date().toISOString().split('T')[0]
|
|
};
|
|
}
|
|
return f;
|
|
});
|
|
|
|
setFactures(facturesUpdated);
|
|
setSelectedFactures([]);
|
|
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Relances envoyées',
|
|
detail: `${selectedFactures.length} relances envoyées avec succès`,
|
|
life: 4000
|
|
});
|
|
|
|
} catch (error) {
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Erreur lors de l\'envoi groupé',
|
|
life: 3000
|
|
});
|
|
}
|
|
};
|
|
|
|
const retardBodyTemplate = (rowData: any) => {
|
|
if (rowData.joursRetard <= 0) {
|
|
return <Tag value="À échoir" severity="info" />;
|
|
}
|
|
|
|
let severity: "warning" | "danger" = 'warning';
|
|
if (rowData.joursRetard > 60) severity = 'danger';
|
|
else if (rowData.joursRetard > 30) severity = 'warning';
|
|
|
|
return (
|
|
<div className="flex align-items-center gap-2">
|
|
<Tag value={`+${rowData.joursRetard}j`} severity={severity} />
|
|
{rowData.joursRetard > 60 && <i className="pi pi-exclamation-triangle text-red-500" />}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const relancesBodyTemplate = (rowData: any) => {
|
|
return (
|
|
<div className="flex align-items-center gap-2">
|
|
<Badge value={rowData.nbRelances} severity={rowData.nbRelances > 2 ? 'danger' : 'warning'} />
|
|
{rowData.derniereRelance && (
|
|
<span className="text-sm text-color-secondary">
|
|
{new Date(rowData.derniereRelance).toLocaleDateString('fr-FR')}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const risqueBodyTemplate = (rowData: any) => {
|
|
const config: Record<string, { color: "success" | "warning" | "danger", icon: string }> = {
|
|
'FAIBLE': { color: 'success', icon: 'pi-check-circle' },
|
|
'MOYEN': { color: 'warning', icon: 'pi-exclamation-triangle' },
|
|
'FORT': { color: 'danger', icon: 'pi-times-circle' }
|
|
};
|
|
|
|
const riskConfig = config[rowData.risqueClient as keyof typeof config];
|
|
return (
|
|
<div className="flex align-items-center gap-2">
|
|
<i className={`pi ${riskConfig.icon} text-${riskConfig.color}`} />
|
|
<Tag value={rowData.risqueClient} severity={riskConfig.color} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const actionsBodyTemplate = (rowData: any) => {
|
|
return (
|
|
<ActionButtonGroup>
|
|
<ActionButton
|
|
icon="pi pi-send"
|
|
color="orange"
|
|
tooltip="Envoyer relance"
|
|
onClick={() => ouvrirRelance(rowData)}
|
|
disabled={rowData.joursRetard <= 0}
|
|
/>
|
|
|
|
<ActionButton
|
|
icon="pi pi-phone"
|
|
color="blue"
|
|
tooltip="Appeler client"
|
|
onClick={() => {}}
|
|
/>
|
|
|
|
<ViewButton
|
|
tooltip="Voir historique"
|
|
onClick={() => {}}
|
|
/>
|
|
</ActionButtonGroup>
|
|
);
|
|
};
|
|
|
|
const header = (
|
|
<div className="flex justify-content-between align-items-center">
|
|
<h5 className="m-0">Gestion des Relances</h5>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
label="Relances groupées"
|
|
icon="pi pi-send"
|
|
severity="warning"
|
|
onClick={envoyerRelancesGroupees}
|
|
disabled={selectedFactures.length === 0}
|
|
/>
|
|
<Button
|
|
label="Configuration"
|
|
icon="pi pi-cog"
|
|
outlined
|
|
onClick={() => setConfigDialog(true)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="grid">
|
|
<Toast ref={toast} />
|
|
|
|
{/* Métriques Relances */}
|
|
<div className="col-12">
|
|
<div className="grid">
|
|
<div className="col-12 md:col-2">
|
|
<Card>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-red-500">
|
|
{new Intl.NumberFormat('fr-FR', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
notation: 'compact'
|
|
}).format(metriques.montantEnAttente)}
|
|
</div>
|
|
<div className="text-color-secondary">En attente</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-2">
|
|
<Card>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-orange-500">{metriques.nombreFacturesImpayees}</div>
|
|
<div className="text-color-secondary">Factures impayées</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-2">
|
|
<Card>
|
|
<div className="text-center">
|
|
<Knob
|
|
value={metriques.tauxRecouvrement}
|
|
size={60}
|
|
strokeWidth={8}
|
|
valueColor="#10B981"
|
|
/>
|
|
<div className="text-color-secondary mt-2">Taux recouvrement</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-2">
|
|
<Card>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-blue-500">{metriques.delaiMoyenPaiement}j</div>
|
|
<div className="text-color-secondary">Délai moyen</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-2">
|
|
<Card>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-cyan-500">{metriques.relancesEnvoyees}</div>
|
|
<div className="text-color-secondary">Relances ce mois</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-2">
|
|
<Card>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-green-500">
|
|
{new Intl.NumberFormat('fr-FR', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
notation: 'compact'
|
|
}).format(metriques.montantRecouvre)}
|
|
</div>
|
|
<div className="text-color-secondary">Recouvré ce mois</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-12">
|
|
<Divider />
|
|
</div>
|
|
|
|
{/* Tableau factures */}
|
|
<div className="col-12">
|
|
<Card>
|
|
<DataTable
|
|
value={factures}
|
|
loading={loading}
|
|
selection={selectedFactures}
|
|
onSelectionChange={(e) => setSelectedFactures(e.value)}
|
|
paginator
|
|
rows={15}
|
|
dataKey="id"
|
|
header={header}
|
|
emptyMessage="Aucune facture trouvée"
|
|
responsiveLayout="scroll"
|
|
>
|
|
<Column selectionMode="multiple" headerStyle={{ width: '3rem' }} />
|
|
<Column field="numero" header="Facture" sortable style={{ minWidth: '120px' }} />
|
|
<Column field="client" header="Client" sortable style={{ minWidth: '150px' }} />
|
|
<Column
|
|
field="montantTTC"
|
|
header="Montant"
|
|
body={(rowData) => new Intl.NumberFormat('fr-FR', {
|
|
style: 'currency',
|
|
currency: 'EUR'
|
|
}).format(rowData.montantTTC)}
|
|
sortable
|
|
style={{ minWidth: '120px' }}
|
|
/>
|
|
<Column
|
|
field="dateEcheance"
|
|
header="Échéance"
|
|
body={(rowData) => new Date(rowData.dateEcheance).toLocaleDateString('fr-FR')}
|
|
sortable
|
|
style={{ minWidth: '100px' }}
|
|
/>
|
|
<Column header="Retard" body={retardBodyTemplate} sortable style={{ minWidth: '100px' }} />
|
|
<Column header="Relances" body={relancesBodyTemplate} style={{ minWidth: '120px' }} />
|
|
<Column header="Risque" body={risqueBodyTemplate} style={{ minWidth: '120px' }} />
|
|
<Column body={actionsBodyTemplate} style={{ minWidth: '150px' }} />
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Dialog Relance */}
|
|
<Dialog
|
|
visible={relanceDialog}
|
|
style={{ width: '800px' }}
|
|
header={`Relance - ${selectedFacture?.numero}`}
|
|
modal
|
|
onHide={() => setRelanceDialog(false)}
|
|
>
|
|
{selectedFacture && (
|
|
<div className="grid">
|
|
<div className="col-12 md:col-6">
|
|
<Card title="Informations Facture">
|
|
<div className="flex flex-column gap-3">
|
|
<div><strong>Client:</strong> {selectedFacture.client}</div>
|
|
<div><strong>Montant:</strong> {new Intl.NumberFormat('fr-FR', {
|
|
style: 'currency',
|
|
currency: 'EUR'
|
|
}).format(selectedFacture.montantTTC)}</div>
|
|
<div><strong>Échéance:</strong> {new Date(selectedFacture.dateEcheance).toLocaleDateString('fr-FR')}</div>
|
|
<div><strong>Retard:</strong> <Tag value={`${selectedFacture.joursRetard} jours`} severity="danger" /></div>
|
|
<div><strong>Relances déjà envoyées:</strong> {selectedFacture.nbRelances}</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-6">
|
|
<Card title="Type de Relance">
|
|
<div className="flex flex-column gap-3">
|
|
<Dropdown
|
|
value={typeRelance}
|
|
options={typesRelance}
|
|
onChange={(e) => {
|
|
setTypeRelance(e.value);
|
|
setMessageRelance(modeleMessages[e.value as keyof typeof modeleMessages] || '');
|
|
}}
|
|
placeholder="Sélectionnez le type"
|
|
className="w-full"
|
|
/>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">Date d'envoi</label>
|
|
<Calendar
|
|
value={dateRelance}
|
|
onChange={(e) => setDateRelance(e.value || null)}
|
|
dateFormat="dd/mm/yy"
|
|
showIcon
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12">
|
|
<Card title="Message de Relance">
|
|
<InputTextarea
|
|
value={messageRelance}
|
|
onChange={(e) => setMessageRelance(e.target.value)}
|
|
rows={12}
|
|
className="w-full"
|
|
placeholder="Rédigez votre message de relance..."
|
|
/>
|
|
|
|
<div className="flex justify-content-end gap-2 mt-4">
|
|
<Button
|
|
label="Annuler"
|
|
icon="pi pi-times"
|
|
outlined
|
|
onClick={() => setRelanceDialog(false)}
|
|
/>
|
|
<Button
|
|
label="Envoyer Relance"
|
|
icon="pi pi-send"
|
|
severity="warning"
|
|
onClick={envoyerRelance}
|
|
/>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Dialog>
|
|
|
|
{/* Dialog Configuration */}
|
|
<Dialog
|
|
visible={configDialog}
|
|
style={{ width: '600px' }}
|
|
header="Configuration des Relances"
|
|
modal
|
|
onHide={() => setConfigDialog(false)}
|
|
>
|
|
<div className="flex flex-column gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">Délais automatiques (jours)</label>
|
|
<div className="grid">
|
|
<div className="col-6">
|
|
<label className="text-sm">1ère relance amiable</label>
|
|
<div className="p-inputgroup">
|
|
<span className="p-inputgroup-addon">
|
|
<i className="pi pi-calendar"></i>
|
|
</span>
|
|
<input className="p-inputtext" value={configRelances.delaiAmiable1} readOnly />
|
|
<span className="p-inputgroup-addon">jours</span>
|
|
</div>
|
|
</div>
|
|
<div className="col-6">
|
|
<label className="text-sm">2ème relance amiable</label>
|
|
<div className="p-inputgroup">
|
|
<span className="p-inputgroup-addon">
|
|
<i className="pi pi-calendar"></i>
|
|
</span>
|
|
<input className="p-inputtext" value={configRelances.delaiAmiable2} readOnly />
|
|
<span className="p-inputgroup-addon">jours</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex align-items-center gap-2">
|
|
<Checkbox
|
|
checked={configRelances.automatique}
|
|
onChange={(e) => setConfigRelances({...configRelances, automatique: e.checked})}
|
|
/>
|
|
<label>Envoi automatique des relances</label>
|
|
</div>
|
|
|
|
<div className="flex justify-content-end gap-2">
|
|
<Button
|
|
label="Annuler"
|
|
icon="pi pi-times"
|
|
outlined
|
|
onClick={() => setConfigDialog(false)}
|
|
/>
|
|
<Button
|
|
label="Sauvegarder"
|
|
icon="pi pi-save"
|
|
severity="success"
|
|
onClick={() => {
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Configuration sauvegardée',
|
|
detail: 'Paramètres de relances mis à jour',
|
|
life: 3000
|
|
});
|
|
setConfigDialog(false);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RelancesFactures; |