diff --git a/pages/Reports.tsx b/pages/Reports.tsx index 1f0d85e..7e54adb 100644 --- a/pages/Reports.tsx +++ b/pages/Reports.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useState, useMemo } from 'react'; import { CondoService } from '../services/mockDb'; -import { Payment, Family, Condo } from '../types'; -import { PieChart, Download, Calendar, Search, CreditCard, Banknote, Filter, ArrowUpRight } from 'lucide-react'; +import { Payment, Family, Condo, CondoExpense } from '../types'; +import { PieChart, Download, Calendar, Search, CreditCard, Banknote, Filter, ArrowUpRight, TrendingDown, Wallet, ArrowDownRight } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; const MONTH_NAMES = [ @@ -13,7 +13,8 @@ const MONTH_NAMES = [ export const ReportsPage: React.FC = () => { const navigate = useNavigate(); const [loading, setLoading] = useState(true); - const [payments, setPayments] = useState([]); + const [payments, setPayments] = useState([]); // Entrate + const [expenses, setExpenses] = useState([]); // Uscite const [families, setFamilies] = useState([]); const [activeCondo, setActiveCondo] = useState(undefined); const [availableYears, setAvailableYears] = useState([]); @@ -22,6 +23,9 @@ export const ReportsPage: React.FC = () => { const [selectedYear, setSelectedYear] = useState(new Date().getFullYear()); const [selectedMonth, setSelectedMonth] = useState('ALL'); const [searchTerm, setSearchTerm] = useState(''); + + // Tab View + const [activeTab, setActiveTab] = useState<'income' | 'expenses' | 'balance'>('balance'); useEffect(() => { const fetchData = async () => { @@ -37,12 +41,14 @@ export const ReportsPage: React.FC = () => { setActiveCondo(condo); if (condo) { - const [payList, famList, years] = await Promise.all([ + const [payList, expList, famList, years] = await Promise.all([ CondoService.getCondoPayments(condo.id), + CondoService.getCondoExpenses(undefined), // Fetch ALL to filter locally or by year later CondoService.getFamilies(condo.id), CondoService.getAvailableYears() ]); setPayments(payList); + setExpenses(expList); setFamilies(famList); setAvailableYears(years); if (years.length > 0 && !years.includes(selectedYear)) { @@ -58,8 +64,8 @@ export const ReportsPage: React.FC = () => { fetchData(); }, [navigate]); - // Filter Logic - const filteredData = useMemo(() => { + // --- FILTERED INCOME DATA (Payments from Families) --- + const filteredIncome = useMemo(() => { return payments.filter(p => { const matchesYear = p.forYear === selectedYear; const matchesMonth = selectedMonth === 'ALL' || p.forMonth === selectedMonth; @@ -82,15 +88,41 @@ export const ReportsPage: React.FC = () => { }); }, [payments, families, selectedYear, selectedMonth, searchTerm]); - // Statistics - const stats = useMemo(() => { - const totalAmount = filteredData.reduce((acc, curr) => acc + curr.amount, 0); - const paypalAmount = filteredData.filter(p => p.method === 'PayPal').reduce((acc, curr) => acc + curr.amount, 0); - const manualAmount = filteredData.filter(p => p.method === 'Manuale').reduce((acc, curr) => acc + curr.amount, 0); - const count = filteredData.length; + // --- FILTERED EXPENSES DATA (Payments to Suppliers) --- + const filteredExpenses = useMemo(() => { + return expenses.filter(e => { + // Determine "Year" and "Month" for expense. Use Payment Date if exists, else CreatedAt + const dateRef = e.paymentDate ? new Date(e.paymentDate) : new Date(e.createdAt); + const expYear = dateRef.getFullYear(); + const expMonth = dateRef.getMonth() + 1; // 1-12 - return { totalAmount, paypalAmount, manualAmount, count }; - }, [filteredData]); + const matchesYear = expYear === selectedYear; + const matchesMonth = selectedMonth === 'ALL' || expMonth === selectedMonth; + + const matchesSearch = searchTerm === '' || + e.description.toLowerCase().includes(searchTerm.toLowerCase()) || + e.supplierName.toLowerCase().includes(searchTerm.toLowerCase()); + + return matchesYear && matchesMonth && matchesSearch; + }); + }, [expenses, selectedYear, selectedMonth, searchTerm]); + + // Statistics Calculation + const stats = useMemo(() => { + // Income Stats + const totalIncome = filteredIncome.reduce((acc, curr) => acc + curr.amount, 0); + const incomeCount = filteredIncome.length; + + // Expense Stats + const totalExpenses = filteredExpenses.reduce((acc, curr) => acc + curr.amount, 0); + const expenseCount = filteredExpenses.length; + const paidExpenses = filteredExpenses.filter(e => e.status === 'PAID').reduce((acc, curr) => acc + curr.amount, 0); + + // Balance + const balance = totalIncome - totalExpenses; + + return { totalIncome, incomeCount, totalExpenses, expenseCount, paidExpenses, balance }; + }, [filteredIncome, filteredExpenses]); const getMonthLabel = (month: number) => { if (month === 13) return "Extra"; @@ -101,17 +133,38 @@ export const ReportsPage: React.FC = () => { const handleExportCSV = () => { if (!activeCondo) return; - const headers = ["Data Pagamento", "Famiglia", "Interno", "Mese Rif.", "Anno Rif.", "Importo", "Metodo", "Note"]; - const rows = filteredData.map(p => [ - new Date(p.datePaid).toLocaleDateString(), - p.familyName, - p.familyUnit, - getMonthLabel(p.forMonth), - p.forYear, - p.amount.toFixed(2), - p.method, - p.notes ? `"${p.notes.replace(/"/g, '""')}"` : "" - ]); + let headers: string[] = []; + let rows: string[][] = []; + let filename = ""; + + if (activeTab === 'income' || activeTab === 'balance') { + // Export Income + headers = ["Data Pagamento", "Famiglia", "Interno", "Mese Rif.", "Anno Rif.", "Importo", "Metodo", "Note"]; + rows = filteredIncome.map(p => [ + new Date(p.datePaid).toLocaleDateString(), + p.familyName, + p.familyUnit, + getMonthLabel(p.forMonth), + p.forYear.toString(), + p.amount.toFixed(2), + p.method, + p.notes ? `"${p.notes.replace(/"/g, '""')}"` : "" + ]); + filename = `Report_Entrate_${activeCondo.name}_${selectedYear}.csv`; + } else { + // Export Expenses + headers = ["Data", "Fornitore", "Descrizione", "Importo", "Stato", "Metodo", "Fattura"]; + rows = filteredExpenses.map(e => [ + e.paymentDate ? new Date(e.paymentDate).toLocaleDateString() : new Date(e.createdAt).toLocaleDateString(), + e.supplierName, + `"${e.description}"`, + e.amount.toFixed(2), + e.status, + e.paymentMethod || '', + e.invoiceNumber || '' + ]); + filename = `Report_Uscite_${activeCondo.name}_${selectedYear}.csv`; + } const csvContent = "data:text/csv;charset=utf-8," + headers.join(",") + "\n" @@ -120,7 +173,7 @@ export const ReportsPage: React.FC = () => { const encodedUri = encodeURI(csvContent); const link = document.createElement("a"); link.setAttribute("href", encodedUri); - link.setAttribute("download", `Report_Pagamenti_${activeCondo.name}_${selectedYear}.csv`); + link.setAttribute("download", filename); document.body.appendChild(link); link.click(); document.body.removeChild(link); @@ -129,13 +182,13 @@ export const ReportsPage: React.FC = () => { if (loading) return
Caricamento report...
; return ( -
+

- Reportistica & Transazioni + Reportistica & Bilancio

-

Analisi incassi per {activeCondo?.name}

+

Analisi finanziaria per {activeCondo?.name}

@@ -171,11 +224,11 @@ export const ReportsPage: React.FC = () => {
- +
setSearchTerm(e.target.value)} className="w-full border p-2 pl-9 rounded-lg text-slate-700 bg-slate-50" @@ -185,94 +238,142 @@ export const ReportsPage: React.FC = () => {
+ + {/* TABS */} +
+ + + +
- {/* Stats Cards */} + {/* Overview Cards (Dynamic based on Tab) */}
-
-

Incasso Totale

-

€ {stats.totalAmount.toLocaleString('it-IT', { minimumFractionDigits: 2 })}

-

- su {stats.count} transazioni -

-
- -
-
-

PayPal / Online

-

€ {stats.paypalAmount.toLocaleString('it-IT', { minimumFractionDigits: 2 })}

-
-
- {stats.count > 0 ? Math.round((stats.paypalAmount / stats.totalAmount) * 100) : 0}% -
+ {/* IN (Always shown or contextual) */} +
+

Totale Entrate

+

€ {stats.totalIncome.toLocaleString('it-IT', { minimumFractionDigits: 2 })}

-
-
-

Manuale / Bonifico

-

€ {stats.manualAmount.toLocaleString('it-IT', { minimumFractionDigits: 2 })}

-
-
- {stats.count > 0 ? Math.round((stats.manualAmount / stats.totalAmount) * 100) : 0}% -
+ {/* OUT */} +
+

Totale Uscite

+

€ {stats.totalExpenses.toLocaleString('it-IT', { minimumFractionDigits: 2 })}

+
+ + {/* BALANCE */} +
+

Saldo Periodo

+

= 0 ? 'text-blue-600' : 'text-red-600'}`}>€ {stats.balance.toLocaleString('it-IT', { minimumFractionDigits: 2 })}

+

Differenza Entrate - Uscite

- {/* Transactions Table */} + {/* MAIN DATA TABLE */}
-

Dettaglio Transazioni

- {filteredData.length} risultati +

+ {activeTab === 'income' ? 'Dettaglio Versamenti Famiglie' : activeTab === 'expenses' ? 'Dettaglio Spese Fornitori' : 'Movimenti Recenti'} +

- - - - - - - - - - - - - {filteredData.length === 0 ? ( - - ) : ( - filteredData.map((t) => ( - - - - - - - - - )) - )} - -
DataFamigliaRiferimentoMetodoNote / ID TransazioneImporto
Nessuna transazione trovata con i filtri attuali.
-
- - {new Date(t.datePaid).toLocaleDateString()} -
-
-
{t.familyName}
-
Int. {t.familyUnit}
-
- - {getMonthLabel(t.forMonth).substring(0, 5)} {t.forYear} - - - - {t.method === 'PayPal' ? : } - {t.method} - - - {t.notes || '-'} - - € {t.amount.toFixed(2)} -
+ {activeTab === 'expenses' ? ( + // EXPENSES TABLE + + + + + + + + + + + + {filteredExpenses.length === 0 ? ( + + ) : ( + filteredExpenses.map(e => ( + + + + + + + + )) + )} + +
DataFornitoreDescrizioneStatoImporto
Nessuna spesa trovata.
+ {e.paymentDate ? new Date(e.paymentDate).toLocaleDateString() : new Date(e.createdAt).toLocaleDateString()} + {e.supplierName}{e.description} + + {e.status === 'PAID' ? 'Saldato' : e.status === 'SUSPENDED' ? 'Sospeso' : 'Insoluto'} + + + € {e.amount.toFixed(2)} +
+ ) : ( + // INCOME TABLE (Default for Balance too) + + + + + + + + + + + + + {filteredIncome.length === 0 ? ( + + ) : ( + filteredIncome.map((t) => ( + + + + + + + + + )) + )} + +
DataFamigliaRiferimentoMetodoNoteImporto
Nessuna transazione trovata.
+
+ + {new Date(t.datePaid).toLocaleDateString()} +
+
+
{t.familyName}
+
Int. {t.familyUnit}
+
+ + {getMonthLabel(t.forMonth).substring(0, 5)} {t.forYear} + + + + {t.method === 'PayPal' ? : } + {t.method} + + + {t.notes || '-'} + + € {t.amount.toFixed(2)} +
+ )}