Files
Condopay/pages/FamilyList.tsx
frakarr 919be985c9 feat: Introduce app feature flags
This commit refactors the application settings to include a new `AppFeatures` interface. This allows for granular control over which features are enabled for the application.

The `AppFeatures` object includes boolean flags for:
- `multiCondo`: Enables or disables the multi-condominium management feature.
- `tickets`: Placeholder for future ticket system integration.
- `payPal`: Enables or disables PayPal payment gateway integration.
- `notices`: Enables or disables the display and management of notices.

These flags are now fetched and stored in the application state, influencing UI elements and logic across various pages to conditionally render features based on their enabled status. For example, the multi-condo selection in `Layout.tsx` and the notice display in `FamilyList.tsx` are now gated by these feature flags. The `FamilyDetail.tsx` page also uses the `payPal` flag to conditionally enable the PayPal payment option.

The `SettingsPage.tsx` has been updated to include a new 'features' tab for managing these flags.
2025-12-07 20:21:01 +01:00

185 lines
8.7 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) {
const condoNotices = allNotices.filter(n => n.condoId === condo.id && n.active);
setNotices(condoNotices);
// Check which ones are read
const readStatuses = await Promise.all(condoNotices.map(n => CondoService.getNoticeReadStatus(n.id)));
const readIds = [];
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>
);
};