feat(expenses): Add delete expense endpoint and functionality
Implements the ability to delete an expense, including its associated items and shares. Also refactors the expense update logic to correctly handle share updates and adds the corresponding API endpoint and mock DB function.
This commit is contained in:
@@ -47,7 +47,7 @@ export const ExtraordinaryAdmin: React.FC = () => {
|
||||
const totalAmount = formItems.reduce((acc, item) => acc + (item.amount || 0), 0);
|
||||
|
||||
const recalculateShares = (selectedIds: string[], manualMode = false) => {
|
||||
if (manualMode || isEditing) return; // Don't auto-calc shares in Edit mode to prevent messing up existing complex logic visually, backend handles logic
|
||||
if (manualMode) return;
|
||||
|
||||
const count = selectedIds.length;
|
||||
if (count === 0) {
|
||||
@@ -56,13 +56,27 @@ export const ExtraordinaryAdmin: React.FC = () => {
|
||||
}
|
||||
|
||||
const percentage = 100 / count;
|
||||
const newShares: ExpenseShare[] = selectedIds.map(fid => ({
|
||||
familyId: fid,
|
||||
percentage: parseFloat(percentage.toFixed(2)),
|
||||
amountDue: parseFloat(((totalAmount * percentage) / 100).toFixed(2)),
|
||||
amountPaid: 0,
|
||||
status: 'UNPAID'
|
||||
}));
|
||||
const newShares: ExpenseShare[] = selectedIds.map(fid => {
|
||||
// Preserve existing data if available
|
||||
const existing = formShares.find(s => s.familyId === fid);
|
||||
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 {
|
||||
...existing,
|
||||
percentage: parseFloat(percentage.toFixed(2)),
|
||||
amountDue: parseFloat(((totalAmount * percentage) / 100).toFixed(2))
|
||||
};
|
||||
}
|
||||
return {
|
||||
familyId: fid,
|
||||
percentage: parseFloat(percentage.toFixed(2)),
|
||||
amountDue: parseFloat(((totalAmount * percentage) / 100).toFixed(2)),
|
||||
amountPaid: 0,
|
||||
status: 'UNPAID'
|
||||
};
|
||||
});
|
||||
|
||||
// Adjust rounding error on last item
|
||||
if (newShares.length > 0) {
|
||||
@@ -99,9 +113,11 @@ export const ExtraordinaryAdmin: React.FC = () => {
|
||||
setFormItems(newItems);
|
||||
};
|
||||
|
||||
// Trigger share recalc when total changes (if not manual/editing)
|
||||
// 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(() => {
|
||||
if (!isEditing) {
|
||||
if (selectedFamilyIds.length > 0) {
|
||||
recalculateShares(selectedFamilyIds);
|
||||
}
|
||||
}, [totalAmount]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
@@ -131,7 +147,7 @@ export const ExtraordinaryAdmin: React.FC = () => {
|
||||
};
|
||||
|
||||
const openEditModal = async (exp: ExtraordinaryExpense) => {
|
||||
// Fetch full details first to get items
|
||||
// Fetch full details first to get items and shares
|
||||
try {
|
||||
const detail = await CondoService.getExpenseDetails(exp.id);
|
||||
setIsEditing(true);
|
||||
@@ -142,15 +158,27 @@ export const ExtraordinaryAdmin: React.FC = () => {
|
||||
setFormEnd(detail.endDate ? new Date(detail.endDate).toISOString().split('T')[0] : '');
|
||||
setFormContractor(detail.contractorName);
|
||||
setFormItems(detail.items || []);
|
||||
// Shares and attachments are not fully editable in this simple view to avoid conflicts
|
||||
// We only allow editing Header Info + Items. Shares will be auto-recalculated by backend based on new total.
|
||||
setFormShares([]);
|
||||
|
||||
// Populate shares for editing
|
||||
const currentShares = detail.shares || [];
|
||||
setFormShares(currentShares);
|
||||
setSelectedFamilyIds(currentShares.map(s => s.familyId));
|
||||
|
||||
// Attachments (Cannot edit attachments in this simple view for now, cleared)
|
||||
setFormAttachments([]);
|
||||
setSelectedFamilyIds([]);
|
||||
|
||||
setShowModal(true);
|
||||
} catch(e) { alert("Errore caricamento dettagli"); }
|
||||
};
|
||||
|
||||
const handleDeleteExpense = async (id: string) => {
|
||||
if (!confirm("Sei sicuro di voler eliminare questo progetto? Questa azione è irreversibile e cancellerà anche lo storico dei pagamenti associati.")) return;
|
||||
try {
|
||||
await CondoService.deleteExpense(id);
|
||||
loadData();
|
||||
} catch (e) { alert("Errore eliminazione progetto"); }
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
@@ -161,8 +189,8 @@ export const ExtraordinaryAdmin: React.FC = () => {
|
||||
startDate: formStart,
|
||||
endDate: formEnd,
|
||||
contractorName: formContractor,
|
||||
items: formItems
|
||||
// Attachments and shares handled by backend logic to keep safe
|
||||
items: formItems,
|
||||
shares: formShares // Now we send shares to sync
|
||||
});
|
||||
} else {
|
||||
await CondoService.createExpense({
|
||||
@@ -217,17 +245,26 @@ export const ExtraordinaryAdmin: React.FC = () => {
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{expenses.map(exp => (
|
||||
<div key={exp.id} className="bg-white p-5 rounded-xl border border-slate-200 shadow-sm hover:shadow-md transition-all relative group">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); openEditModal(exp); }}
|
||||
className="absolute top-4 right-4 p-2 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-full transition-colors z-10"
|
||||
title="Modifica"
|
||||
>
|
||||
<Pencil className="w-4 h-4"/>
|
||||
</button>
|
||||
<div className="absolute top-4 right-4 flex gap-2">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); openEditModal(exp); }}
|
||||
className="p-2 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-full transition-colors"
|
||||
title="Modifica"
|
||||
>
|
||||
<Pencil className="w-4 h-4"/>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteExpense(exp.id); }}
|
||||
className="p-2 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-full transition-colors"
|
||||
title="Elimina"
|
||||
>
|
||||
<Trash2 className="w-4 h-4"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<span className="bg-purple-100 text-purple-700 text-xs font-bold px-2 py-1 rounded uppercase">Lavori</span>
|
||||
<span className="font-bold text-slate-800 pr-8">€ {exp.totalAmount.toLocaleString()}</span>
|
||||
<span className="font-bold text-slate-800 pr-20">€ {exp.totalAmount.toLocaleString()}</span>
|
||||
</div>
|
||||
<h3 className="font-bold text-lg text-slate-800 mb-1">{exp.title}</h3>
|
||||
<p className="text-sm text-slate-500 mb-4 line-clamp-2">{exp.description}</p>
|
||||
@@ -292,43 +329,41 @@ export const ExtraordinaryAdmin: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Distribution - HIDDEN IN EDIT MODE TO AVOID COMPLEXITY */}
|
||||
{!isEditing && (
|
||||
<div>
|
||||
<h4 className="font-bold text-slate-700 mb-2">Ripartizione Famiglie</h4>
|
||||
<div className="max-h-60 overflow-y-auto border rounded-lg divide-y">
|
||||
{families.map(fam => {
|
||||
const share = formShares.find(s => s.familyId === fam.id);
|
||||
return (
|
||||
<div key={fam.id} className="flex items-center justify-between p-3 hover:bg-slate-50">
|
||||
<label className="flex items-center gap-2 cursor-pointer flex-1">
|
||||
<input type="checkbox" checked={selectedFamilyIds.includes(fam.id)} onChange={() => handleFamilyToggle(fam.id)} className="rounded text-blue-600"/>
|
||||
<span className="text-sm font-medium text-slate-700">{fam.name} <span className="text-slate-400 font-normal">({fam.unitNumber})</span></span>
|
||||
</label>
|
||||
{share && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="number"
|
||||
className="w-16 border rounded p-1 text-right text-sm"
|
||||
value={share.percentage}
|
||||
onChange={e => handleShareChange(formShares.indexOf(share), 'percentage', parseFloat(e.target.value))}
|
||||
/>
|
||||
<span className="absolute right-6 top-1 text-xs text-slate-400">%</span>
|
||||
</div>
|
||||
<div className="w-24 text-right text-sm font-bold text-slate-700">€ {share.amountDue.toFixed(2)}</div>
|
||||
{/* Distribution - Visible in BOTH Edit and Create */}
|
||||
<div>
|
||||
<h4 className="font-bold text-slate-700 mb-2">Ripartizione Famiglie</h4>
|
||||
<div className="max-h-60 overflow-y-auto border rounded-lg divide-y">
|
||||
{families.map(fam => {
|
||||
const share = formShares.find(s => s.familyId === fam.id);
|
||||
return (
|
||||
<div key={fam.id} className="flex items-center justify-between p-3 hover:bg-slate-50">
|
||||
<label className="flex items-center gap-2 cursor-pointer flex-1">
|
||||
<input type="checkbox" checked={selectedFamilyIds.includes(fam.id)} onChange={() => handleFamilyToggle(fam.id)} className="rounded text-blue-600"/>
|
||||
<span className="text-sm font-medium text-slate-700">{fam.name} <span className="text-slate-400 font-normal">({fam.unitNumber})</span></span>
|
||||
</label>
|
||||
{share && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="number"
|
||||
className="w-16 border rounded p-1 text-right text-sm"
|
||||
value={share.percentage}
|
||||
onChange={e => handleShareChange(formShares.indexOf(share), 'percentage', parseFloat(e.target.value))}
|
||||
/>
|
||||
<span className="absolute right-6 top-1 text-xs text-slate-400">%</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="w-24 text-right text-sm font-bold text-slate-700">€ {share.amountDue.toFixed(2)}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isEditing && (
|
||||
<div className="bg-amber-50 p-3 rounded text-amber-800 text-sm border border-amber-200 flex items-start gap-2">
|
||||
<CheckCircle2 className="w-5 h-5 flex-shrink-0"/>
|
||||
<p>In modifica le quote delle famiglie vengono ricalcolate automaticamente in proporzione al nuovo totale. I pagamenti già effettuati restano salvati.</p>
|
||||
<p>Nota: Modificando le quote, lo stato dei pagamenti verrà aggiornato in base agli importi già versati dalle famiglie.</p>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user