Update FamilyList.tsx
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { CondoService } from '../services/mockDb';
|
||||
import { Family, Condo, Notice, AppSettings } from '../types';
|
||||
import { Search, ChevronRight, UserCircle, Building, Bell, AlertTriangle, Hammer, Calendar, Info, Link as LinkIcon, Check } from 'lucide-react';
|
||||
import { Family, Condo, Notice, AppSettings, Ticket, TicketStatus } from '../types';
|
||||
import { Search, ChevronRight, UserCircle, Building, Bell, AlertTriangle, Hammer, Calendar, Info, Link as LinkIcon, Check, Wallet, Briefcase, MessageSquareWarning, ArrowRight, AlertCircle, CheckCircle2 } from 'lucide-react';
|
||||
|
||||
export const FamilyList: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [families, setFamilies] = useState<Family[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
@@ -13,7 +14,15 @@ export const FamilyList: React.FC = () => {
|
||||
const [notices, setNotices] = useState<Notice[]>([]);
|
||||
const [userReadIds, setUserReadIds] = useState<string[]>([]);
|
||||
const [settings, setSettings] = useState<AppSettings | null>(null);
|
||||
|
||||
// User Dashboard Data
|
||||
const [myTickets, setMyTickets] = useState<Ticket[]>([]);
|
||||
const [myExtraExpenses, setMyExtraExpenses] = useState<any[]>([]);
|
||||
const [myRegularDebt, setMyRegularDebt] = useState<number>(0);
|
||||
const [myFamily, setMyFamily] = useState<Family | null>(null);
|
||||
|
||||
const currentUser = CondoService.getCurrentUser();
|
||||
const isPrivileged = currentUser?.role === 'admin' || currentUser?.role === 'poweruser';
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
@@ -29,28 +38,50 @@ export const FamilyList: React.FC = () => {
|
||||
setActiveCondo(condo);
|
||||
setSettings(appSettings);
|
||||
|
||||
if (condo && currentUser && appSettings.features.notices) {
|
||||
// Filter notices logic:
|
||||
// 1. Must belong to current condo and be active
|
||||
// 2. If Admin/PowerUser -> See everything
|
||||
// 3. If standard User -> See Public notices (no target) OR Targeted notices containing their familyId
|
||||
const isPrivileged = currentUser.role === 'admin' || currentUser.role === 'poweruser';
|
||||
// --- USER SPECIFIC DASHBOARD DATA ---
|
||||
if (currentUser && !isPrivileged && currentUser.familyId && condo) {
|
||||
// 1. Find My Family
|
||||
const me = fams.find(f => f.id === currentUser.familyId) || null;
|
||||
setMyFamily(me);
|
||||
|
||||
// 2. Fetch Tickets
|
||||
const tickets = await CondoService.getTickets(); // Backend filters for user
|
||||
setMyTickets(tickets);
|
||||
|
||||
// 3. Fetch Extra Expenses
|
||||
if (appSettings.features.extraordinaryExpenses) {
|
||||
const extra = await CondoService.getMyExpenses();
|
||||
setMyExtraExpenses(extra);
|
||||
}
|
||||
|
||||
// 4. Calculate Regular Debt (Current Year)
|
||||
const payments = await CondoService.getPaymentsByFamily(currentUser.familyId);
|
||||
const currentYear = appSettings.currentYear;
|
||||
const now = new Date();
|
||||
const currentMonth = now.getFullYear() === currentYear ? now.getMonth() + 1 : (now.getFullYear() > currentYear ? 12 : 0);
|
||||
|
||||
let debt = 0;
|
||||
const quota = me?.customMonthlyQuota ?? condo.defaultMonthlyQuota;
|
||||
|
||||
for (let m = 1; m <= currentMonth; m++) {
|
||||
const isPaid = payments.some(p => p.forMonth === m && p.forYear === currentYear);
|
||||
if (!isPaid) debt += quota;
|
||||
}
|
||||
setMyRegularDebt(debt);
|
||||
}
|
||||
|
||||
// --- NOTICE LOGIC ---
|
||||
if (condo && currentUser && appSettings.features.notices) {
|
||||
const condoNotices = allNotices.filter(n => {
|
||||
if (n.condoId !== condo.id || !n.active) return false;
|
||||
|
||||
if (isPrivileged) return true;
|
||||
|
||||
// Check targeting
|
||||
const hasTargets = n.targetFamilyIds && n.targetFamilyIds.length > 0;
|
||||
if (!hasTargets) return true; // Public to all
|
||||
|
||||
if (!hasTargets) return true;
|
||||
return currentUser.familyId && n.targetFamilyIds?.includes(currentUser.familyId);
|
||||
});
|
||||
|
||||
setNotices(condoNotices);
|
||||
|
||||
// Check which ones are read
|
||||
const readStatuses = await Promise.all(condoNotices.map(n => CondoService.getNoticeReadStatus(n.id)));
|
||||
const readIds: string[] = [];
|
||||
readStatuses.forEach((reads, idx) => {
|
||||
@@ -68,7 +99,7 @@ export const FamilyList: React.FC = () => {
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
}, [currentUser?.id, isPrivileged]); // Dependencies
|
||||
|
||||
const filteredFamilies = families.filter(f =>
|
||||
f.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
@@ -84,6 +115,10 @@ export const FamilyList: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Dashboard Calculations
|
||||
const activeTicketsCount = myTickets.filter(t => t.status !== TicketStatus.RESOLVED && t.status !== TicketStatus.CLOSED).length;
|
||||
const extraDebt = myExtraExpenses.reduce((acc, exp) => acc + Math.max(0, exp.myShare.amountDue - exp.myShare.amountPaid), 0);
|
||||
|
||||
if (loading) {
|
||||
return <div className="flex justify-center items-center h-64 text-slate-400">Caricamento in corso...</div>;
|
||||
}
|
||||
@@ -99,11 +134,11 @@ export const FamilyList: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8 pb-12">
|
||||
<div className="space-y-8 pb-12 animate-fade-in">
|
||||
|
||||
{/* Notices Section - Dashboard effect */}
|
||||
{/* 1. NOTICES (Bacheca) - High Priority */}
|
||||
{settings?.features.notices && notices.length > 0 && (
|
||||
<div className="space-y-3 mb-6">
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-bold text-slate-700 flex items-center gap-2">
|
||||
<Bell className="w-5 h-5" /> Bacheca Avvisi
|
||||
</h3>
|
||||
@@ -138,10 +173,92 @@ export const FamilyList: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Responsive Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||
{/* 2. USER DASHBOARD (Widgets) - Only for Regular Users with a linked Family */}
|
||||
{!isPrivileged && myFamily && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-bold text-slate-700 flex items-center gap-2">
|
||||
<UserCircle className="w-5 h-5" /> La Tua Situazione
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
|
||||
{/* Regular Payments Widget */}
|
||||
<div className={`p-5 rounded-xl border shadow-sm flex flex-col justify-between ${myRegularDebt > 0 ? 'bg-white border-red-200' : 'bg-white border-slate-200'}`}>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-slate-800">Elenco Condomini</h2>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div className={`p-2 rounded-lg ${myRegularDebt > 0 ? 'bg-red-50 text-red-600' : 'bg-green-50 text-green-600'}`}>
|
||||
<Wallet className="w-6 h-6" />
|
||||
</div>
|
||||
{myRegularDebt > 0 && <span className="bg-red-100 text-red-700 text-[10px] font-bold px-2 py-1 rounded uppercase">Insoluto</span>}
|
||||
</div>
|
||||
<p className="text-slate-500 text-xs font-bold uppercase tracking-wide">Rate Condominiali {settings?.currentYear}</p>
|
||||
<h4 className={`text-2xl font-bold mt-1 ${myRegularDebt > 0 ? 'text-red-600' : 'text-slate-800'}`}>
|
||||
{myRegularDebt > 0 ? `€ -${myRegularDebt.toFixed(2)}` : 'In Regola'}
|
||||
</h4>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate(`/family/${myFamily.id}`)}
|
||||
className="mt-4 w-full py-2 bg-slate-50 hover:bg-slate-100 text-slate-700 rounded-lg text-sm font-medium flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
{myRegularDebt > 0 ? 'Paga Ora' : 'Vedi Storico'} <ArrowRight className="w-4 h-4"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Extra Expenses Widget */}
|
||||
{settings?.features.extraordinaryExpenses && (
|
||||
<div className={`p-5 rounded-xl border shadow-sm flex flex-col justify-between ${extraDebt > 0 ? 'bg-white border-orange-200' : 'bg-white border-slate-200'}`}>
|
||||
<div>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div className={`p-2 rounded-lg ${extraDebt > 0 ? 'bg-orange-50 text-orange-600' : 'bg-slate-100 text-slate-500'}`}>
|
||||
<Briefcase className="w-6 h-6" />
|
||||
</div>
|
||||
{extraDebt > 0 && <span className="bg-orange-100 text-orange-700 text-[10px] font-bold px-2 py-1 rounded uppercase">Da Saldare</span>}
|
||||
</div>
|
||||
<p className="text-slate-500 text-xs font-bold uppercase tracking-wide">Spese Straordinarie</p>
|
||||
<h4 className={`text-2xl font-bold mt-1 ${extraDebt > 0 ? 'text-orange-600' : 'text-slate-800'}`}>
|
||||
{extraDebt > 0 ? `€ -${extraDebt.toFixed(2)}` : 'Nessuna'}
|
||||
</h4>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate('/extraordinary')}
|
||||
className="mt-4 w-full py-2 bg-slate-50 hover:bg-slate-100 text-slate-700 rounded-lg text-sm font-medium flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
{extraDebt > 0 ? 'Dettagli & Saldo' : 'Vedi Progetti'} <ArrowRight className="w-4 h-4"/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tickets Widget */}
|
||||
{settings?.features.tickets && (
|
||||
<div className="p-5 rounded-xl border border-slate-200 shadow-sm bg-white flex flex-col justify-between">
|
||||
<div>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div className={`p-2 rounded-lg ${activeTicketsCount > 0 ? 'bg-blue-50 text-blue-600' : 'bg-slate-100 text-slate-500'}`}>
|
||||
<MessageSquareWarning className="w-6 h-6" />
|
||||
</div>
|
||||
{activeTicketsCount > 0 && <span className="bg-blue-100 text-blue-700 text-[10px] font-bold px-2 py-1 rounded uppercase">{activeTicketsCount} Attive</span>}
|
||||
</div>
|
||||
<p className="text-slate-500 text-xs font-bold uppercase tracking-wide">Le tue Segnalazioni</p>
|
||||
<h4 className="text-2xl font-bold mt-1 text-slate-800">
|
||||
{activeTicketsCount > 0 ? `${activeTicketsCount} in corso` : 'Tutto OK'}
|
||||
</h4>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate('/tickets')}
|
||||
className="mt-4 w-full py-2 bg-slate-50 hover:bg-slate-100 text-slate-700 rounded-lg text-sm font-medium flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
{activeTicketsCount > 0 ? 'Vedi Risposte' : 'Nuova Segnalazione'} <ArrowRight className="w-4 h-4"/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 3. DIRECTORY HEADER */}
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4 pt-4 border-t border-slate-200">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-slate-800">Rubrica Condominiale</h2>
|
||||
<p className="text-slate-500 text-sm md:text-base flex items-center gap-1.5">
|
||||
<Building className="w-4 h-4" />
|
||||
{activeCondo.name}
|
||||
@@ -155,14 +272,14 @@ export const FamilyList: React.FC = () => {
|
||||
<input
|
||||
type="text"
|
||||
className="block w-full pl-10 pr-3 py-2.5 border border-slate-300 rounded-xl leading-5 bg-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-150 ease-in-out sm:text-sm shadow-sm text-slate-700"
|
||||
placeholder="Cerca nome o interno..."
|
||||
placeholder="Cerca condomino..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
{/* 4. LIST */}
|
||||
<div className="bg-white shadow-sm rounded-xl overflow-hidden border border-slate-200">
|
||||
<ul className="divide-y divide-slate-100">
|
||||
{filteredFamilies.length === 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user