Initial commit
This commit is contained in:
603
app/(main)/factures/relances/[id]/page.tsx
Normal file
603
app/(main)/factures/relances/[id]/page.tsx
Normal file
@@ -0,0 +1,603 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { Card } from 'primereact/card';
|
||||
import { Button } from 'primereact/button';
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { Calendar } from 'primereact/calendar';
|
||||
import { DataTable } from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { ProgressSpinner } from 'primereact/progressspinner';
|
||||
import { Toolbar } from 'primereact/toolbar';
|
||||
import { Timeline } from 'primereact/timeline';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { Badge } from 'primereact/badge';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { Checkbox } from 'primereact/checkbox';
|
||||
import { factureService } from '../../../../../services/api';
|
||||
import { formatDate, formatCurrency } from '../../../../../utils/formatters';
|
||||
import type { Facture } from '../../../../../types/btp';
|
||||
|
||||
interface Relance {
|
||||
id: string;
|
||||
type: 'EMAIL' | 'COURRIER' | 'TELEPHONE' | 'SMS';
|
||||
niveau: number;
|
||||
dateEnvoi: Date;
|
||||
destinataire: string;
|
||||
objet: string;
|
||||
message: string;
|
||||
statut: 'ENVOYEE' | 'LUE' | 'REPONDUE' | 'ECHEC';
|
||||
reponse?: string;
|
||||
dateReponse?: Date;
|
||||
}
|
||||
|
||||
interface RelanceTemplate {
|
||||
id: string;
|
||||
nom: string;
|
||||
type: 'EMAIL' | 'COURRIER' | 'TELEPHONE' | 'SMS';
|
||||
niveau: number;
|
||||
objet: string;
|
||||
message: string;
|
||||
delaiJours: number;
|
||||
}
|
||||
|
||||
const FactureRelancePage = () => {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const toast = useRef<Toast>(null);
|
||||
|
||||
const [facture, setFacture] = useState<Facture | null>(null);
|
||||
const [relances, setRelances] = useState<Relance[]>([]);
|
||||
const [templates, setTemplates] = useState<RelanceTemplate[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [showRelanceDialog, setShowRelanceDialog] = useState(false);
|
||||
|
||||
const [nouvelleRelance, setNouvelleRelance] = useState({
|
||||
type: 'EMAIL' as 'EMAIL' | 'COURRIER' | 'TELEPHONE' | 'SMS',
|
||||
destinataire: '',
|
||||
objet: '',
|
||||
message: '',
|
||||
dateEnvoi: new Date(),
|
||||
utiliserTemplate: false,
|
||||
templateId: ''
|
||||
});
|
||||
|
||||
const factureId = params.id as string;
|
||||
|
||||
const typeOptions = [
|
||||
{ label: 'Email', value: 'EMAIL', icon: 'pi pi-envelope' },
|
||||
{ label: 'Courrier', value: 'COURRIER', icon: 'pi pi-send' },
|
||||
{ label: 'Téléphone', value: 'TELEPHONE', icon: 'pi pi-phone' },
|
||||
{ label: 'SMS', value: 'SMS', icon: 'pi pi-mobile' }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [factureId]);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Charger la facture
|
||||
const factureResponse = await factureService.getById(factureId);
|
||||
setFacture(factureResponse.data);
|
||||
|
||||
// TODO: Charger les relances et templates depuis l'API
|
||||
// const relancesResponse = await factureService.getRelances(factureId);
|
||||
// const templatesResponse = await factureService.getRelanceTemplates();
|
||||
|
||||
// Données simulées pour la démonstration
|
||||
const mockRelances: Relance[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'EMAIL',
|
||||
niveau: 1,
|
||||
dateEnvoi: new Date('2024-03-01'),
|
||||
destinataire: 'client@example.com',
|
||||
objet: 'Rappel - Facture en attente de paiement',
|
||||
message: 'Nous vous rappelons que votre facture est en attente de paiement...',
|
||||
statut: 'LUE'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'TELEPHONE',
|
||||
niveau: 2,
|
||||
dateEnvoi: new Date('2024-03-15'),
|
||||
destinataire: '01 23 45 67 89',
|
||||
objet: 'Appel de relance',
|
||||
message: 'Appel téléphonique pour relance de paiement',
|
||||
statut: 'REPONDUE',
|
||||
reponse: 'Client confirme le paiement sous 48h',
|
||||
dateReponse: new Date('2024-03-15')
|
||||
}
|
||||
];
|
||||
|
||||
const mockTemplates: RelanceTemplate[] = [
|
||||
{
|
||||
id: '1',
|
||||
nom: 'Première relance aimable',
|
||||
type: 'EMAIL',
|
||||
niveau: 1,
|
||||
objet: 'Rappel - Facture #{numero} en attente de paiement',
|
||||
message: 'Madame, Monsieur,\n\nNous vous rappelons que votre facture #{numero} d\'un montant de {montant} est en attente de paiement depuis le {dateEcheance}.\n\nMerci de bien vouloir régulariser cette situation dans les meilleurs délais.\n\nCordialement,',
|
||||
delaiJours: 7
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
nom: 'Relance ferme',
|
||||
type: 'COURRIER',
|
||||
niveau: 2,
|
||||
objet: 'Mise en demeure - Facture #{numero}',
|
||||
message: 'Madame, Monsieur,\n\nMalgré notre précédent rappel, votre facture #{numero} d\'un montant de {montant} demeure impayée.\n\nNous vous mettons en demeure de procéder au règlement sous 8 jours, faute de quoi nous nous verrons contraints d\'engager des poursuites.\n\nCordialement,',
|
||||
delaiJours: 15
|
||||
}
|
||||
];
|
||||
|
||||
setRelances(mockRelances);
|
||||
setTemplates(mockTemplates);
|
||||
|
||||
// Pré-remplir le destinataire
|
||||
if (factureResponse.data.client) {
|
||||
const client = factureResponse.data.client;
|
||||
setNouvelleRelance(prev => ({
|
||||
...prev,
|
||||
destinataire: typeof client === 'string' ? client : client.email || client.nom
|
||||
}));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement:', error);
|
||||
toast.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Erreur',
|
||||
detail: 'Impossible de charger les données'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTemplateChange = (templateId: string) => {
|
||||
const template = templates.find(t => t.id === templateId);
|
||||
if (template && facture) {
|
||||
setNouvelleRelance(prev => ({
|
||||
...prev,
|
||||
templateId,
|
||||
type: template.type,
|
||||
objet: template.objet
|
||||
.replace('{numero}', facture.numero)
|
||||
.replace('{montant}', formatCurrency(facture.montantTTC)),
|
||||
message: template.message
|
||||
.replace('{numero}', facture.numero)
|
||||
.replace('{montant}', formatCurrency(facture.montantTTC))
|
||||
.replace('{dateEcheance}', formatDate(facture.dateEcheance))
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendRelance = async () => {
|
||||
try {
|
||||
setSending(true);
|
||||
|
||||
if (!nouvelleRelance.destinataire || !nouvelleRelance.objet || !nouvelleRelance.message) {
|
||||
toast.current?.show({
|
||||
severity: 'warn',
|
||||
summary: 'Attention',
|
||||
detail: 'Veuillez remplir tous les champs obligatoires'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Appel API pour envoyer la relance
|
||||
// await factureService.sendRelance(factureId, nouvelleRelance);
|
||||
|
||||
toast.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Succès',
|
||||
detail: 'Relance envoyée avec succès'
|
||||
});
|
||||
|
||||
setShowRelanceDialog(false);
|
||||
loadData();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'envoi:', error);
|
||||
toast.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Erreur',
|
||||
detail: 'Erreur lors de l\'envoi de la relance'
|
||||
});
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatutSeverity = (statut: string) => {
|
||||
switch (statut) {
|
||||
case 'ENVOYEE': return 'info';
|
||||
case 'LUE': return 'warning';
|
||||
case 'REPONDUE': return 'success';
|
||||
case 'ECHEC': return 'danger';
|
||||
default: return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'EMAIL': return 'pi pi-envelope';
|
||||
case 'COURRIER': return 'pi pi-send';
|
||||
case 'TELEPHONE': return 'pi pi-phone';
|
||||
case 'SMS': return 'pi pi-mobile';
|
||||
default: return 'pi pi-circle';
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeColor = (type: string) => {
|
||||
switch (type) {
|
||||
case 'EMAIL': return '#3B82F6';
|
||||
case 'COURRIER': return '#8B5CF6';
|
||||
case 'TELEPHONE': return '#10B981';
|
||||
case 'SMS': return '#F59E0B';
|
||||
default: return '#6B7280';
|
||||
}
|
||||
};
|
||||
|
||||
const toolbarStartTemplate = () => (
|
||||
<div className="flex align-items-center gap-2">
|
||||
<Button
|
||||
icon="pi pi-arrow-left"
|
||||
label="Retour"
|
||||
className="p-button-outlined"
|
||||
onClick={() => router.push(`/factures/${factureId}`)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const toolbarEndTemplate = () => (
|
||||
<div className="flex align-items-center gap-2">
|
||||
<Button
|
||||
label="Nouvelle relance"
|
||||
icon="pi pi-plus"
|
||||
onClick={() => setShowRelanceDialog(true)}
|
||||
disabled={!facture || facture.statut === 'PAYEE'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-content-center align-items-center min-h-screen">
|
||||
<ProgressSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!facture) {
|
||||
return (
|
||||
<div className="flex justify-content-center align-items-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<i className="pi pi-exclamation-triangle text-6xl text-orange-500 mb-3"></i>
|
||||
<h3>Facture introuvable</h3>
|
||||
<p className="text-600 mb-4">La facture demandée n'existe pas</p>
|
||||
<Button
|
||||
label="Retour à la liste"
|
||||
icon="pi pi-arrow-left"
|
||||
onClick={() => router.push('/factures')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<Toast ref={toast} />
|
||||
|
||||
<div className="col-12">
|
||||
<Toolbar start={toolbarStartTemplate} end={toolbarEndTemplate} />
|
||||
</div>
|
||||
|
||||
{/* Informations de la facture */}
|
||||
<div className="col-12">
|
||||
<Card>
|
||||
<div className="flex justify-content-between align-items-start mb-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-2">Relances - Facture #{facture.numero}</h2>
|
||||
<p className="text-600 mb-3">{facture.objet}</p>
|
||||
<Tag
|
||||
value={facture.statut}
|
||||
severity={facture.statut === 'EN_RETARD' ? 'danger' : 'warning'}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-3xl font-bold text-red-500 mb-2">
|
||||
{formatCurrency(facture.montantTTC - (facture.montantPaye || 0))}
|
||||
</div>
|
||||
<div className="text-sm text-600">
|
||||
Montant en retard
|
||||
</div>
|
||||
<div className="text-sm text-600">
|
||||
Échéance: {formatDate(facture.dateEcheance)}
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-red-600">
|
||||
Retard: {Math.ceil((new Date().getTime() - new Date(facture.dateEcheance).getTime()) / (1000 * 60 * 60 * 24))} jours
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Historique des relances */}
|
||||
<div className="col-12 lg:col-8">
|
||||
<Card title="Historique des relances">
|
||||
{relances.length > 0 ? (
|
||||
<Timeline
|
||||
value={relances}
|
||||
opposite={(item) => (
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-semibold">{formatDate(item.dateEnvoi)}</div>
|
||||
<div className="text-xs text-600">{item.destinataire}</div>
|
||||
</div>
|
||||
)}
|
||||
content={(item) => (
|
||||
<div className="flex align-items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex align-items-center mb-2">
|
||||
<Badge
|
||||
value={`Niveau ${item.niveau}`}
|
||||
severity="info"
|
||||
className="mr-2"
|
||||
/>
|
||||
<Tag
|
||||
value={item.type}
|
||||
style={{ backgroundColor: getTypeColor(item.type) }}
|
||||
className="mr-2"
|
||||
/>
|
||||
<Tag
|
||||
value={item.statut}
|
||||
severity={getStatutSeverity(item.statut)}
|
||||
/>
|
||||
</div>
|
||||
<div className="font-semibold mb-1">{item.objet}</div>
|
||||
<div className="text-sm text-600 mb-2 line-height-3">
|
||||
{item.message.length > 100
|
||||
? `${item.message.substring(0, 100)}...`
|
||||
: item.message
|
||||
}
|
||||
</div>
|
||||
{item.reponse && (
|
||||
<div className="p-2 border-round bg-green-50 border-left-3 border-green-500">
|
||||
<div className="text-sm font-semibold text-green-900 mb-1">
|
||||
Réponse ({formatDate(item.dateReponse!)})
|
||||
</div>
|
||||
<div className="text-sm text-green-800">{item.reponse}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
marker={(item) => (
|
||||
<span
|
||||
className={`flex w-2rem h-2rem align-items-center justify-content-center text-white border-circle z-1 shadow-1`}
|
||||
style={{ backgroundColor: getTypeColor(item.type) }}
|
||||
>
|
||||
<i className={getTypeIcon(item.type)}></i>
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-center p-4">
|
||||
<i className="pi pi-inbox text-4xl text-400 mb-3"></i>
|
||||
<p className="text-600">Aucune relance envoyée pour cette facture</p>
|
||||
<Button
|
||||
label="Envoyer la première relance"
|
||||
icon="pi pi-plus"
|
||||
onClick={() => setShowRelanceDialog(true)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Statistiques et actions */}
|
||||
<div className="col-12 lg:col-4">
|
||||
<Card title="Statistiques">
|
||||
<div className="grid">
|
||||
<div className="col-12">
|
||||
<div className="text-center p-3 border-round bg-blue-50">
|
||||
<div className="text-blue-600 font-bold text-xl mb-2">
|
||||
{relances.length}
|
||||
</div>
|
||||
<div className="text-blue-900 font-semibold">Relances envoyées</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="text-center p-3 border-round bg-orange-50">
|
||||
<div className="text-orange-600 font-bold text-xl mb-2">
|
||||
{relances.filter(r => r.statut === 'REPONDUE').length}
|
||||
</div>
|
||||
<div className="text-orange-900 font-semibold">Réponses reçues</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="text-center p-3 border-round bg-red-50">
|
||||
<div className="text-red-600 font-bold text-xl mb-2">
|
||||
{Math.ceil((new Date().getTime() - new Date(facture.dateEcheance).getTime()) / (1000 * 60 * 60 * 24))}
|
||||
</div>
|
||||
<div className="text-red-900 font-semibold">Jours de retard</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<h6>Actions recommandées</h6>
|
||||
<div className="flex flex-column gap-2">
|
||||
<Button
|
||||
label="Appel téléphonique"
|
||||
icon="pi pi-phone"
|
||||
className="p-button-outlined p-button-sm"
|
||||
onClick={() => {
|
||||
setNouvelleRelance(prev => ({ ...prev, type: 'TELEPHONE' }));
|
||||
setShowRelanceDialog(true);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label="Mise en demeure"
|
||||
icon="pi pi-exclamation-triangle"
|
||||
className="p-button-outlined p-button-sm p-button-warning"
|
||||
onClick={() => {
|
||||
setNouvelleRelance(prev => ({
|
||||
...prev,
|
||||
type: 'COURRIER',
|
||||
utiliserTemplate: true,
|
||||
templateId: '2'
|
||||
}));
|
||||
handleTemplateChange('2');
|
||||
setShowRelanceDialog(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Dialog de nouvelle relance */}
|
||||
<Dialog
|
||||
header="Nouvelle relance"
|
||||
visible={showRelanceDialog}
|
||||
onHide={() => setShowRelanceDialog(false)}
|
||||
style={{ width: '800px' }}
|
||||
footer={
|
||||
<div className="flex justify-content-end gap-2">
|
||||
<Button
|
||||
label="Annuler"
|
||||
icon="pi pi-times"
|
||||
className="p-button-outlined"
|
||||
onClick={() => setShowRelanceDialog(false)}
|
||||
/>
|
||||
<Button
|
||||
label="Envoyer"
|
||||
icon="pi pi-send"
|
||||
onClick={handleSendRelance}
|
||||
loading={sending}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="grid">
|
||||
<div className="col-12">
|
||||
<div className="flex align-items-center mb-3">
|
||||
<Checkbox
|
||||
inputId="utiliserTemplate"
|
||||
checked={nouvelleRelance.utiliserTemplate}
|
||||
onChange={(e) => setNouvelleRelance(prev => ({
|
||||
...prev,
|
||||
utiliserTemplate: e.checked || false,
|
||||
templateId: e.checked ? templates[0]?.id || '' : ''
|
||||
}))}
|
||||
/>
|
||||
<label htmlFor="utiliserTemplate" className="ml-2">Utiliser un template</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{nouvelleRelance.utiliserTemplate && (
|
||||
<div className="col-12">
|
||||
<div className="field">
|
||||
<label htmlFor="template" className="font-semibold">Template</label>
|
||||
<Dropdown
|
||||
id="template"
|
||||
value={nouvelleRelance.templateId}
|
||||
options={templates.map(t => ({ label: t.nom, value: t.id }))}
|
||||
onChange={(e) => handleTemplateChange(e.value)}
|
||||
className="w-full"
|
||||
placeholder="Sélectionner un template"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<div className="field">
|
||||
<label htmlFor="type" className="font-semibold">Type de relance *</label>
|
||||
<Dropdown
|
||||
id="type"
|
||||
value={nouvelleRelance.type}
|
||||
options={typeOptions}
|
||||
onChange={(e) => setNouvelleRelance(prev => ({ ...prev, type: e.value }))}
|
||||
className="w-full"
|
||||
itemTemplate={(option) => (
|
||||
<div className="flex align-items-center">
|
||||
<i className={`${option.icon} mr-2`}></i>
|
||||
{option.label}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<div className="field">
|
||||
<label htmlFor="dateEnvoi" className="font-semibold">Date d'envoi</label>
|
||||
<Calendar
|
||||
id="dateEnvoi"
|
||||
value={nouvelleRelance.dateEnvoi}
|
||||
onChange={(e) => setNouvelleRelance(prev => ({ ...prev, dateEnvoi: e.value || new Date() }))}
|
||||
className="w-full"
|
||||
dateFormat="dd/mm/yy"
|
||||
showTime
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="field">
|
||||
<label htmlFor="destinataire" className="font-semibold">Destinataire *</label>
|
||||
<InputTextarea
|
||||
id="destinataire"
|
||||
value={nouvelleRelance.destinataire}
|
||||
onChange={(e) => setNouvelleRelance(prev => ({ ...prev, destinataire: e.target.value }))}
|
||||
className="w-full"
|
||||
rows={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="field">
|
||||
<label htmlFor="objet" className="font-semibold">Objet *</label>
|
||||
<InputTextarea
|
||||
id="objet"
|
||||
value={nouvelleRelance.objet}
|
||||
onChange={(e) => setNouvelleRelance(prev => ({ ...prev, objet: e.target.value }))}
|
||||
className="w-full"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="field">
|
||||
<label htmlFor="message" className="font-semibold">Message *</label>
|
||||
<InputTextarea
|
||||
id="message"
|
||||
value={nouvelleRelance.message}
|
||||
onChange={(e) => setNouvelleRelance(prev => ({ ...prev, message: e.target.value }))}
|
||||
className="w-full"
|
||||
rows={8}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FactureRelancePage;
|
||||
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