Update FamilyList.tsx

This commit is contained in:
2025-12-11 22:58:38 +01:00
committed by GitHub
parent 6d07135b97
commit b774661166

View File

@@ -3,7 +3,7 @@ import React, { useEffect, useState, useMemo } from 'react';
import { Link, useNavigate } 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, Ticket, TicketStatus } 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, Wallet, Briefcase, MessageSquareWarning, ArrowRight, AlertCircle, CheckCircle2 } from 'lucide-react'; import { Search, ChevronRight, UserCircle, Building, Bell, AlertTriangle, Hammer, Calendar, Info, Link as LinkIcon, Check, Wallet, Briefcase, MessageSquareWarning, ArrowRight, AlertCircle, CheckCircle2, ChevronDown, ChevronUp } from 'lucide-react';
export const FamilyList: React.FC = () => { export const FamilyList: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -24,6 +24,9 @@ export const FamilyList: React.FC = () => {
const [regularPaymentStatus, setRegularPaymentStatus] = useState<'OK' | 'PENDING' | 'OVERDUE'>('OK'); const [regularPaymentStatus, setRegularPaymentStatus] = useState<'OK' | 'PENDING' | 'OVERDUE'>('OK');
const [regularDebtAmount, setRegularDebtAmount] = useState<number>(0); const [regularDebtAmount, setRegularDebtAmount] = useState<number>(0);
// UI State for Notices
const [expandedNoticeId, setExpandedNoticeId] = useState<string | null>(null);
const currentUser = CondoService.getCurrentUser(); const currentUser = CondoService.getCurrentUser();
const isPrivileged = currentUser?.role === 'admin' || currentUser?.role === 'poweruser'; const isPrivileged = currentUser?.role === 'admin' || currentUser?.role === 'poweruser';
@@ -72,8 +75,6 @@ export const FamilyList: React.FC = () => {
// Check previous months first (Always Overdue if unpaid) // Check previous months first (Always Overdue if unpaid)
for (let m = 1; m < currentRealMonth; m++) { for (let m = 1; m < currentRealMonth; m++) {
// Simplified: assuming user only cares about current active year for dashboard alert
// In a real app, check past years too.
const isPaid = payments.some(p => p.forMonth === m && p.forYear === currentYear); const isPaid = payments.some(p => p.forMonth === m && p.forYear === currentYear);
if (!isPaid) { if (!isPaid) {
totalDebt += quota; totalDebt += quota;
@@ -84,14 +85,11 @@ export const FamilyList: React.FC = () => {
// Check current month // Check current month
const isCurrentMonthPaid = payments.some(p => p.forMonth === currentRealMonth && p.forYear === currentYear); const isCurrentMonthPaid = payments.some(p => p.forMonth === currentRealMonth && p.forYear === currentYear);
if (!isCurrentMonthPaid) { if (!isCurrentMonthPaid) {
// If today > dueDay -> Overdue
if (currentDay > dueDay) { if (currentDay > dueDay) {
totalDebt += quota; totalDebt += quota;
status = 'OVERDUE'; status = 'OVERDUE';
} } else if (currentDay >= (dueDay - 10)) {
// If today is within 10 days before dueDay -> Pending totalDebt += quota;
else if (currentDay >= (dueDay - 10)) {
totalDebt += quota; // It's due soon, so we count it
if (status !== 'OVERDUE') status = 'PENDING'; if (status !== 'OVERDUE') status = 'PENDING';
} }
} }
@@ -101,29 +99,30 @@ export const FamilyList: React.FC = () => {
} }
// --- NOTICE LOGIC --- // --- NOTICE LOGIC ---
// Ensure notices are loaded even if user has no family yet (but belongs to condo)
if (condo && currentUser && appSettings.features.notices) { if (condo && currentUser && appSettings.features.notices) {
const condoNotices = allNotices.filter(n => { // Filter: Must be same condo AND Active
// Visibility: Admin sees all. User sees Public OR Targeted.
const relevantNotices = allNotices.filter(n => {
if (n.condoId !== condo.id || !n.active) return false; if (n.condoId !== condo.id || !n.active) return false;
// Admin sees all active
if (isPrivileged) return true; if (isPrivileged) return true;
// Public notices (targetFamilyIds is null/empty) const isPublic = !n.targetFamilyIds || n.targetFamilyIds.length === 0;
if (!n.targetFamilyIds || n.targetFamilyIds.length === 0) return true; const isTargeted = currentUser.familyId && n.targetFamilyIds?.includes(currentUser.familyId);
// Targeted notices return isPublic || isTargeted;
return currentUser.familyId && n.targetFamilyIds.includes(currentUser.familyId);
}); });
setNotices(condoNotices); // Sort: Newest first
relevantNotices.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
setNotices(relevantNotices);
// Check read status // Check read status
const readStatuses = await Promise.all(condoNotices.map(n => CondoService.getNoticeReadStatus(n.id))); const readStatuses = await Promise.all(relevantNotices.map(n => CondoService.getNoticeReadStatus(n.id)));
const readIds: string[] = []; const readIds: string[] = [];
readStatuses.forEach((reads, idx) => { readStatuses.forEach((reads, idx) => {
if (reads.find(r => r.userId === currentUser.id)) { if (reads.find(r => r.userId === currentUser.id)) {
readIds.push(condoNotices[idx].id); readIds.push(relevantNotices[idx].id);
} }
}); });
setUserReadIds(readIds); setUserReadIds(readIds);
@@ -138,6 +137,18 @@ export const FamilyList: React.FC = () => {
fetchData(); fetchData();
}, [currentUser?.id, isPrivileged]); }, [currentUser?.id, isPrivileged]);
const handleMarkAsRead = async (noticeId: string) => {
if (!currentUser) return;
try {
await CondoService.markNoticeAsRead(noticeId, currentUser.id);
setUserReadIds(prev => [...prev, noticeId]);
} catch (e) { console.error("Error marking read", e); }
};
const toggleExpandNotice = (id: string) => {
setExpandedNoticeId(expandedNoticeId === id ? null : id);
};
const filteredFamilies = families.filter(f => const filteredFamilies = families.filter(f =>
f.name.toLowerCase().includes(searchTerm.toLowerCase()) || f.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
f.unitNumber.toLowerCase().includes(searchTerm.toLowerCase()) f.unitNumber.toLowerCase().includes(searchTerm.toLowerCase())
@@ -145,10 +156,10 @@ export const FamilyList: React.FC = () => {
const NoticeIcon = ({type}: {type: string}) => { const NoticeIcon = ({type}: {type: string}) => {
switch(type) { switch(type) {
case 'warning': return <AlertTriangle className="w-5 h-5 text-amber-500" />; case 'warning': return <AlertTriangle className="w-5 h-5 text-amber-600" />;
case 'maintenance': return <Hammer className="w-5 h-5 text-orange-500" />; case 'maintenance': return <Hammer className="w-5 h-5 text-orange-600" />;
case 'event': return <Calendar className="w-5 h-5 text-purple-500" />; case 'event': return <Calendar className="w-5 h-5 text-purple-600" />;
default: return <Info className="w-5 h-5 text-blue-500" />; default: return <Info className="w-5 h-5 text-blue-600" />;
} }
}; };
@@ -172,34 +183,79 @@ export const FamilyList: React.FC = () => {
return ( return (
<div className="space-y-8 pb-12 animate-fade-in"> <div className="space-y-8 pb-12 animate-fade-in">
{/* 1. NOTICES (Bacheca) - ALWAYS VISIBLE IF ENABLED */} {/* 1. BACHECA CONDOMINIALE (Notices) */}
{settings?.features.notices && notices.length > 0 && ( {settings?.features.notices && notices.length > 0 && (
<div className="space-y-3"> <div className="space-y-4">
<h3 className="font-bold text-slate-700 flex items-center gap-2"> <h3 className="font-bold text-slate-800 text-lg flex items-center gap-2">
<Bell className="w-5 h-5" /> Bacheca Avvisi <Bell className="w-5 h-5 text-blue-600" /> Bacheca Condominiale
</h3> </h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 gap-4">
{notices.map(notice => { {notices.map(notice => {
const isRead = userReadIds.includes(notice.id); const isRead = userReadIds.includes(notice.id);
const isExpanded = expandedNoticeId === notice.id;
// Style configuration based on type and read status
let borderClass = isRead ? 'border-slate-200' : 'border-blue-300 shadow-md ring-1 ring-blue-50';
let bgClass = isRead ? 'bg-white opacity-75' : 'bg-white';
if (notice.type === 'warning' && !isRead) {
borderClass = 'border-amber-300 shadow-md ring-1 ring-amber-50';
bgClass = 'bg-amber-50/30';
}
return ( return (
<div key={notice.id} className={`bg-white p-4 rounded-xl border relative transition-all ${isRead ? 'border-slate-100 opacity-80' : 'border-blue-200 shadow-sm ring-1 ring-blue-100'}`}> <div key={notice.id} className={`rounded-xl border p-4 transition-all duration-300 ${borderClass} ${bgClass}`}>
<div className="flex items-start gap-3"> <div className="flex items-start gap-4">
<div className={`p-2 rounded-lg flex-shrink-0 ${notice.type === 'warning' ? 'bg-amber-50' : 'bg-slate-50'}`}> {/* Icon Column */}
<div className={`p-3 rounded-full flex-shrink-0 ${isRead ? 'bg-slate-100 grayscale' : 'bg-white shadow-sm'}`}>
<NoticeIcon type={notice.type} /> <NoticeIcon type={notice.type} />
</div> </div>
<div className="min-w-0 flex-1">
{/* Content Column */}
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2 mb-1"> <div className="flex items-center justify-between gap-2 mb-1">
<h4 className={`font-bold text-sm truncate ${isRead ? 'text-slate-600' : 'text-slate-800'}`}>{notice.title}</h4> <div className="flex items-center gap-2 overflow-hidden">
{isRead && <span className="text-[10px] bg-slate-100 text-slate-400 px-1.5 py-0.5 rounded font-bold uppercase">Letto</span>} <h4 className={`font-bold text-base truncate ${isRead ? 'text-slate-600' : 'text-slate-900'}`}>{notice.title}</h4>
{!isRead && <span className="text-[10px] bg-blue-100 text-blue-600 px-1.5 py-0.5 rounded font-bold uppercase">Nuovo</span>} {!isRead && <span className="text-[10px] bg-blue-600 text-white px-2 py-0.5 rounded-full font-bold uppercase tracking-wide animate-pulse">Nuovo</span>}
{isRead && <span className="text-[10px] bg-slate-200 text-slate-500 px-2 py-0.5 rounded-full font-bold uppercase tracking-wide">Letto</span>}
</div>
<span className="text-xs text-slate-400 whitespace-nowrap">{new Date(notice.date).toLocaleDateString()}</span>
</div>
<div className={`text-sm text-slate-600 leading-relaxed whitespace-pre-wrap transition-all ${isExpanded ? '' : 'line-clamp-2'}`}>
{notice.content}
</div>
{/* Action Footer */}
<div className="flex items-center justify-between mt-3 pt-2 border-t border-slate-100/50">
<div className="flex items-center gap-4">
{/* Expand Button */}
{notice.content.length > 100 && (
<button
onClick={() => toggleExpandNotice(notice.id)}
className="text-xs font-medium text-blue-600 hover:text-blue-800 flex items-center gap-1"
>
{isExpanded ? <><ChevronUp className="w-3 h-3"/> Riduci</> : <><ChevronDown className="w-3 h-3"/> Leggi tutto</>}
</button>
)}
{/* External Link */}
{notice.link && (
<a href={notice.link} target="_blank" rel="noopener noreferrer" className="text-xs font-medium text-blue-600 hover:underline flex items-center gap-1">
<LinkIcon className="w-3 h-3"/> Apri Allegato
</a>
)}
</div>
{/* Mark as Read Button */}
{!isRead && (
<button
onClick={() => handleMarkAsRead(notice.id)}
className="text-xs bg-slate-100 hover:bg-slate-200 text-slate-600 px-3 py-1.5 rounded-lg font-medium transition-colors flex items-center gap-1"
>
<CheckCircle2 className="w-3 h-3"/> Segna come letto
</button>
)}
</div> </div>
<p className="text-xs text-slate-400 mb-2">{new Date(notice.date).toLocaleDateString()}</p>
<p className="text-sm text-slate-600 line-clamp-2 mb-2">{notice.content}</p>
{notice.link && (
<a href={notice.link} target="_blank" rel="noopener noreferrer" className="text-xs text-blue-600 font-medium hover:underline flex items-center gap-1">
<LinkIcon className="w-3 h-3"/> Apri Link
</a>
)}
</div> </div>
</div> </div>
</div> </div>