Files
btpxpress-frontend/app/(main)/factures/relances/[id]/page.tsx
dahoud a8825a058b Fix: Corriger toutes les erreurs de build du frontend
- 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>
2025-10-18 13:23:08 +00:00

607 lines
26 KiB
TypeScript

'use client';
export const dynamic = 'force-dynamic';
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';
import { StatutFacture } 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);
// 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.client) {
const client = factureResponse.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 === StatutFacture.ECHUE ? '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;