Initial commit
This commit is contained in:
684
app/(main)/factures/relances/page.tsx
Normal file
684
app/(main)/factures/relances/page.tsx
Normal file
@@ -0,0 +1,684 @@
|
||||
'use client';
|
||||
|
||||
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';
|
||||
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 = {
|
||||
'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="warning"
|
||||
tooltip="Envoyer relance"
|
||||
onClick={() => ouvrirRelance(rowData)}
|
||||
disabled={rowData.joursRetard <= 0}
|
||||
/>
|
||||
|
||||
<ActionButton
|
||||
icon="pi pi-phone"
|
||||
color="info"
|
||||
tooltip="Appeler client"
|
||||
/>
|
||||
|
||||
<ViewButton
|
||||
tooltip="Voir historique"
|
||||
/>
|
||||
</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;
|
||||
Reference in New Issue
Block a user