Files
MAD-Platform/components/BudgetContent.tsx
2026-02-15 15:05:59 +01:00

556 lines
26 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react';
import { useNotification } from './NotificationProvider';
import Link from 'next/link';
interface HistoriqueItem {
id: string;
date: string;
adherentNom: string;
montant: number;
}
interface PrescripteurBudget {
id: string;
value: string;
montant: number;
consommé: number;
restant: number;
ajustementConsomme: number;
historique: HistoriqueItem[];
}
interface BudgetData {
global: {
totalAlloue: number;
totalConsomme: number;
totalRestant: number;
};
prescripteurs: PrescripteurBudget[];
}
export default function BudgetContent() {
const { showNotification } = useNotification();
const [data, setData] = useState<BudgetData | null>(null);
const [loading, setLoading] = useState(true);
const [savingId, setSavingId] = useState<string | null>(null);
const [addModalPrescripteur, setAddModalPrescripteur] = useState<string | null>(null);
const [addAmount, setAddAmount] = useState('');
const [rectifierModalPrescripteur, setRectifierModalPrescripteur] = useState<PrescripteurBudget | null>(null);
const [rectifierBudget, setRectifierBudget] = useState('');
const [rectifierAjustement, setRectifierAjustement] = useState('');
const [expandedPrescripteur, setExpandedPrescripteur] = useState<string | null>(null);
useEffect(() => {
fetchBudgets();
}, []);
const fetchBudgets = async () => {
setLoading(true);
try {
const response = await fetch('/api/budget');
if (response.ok) {
const res = await response.json();
setData(res);
} else {
showNotification('error', 'Erreur lors du chargement des budgets');
}
} catch {
showNotification('error', 'Erreur lors du chargement des budgets');
} finally {
setLoading(false);
}
};
const handleAddMoney = async () => {
if (!addModalPrescripteur) return;
const raw = addAmount.replace(',', '.');
const montantToAdd = parseFloat(raw);
if (isNaN(montantToAdd) || montantToAdd <= 0) {
showNotification('error', 'Montant invalide');
return;
}
setSavingId(addModalPrescripteur);
try {
const response = await fetch('/api/budget', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prescripteurValue: addModalPrescripteur, montantToAdd }),
});
if (response.ok) {
showNotification('success', 'Budget mis à jour');
setAddModalPrescripteur(null);
setAddAmount('');
fetchBudgets();
} else {
const res = await response.json();
showNotification('error', res.error || "Erreur lors de l'ajout");
}
} catch {
showNotification('error', "Erreur lors de l'ajout");
} finally {
setSavingId(null);
}
};
const handleRectifier = async () => {
if (!rectifierModalPrescripteur) return;
const montant = parseFloat(rectifierBudget.replace(',', '.'));
const ajustement = parseFloat(rectifierAjustement.replace(',', '.'));
if (isNaN(montant) || montant < 0) {
showNotification('error', 'Budget invalide');
return;
}
setSavingId(rectifierModalPrescripteur.value);
try {
const response = await fetch('/api/budget', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prescripteurValue: rectifierModalPrescripteur.value,
rectifier: true,
montant,
ajustementConsomme: isNaN(ajustement) ? 0 : ajustement,
}),
});
if (response.ok) {
showNotification('success', 'Corrections enregistrées');
setRectifierModalPrescripteur(null);
setRectifierBudget('');
setRectifierAjustement('');
fetchBudgets();
} else {
const res = await response.json();
showNotification('error', res.error || 'Erreur lors de la correction');
}
} catch {
showNotification('error', 'Erreur lors de la correction');
} finally {
setSavingId(null);
}
};
const formatEuro = (n: number) => n.toFixed(2).replace('.', ',') + ' €';
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('fr-FR', {
day: '2-digit',
month: 'short',
year: 'numeric',
});
};
if (loading) {
return (
<div className="bg-white rounded-lg shadow-sm p-8">
<div className="text-center text-sm text-gray-500">Chargement...</div>
</div>
);
}
if (!data || data.prescripteurs.length === 0) {
return (
<div className="bg-white rounded-lg shadow-sm p-8">
<div className="text-center text-gray-500">
<p className="mb-4">Aucun prescripteur configuré.</p>
<p className="text-sm mb-6">
Ajoutez des prescripteurs dans la configuration pour pouvoir définir leurs budgets.
</p>
<Link
href="/dashboard/parametres/configuration"
className="inline-flex items-center gap-2 px-4 py-2 bg-lblue text-white rounded-lg hover:bg-dblue transition-colors text-sm font-medium"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Aller à la configuration
</Link>
</div>
</div>
);
}
const { global, prescripteurs } = data;
return (
<div className="space-y-6">
{/* Blocs budget global - style dashboard */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 sm:gap-5">
<div className="bg-white rounded-xl shadow-sm p-4 sm:p-6 border border-gray-100 hover:shadow-lg hover:border-dyellow/30 transition-all duration-300 group">
<div className="flex items-start justify-between mb-3 sm:mb-4">
<div className="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-gradient-to-br from-dyellow/20 to-dyellow/10 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<svg className="w-5 h-5 sm:w-6 sm:h-6 text-dyellow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
<p className="text-[10px] sm:text-xs text-gray-600 mb-1.5 sm:mb-2 font-medium uppercase tracking-wide">
Budget total alloué
</p>
<p className="text-2xl sm:text-3xl font-bold text-gray-900">{formatEuro(global.totalAlloue)}</p>
</div>
<div className="bg-white rounded-xl shadow-sm p-4 sm:p-6 border border-gray-100 hover:shadow-lg hover:border-dyellow/30 transition-all duration-300 group">
<div className="flex items-start justify-between mb-3 sm:mb-4">
<div className="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-gradient-to-br from-orange-100 to-orange-50 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<svg className="w-5 h-5 sm:w-6 sm:h-6 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
</div>
<p className="text-[10px] sm:text-xs text-gray-600 mb-1.5 sm:mb-2 font-medium uppercase tracking-wide">
Budget consommé
</p>
<p className="text-2xl sm:text-3xl font-bold text-orange-600">{formatEuro(global.totalConsomme)}</p>
</div>
<div className="bg-white rounded-xl shadow-sm p-4 sm:p-6 border border-gray-100 hover:shadow-lg hover:border-lgreen/30 transition-all duration-300 group">
<div className="flex items-start justify-between mb-3 sm:mb-4">
<div className="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-gradient-to-br from-lgreen/20 to-lgreen/10 flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform duration-300">
<svg className="w-5 h-5 sm:w-6 sm:h-6 text-lgreen" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
<p className="text-[10px] sm:text-xs text-gray-600 mb-1.5 sm:mb-2 font-medium uppercase tracking-wide">
Budget restant
</p>
<p className="text-2xl sm:text-3xl font-bold text-lgreen">{formatEuro(global.totalRestant)}</p>
</div>
</div>
{/* Liste des budgets par prescripteur */}
<div className="bg-white rounded-lg shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900">Budget par prescripteur</h2>
<p className="text-sm text-gray-500 mt-1">
Cliquez sur un prescripteur pour voir l&apos;historique des participations. Utilisez « Ajouter » pour créditer le budget.
</p>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Prescripteur
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Consommé
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Restant
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider w-56">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{prescripteurs.map((item) => (
<React.Fragment key={item.value}>
<tr
key={item.value}
className="hover:bg-gray-50 cursor-pointer"
onClick={() => setExpandedPrescripteur(expandedPrescripteur === item.value ? null : item.value)}
>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900">{item.value}</span>
{item.historique.length > 0 && (
<span className="text-gray-400">
<svg
className={`w-4 h-4 transition-transform ${expandedPrescripteur === item.value ? 'rotate-90' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</span>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-orange-600 font-medium">
{formatEuro(item.consommé)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`text-sm font-medium ${
item.consommé > item.montant || (item.montant > 0 && item.restant < item.montant * 0.2)
? 'text-red-600'
: 'text-lgreen'
}`}
>
{formatEuro(item.restant)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center gap-2 justify-end">
<button
onClick={() => {
setAddModalPrescripteur(item.value);
setAddAmount('');
}}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white bg-lgreen rounded-lg hover:bg-dgreen transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Ajouter
</button>
<button
onClick={() => {
setRectifierModalPrescripteur(item);
setRectifierBudget(item.montant.toString().replace('.', ','));
setRectifierAjustement((item.ajustementConsomme ?? 0).toString().replace('.', ','));
}}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white bg-lblue rounded-lg hover:bg-dblue transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Rectifier
</button>
</div>
</td>
</tr>
{expandedPrescripteur === item.value && item.historique.length > 0 && (
<tr key={`${item.value}-hist`} className="bg-gray-50">
<td colSpan={4} className="px-6 py-4">
<div className="space-y-2">
<h4 className="text-xs font-semibold text-gray-700 uppercase tracking-wide">
Historique des participations
</h4>
<div className="rounded-lg border border-gray-200 overflow-hidden">
<table className="min-w-full text-sm">
<thead className="bg-gray-100">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500">Date</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500">Adhérent</th>
<th className="px-4 py-2 text-right text-xs font-medium text-gray-500">Montant</th>
<th className="px-4 py-2 text-right text-xs font-medium text-gray-500 w-24">Lien</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
{item.historique.map((h) => (
<tr key={h.id}>
<td className="px-4 py-2 text-gray-700">{formatDate(h.date)}</td>
<td className="px-4 py-2 text-gray-700">{h.adherentNom}</td>
<td className="px-4 py-2 text-right text-orange-600 font-medium">
{formatEuro(h.montant)}
</td>
<td className="px-4 py-2 text-right">
<Link
href="/dashboard/factures"
className="text-lblue hover:underline text-xs"
>
Voir
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</td>
</tr>
)}
{expandedPrescripteur === item.value && item.historique.length === 0 && (
<tr className="bg-gray-50">
<td colSpan={4} className="px-6 py-4">
<p className="text-sm text-gray-500 italic">Aucune participation enregistrée pour ce prescripteur.</p>
</td>
</tr>
)}
</React.Fragment>
))}
</tbody>
</table>
</div>
<div className="px-6 py-3 bg-gray-50 border-t border-gray-200">
<p className="text-xs text-gray-500">
Les prescripteurs sont définis dans{' '}
<Link href="/dashboard/parametres/configuration" className="text-lblue hover:underline">
Paramètres Configuration
</Link>
.
</p>
</div>
</div>
{/* Modal Rectifier */}
{rectifierModalPrescripteur && (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50"
onClick={() => {
setRectifierModalPrescripteur(null);
setRectifierBudget('');
setRectifierAjustement('');
}}
>
<div
className="bg-white rounded-xl shadow-xl max-w-sm w-full p-6"
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
aria-labelledby="rectifier-budget-title"
>
<h3 id="rectifier-budget-title" className="text-lg font-semibold text-gray-900 mb-2">
Rectifier {rectifierModalPrescripteur.value}
</h3>
<p className="text-sm text-gray-500 mb-4">
Corrigez le budget alloué et/ou l&apos;ajustement du consommé en cas d&apos;erreur.
</p>
<div className="space-y-4 mb-6">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1.5">Budget alloué ()</label>
<div className="flex items-center gap-2">
<input
type="text"
inputMode="decimal"
value={rectifierBudget}
onChange={(e) => setRectifierBudget(e.target.value.replace(/[^0-9.,]/g, ''))}
placeholder="0,00"
className="flex-1 px-4 py-2.5 text-sm text-gray-900 border border-gray-300 rounded-lg focus:ring-2 focus:ring-lblue focus:border-transparent"
/>
<span className="text-sm text-gray-500"></span>
</div>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1.5">
Ajustement consommé ()
</label>
<p className="text-xs text-gray-500 mb-1">
Valeur à ajouter au consommé calculé. Négatif pour diminuer (ex: -6,80).
</p>
<div className="flex items-center gap-2">
<input
type="text"
inputMode="decimal"
value={rectifierAjustement}
onChange={(e) => setRectifierAjustement(e.target.value.replace(/[^0-9.,-]/g, ''))}
placeholder="0,00"
className="flex-1 px-4 py-2.5 text-sm text-gray-900 border border-gray-300 rounded-lg focus:ring-2 focus:ring-lblue focus:border-transparent"
/>
<span className="text-sm text-gray-500"></span>
</div>
</div>
</div>
<div className="flex gap-3 justify-end">
<button
onClick={() => {
setRectifierModalPrescripteur(null);
setRectifierBudget('');
setRectifierAjustement('');
}}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
>
Annuler
</button>
<button
onClick={handleRectifier}
disabled={savingId === rectifierModalPrescripteur.value}
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-lblue rounded-lg hover:bg-dblue disabled:opacity-50 transition-colors"
>
{savingId === rectifierModalPrescripteur.value ? (
<>
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Enregistrement...
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
Enregistrer
</>
)}
</button>
</div>
</div>
</div>
)}
{/* Modal Ajouter de l'argent */}
{addModalPrescripteur && (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50"
onClick={() => {
setAddModalPrescripteur(null);
setAddAmount('');
}}
>
<div
className="bg-white rounded-xl shadow-xl max-w-sm w-full p-6"
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
aria-labelledby="add-budget-title"
>
<h3 id="add-budget-title" className="text-lg font-semibold text-gray-900 mb-2">
Ajouter du budget {addModalPrescripteur}
</h3>
<p className="text-sm text-gray-500 mb-4">
Entrez le montant à ajouter au budget existant.
</p>
<div className="flex items-center gap-2 mb-6">
<input
type="text"
inputMode="decimal"
value={addAmount}
onChange={(e) => setAddAmount(e.target.value.replace(/[^0-9.,]/g, ''))}
placeholder="0,00"
className="flex-1 px-4 py-2.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-lblue focus:border-transparent"
autoFocus
/>
<span className="text-sm text-gray-500"></span>
</div>
<div className="flex gap-3 justify-end">
<button
onClick={() => {
setAddModalPrescripteur(null);
setAddAmount('');
}}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
>
Annuler
</button>
<button
onClick={handleAddMoney}
disabled={savingId === addModalPrescripteur}
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-lgreen rounded-lg hover:bg-dgreen disabled:opacity-50 transition-colors"
>
{savingId === addModalPrescripteur ? (
<>
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Ajout...
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Ajouter
</>
)}
</button>
</div>
</div>
</div>
)}
</div>
);
}