Update FamilyList.tsx
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { CondoService } from '../services/mockDb';
|
import { CondoService } from '../services/mockDb';
|
||||||
import { Family, Condo, Notice, AppSettings } from '../types';
|
import { Family, Condo, Notice, AppSettings, Ticket, TicketStatus } from '../types';
|
||||||
import { Search, ChevronRight, UserCircle, Building, Bell, AlertTriangle, Hammer, Calendar, Info, Link as LinkIcon, Check } from 'lucide-react';
|
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 = () => {
|
export const FamilyList: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [families, setFamilies] = useState<Family[]>([]);
|
const [families, setFamilies] = useState<Family[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
@@ -13,7 +14,15 @@ export const FamilyList: React.FC = () => {
|
|||||||
const [notices, setNotices] = useState<Notice[]>([]);
|
const [notices, setNotices] = useState<Notice[]>([]);
|
||||||
const [userReadIds, setUserReadIds] = useState<string[]>([]);
|
const [userReadIds, setUserReadIds] = useState<string[]>([]);
|
||||||
const [settings, setSettings] = useState<AppSettings | null>(null);
|
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 currentUser = CondoService.getCurrentUser();
|
||||||
|
const isPrivileged = currentUser?.role === 'admin' || currentUser?.role === 'poweruser';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@@ -29,28 +38,50 @@ export const FamilyList: React.FC = () => {
|
|||||||
setActiveCondo(condo);
|
setActiveCondo(condo);
|
||||||
setSettings(appSettings);
|
setSettings(appSettings);
|
||||||
|
|
||||||
if (condo && currentUser && appSettings.features.notices) {
|
// --- USER SPECIFIC DASHBOARD DATA ---
|
||||||
// Filter notices logic:
|
if (currentUser && !isPrivileged && currentUser.familyId && condo) {
|
||||||
// 1. Must belong to current condo and be active
|
// 1. Find My Family
|
||||||
// 2. If Admin/PowerUser -> See everything
|
const me = fams.find(f => f.id === currentUser.familyId) || null;
|
||||||
// 3. If standard User -> See Public notices (no target) OR Targeted notices containing their familyId
|
setMyFamily(me);
|
||||||
const isPrivileged = currentUser.role === 'admin' || currentUser.role === 'poweruser';
|
|
||||||
|
|
||||||
|
// 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 => {
|
const condoNotices = allNotices.filter(n => {
|
||||||
if (n.condoId !== condo.id || !n.active) return false;
|
if (n.condoId !== condo.id || !n.active) return false;
|
||||||
|
|
||||||
if (isPrivileged) return true;
|
if (isPrivileged) return true;
|
||||||
|
|
||||||
// Check targeting
|
|
||||||
const hasTargets = n.targetFamilyIds && n.targetFamilyIds.length > 0;
|
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);
|
return currentUser.familyId && n.targetFamilyIds?.includes(currentUser.familyId);
|
||||||
});
|
});
|
||||||
|
|
||||||
setNotices(condoNotices);
|
setNotices(condoNotices);
|
||||||
|
|
||||||
// Check which ones are read
|
|
||||||
const readStatuses = await Promise.all(condoNotices.map(n => CondoService.getNoticeReadStatus(n.id)));
|
const readStatuses = await Promise.all(condoNotices.map(n => CondoService.getNoticeReadStatus(n.id)));
|
||||||
const readIds: string[] = [];
|
const readIds: string[] = [];
|
||||||
readStatuses.forEach((reads, idx) => {
|
readStatuses.forEach((reads, idx) => {
|
||||||
@@ -68,7 +99,7 @@ export const FamilyList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, [currentUser?.id, isPrivileged]); // Dependencies
|
||||||
|
|
||||||
const filteredFamilies = families.filter(f =>
|
const filteredFamilies = families.filter(f =>
|
||||||
f.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
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) {
|
if (loading) {
|
||||||
return <div className="flex justify-center items-center h-64 text-slate-400">Caricamento in corso...</div>;
|
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 (
|
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 && (
|
{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">
|
<h3 className="font-bold text-slate-700 flex items-center gap-2">
|
||||||
<Bell className="w-5 h-5" /> Bacheca Avvisi
|
<Bell className="w-5 h-5" /> Bacheca Avvisi
|
||||||
</h3>
|
</h3>
|
||||||
@@ -138,10 +173,92 @@ export const FamilyList: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Responsive Header */}
|
{/* 2. USER DASHBOARD (Widgets) - Only for Regular Users with a linked Family */}
|
||||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
{!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>
|
<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">
|
<p className="text-slate-500 text-sm md:text-base flex items-center gap-1.5">
|
||||||
<Building className="w-4 h-4" />
|
<Building className="w-4 h-4" />
|
||||||
{activeCondo.name}
|
{activeCondo.name}
|
||||||
@@ -155,14 +272,14 @@ export const FamilyList: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
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"
|
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}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* List */}
|
{/* 4. LIST */}
|
||||||
<div className="bg-white shadow-sm rounded-xl overflow-hidden border border-slate-200">
|
<div className="bg-white shadow-sm rounded-xl overflow-hidden border border-slate-200">
|
||||||
<ul className="divide-y divide-slate-100">
|
<ul className="divide-y divide-slate-100">
|
||||||
{filteredFamilies.length === 0 ? (
|
{filteredFamilies.length === 0 ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user