Update FamilyList.tsx
This commit is contained in:
@@ -3,7 +3,7 @@ import React, { useEffect, useState } 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, CheckCircle2, ChevronDown, ChevronUp, Eye } from 'lucide-react';
|
import { Search, ChevronRight, UserCircle, Building, Bell, AlertTriangle, Hammer, Calendar, Info, Link as LinkIcon, Check, Wallet, Briefcase, MessageSquareWarning, ArrowRight, CheckCircle2, ChevronDown, ChevronUp, Eye, Inbox } from 'lucide-react';
|
||||||
|
|
||||||
export const FamilyList: React.FC = () => {
|
export const FamilyList: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -34,18 +34,30 @@ export const FamilyList: React.FC = () => {
|
|||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
CondoService.seedPayments();
|
CondoService.seedPayments();
|
||||||
const [fams, condo, allNotices, appSettings] = await Promise.all([
|
|
||||||
CondoService.getFamilies(),
|
// 1. Fetch Global Settings & Active Condo FIRST
|
||||||
CondoService.getActiveCondo(),
|
const [appSettings, condo] = await Promise.all([
|
||||||
CondoService.getNotices(),
|
CondoService.getSettings(),
|
||||||
CondoService.getSettings()
|
CondoService.getActiveCondo()
|
||||||
|
]);
|
||||||
|
|
||||||
|
setSettings(appSettings);
|
||||||
|
setActiveCondo(condo);
|
||||||
|
|
||||||
|
if (!condo) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fetch specific data using condo.id (prevents race conditions)
|
||||||
|
const [fams, allNotices] = await Promise.all([
|
||||||
|
CondoService.getFamilies(condo.id),
|
||||||
|
CondoService.getNotices(condo.id)
|
||||||
]);
|
]);
|
||||||
setFamilies(fams);
|
setFamilies(fams);
|
||||||
setActiveCondo(condo);
|
|
||||||
setSettings(appSettings);
|
|
||||||
|
|
||||||
// --- USER SPECIFIC DASHBOARD DATA ---
|
// --- USER SPECIFIC DASHBOARD DATA ---
|
||||||
if (currentUser && !isPrivileged && currentUser.familyId && condo) {
|
if (currentUser && !isPrivileged && currentUser.familyId) {
|
||||||
// 1. Find My Family
|
// 1. Find My Family
|
||||||
const me = fams.find(f => f.id === currentUser.familyId) || null;
|
const me = fams.find(f => f.id === currentUser.familyId) || null;
|
||||||
setMyFamily(me);
|
setMyFamily(me);
|
||||||
@@ -98,7 +110,7 @@ export const FamilyList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- NOTICE LOGIC ---
|
// --- NOTICE LOGIC ---
|
||||||
if (condo && currentUser && appSettings.features.notices) {
|
if (currentUser && appSettings.features.notices) {
|
||||||
// Filter: Must be same condo AND Active
|
// Filter: Must be same condo AND Active
|
||||||
// Visibility: Admin sees all. User sees Public OR Targeted.
|
// Visibility: Admin sees all. User sees Public OR Targeted.
|
||||||
const relevantNotices = allNotices.filter(n => {
|
const relevantNotices = allNotices.filter(n => {
|
||||||
@@ -184,104 +196,99 @@ export const FamilyList: React.FC = () => {
|
|||||||
<div className="space-y-8 pb-12 animate-fade-in">
|
<div className="space-y-8 pb-12 animate-fade-in">
|
||||||
|
|
||||||
{/* 1. BACHECA CONDOMINIALE (Notices) */}
|
{/* 1. BACHECA CONDOMINIALE (Notices) */}
|
||||||
{/* This section is rendered if notices feature is enabled and there are notices */}
|
{/* Visualizzata SEMPRE se la feature è attiva, anche se vuota */}
|
||||||
{settings?.features.notices && notices.length > 0 && (
|
{settings?.features.notices && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="font-bold text-slate-800 text-lg 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 text-blue-600" /> Bacheca Condominiale
|
<Bell className="w-5 h-5 text-blue-600" /> Bacheca Condominiale
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 gap-4">
|
|
||||||
{notices.map(notice => {
|
|
||||||
const isRead = userReadIds.includes(notice.id);
|
|
||||||
const isExpanded = expandedNoticeId === notice.id;
|
|
||||||
|
|
||||||
// Style configuration
|
{notices.length === 0 ? (
|
||||||
// New notices get a distinct border and white background.
|
<div className="bg-white rounded-xl border border-slate-200 p-6 text-center shadow-sm">
|
||||||
// Read notices get a slate background and are slightly dimmed to indicate "archive" status.
|
<Inbox className="w-10 h-10 text-slate-300 mx-auto mb-2" />
|
||||||
let containerClass = isRead
|
<p className="text-slate-500 text-sm">Nessun avviso in bacheca.</p>
|
||||||
? 'bg-slate-50 border-slate-200'
|
</div>
|
||||||
: 'bg-white border-blue-200 shadow-sm ring-1 ring-blue-50';
|
) : (
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
{notices.map(notice => {
|
||||||
|
const isRead = userReadIds.includes(notice.id);
|
||||||
|
const isExpanded = expandedNoticeId === notice.id;
|
||||||
|
|
||||||
if (notice.type === 'warning' && !isRead) {
|
let containerClass = isRead
|
||||||
containerClass = 'bg-amber-50/50 border-amber-300 shadow-sm ring-1 ring-amber-50';
|
? 'bg-slate-50 border-slate-200'
|
||||||
}
|
: 'bg-white border-blue-200 shadow-sm ring-1 ring-blue-50';
|
||||||
|
|
||||||
return (
|
if (notice.type === 'warning' && !isRead) {
|
||||||
<div key={notice.id} className={`rounded-xl border p-4 transition-all duration-300 ${containerClass}`}>
|
containerClass = 'bg-amber-50/50 border-amber-300 shadow-sm ring-1 ring-amber-50';
|
||||||
<div className="flex items-start gap-4">
|
}
|
||||||
{/* Icon Column */}
|
|
||||||
<div className={`p-2.5 rounded-full flex-shrink-0 ${isRead ? 'bg-slate-200 grayscale opacity-70' : 'bg-white shadow-sm'}`}>
|
|
||||||
<NoticeIcon type={notice.type} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content Column */}
|
return (
|
||||||
<div className="flex-1 min-w-0">
|
<div key={notice.id} className={`rounded-xl border p-4 transition-all duration-300 ${containerClass}`}>
|
||||||
{/* Header: Title + Badges */}
|
<div className="flex items-start gap-4">
|
||||||
<div className="flex items-center justify-between gap-2 mb-1.5">
|
<div className={`p-2.5 rounded-full flex-shrink-0 ${isRead ? 'bg-slate-200 grayscale opacity-70' : 'bg-white shadow-sm'}`}>
|
||||||
<div className="flex items-center gap-2 overflow-hidden">
|
<NoticeIcon type={notice.type} />
|
||||||
<h4 className={`font-bold text-base truncate ${isRead ? 'text-slate-500' : 'text-slate-900'}`}>{notice.title}</h4>
|
</div>
|
||||||
{!isRead && (
|
|
||||||
<span className="text-[10px] bg-blue-600 text-white px-2 py-0.5 rounded-full font-bold uppercase tracking-wide animate-pulse">
|
<div className="flex-1 min-w-0">
|
||||||
Nuovo
|
<div className="flex items-center justify-between gap-2 mb-1.5">
|
||||||
</span>
|
<div className="flex items-center gap-2 overflow-hidden">
|
||||||
)}
|
<h4 className={`font-bold text-base truncate ${isRead ? 'text-slate-500' : 'text-slate-900'}`}>{notice.title}</h4>
|
||||||
{isRead && (
|
{!isRead && (
|
||||||
<span className="text-[10px] bg-slate-200 text-slate-500 px-2 py-0.5 rounded-full font-bold uppercase tracking-wide flex items-center gap-1">
|
<span className="text-[10px] bg-blue-600 text-white px-2 py-0.5 rounded-full font-bold uppercase tracking-wide animate-pulse">
|
||||||
<Check className="w-3 h-3"/> Letto
|
Nuovo
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{isRead && (
|
||||||
|
<span className="text-[10px] bg-slate-200 text-slate-500 px-2 py-0.5 rounded-full font-bold uppercase tracking-wide flex items-center gap-1">
|
||||||
|
<Check className="w-3 h-3"/> Letto
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-slate-400 whitespace-nowrap">{new Date(notice.date).toLocaleDateString()}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-slate-400 whitespace-nowrap">{new Date(notice.date).toLocaleDateString()}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Body Text */}
|
<div className={`text-sm leading-relaxed whitespace-pre-wrap transition-all ${isRead ? 'text-slate-500' : 'text-slate-700'} ${isExpanded ? '' : 'line-clamp-2'}`}>
|
||||||
<div className={`text-sm leading-relaxed whitespace-pre-wrap transition-all ${isRead ? 'text-slate-500' : 'text-slate-700'} ${isExpanded ? '' : 'line-clamp-2'}`}>
|
{notice.content}
|
||||||
{notice.content}
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer Actions */}
|
<div className="flex items-center justify-between mt-3 pt-2 border-t border-slate-200/50">
|
||||||
<div className="flex items-center justify-between mt-3 pt-2 border-t border-slate-200/50">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex items-center gap-4">
|
{(notice.content.length > 120 || notice.content.includes('\n')) && (
|
||||||
{/* Expand Button (only if long content) */}
|
<button
|
||||||
{(notice.content.length > 120 || notice.content.includes('\n')) && (
|
onClick={() => toggleExpandNotice(notice.id)}
|
||||||
|
className="text-xs font-medium text-slate-500 hover:text-blue-600 flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{isExpanded ? <><ChevronUp className="w-3 h-3"/> Riduci</> : <><ChevronDown className="w-3 h-3"/> Leggi tutto</>}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{notice.link && (
|
||||||
|
<a href={notice.link} target="_blank" rel="noopener noreferrer" className="text-xs font-medium text-blue-600 hover:text-blue-800 hover:underline flex items-center gap-1">
|
||||||
|
<LinkIcon className="w-3 h-3"/> Apri Allegato
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isRead && (
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleExpandNotice(notice.id)}
|
onClick={() => handleMarkAsRead(notice.id)}
|
||||||
className="text-xs font-medium text-slate-500 hover:text-blue-600 flex items-center gap-1"
|
className="text-xs bg-white border border-slate-200 hover:bg-blue-50 hover:text-blue-700 hover:border-blue-200 text-slate-600 px-3 py-1.5 rounded-lg font-medium transition-all flex items-center gap-1 shadow-sm"
|
||||||
>
|
>
|
||||||
{isExpanded ? <><ChevronUp className="w-3 h-3"/> Riduci</> : <><ChevronDown className="w-3 h-3"/> Leggi tutto</>}
|
<CheckCircle2 className="w-3 h-3"/> Segna come letto
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{isRead && (
|
||||||
{/* Link Button */}
|
<span className="text-[10px] text-slate-400 flex items-center gap-1">
|
||||||
{notice.link && (
|
<Eye className="w-3 h-3"/> Archiviato
|
||||||
<a href={notice.link} target="_blank" rel="noopener noreferrer" className="text-xs font-medium text-blue-600 hover:text-blue-800 hover:underline flex items-center gap-1">
|
</span>
|
||||||
<LinkIcon className="w-3 h-3"/> Apri Allegato
|
|
||||||
</a>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mark as Read Button (Only if not read) */}
|
|
||||||
{!isRead && (
|
|
||||||
<button
|
|
||||||
onClick={() => handleMarkAsRead(notice.id)}
|
|
||||||
className="text-xs bg-white border border-slate-200 hover:bg-blue-50 hover:text-blue-700 hover:border-blue-200 text-slate-600 px-3 py-1.5 rounded-lg font-medium transition-all flex items-center gap-1 shadow-sm"
|
|
||||||
>
|
|
||||||
<CheckCircle2 className="w-3 h-3"/> Segna come letto
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{/* Re-read indicator (optional visual cue) */}
|
|
||||||
{isRead && (
|
|
||||||
<span className="text-[10px] text-slate-400 flex items-center gap-1">
|
|
||||||
<Eye className="w-3 h-3"/> Archiviato
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user