feat: Introduce multi-condo management and notices

This commit refactors the application to support managing multiple condominiums.

Key changes include:
- Introduction of `Condo` and `Notice` data types.
- Implementation of multi-condo selection and management, including active condo context.
- Addition of a notice system to inform users about important updates or events within a condo.
- Styling adjustments to ensure better visibility of form elements.
- Mock database updates to accommodate new entities and features.
This commit is contained in:
2025-12-07 01:37:19 +01:00
parent fdd912a932
commit 3f954c65b1
11 changed files with 1169 additions and 1283 deletions

View File

@@ -1,7 +1,8 @@
import React, { useEffect, useState, useMemo } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { CondoService } from '../services/mockDb';
import { Family, Payment, AppSettings, MonthStatus, PaymentStatus } from '../types';
import { Family, Payment, AppSettings, MonthStatus, PaymentStatus, Condo } from '../types';
import { ArrowLeft, CheckCircle2, AlertCircle, Plus, Calendar, CreditCard, TrendingUp } from 'lucide-react';
const MONTH_NAMES = [
@@ -16,6 +17,7 @@ export const FamilyDetail: React.FC = () => {
const [family, setFamily] = useState<Family | null>(null);
const [payments, setPayments] = useState<Payment[]>([]);
const [settings, setSettings] = useState<AppSettings | null>(null);
const [condo, setCondo] = useState<Condo | undefined>(undefined);
const [loading, setLoading] = useState(true);
const [selectedYear, setSelectedYear] = useState<number>(new Date().getFullYear());
const [availableYears, setAvailableYears] = useState<number[]>([]);
@@ -31,11 +33,12 @@ export const FamilyDetail: React.FC = () => {
const loadData = async () => {
setLoading(true);
try {
const [famList, famPayments, appSettings, years] = await Promise.all([
const [famList, famPayments, appSettings, years, activeCondo] = await Promise.all([
CondoService.getFamilies(),
CondoService.getPaymentsByFamily(id),
CondoService.getSettings(),
CondoService.getAvailableYears()
CondoService.getAvailableYears(),
CondoService.getActiveCondo()
]);
const foundFamily = famList.find(f => f.id === id);
@@ -43,7 +46,12 @@ export const FamilyDetail: React.FC = () => {
setFamily(foundFamily);
setPayments(famPayments);
setSettings(appSettings);
setNewPaymentAmount(appSettings.defaultMonthlyQuota);
setCondo(activeCondo);
// Use Family Custom Quota OR Condo Default
const defaultAmount = foundFamily.customMonthlyQuota ?? activeCondo?.defaultMonthlyQuota ?? 100;
setNewPaymentAmount(defaultAmount);
setAvailableYears(years);
setSelectedYear(appSettings.currentYear);
} else {
@@ -104,9 +112,10 @@ export const FamilyDetail: React.FC = () => {
const maxChartValue = useMemo(() => {
const max = Math.max(...chartData.map(d => d.amount));
const baseline = settings?.defaultMonthlyQuota || 100;
// Check family specific quota first
const baseline = family?.customMonthlyQuota ?? condo?.defaultMonthlyQuota ?? 100;
return max > 0 ? Math.max(max * 1.2, baseline) : baseline;
}, [chartData, settings]);
}, [chartData, condo, family]);
const handleAddPayment = async (e: React.FormEvent) => {
e.preventDefault();
@@ -154,6 +163,11 @@ export const FamilyDetail: React.FC = () => {
<div className="flex items-center gap-2 text-slate-500 mt-1 text-sm md:text-base">
<BuildingIcon className="w-4 h-4 flex-shrink-0" />
<span>Interno: {family.unitNumber}</span>
{family.customMonthlyQuota && (
<span className="ml-2 text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full font-bold">
Quota Personalizzata: {family.customMonthlyQuota}
</span>
)}
</div>
</div>
</div>

View File

@@ -1,25 +1,46 @@
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { CondoService } from '../services/mockDb';
import { Family, AppSettings } from '../types';
import { Search, ChevronRight, UserCircle } from 'lucide-react';
import { Family, Condo, Notice } 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 [settings, setSettings] = useState<AppSettings | null>(null);
const [activeCondo, setActiveCondo] = useState<Condo | undefined>(undefined);
const [notices, setNotices] = useState<Notice[]>([]);
const [userReadIds, setUserReadIds] = useState<string[]>([]);
const currentUser = CondoService.getCurrentUser();
useEffect(() => {
const fetchData = async () => {
try {
CondoService.seedPayments();
const [fams, sets] = await Promise.all([
const [fams, condo, allNotices] = await Promise.all([
CondoService.getFamilies(),
CondoService.getSettings()
CondoService.getActiveCondo(),
CondoService.getNotices()
]);
setFamilies(fams);
setSettings(sets);
setActiveCondo(condo);
if (condo && currentUser) {
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 {
@@ -34,17 +55,39 @@ export const FamilyList: React.FC = () => {
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-6">
<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">{settings?.condoName || 'Gestione Condominiale'}</p>
<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">
@@ -53,7 +96,7 @@ export const FamilyList: React.FC = () => {
</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"
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)}
@@ -61,13 +104,50 @@ export const FamilyList: React.FC = () => {
</div>
</div>
{/* Notices Section (Visible to Users) */}
{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-8 text-center text-slate-500 flex flex-col items-center gap-2">
<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.</span>
<span>Nessuna famiglia trovata in questo condominio.</span>
</li>
) : (
filteredFamilies.map((family) => (
@@ -99,4 +179,4 @@ export const FamilyList: React.FC = () => {
</div>
</div>
);
};
};

File diff suppressed because it is too large Load Diff