Implement granular notice filtering logic based on user roles and notice targeting. Update Dockerfiles and .dockerignore for a cleaner build process.
203 lines
9.5 KiB
TypeScript
203 lines
9.5 KiB
TypeScript
|
|
import React, { useEffect, useState } from 'react';
|
|
import { Link } 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';
|
|
|
|
export const FamilyList: React.FC = () => {
|
|
const [families, setFamilies] = useState<Family[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [activeCondo, setActiveCondo] = useState<Condo | undefined>(undefined);
|
|
const [notices, setNotices] = useState<Notice[]>([]);
|
|
const [userReadIds, setUserReadIds] = useState<string[]>([]);
|
|
const [settings, setSettings] = useState<AppSettings | null>(null);
|
|
const currentUser = CondoService.getCurrentUser();
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
try {
|
|
CondoService.seedPayments();
|
|
const [fams, condo, allNotices, appSettings] = await Promise.all([
|
|
CondoService.getFamilies(),
|
|
CondoService.getActiveCondo(),
|
|
CondoService.getNotices(),
|
|
CondoService.getSettings()
|
|
]);
|
|
setFamilies(fams);
|
|
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';
|
|
|
|
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
|
|
|
|
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) => {
|
|
if (reads.find(r => r.userId === currentUser.id)) {
|
|
readIds.push(condoNotices[idx].id);
|
|
}
|
|
});
|
|
setUserReadIds(readIds);
|
|
}
|
|
|
|
} catch (e) {
|
|
console.error("Error fetching data", e);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchData();
|
|
}, []);
|
|
|
|
const filteredFamilies = families.filter(f =>
|
|
f.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
f.unitNumber.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
|
|
const NoticeIcon = ({type}: {type: string}) => {
|
|
switch(type) {
|
|
case 'warning': return <AlertTriangle className="w-5 h-5 text-amber-500" />;
|
|
case 'maintenance': return <Hammer className="w-5 h-5 text-orange-500" />;
|
|
case 'event': return <Calendar className="w-5 h-5 text-purple-500" />;
|
|
default: return <Info className="w-5 h-5 text-blue-500" />;
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return <div className="flex justify-center items-center h-64 text-slate-400">Caricamento in corso...</div>;
|
|
}
|
|
|
|
if (!activeCondo) {
|
|
return (
|
|
<div className="text-center p-12 text-slate-500">
|
|
<Building className="w-12 h-12 mx-auto mb-4 text-slate-300" />
|
|
<h2 className="text-xl font-bold text-slate-700">Nessun Condominio Selezionato</h2>
|
|
<p>Seleziona o crea un condominio dalle impostazioni.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-8 pb-12">
|
|
{/* Responsive Header */}
|
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-slate-800">Elenco Condomini</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}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="relative w-full md:w-80 lg:w-96">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<Search className="h-5 w-5 text-slate-400" />
|
|
</div>
|
|
<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..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Notices Section (Visible to Users only if feature enabled) */}
|
|
{settings?.features.notices && notices.length > 0 && (
|
|
<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>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{notices.map(notice => {
|
|
const isRead = userReadIds.includes(notice.id);
|
|
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 className="flex items-start gap-3">
|
|
<div className={`p-2 rounded-lg flex-shrink-0 ${notice.type === 'warning' ? 'bg-amber-50' : 'bg-slate-50'}`}>
|
|
<NoticeIcon type={notice.type} />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<h4 className={`font-bold text-sm truncate ${isRead ? 'text-slate-600' : 'text-slate-800'}`}>{notice.title}</h4>
|
|
{isRead && <span className="text-[10px] bg-slate-100 text-slate-400 px-1.5 py-0.5 rounded font-bold uppercase">Letto</span>}
|
|
{!isRead && <span className="text-[10px] bg-blue-100 text-blue-600 px-1.5 py-0.5 rounded font-bold uppercase">Nuovo</span>}
|
|
</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-3 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>
|
|
)}
|
|
|
|
{/* 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 ? (
|
|
<li className="p-12 text-center text-slate-500 flex flex-col items-center gap-2">
|
|
<Search className="w-8 h-8 text-slate-300" />
|
|
<span>Nessuna famiglia trovata in questo condominio.</span>
|
|
</li>
|
|
) : (
|
|
filteredFamilies.map((family) => (
|
|
<li key={family.id} className="hover:bg-slate-50 transition-colors active:bg-slate-100">
|
|
<Link to={`/family/${family.id}`} className="block p-4 sm:p-5">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="flex items-center gap-3 sm:gap-4 overflow-hidden">
|
|
<div className="bg-blue-100 p-2 sm:p-2.5 rounded-full flex-shrink-0">
|
|
<UserCircle className="w-6 h-6 sm:w-8 sm:h-8 text-blue-600" />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<p className="text-base sm:text-lg font-semibold text-blue-600 truncate">
|
|
{family.name}
|
|
</p>
|
|
<p className="flex items-center text-sm text-slate-500 truncate">
|
|
Interno: <span className="font-medium text-slate-700 ml-1">{family.unitNumber}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex-shrink-0">
|
|
<ChevronRight className="h-5 w-5 text-slate-400" />
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
</li>
|
|
))
|
|
)}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|