Update ExtraordinaryAdmin.tsx
This commit is contained in:
@@ -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-blue-50 text-blue-600 hover:bg-blue-100 px-2 py-1 rounded border border-blue-200 font-medium"
|
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"
|
||||||
title="Registra Pagamento Manuale"
|
title="Gestisci Pagamenti"
|
||||||
>
|
>
|
||||||
Paga
|
Gestisci
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -528,23 +559,82 @@ 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>
|
||||||
|
<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 className="text-right">
|
||||||
|
<p className="text-slate-500 text-xs uppercase font-bold">Restante</p>
|
||||||
|
<p className="font-bold text-red-600 text-lg">€ {(payShare.amountDue - payShare.amountPaid).toFixed(2)}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* History Section */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h4 className="text-xs font-bold text-slate-500 uppercase mb-2 flex items-center gap-1">
|
||||||
|
<History className="w-3 h-3"/> Storico Versamenti
|
||||||
|
</h4>
|
||||||
|
{loadingHistory ? (
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Add New Section */}
|
||||||
|
<div className="border-t pt-4">
|
||||||
|
<h4 className="text-sm font-bold text-slate-800 mb-3">Registra Nuovo Incasso</h4>
|
||||||
|
<form onSubmit={handleManualPayment} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">Importo Incassato (€)</label>
|
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">Importo (€)</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Banknote className="absolute left-3 top-2.5 w-4 h-4 text-slate-400"/>
|
<Banknote className="absolute left-3 top-2.5 w-4 h-4 text-slate-400"/>
|
||||||
<input
|
<input
|
||||||
@@ -568,15 +658,14 @@ export const ExtraordinaryAdmin: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 pt-2">
|
<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">
|
||||||
<button type="button" onClick={() => setShowPayModal(false)} className="flex-1 border p-2 rounded-lg text-slate-600 hover:bg-slate-50">Annulla</button>
|
{isPaying ? 'Registrazione...' : <><Plus className="w-4 h-4"/> Conferma Pagamento</>}
|
||||||
<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">
|
|
||||||
{isPaying ? '...' : 'Conferma'}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user