diff --git a/pages/CondoFinancials.tsx b/pages/CondoFinancials.tsx new file mode 100644 index 0000000..073f3dc --- /dev/null +++ b/pages/CondoFinancials.tsx @@ -0,0 +1,365 @@ + +import React, { useEffect, useState, useMemo } from 'react'; +import { CondoService } from '../services/mockDb'; +import { CondoExpense, Condo } from '../types'; +import { Plus, Search, Filter, Paperclip, X, Save, FileText, Download, Euro, Trash2, Pencil, Briefcase } from 'lucide-react'; + +export const CondoFinancialsPage: React.FC = () => { + const user = CondoService.getCurrentUser(); + const isPrivileged = user?.role === 'admin' || user?.role === 'poweruser'; + + const [expenses, setExpenses] = useState([]); + const [loading, setLoading] = useState(true); + const [activeCondo, setActiveCondo] = useState(undefined); + + // Filters + const [selectedYear, setSelectedYear] = useState(new Date().getFullYear()); + const [availableYears, setAvailableYears] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + const [statusFilter, setStatusFilter] = useState<'ALL' | 'PAID' | 'UNPAID' | 'SUSPENDED'>('ALL'); + + // Modal State + const [showModal, setShowModal] = useState(false); + const [editingExpense, setEditingExpense] = useState(null); + + // Form + const [formData, setFormData] = useState<{ + description: string; + supplierName: string; + amount: string; + paymentDate: string; + status: 'PAID' | 'UNPAID' | 'SUSPENDED'; + paymentMethod: string; + invoiceNumber: string; + notes: string; + attachments: {fileName: string, fileType: string, data: string}[]; + }>({ description: '', supplierName: '', amount: '', paymentDate: '', status: 'UNPAID', paymentMethod: '', invoiceNumber: '', notes: '', attachments: [] }); + + // Distinct Suppliers for Datalist + const suppliers = useMemo(() => { + return Array.from(new Set(expenses.map(e => e.supplierName).filter(Boolean))); + }, [expenses]); + + useEffect(() => { + loadData(); + }, [selectedYear]); + + const loadData = async () => { + setLoading(true); + try { + const condo = await CondoService.getActiveCondo(); + setActiveCondo(condo); + + const [data, years] = await Promise.all([ + CondoService.getCondoExpenses(selectedYear), + CondoService.getAvailableYears() + ]); + + setExpenses(data); + if (!availableYears.includes(selectedYear) && years.includes(selectedYear)) { + setAvailableYears(years); + } else if (years.length > 0) { + setAvailableYears(prev => Array.from(new Set([...prev, ...years, new Date().getFullYear()])).sort((a,b)=>b-a)); + } + } catch(e) { console.error(e); } + finally { setLoading(false); } + }; + + const handleFileChange = async (e: React.ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + const newAtts = []; + for (let i = 0; i < e.target.files.length; i++) { + const file = e.target.files[i]; + const base64 = await new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.readAsDataURL(file); + }); + newAtts.push({ fileName: file.name, fileType: file.type, data: base64 }); + } + setFormData(prev => ({ ...prev, attachments: [...prev.attachments, ...newAtts] })); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const payload = { + id: editingExpense?.id, + description: formData.description, + supplierName: formData.supplierName, + amount: parseFloat(formData.amount), + paymentDate: formData.paymentDate || null, + status: formData.status, + paymentMethod: formData.paymentMethod, + invoiceNumber: formData.invoiceNumber, + notes: formData.notes, + attachments: formData.attachments + }; + + await CondoService.saveCondoExpense(payload); + setShowModal(false); + loadData(); + } catch(e) { alert("Errore salvataggio spesa"); } + }; + + const handleDelete = async (id: string) => { + if (!confirm("Eliminare questa spesa?")) return; + try { + await CondoService.deleteCondoExpense(id); + setExpenses(expenses.filter(e => e.id !== id)); + } catch(e) { alert("Errore eliminazione"); } + }; + + const openModal = (exp?: CondoExpense) => { + if (exp) { + setEditingExpense(exp); + setFormData({ + description: exp.description, + supplierName: exp.supplierName, + amount: exp.amount.toString(), + paymentDate: exp.paymentDate ? new Date(exp.paymentDate).toISOString().split('T')[0] : '', + status: exp.status, + paymentMethod: exp.paymentMethod || '', + invoiceNumber: exp.invoiceNumber || '', + notes: exp.notes || '', + attachments: [] // We don't load attachments content back for editing, only new ones + }); + } else { + setEditingExpense(null); + setFormData({ description: '', supplierName: '', amount: '', paymentDate: '', status: 'UNPAID', paymentMethod: '', invoiceNumber: '', notes: '', attachments: [] }); + } + setShowModal(true); + }; + + const openAttachment = async (expId: string, attId: string) => { + try { + const file = await CondoService.getCondoExpenseAttachment(expId, attId); + const win = window.open(); + if (win) { + if (file.fileType.startsWith('image/') || file.fileType === 'application/pdf') { + win.document.write(``); + } else { + win.document.write(`Download ${file.fileName}`); + } + } + } catch(e) { alert("Errore apertura file"); } + }; + + const filtered = expenses.filter(e => { + const matchesSearch = e.description.toLowerCase().includes(searchTerm.toLowerCase()) || + e.supplierName.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesStatus = statusFilter === 'ALL' || e.status === statusFilter; + return matchesSearch && matchesStatus; + }); + + const totalSpent = filtered.reduce((acc, curr) => acc + curr.amount, 0); + + return ( +
+
+
+

Spese Condominiali (Uscite)

+

Registro fatture e pagamenti fornitori per {activeCondo?.name}

+
+ {isPrivileged && ( + + )} +
+ + {/* Filters */} +
+
+ + +
+
+ +
+ setSearchTerm(e.target.value)} + className="w-full border p-2 pl-9 rounded-lg text-slate-700 bg-slate-50" + /> + +
+
+
+ + +
+
+
+

