Files
Condopay/pages/Reports.tsx
2025-12-11 20:51:13 +01:00

281 lines
14 KiB
TypeScript

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 { useNavigate } from 'react-router-dom';
const MONTH_NAMES = [
"Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno",
"Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"
];
export const ReportsPage: React.FC = () => {
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [payments, setPayments] = useState<Payment[]>([]);
const [families, setFamilies] = useState<Family[]>([]);
const [activeCondo, setActiveCondo] = useState<Condo | undefined>(undefined);
const [availableYears, setAvailableYears] = useState<number[]>([]);
// Filters
const [selectedYear, setSelectedYear] = useState<number>(new Date().getFullYear());
const [selectedMonth, setSelectedMonth] = useState<number | 'ALL'>('ALL');
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const settings = await CondoService.getSettings();
if (!settings.features.reports) {
navigate('/');
return;
}
const condo = await CondoService.getActiveCondo();
setActiveCondo(condo);
if (condo) {
const [payList, famList, years] = await Promise.all([
CondoService.getCondoPayments(condo.id),
CondoService.getFamilies(condo.id),
CondoService.getAvailableYears()
]);
setPayments(payList);
setFamilies(famList);
setAvailableYears(years);
if (years.length > 0 && !years.includes(selectedYear)) {
setSelectedYear(years[0]);
}
}
} catch (e) {
console.error("Error loading reports", e);
} finally {
setLoading(false);
}
};
fetchData();
}, [navigate]);
// Filter Logic
const filteredData = useMemo(() => {
return payments.filter(p => {
const matchesYear = p.forYear === selectedYear;
const matchesMonth = selectedMonth === 'ALL' || p.forMonth === selectedMonth;
const familyName = families.find(f => f.id === p.familyId)?.name.toLowerCase() || '';
const matchesSearch = searchTerm === '' ||
familyName.includes(searchTerm.toLowerCase()) ||
(p.notes && p.notes.toLowerCase().includes(searchTerm.toLowerCase()));
return matchesYear && matchesMonth && matchesSearch;
}).map(p => {
const isPayPal = p.notes && p.notes.includes("PayPal");
const family = families.find(f => f.id === p.familyId);
return {
...p,
familyName: family ? family.name : 'Sconosciuto',
familyUnit: family ? family.unitNumber : '-',
method: isPayPal ? 'PayPal' : 'Manuale'
};
});
}, [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;
return { totalAmount, paypalAmount, manualAmount, count };
}, [filteredData]);
const getMonthLabel = (month: number) => {
if (month === 13) return "Extra";
if (month >= 1 && month <= 12) return MONTH_NAMES[month - 1];
return "-";
};
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, '""')}"` : ""
]);
const csvContent = "data:text/csv;charset=utf-8,"
+ headers.join(",") + "\n"
+ rows.map(e => e.join(",")).join("\n");
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", `Report_Pagamenti_${activeCondo.name}_${selectedYear}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
if (loading) return <div className="p-8 text-center text-slate-400">Caricamento report...</div>;
return (
<div className="space-y-8 pb-20 animate-fade-in">
<div className="flex flex-col md:flex-row justify-between md:items-end gap-4">
<div>
<h2 className="text-2xl font-bold text-slate-800 flex items-center gap-2">
<PieChart className="w-6 h-6 text-blue-600"/> Reportistica & Transazioni
</h2>
<p className="text-slate-500 text-sm">Analisi incassi per {activeCondo?.name}</p>
</div>
<div className="flex gap-2">
<button onClick={handleExportCSV} className="bg-slate-800 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 hover:bg-slate-900 transition-colors">
<Download className="w-4 h-4"/> Export CSV
</button>
</div>
</div>
{/* Filters Bar */}
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm flex flex-col md:flex-row gap-4">
<div className="flex-1 grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Anno</label>
<select
value={selectedYear}
onChange={e => setSelectedYear(parseInt(e.target.value))}
className="w-full border p-2 rounded-lg text-slate-700 bg-slate-50 font-medium"
>
{availableYears.map(y => <option key={y} value={y}>{y}</option>)}
</select>
</div>
<div>
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Mese</label>
<select
value={selectedMonth}
onChange={e => setSelectedMonth(e.target.value === 'ALL' ? 'ALL' : parseInt(e.target.value))}
className="w-full border p-2 rounded-lg text-slate-700 bg-slate-50 font-medium"
>
<option value="ALL">Tutti</option>
{MONTH_NAMES.map((m, i) => <option key={i} value={i+1}>{m}</option>)}
<option value={13}>Spese Extra</option>
</select>
</div>
<div className="col-span-2">
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Cerca Famiglia/ID</label>
<div className="relative">
<input
type="text"
placeholder="Cerca..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className="w-full border p-2 pl-9 rounded-lg text-slate-700 bg-slate-50"
/>
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-2.5"/>
</div>
</div>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-blue-600 text-white p-6 rounded-xl shadow-lg shadow-blue-200">
<p className="text-blue-100 text-sm font-medium mb-1">Incasso Totale</p>
<h3 className="text-3xl font-bold"> {stats.totalAmount.toLocaleString('it-IT', { minimumFractionDigits: 2 })}</h3>
<p className="text-blue-200 text-xs mt-2 flex items-center gap-1">
<ArrowUpRight className="w-3 h-3"/> su {stats.count} transazioni
</p>
</div>
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm flex items-center justify-between">
<div>
<p className="text-slate-500 text-xs font-bold uppercase mb-1 flex items-center gap-1"><CreditCard className="w-3 h-3"/> PayPal / Online</p>
<h3 className="text-2xl font-bold text-slate-800"> {stats.paypalAmount.toLocaleString('it-IT', { minimumFractionDigits: 2 })}</h3>
</div>
<div className="h-10 w-10 bg-blue-50 text-blue-600 rounded-full flex items-center justify-center font-bold text-xs">
{stats.count > 0 ? Math.round((stats.paypalAmount / stats.totalAmount) * 100) : 0}%
</div>
</div>
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm flex items-center justify-between">
<div>
<p className="text-slate-500 text-xs font-bold uppercase mb-1 flex items-center gap-1"><Banknote className="w-3 h-3"/> Manuale / Bonifico</p>
<h3 className="text-2xl font-bold text-slate-800"> {stats.manualAmount.toLocaleString('it-IT', { minimumFractionDigits: 2 })}</h3>
</div>
<div className="h-10 w-10 bg-slate-100 text-slate-600 rounded-full flex items-center justify-center font-bold text-xs">
{stats.count > 0 ? Math.round((stats.manualAmount / stats.totalAmount) * 100) : 0}%
</div>
</div>
</div>
{/* Transactions Table */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div className="p-4 border-b border-slate-100 bg-slate-50/50 flex justify-between items-center">
<h3 className="font-bold text-slate-800">Dettaglio Transazioni</h3>
<span className="text-xs bg-slate-200 text-slate-600 px-2 py-1 rounded-full font-bold">{filteredData.length} risultati</span>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm text-slate-600">
<thead className="bg-slate-50 text-slate-700 font-semibold border-b border-slate-200">
<tr>
<th className="px-6 py-3 whitespace-nowrap">Data</th>
<th className="px-6 py-3">Famiglia</th>
<th className="px-6 py-3">Riferimento</th>
<th className="px-6 py-3">Metodo</th>
<th className="px-6 py-3">Note / ID Transazione</th>
<th className="px-6 py-3 text-right">Importo</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{filteredData.length === 0 ? (
<tr><td colSpan={6} className="p-8 text-center text-slate-400">Nessuna transazione trovata con i filtri attuali.</td></tr>
) : (
filteredData.map((t) => (
<tr key={t.id} className="hover:bg-slate-50 transition-colors">
<td className="px-6 py-4 whitespace-nowrap text-slate-500 font-medium">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-slate-300"/>
{new Date(t.datePaid).toLocaleDateString()}
</div>
</td>
<td className="px-6 py-4">
<div className="font-bold text-slate-700">{t.familyName}</div>
<div className="text-xs text-slate-400">Int. {t.familyUnit}</div>
</td>
<td className="px-6 py-4">
<span className="bg-slate-100 text-slate-600 px-2 py-1 rounded text-xs font-bold uppercase">
{getMonthLabel(t.forMonth).substring(0, 5)} {t.forYear}
</span>
</td>
<td className="px-6 py-4">
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-bold uppercase ${t.method === 'PayPal' ? 'bg-blue-100 text-blue-700' : 'bg-green-100 text-green-700'}`}>
{t.method === 'PayPal' ? <CreditCard className="w-3 h-3"/> : <Banknote className="w-3 h-3"/>}
{t.method}
</span>
</td>
<td className="px-6 py-4 max-w-xs truncate text-slate-500 text-xs" title={t.notes}>
{t.notes || '-'}
</td>
<td className="px-6 py-4 text-right font-bold text-slate-800">
{t.amount.toFixed(2)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
);
};