feat: Refactor API services and UI components
This commit refactors the API service to use a consistent `fetch` wrapper for all requests, improving error handling and authorization logic. It also updates UI components to reflect changes in API endpoints and data structures, particularly around notifications and extraordinary expenses. Docker configurations are removed as they are no longer relevant for this stage of development.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { CondoService } from '../services/mockDb';
|
||||
import { ExtraordinaryExpense, Family, ExpenseItem, ExpenseShare } from '../types';
|
||||
import { Plus, Calendar, FileText, CheckCircle2, Clock, Users, X, Save, Paperclip, Euro, Trash2, Eye, Briefcase } from 'lucide-react';
|
||||
import { Plus, Calendar, FileText, CheckCircle2, Clock, Users, X, Save, Paperclip, Euro, Trash2, Eye, Briefcase, Pencil } from 'lucide-react';
|
||||
|
||||
export const ExtraordinaryAdmin: React.FC = () => {
|
||||
const [expenses, setExpenses] = useState<ExtraordinaryExpense[]>([]);
|
||||
@@ -10,6 +11,9 @@ export const ExtraordinaryAdmin: React.FC = () => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [showDetailsModal, setShowDetailsModal] = useState(false);
|
||||
const [selectedExpense, setSelectedExpense] = useState<ExtraordinaryExpense | null>(null);
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
|
||||
// Form State
|
||||
const [formTitle, setFormTitle] = useState('');
|
||||
@@ -43,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) return; // If manually editing, don't auto-calc
|
||||
if (manualMode || isEditing) return; // Don't auto-calc shares in Edit mode to prevent messing up existing complex logic visually, backend handles logic
|
||||
|
||||
const count = selectedIds.length;
|
||||
if (count === 0) {
|
||||
@@ -93,13 +97,13 @@ export const ExtraordinaryAdmin: React.FC = () => {
|
||||
// @ts-ignore
|
||||
newItems[index][field] = value;
|
||||
setFormItems(newItems);
|
||||
// Recalculate shares based on new total
|
||||
// We need a small delay or effect, but for simplicity let's force recalc next render or manual
|
||||
};
|
||||
|
||||
// Trigger share recalc when total changes (if not manual override mode - implementing simple auto mode here)
|
||||
// Trigger share recalc when total changes (if not manual/editing)
|
||||
useEffect(() => {
|
||||
recalculateShares(selectedFamilyIds);
|
||||
if (!isEditing) {
|
||||
recalculateShares(selectedFamilyIds);
|
||||
}
|
||||
}, [totalAmount]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -118,24 +122,63 @@ export const ExtraordinaryAdmin: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const openCreateModal = () => {
|
||||
setIsEditing(false);
|
||||
setEditingId(null);
|
||||
setFormTitle(''); setFormDesc(''); setFormStart(''); setFormEnd(''); setFormContractor('');
|
||||
setFormItems([{description:'', amount:0}]); setFormShares([]); setFormAttachments([]); setSelectedFamilyIds([]);
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const openEditModal = async (exp: ExtraordinaryExpense) => {
|
||||
// Fetch full details first to get items
|
||||
try {
|
||||
const detail = await CondoService.getExpenseDetails(exp.id);
|
||||
setIsEditing(true);
|
||||
setEditingId(exp.id);
|
||||
setFormTitle(detail.title);
|
||||
setFormDesc(detail.description);
|
||||
setFormStart(detail.startDate ? new Date(detail.startDate).toISOString().split('T')[0] : '');
|
||||
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([]);
|
||||
setFormAttachments([]);
|
||||
setSelectedFamilyIds([]);
|
||||
setShowModal(true);
|
||||
} catch(e) { alert("Errore caricamento dettagli"); }
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await CondoService.createExpense({
|
||||
title: formTitle,
|
||||
description: formDesc,
|
||||
startDate: formStart,
|
||||
endDate: formEnd,
|
||||
contractorName: formContractor,
|
||||
items: formItems,
|
||||
shares: formShares,
|
||||
attachments: formAttachments
|
||||
});
|
||||
if (isEditing && editingId) {
|
||||
await CondoService.updateExpense(editingId, {
|
||||
title: formTitle,
|
||||
description: formDesc,
|
||||
startDate: formStart,
|
||||
endDate: formEnd,
|
||||
contractorName: formContractor,
|
||||
items: formItems
|
||||
// Attachments and shares handled by backend logic to keep safe
|
||||
});
|
||||
} else {
|
||||
await CondoService.createExpense({
|
||||
title: formTitle,
|
||||
description: formDesc,
|
||||
startDate: formStart,
|
||||
endDate: formEnd,
|
||||
contractorName: formContractor,
|
||||
items: formItems,
|
||||
shares: formShares,
|
||||
attachments: formAttachments
|
||||
});
|
||||
}
|
||||
setShowModal(false);
|
||||
loadData();
|
||||
// Reset form
|
||||
setFormTitle(''); setFormDesc(''); setFormItems([{description:'', amount:0}]); setSelectedFamilyIds([]); setFormShares([]); setFormAttachments([]);
|
||||
} catch(e) { alert('Errore creazione'); }
|
||||
} catch(e) { alert('Errore salvataggio'); }
|
||||
};
|
||||
|
||||
const openDetails = async (expense: ExtraordinaryExpense) => {
|
||||
@@ -165,7 +208,7 @@ export const ExtraordinaryAdmin: React.FC = () => {
|
||||
<h2 className="text-2xl font-bold text-slate-800">Spese Straordinarie</h2>
|
||||
<p className="text-slate-500 text-sm">Gestione lavori e appalti</p>
|
||||
</div>
|
||||
<button onClick={() => setShowModal(true)} className="bg-blue-600 text-white px-4 py-2 rounded-lg font-medium flex gap-2 hover:bg-blue-700">
|
||||
<button onClick={openCreateModal} className="bg-blue-600 text-white px-4 py-2 rounded-lg font-medium flex gap-2 hover:bg-blue-700">
|
||||
<Plus className="w-5 h-5"/> Nuova Spesa
|
||||
</button>
|
||||
</div>
|
||||
@@ -173,10 +216,18 @@ export const ExtraordinaryAdmin: React.FC = () => {
|
||||
{/* List */}
|
||||
<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">
|
||||
<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="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">€ {exp.totalAmount.toLocaleString()}</span>
|
||||
<span className="font-bold text-slate-800 pr-8">€ {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>
|
||||
@@ -193,12 +244,12 @@ export const ExtraordinaryAdmin: React.FC = () => {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* CREATE MODAL */}
|
||||
{/* CREATE/EDIT MODAL */}
|
||||
{showModal && (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-4xl p-6 animate-in fade-in zoom-in duration-200 flex flex-col max-h-[90vh]">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="font-bold text-xl text-slate-800">Crea Progetto Straordinario</h3>
|
||||
<h3 className="font-bold text-xl text-slate-800">{isEditing ? 'Modifica Progetto' : 'Crea Progetto Straordinario'}</h3>
|
||||
<button onClick={() => setShowModal(false)}><X className="w-6 h-6 text-slate-400"/></button>
|
||||
</div>
|
||||
|
||||
@@ -215,10 +266,12 @@ export const ExtraordinaryAdmin: React.FC = () => {
|
||||
<div><label className="text-xs font-bold text-slate-500">Inizio</label><input type="date" className="w-full border p-2 rounded" value={formStart} onChange={e => setFormStart(e.target.value)} required /></div>
|
||||
<div><label className="text-xs font-bold text-slate-500">Fine (Prevista)</label><input type="date" className="w-full border p-2 rounded" value={formEnd} onChange={e => setFormEnd(e.target.value)} /></div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-bold text-slate-500 block mb-1">Allegati (Preventivi, Contratti)</label>
|
||||
<input type="file" multiple onChange={handleFileChange} className="w-full text-sm text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"/>
|
||||
</div>
|
||||
{!isEditing && (
|
||||
<div>
|
||||
<label className="text-xs font-bold text-slate-500 block mb-1">Allegati (Preventivi, Contratti)</label>
|
||||
<input type="file" multiple onChange={handleFileChange} className="w-full text-sm text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Items */}
|
||||
@@ -239,42 +292,50 @@ export const ExtraordinaryAdmin: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Distribution */}
|
||||
<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>
|
||||
{/* 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>
|
||||
</div>
|
||||
<div className="w-24 text-right text-sm font-bold text-slate-700">€ {share.amountDue.toFixed(2)}</div>
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
<div className="pt-4 border-t mt-4 flex justify-end gap-2">
|
||||
<button onClick={() => setShowModal(false)} className="px-4 py-2 text-slate-600 border rounded-lg hover:bg-slate-50">Annulla</button>
|
||||
<button type="submit" form="createForm" className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-bold">Crea Progetto</button>
|
||||
<button type="submit" form="createForm" className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-bold">{isEditing ? 'Aggiorna Progetto' : 'Crea Progetto'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -384,4 +445,4 @@ export const ExtraordinaryAdmin: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -19,6 +19,12 @@ export const ExtraordinaryUser: React.FC = () => {
|
||||
]);
|
||||
setExpenses(myExp);
|
||||
if (condo?.paypalClientId) setPaypalClientId(condo.paypalClientId);
|
||||
|
||||
// Update "Last Viewed" timestamp to clear notification
|
||||
localStorage.setItem('lastViewedExpensesTime', Date.now().toString());
|
||||
// Trigger event to update Sidebar immediately
|
||||
window.dispatchEvent(new Event('expenses-viewed'));
|
||||
|
||||
} catch(e) { console.error(e); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user