Totale

+

€ {totalSpent.toLocaleString()}

+
+
+
+ + {/* Table */} +
+ + + + + + + + + {isPrivileged && } + + + + {loading ? ( + + ) : filtered.length === 0 ? ( + + ) : ( + filtered.map(exp => ( + + + + + + + {isPrivileged && ( + + )} + + )) + )} + +
Data / ScadenzaFornitore & DescrizioneImportoStatoDettagliAzioni
Caricamento...
Nessuna spesa registrata.
+
+ {exp.paymentDate ? new Date(exp.paymentDate).toLocaleDateString() : '-'} +
+
+
{exp.supplierName}
+
{exp.description}
+
+ € {exp.amount.toFixed(2)} + + + {exp.status === 'PAID' ? 'Saldato' : exp.status === 'SUSPENDED' ? 'Sospeso' : 'Insoluto'} + + + {exp.invoiceNumber &&
Fatt. {exp.invoiceNumber}
} + {exp.paymentMethod &&
{exp.paymentMethod}
} + {exp.attachments && exp.attachments.length > 0 && ( +
+ {exp.attachments.map(att => ( + + ))} +
+ )} +
+
+ + +
+
+
+ + {/* MODAL */} + {showModal && ( +
+
+
+

{editingExpense ? 'Modifica Spesa' : 'Nuova Spesa'}

+ +
+
+
+ + setFormData({...formData, description: e.target.value})} required placeholder="Es. Pulizia scale Gennaio" /> +
+ +
+ + setFormData({...formData, supplierName: e.target.value})} + placeholder="Seleziona o scrivi nuovo..." + required + /> + + {suppliers.map((s, i) => +
+ +
+
+ + setFormData({...formData, amount: e.target.value})} required /> +
+
+ + setFormData({...formData, invoiceNumber: e.target.value})} /> +
+
+ +
+
+ + +
+
+ + setFormData({...formData, paymentDate: e.target.value})} /> +
+
+ +
+ + setFormData({...formData, paymentMethod: e.target.value})} placeholder="Es. Bonifico, RID..." /> +
+ +
+ + + {formData.attachments.length > 0 &&

{formData.attachments.length} file pronti per upload

} +
+ +
+ +