Update ExtraordinaryAdmin.tsx

This commit is contained in:
2025-12-11 00:14:54 +01:00
committed by GitHub
parent 4b63183003
commit 32adfbbc58

View File

@@ -2,7 +2,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { CondoService } from '../services/mockDb'; import { CondoService } from '../services/mockDb';
import { ExtraordinaryExpense, Family, ExpenseItem, ExpenseShare } from '../types'; import { ExtraordinaryExpense, Family, ExpenseItem, ExpenseShare } from '../types';
import { Plus, Calendar, FileText, CheckCircle2, Clock, Users, X, Save, Paperclip, Euro, Trash2, Eye, Briefcase, Pencil, Banknote } from 'lucide-react'; import { Plus, Calendar, FileText, CheckCircle2, Clock, Users, X, Save, Paperclip, Euro, Trash2, Eye, Briefcase, Pencil, Banknote, History } from 'lucide-react';
export const ExtraordinaryAdmin: React.FC = () => { export const ExtraordinaryAdmin: React.FC = () => {
const [expenses, setExpenses] = useState<ExtraordinaryExpense[]>([]); const [expenses, setExpenses] = useState<ExtraordinaryExpense[]>([]);
@@ -32,6 +32,8 @@ export const ExtraordinaryAdmin: React.FC = () => {
const [payAmount, setPayAmount] = useState<number>(0); const [payAmount, setPayAmount] = useState<number>(0);
const [payNotes, setPayNotes] = useState(''); const [payNotes, setPayNotes] = useState('');
const [isPaying, setIsPaying] = useState(false); const [isPaying, setIsPaying] = useState(false);
const [paymentHistory, setPaymentHistory] = useState<any[]>([]);
const [loadingHistory, setLoadingHistory] = useState(false);
useEffect(() => { useEffect(() => {
loadData(); loadData();
@@ -67,9 +69,6 @@ export const ExtraordinaryAdmin: React.FC = () => {
// Preserve existing data if available // Preserve existing data if available
const existing = formShares.find(s => s.familyId === fid); const existing = formShares.find(s => s.familyId === fid);
if (existing) { if (existing) {
// If editing and we just toggled someone else, re-calc percentages evenly?
// Or keep manual adjustments?
// For simplicity: auto-recalc resets percentages evenly.
return { return {
...existing, ...existing,
percentage: parseFloat(percentage.toFixed(2)), percentage: parseFloat(percentage.toFixed(2)),
@@ -120,14 +119,11 @@ export const ExtraordinaryAdmin: React.FC = () => {
setFormItems(newItems); setFormItems(newItems);
}; };
// Trigger share recalc when total changes (if not manual)
// We only trigger auto-recalc if not editing existing complex shares,
// OR if editing but user hasn't manually messed with them yet (simplification: always recalc on total change for now)
useEffect(() => { useEffect(() => {
if (selectedFamilyIds.length > 0) { if (selectedFamilyIds.length > 0) {
recalculateShares(selectedFamilyIds); recalculateShares(selectedFamilyIds);
} }
}, [totalAmount]); // eslint-disable-line react-hooks/exhaustive-deps }, [totalAmount]);
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) { if (e.target.files && e.target.files.length > 0) {
@@ -154,7 +150,6 @@ export const ExtraordinaryAdmin: React.FC = () => {
}; };
const openEditModal = async (exp: ExtraordinaryExpense) => { const openEditModal = async (exp: ExtraordinaryExpense) => {
// Fetch full details first to get items and shares
try { try {
const detail = await CondoService.getExpenseDetails(exp.id); const detail = await CondoService.getExpenseDetails(exp.id);
setIsEditing(true); setIsEditing(true);
@@ -166,12 +161,10 @@ export const ExtraordinaryAdmin: React.FC = () => {
setFormContractor(detail.contractorName); setFormContractor(detail.contractorName);
setFormItems(detail.items || []); setFormItems(detail.items || []);
// Populate shares for editing
const currentShares = detail.shares || []; const currentShares = detail.shares || [];
setFormShares(currentShares); setFormShares(currentShares);
setSelectedFamilyIds(currentShares.map(s => s.familyId)); setSelectedFamilyIds(currentShares.map(s => s.familyId));
// Attachments (Cannot edit attachments in this simple view for now, cleared)
setFormAttachments([]); setFormAttachments([]);
setShowModal(true); setShowModal(true);
@@ -197,7 +190,7 @@ export const ExtraordinaryAdmin: React.FC = () => {
endDate: formEnd, endDate: formEnd,
contractorName: formContractor, contractorName: formContractor,
items: formItems, items: formItems,
shares: formShares // Now we send shares to sync shares: formShares
}); });
} else { } else {
await CondoService.createExpense({ await CondoService.createExpense({
@@ -237,12 +230,25 @@ export const ExtraordinaryAdmin: React.FC = () => {
}; };
// MANUAL PAYMENT HANDLERS // MANUAL PAYMENT HANDLERS
const openPayModal = (share: ExpenseShare) => { const openPayModal = async (share: ExpenseShare) => {
if (!selectedExpense) return;
const remaining = Math.max(0, share.amountDue - share.amountPaid); const remaining = Math.max(0, share.amountDue - share.amountPaid);
setPayShare(share); setPayShare(share);
setPayAmount(remaining); setPayAmount(remaining);
setPayNotes('Saldo manuale'); setPayNotes('Saldo manuale');
setShowPayModal(true); setShowPayModal(true);
// Fetch History
setLoadingHistory(true);
try {
const history = await CondoService.getExpensePayments(selectedExpense.id, share.familyId);
setPaymentHistory(history);
} catch (e) {
console.error(e);
setPaymentHistory([]);
} finally {
setLoadingHistory(false);
}
}; };
const handleManualPayment = async (e: React.FormEvent) => { const handleManualPayment = async (e: React.FormEvent) => {
@@ -251,21 +257,48 @@ export const ExtraordinaryAdmin: React.FC = () => {
setIsPaying(true); setIsPaying(true);
try { try {
// Updated API allows Admins to pass familyId to pay on their behalf
await CondoService.payExpense(selectedExpense.id, payAmount, payShare.familyId); await CondoService.payExpense(selectedExpense.id, payAmount, payShare.familyId);
await refreshShareData();
// Refresh Details // Reset input
const updated = await CondoService.getExpenseDetails(selectedExpense.id); setPayAmount(0);
setSelectedExpense(updated); setPayNotes('');
setShowPayModal(false);
} catch (e: any) { } catch (e: any) {
console.error(e);
alert("Errore registrazione pagamento: " + (e.message || "Errore sconosciuto")); alert("Errore registrazione pagamento: " + (e.message || "Errore sconosciuto"));
} finally { } finally {
setIsPaying(false); setIsPaying(false);
} }
}; };
const handleDeletePayment = async (paymentId: string) => {
if (!confirm("Annullare questo pagamento? L'importo verrà stornato dal saldo versato.")) return;
try {
await CondoService.deleteExpensePayment(paymentId);
await refreshShareData();
} catch(e) {
alert("Errore annullamento pagamento");
}
};
const refreshShareData = async () => {
if (!selectedExpense || !payShare) return;
// Refresh Expense Details to update the table in background
const updatedExpense = await CondoService.getExpenseDetails(selectedExpense.id);
setSelectedExpense(updatedExpense);
// Refresh Payment History inside modal
const history = await CondoService.getExpensePayments(selectedExpense.id, payShare.familyId);
setPaymentHistory(history);
// Update local PayShare reference for amount calculation
const updatedShare = updatedExpense.shares?.find(s => s.familyId === payShare.familyId);
if (updatedShare) {
setPayShare(updatedShare);
setPayAmount(Math.max(0, updatedShare.amountDue - updatedShare.amountPaid));
}
};
return ( return (
<div className="space-y-6 pb-20 animate-fade-in"> <div className="space-y-6 pb-20 animate-fade-in">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
@@ -497,15 +530,13 @@ export const ExtraordinaryAdmin: React.FC = () => {
</span> </span>
</td> </td>
<td className="p-3 text-center"> <td className="p-3 text-center">
{share.status !== 'PAID' && ( <button
<button onClick={() => openPayModal(share)}
onClick={() => openPayModal(share)} className="text-xs bg-slate-100 text-slate-700 hover:bg-slate-200 px-3 py-1 rounded border border-slate-200 font-medium transition-colors"
className="text-xs bg-blue-50 text-blue-600 hover:bg-blue-100 px-2 py-1 rounded border border-blue-200 font-medium" title="Gestisci Pagamenti"
title="Registra Pagamento Manuale" >
> Gestisci
Paga </button>
</button>
)}
</td> </td>
</tr> </tr>
))} ))}
@@ -528,53 +559,111 @@ export const ExtraordinaryAdmin: React.FC = () => {
</div> </div>
)} )}
{/* MANUAL PAYMENT MODAL */} {/* MANUAL PAYMENT / MANAGEMENT MODAL */}
{showPayModal && payShare && ( {showPayModal && payShare && (
<div className="fixed inset-0 bg-black/50 z-[60] flex items-center justify-center p-4 backdrop-blur-sm"> <div className="fixed inset-0 bg-black/50 z-[60] flex items-center justify-center p-4 backdrop-blur-sm">
<div className="bg-white rounded-xl shadow-xl w-full max-w-sm p-6 animate-in fade-in zoom-in duration-200"> <div className="bg-white rounded-xl shadow-xl w-full max-w-md p-0 animate-in fade-in zoom-in duration-200 overflow-hidden flex flex-col max-h-[90vh]">
<div className="flex justify-between items-center mb-4 border-b pb-2"> <div className="flex justify-between items-center p-4 border-b bg-slate-50">
<h3 className="font-bold text-lg text-slate-800">Registra Pagamento</h3> <div>
<h3 className="font-bold text-lg text-slate-800">Gestione Pagamenti</h3>
<p className="text-xs text-slate-500">{payShare.familyName}</p>
</div>
<button onClick={() => setShowPayModal(false)}><X className="w-5 h-5 text-slate-400"/></button> <button onClick={() => setShowPayModal(false)}><X className="w-5 h-5 text-slate-400"/></button>
</div> </div>
<form onSubmit={handleManualPayment} className="space-y-4"> <div className="p-6 overflow-y-auto">
<div className="bg-blue-50 p-3 rounded-lg border border-blue-100 text-sm"> {/* Summary Card */}
<p className="text-slate-500">Famiglia: <span className="font-bold text-slate-800">{payShare.familyName}</span></p> <div className="bg-blue-50 p-4 rounded-xl border border-blue-100 mb-6 text-sm flex justify-between items-center">
<p className="text-slate-500">Restante da pagare: <span className="font-bold text-red-600"> {(payShare.amountDue - payShare.amountPaid).toFixed(2)}</span></p> <div>
</div> <p className="text-slate-500 text-xs uppercase font-bold">Quota Totale</p>
<p className="font-bold text-slate-800 text-lg"> {payShare.amountDue.toFixed(2)}</p>
<div> </div>
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">Importo Incassato ()</label> <div className="text-right">
<div className="relative"> <p className="text-slate-500 text-xs uppercase font-bold">Restante</p>
<Banknote className="absolute left-3 top-2.5 w-4 h-4 text-slate-400"/> <p className="font-bold text-red-600 text-lg"> {(payShare.amountDue - payShare.amountPaid).toFixed(2)}</p>
<input
type="number"
step="0.01"
className="w-full border p-2 pl-9 rounded-lg text-slate-700 font-bold"
value={payAmount}
onChange={e => setPayAmount(parseFloat(e.target.value))}
required
/>
</div> </div>
</div> </div>
<div> {/* History Section */}
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">Note (Opzionale)</label> <div className="mb-6">
<input <h4 className="text-xs font-bold text-slate-500 uppercase mb-2 flex items-center gap-1">
className="w-full border p-2 rounded-lg text-slate-700 text-sm" <History className="w-3 h-3"/> Storico Versamenti
placeholder="Es. Bonifico, Contanti..." </h4>
value={payNotes} {loadingHistory ? (
onChange={e => setPayNotes(e.target.value)} <div className="text-center py-4 text-xs text-slate-400">Caricamento...</div>
/> ) : paymentHistory.length === 0 ? (
<div className="text-center py-4 bg-slate-50 rounded border border-dashed border-slate-200 text-xs text-slate-400">
Nessun pagamento registrato.
</div>
) : (
<div className="border rounded-lg overflow-hidden">
<table className="w-full text-xs">
<thead className="bg-slate-50 text-slate-500">
<tr>
<th className="p-2 text-left">Data</th>
<th className="p-2 text-left">Note</th>
<th className="p-2 text-right">Importo</th>
<th className="p-2"></th>
</tr>
</thead>
<tbody className="divide-y">
{paymentHistory.map(ph => (
<tr key={ph.id}>
<td className="p-2 text-slate-600">{new Date(ph.datePaid).toLocaleDateString()}</td>
<td className="p-2 text-slate-500 truncate max-w-[100px]" title={ph.notes}>{ph.notes}</td>
<td className="p-2 text-right font-bold text-slate-700"> {ph.amount.toFixed(2)}</td>
<td className="p-2 text-center">
<button
onClick={() => handleDeletePayment(ph.id)}
className="text-red-400 hover:text-red-600 p-1 hover:bg-red-50 rounded"
title="Annulla pagamento"
>
<Trash2 className="w-3 h-3"/>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div> </div>
<div className="flex gap-2 pt-2"> {/* Add New Section */}
<button type="button" onClick={() => setShowPayModal(false)} className="flex-1 border p-2 rounded-lg text-slate-600 hover:bg-slate-50">Annulla</button> <div className="border-t pt-4">
<button type="submit" disabled={isPaying} className="flex-1 bg-green-600 text-white p-2 rounded-lg hover:bg-green-700 font-bold disabled:opacity-50"> <h4 className="text-sm font-bold text-slate-800 mb-3">Registra Nuovo Incasso</h4>
{isPaying ? '...' : 'Conferma'} <form onSubmit={handleManualPayment} className="space-y-4">
</button> <div>
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">Importo ()</label>
<div className="relative">
<Banknote className="absolute left-3 top-2.5 w-4 h-4 text-slate-400"/>
<input
type="number"
step="0.01"
className="w-full border p-2 pl-9 rounded-lg text-slate-700 font-bold"
value={payAmount}
onChange={e => setPayAmount(parseFloat(e.target.value))}
required
/>
</div>
</div>
<div>
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">Note (Opzionale)</label>
<input
className="w-full border p-2 rounded-lg text-slate-700 text-sm"
placeholder="Es. Bonifico, Contanti..."
value={payNotes}
onChange={e => setPayNotes(e.target.value)}
/>
</div>
<button type="submit" disabled={isPaying || payAmount <= 0} className="w-full bg-green-600 text-white p-2.5 rounded-lg hover:bg-green-700 font-bold disabled:opacity-50 flex items-center justify-center gap-2">
{isPaying ? 'Registrazione...' : <><Plus className="w-4 h-4"/> Conferma Pagamento</>}
</button>
</form>
</div> </div>
</form> </div>
</div> </div>
</div> </div>
)} )}