From 3f954c65b11b1d72ab25219c123966cafd75c3cf Mon Sep 17 00:00:00 2001 From: frakarr Date: Sun, 7 Dec 2025 01:37:19 +0100 Subject: [PATCH] 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. --- .dockerignore | Bin 159 -> 221 bytes Dockerfile | 29 - components/Layout.tsx | 178 ++++- index.html | 9 +- nginx.conf | 28 +- pages/FamilyDetail.tsx | 26 +- pages/FamilyList.tsx | 104 ++- pages/Settings.tsx | 1481 +++++++++++++++++----------------------- server/Dockerfile | 18 - services/mockDb.ts | 541 +++++++-------- types.ts | 38 +- 11 files changed, 1169 insertions(+), 1283 deletions(-) diff --git a/.dockerignore b/.dockerignore index b870f406047bb9235a75f1c44adde59b9b659779..81c07c04b021f08f326e72274987ed1708ffa0c4 100644 GIT binary patch literal 221 zcmaFAfA9PKd*gsOOBP6k12G(xLRg#`K>EfSxG6G$)tF-Yb0HB~_J-g+PbL{R&0F9iuAj0JMoAqtMKhS10)vZDcmFrpCv literal 159 zcmY+8!3x7L3`6h!mwMkYNRPX$A5duQg)loVaT3_yuaph8n~;#6iV?4B)P)_&t`qUr z6N)9E> { - const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false); + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const user = CondoService.getCurrentUser(); - const isAdmin = user?.role === 'admin'; + const isAdmin = user?.role === 'admin' || user?.role === 'poweruser'; + + const [condos, setCondos] = useState([]); + const [activeCondo, setActiveCondo] = useState(undefined); + const [showCondoDropdown, setShowCondoDropdown] = useState(false); + + // Notice Modal State + const [activeNotice, setActiveNotice] = useState(null); + + const fetchContext = async () => { + if (isAdmin) { + const list = await CondoService.getCondos(); + setCondos(list); + } + const active = await CondoService.getActiveCondo(); + setActiveCondo(active); + + // Check for notices for User (not admin, to avoid spamming admin managing multiple condos) + if (!isAdmin && active && user) { + const unread = await CondoService.getUnreadNoticesForUser(user.id, active.id); + if (unread.length > 0) { + // Show the most recent unread notice + setActiveNotice(unread[0]); + } + } + }; + + useEffect(() => { + fetchContext(); + + // Listen for updates from Settings + const handleCondoUpdate = () => fetchContext(); + window.addEventListener('condo-updated', handleCondoUpdate); + return () => window.removeEventListener('condo-updated', handleCondoUpdate); + }, [isAdmin]); + + const handleCondoSwitch = (condoId: string) => { + CondoService.setActiveCondo(condoId); + setShowCondoDropdown(false); + }; + + const handleReadNotice = async () => { + if (activeNotice && user) { + await CondoService.markNoticeAsRead(activeNotice.id, user.id); + setActiveNotice(null); + } + }; + + const closeNoticeModal = () => setActiveNotice(null); const navClass = ({ isActive }: { isActive: boolean }) => `flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 ${ @@ -17,16 +67,61 @@ export const Layout: React.FC = () => { const closeMenu = () => setIsMobileMenuOpen(false); + const NoticeIcon = ({type}: {type: string}) => { + switch(type) { + case 'warning': return ; + case 'maintenance': return ; + case 'event': return ; + default: return ; + } + }; + return (
+ {/* Active Notice Modal */} + {activeNotice && ( +
+
+
+
+ +
+
+

{activeNotice.title}

+

{new Date(activeNotice.date).toLocaleDateString()}

+
+
+
+ {activeNotice.content} + {activeNotice.link && ( + + )} +
+
+ + +
+
+
+ )} + {/* Mobile Header */}
-
-
+
+
-

CondoPay

+
+

CondoPay

+ {activeCondo &&

{activeCondo.name}

} +
+ + {showCondoDropdown && ( +
+ {condos.map(c => ( + + ))} +
+ )} +
+ )} + {!isAdmin && activeCondo && ( +
+ {activeCondo.name} +
+ )}
- {/* Mobile Header inside drawer to align content */} + {/* Mobile Header inside drawer */}
Menu
+ {/* Mobile Condo Switcher */} + {isAdmin && ( +
+

+ + Condominio Attivo +

+ +
+ )} +
diff --git a/pages/FamilyList.tsx b/pages/FamilyList.tsx index 6aa6599..2241b1d 100644 --- a/pages/FamilyList.tsx +++ b/pages/FamilyList.tsx @@ -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([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); - const [settings, setSettings] = useState(null); + const [activeCondo, setActiveCondo] = useState(undefined); + const [notices, setNotices] = useState([]); + const [userReadIds, setUserReadIds] = useState([]); + 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 ; + case 'maintenance': return ; + case 'event': return ; + default: return ; + } + }; + if (loading) { return
Caricamento in corso...
; } + if (!activeCondo) { + return ( +
+ +

Nessun Condominio Selezionato

+

Seleziona o crea un condominio dalle impostazioni.

+
+ ); + } + return ( -
+
{/* Responsive Header */}

Elenco Condomini

-

{settings?.condoName || 'Gestione Condominiale'}

+

+ + {activeCondo.name} +

@@ -53,7 +96,7 @@ export const FamilyList: React.FC = () => {
setSearchTerm(e.target.value)} @@ -61,13 +104,50 @@ export const FamilyList: React.FC = () => {
+ {/* Notices Section (Visible to Users) */} + {notices.length > 0 && ( +
+

+ Bacheca Avvisi +

+
+ {notices.map(notice => { + const isRead = userReadIds.includes(notice.id); + return ( +
+
+
+ +
+
+
+

{notice.title}

+ {isRead && Letto} + {!isRead && Nuovo} +
+

{new Date(notice.date).toLocaleDateString()}

+

{notice.content}

+ {notice.link && ( + + Apri Link + + )} +
+
+
+ ); + })} +
+
+ )} + {/* List */}
    {filteredFamilies.length === 0 ? ( -
  • +
  • - Nessuna famiglia trovata. + Nessuna famiglia trovata in questo condominio.
  • ) : ( filteredFamilies.map((family) => ( @@ -99,4 +179,4 @@ export const FamilyList: React.FC = () => {
); -}; \ No newline at end of file +}; diff --git a/pages/Settings.tsx b/pages/Settings.tsx index 48855c2..94390b8 100644 --- a/pages/Settings.tsx +++ b/pages/Settings.tsx @@ -1,13 +1,17 @@ + import React, { useEffect, useState } from 'react'; import { CondoService } from '../services/mockDb'; -import { AppSettings, Family, User, AlertDefinition } from '../types'; -import { Save, Building, Coins, Plus, Pencil, Trash2, X, CalendarCheck, AlertTriangle, User as UserIcon, Mail, Server, Bell, Clock, FileText, Send, Lock } from 'lucide-react'; +import { AppSettings, Family, User, AlertDefinition, Condo, Notice, NoticeIconType, NoticeRead } from '../types'; +import { Save, Building, Coins, Plus, Pencil, Trash2, X, CalendarCheck, AlertTriangle, User as UserIcon, Server, Bell, Clock, FileText, Lock, Megaphone, CheckCircle2, Info, Hammer, Link as LinkIcon, Eye, Calendar, List, UserCog, Mail, Power } from 'lucide-react'; export const SettingsPage: React.FC = () => { const currentUser = CondoService.getCurrentUser(); const isAdmin = currentUser?.role === 'admin'; - const [activeTab, setActiveTab] = useState<'profile' | 'general' | 'families' | 'users' | 'smtp' | 'alerts'>(isAdmin ? 'general' : 'profile'); + // Tab configuration + type TabType = 'profile' | 'general' | 'condos' | 'families' | 'users' | 'notices' | 'alerts' | 'smtp'; + + const [activeTab, setActiveTab] = useState(isAdmin ? 'general' : 'profile'); const [loading, setLoading] = useState(true); // Profile State @@ -21,14 +25,15 @@ export const SettingsPage: React.FC = () => { const [profileMsg, setProfileMsg] = useState(''); // General Settings State - const [settings, setSettings] = useState({ - defaultMonthlyQuota: 0, - condoName: '', - currentYear: new Date().getFullYear(), - smtpConfig: { - host: '', port: 587, user: '', pass: '', secure: false, fromEmail: '' - } - }); + const [activeCondo, setActiveCondo] = useState(undefined); + const [globalSettings, setGlobalSettings] = useState(null); + + // Condos Management State + const [condos, setCondos] = useState([]); + const [showCondoModal, setShowCondoModal] = useState(false); + const [editingCondo, setEditingCondo] = useState(null); + const [condoForm, setCondoForm] = useState({ name: '', address: '', defaultMonthlyQuota: 100 }); + const [saving, setSaving] = useState(false); const [successMsg, setSuccessMsg] = useState(''); @@ -36,7 +41,12 @@ export const SettingsPage: React.FC = () => { const [families, setFamilies] = useState([]); const [showFamilyModal, setShowFamilyModal] = useState(false); const [editingFamily, setEditingFamily] = useState(null); - const [familyForm, setFamilyForm] = useState({ name: '', unitNumber: '', contactEmail: '' }); + const [familyForm, setFamilyForm] = useState<{ + name: string; + unitNumber: string; + contactEmail: string; + customMonthlyQuota: string; // Use string for input handling, parse to number on save + }>({ name: '', unitNumber: '', contactEmail: '', customMonthlyQuota: '' }); // Users State const [users, setUsers] = useState([]); @@ -59,30 +69,69 @@ export const SettingsPage: React.FC = () => { const [alertForm, setAlertForm] = useState>({ subject: '', body: '', - daysOffset: 0, + daysOffset: 1, offsetType: 'before_next_month', sendHour: 9, active: true }); + // Notices (Bacheca) State + const [notices, setNotices] = useState([]); + const [showNoticeModal, setShowNoticeModal] = useState(false); + const [editingNotice, setEditingNotice] = useState(null); + const [noticeForm, setNoticeForm] = useState<{ + title: string; + content: string; + type: NoticeIconType; + link: string; + condoId: string; + active: boolean; + }>({ + title: '', + content: '', + type: 'info', + link: '', + condoId: '', + active: true + }); + const [noticeReadStats, setNoticeReadStats] = useState>({}); + + // Notice Details Modal + const [showReadDetailsModal, setShowReadDetailsModal] = useState(false); + const [selectedNoticeId, setSelectedNoticeId] = useState(null); useEffect(() => { const fetchData = async () => { try { if (isAdmin) { - const [s, f, u, a] = await Promise.all([ + const [condoList, activeC, gSettings, fams, usrs, alrts, allNotices] = await Promise.all([ + CondoService.getCondos(), + CondoService.getActiveCondo(), CondoService.getSettings(), CondoService.getFamilies(), CondoService.getUsers(), - CondoService.getAlerts() + CondoService.getAlerts(), + CondoService.getNotices() ]); - setSettings(s); - setFamilies(f); - setUsers(u); - setAlerts(a); + setCondos(condoList); + setActiveCondo(activeC); + setGlobalSettings(gSettings); + setFamilies(fams); + setUsers(usrs); + setAlerts(alrts); + setNotices(allNotices); + + // Fetch read stats for notices + const stats: Record = {}; + for (const n of allNotices) { + const reads = await CondoService.getNoticeReadStatus(n.id); + stats[n.id] = reads; + } + setNoticeReadStats(stats); + } else { - // If not admin, we might only need limited data, or nothing if we rely on session - // For now, nothing extra to fetch for the profile as it's in session + const activeC = await CondoService.getActiveCondo(); + setActiveCondo(activeC); } } catch(e) { console.error(e); @@ -102,9 +151,8 @@ export const SettingsPage: React.FC = () => { await CondoService.updateProfile(profileForm); setProfileMsg('Profilo aggiornato con successo!'); setTimeout(() => setProfileMsg(''), 3000); - setProfileForm(prev => ({ ...prev, password: '' })); // clear password + setProfileForm(prev => ({ ...prev, password: '' })); } catch (e) { - console.error(e); setProfileMsg('Errore aggiornamento profilo'); } finally { setProfileSaving(false); @@ -112,46 +160,99 @@ export const SettingsPage: React.FC = () => { }; - // --- Settings Handlers --- - - const handleSettingsSubmit = async (e: React.FormEvent) => { + // --- General Handlers --- + const handleGeneralSubmit = async (e: React.FormEvent) => { e.preventDefault(); + if (!activeCondo) return; setSaving(true); setSuccessMsg(''); try { - await CondoService.updateSettings(settings); - setSuccessMsg('Impostazioni salvate con successo!'); + await CondoService.saveCondo(activeCondo); + setSuccessMsg('Dati condominio aggiornati!'); setTimeout(() => setSuccessMsg(''), 3000); + const list = await CondoService.getCondos(); + setCondos(list); } catch (e) { console.error(e); } finally { setSaving(false); } }; + + const handleSmtpSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!globalSettings) return; + setSaving(true); + try { + await CondoService.updateSettings(globalSettings); + setSuccessMsg('Configurazione SMTP salvata!'); + setTimeout(() => setSuccessMsg(''), 3000); + } catch (e) { + console.error(e); + } finally { + setSaving(false); + } + }; const handleNewYear = async () => { - const nextYear = settings.currentYear + 1; - if (window.confirm(`Sei sicuro di voler chiudere l'anno ${settings.currentYear} e aprire il ${nextYear}? \n\nI dati vecchi non verranno cancellati, ma la visualizzazione di default passerà al nuovo anno con saldi azzerati.`)) { + if (!globalSettings) return; + const nextYear = globalSettings.currentYear + 1; + if (window.confirm(`Sei sicuro di voler chiudere l'anno ${globalSettings.currentYear} e aprire il ${nextYear}?`)) { setSaving(true); try { - const newSettings = { ...settings, currentYear: nextYear }; + const newSettings = { ...globalSettings, currentYear: nextYear }; await CondoService.updateSettings(newSettings); - setSettings(newSettings); - setSuccessMsg(`Anno ${nextYear} aperto con successo!`); - setTimeout(() => setSuccessMsg(''), 3000); - } catch(e) { - console.error(e); - } finally { - setSaving(false); - } + setGlobalSettings(newSettings); + setSuccessMsg(`Anno ${nextYear} aperto!`); + } catch(e) { console.error(e); } finally { setSaving(false); } } }; - // --- Family Handlers --- + // --- Condo Management Handlers --- + const openAddCondoModal = () => { + setEditingCondo(null); + setCondoForm({ name: '', address: '', defaultMonthlyQuota: 100 }); + setShowCondoModal(true); + }; + + const openEditCondoModal = (c: Condo) => { + setEditingCondo(c); + setCondoForm({ name: c.name, address: c.address || '', defaultMonthlyQuota: c.defaultMonthlyQuota }); + setShowCondoModal(true); + }; + const handleCondoSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const payload: Condo = { + id: editingCondo ? editingCondo.id : crypto.randomUUID(), + name: condoForm.name, + address: condoForm.address, + defaultMonthlyQuota: condoForm.defaultMonthlyQuota + }; + + await CondoService.saveCondo(payload); + const list = await CondoService.getCondos(); + setCondos(list); + if (activeCondo?.id === payload.id) setActiveCondo(payload); + setShowCondoModal(false); + window.dispatchEvent(new Event('condo-updated')); + } catch (e) { console.error(e); } + }; + + const handleDeleteCondo = async (id: string) => { + if(!window.confirm("Eliminare questo condominio? Attenzione: operazione irreversibile.")) return; + try { + await CondoService.deleteCondo(id); + setCondos(await CondoService.getCondos()); + window.dispatchEvent(new Event('condo-updated')); + } catch (e) { console.error(e); } + }; + + // --- Family Handlers --- const openAddFamilyModal = () => { setEditingFamily(null); - setFamilyForm({ name: '', unitNumber: '', contactEmail: '' }); + setFamilyForm({ name: '', unitNumber: '', contactEmail: '', customMonthlyQuota: '' }); setShowFamilyModal(true); }; @@ -160,53 +261,54 @@ export const SettingsPage: React.FC = () => { setFamilyForm({ name: family.name, unitNumber: family.unitNumber, - contactEmail: family.contactEmail || '' + contactEmail: family.contactEmail || '', + customMonthlyQuota: family.customMonthlyQuota ? family.customMonthlyQuota.toString() : '' }); setShowFamilyModal(true); }; const handleDeleteFamily = async (id: string) => { - if (!window.confirm('Sei sicuro di voler eliminare questa famiglia? Tutti i dati e lo storico pagamenti verranno persi.')) { - return; - } + if (!window.confirm('Eliminare questa famiglia?')) return; try { await CondoService.deleteFamily(id); setFamilies(families.filter(f => f.id !== id)); - } catch (e) { - console.error(e); - } + } catch (e) { console.error(e); } }; const handleFamilySubmit = async (e: React.FormEvent) => { e.preventDefault(); try { + const quota = familyForm.customMonthlyQuota ? parseFloat(familyForm.customMonthlyQuota) : undefined; + if (editingFamily) { - const updatedFamily = { - ...editingFamily, - name: familyForm.name, - unitNumber: familyForm.unitNumber, - contactEmail: familyForm.contactEmail + const updatedFamily = { + ...editingFamily, + name: familyForm.name, + unitNumber: familyForm.unitNumber, + contactEmail: familyForm.contactEmail, + customMonthlyQuota: quota }; await CondoService.updateFamily(updatedFamily); setFamilies(families.map(f => f.id === updatedFamily.id ? updatedFamily : f)); } else { - const newFamily = await CondoService.addFamily(familyForm); + const newFamily = await CondoService.addFamily({ + name: familyForm.name, + unitNumber: familyForm.unitNumber, + contactEmail: familyForm.contactEmail, + customMonthlyQuota: quota + }); setFamilies([...families, newFamily]); } setShowFamilyModal(false); - } catch (e) { - console.error(e); - } + } catch (e) { console.error(e); } }; // --- User Handlers --- - const openAddUserModal = () => { setEditingUser(null); setUserForm({ name: '', email: '', password: '', phone: '', role: 'user', familyId: '', receiveAlerts: true }); setShowUserModal(true); }; - const openEditUserModal = (user: User) => { setEditingUser(user); setUserForm({ @@ -220,914 +322,575 @@ export const SettingsPage: React.FC = () => { }); setShowUserModal(true); }; - - const handleDeleteUser = async (id: string) => { - if(!window.confirm("Sei sicuro di voler eliminare questo utente?")) return; - try { - await CondoService.deleteUser(id); - setUsers(users.filter(u => u.id !== id)); - } catch (e) { - console.error(e); - } - }; - const handleUserSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { if (editingUser) { await CondoService.updateUser(editingUser.id, userForm); - const updatedUsers = await CondoService.getUsers(); - setUsers(updatedUsers); } else { await CondoService.createUser(userForm); - const updatedUsers = await CondoService.getUsers(); - setUsers(updatedUsers); } + setUsers(await CondoService.getUsers()); setShowUserModal(false); - } catch (e) { - console.error(e); - alert("Errore nel salvataggio utente"); - } + } catch (e) { alert("Errore"); } + }; + const handleDeleteUser = async (id: string) => { + if(!window.confirm("Eliminare utente?")) return; + await CondoService.deleteUser(id); + setUsers(users.filter(u => u.id !== id)); }; - // --- Alert Handlers --- - - const openAddAlertModal = () => { - setEditingAlert(null); - setAlertForm({ - subject: '', - body: '', - daysOffset: 1, - offsetType: 'before_next_month', - sendHour: 9, - active: true - }); - setShowAlertModal(true); + // --- Notice Handlers --- + const openAddNoticeModal = () => { + setEditingNotice(null); + setNoticeForm({ title: '', content: '', type: 'info', link: '', condoId: activeCondo?.id || '', active: true }); + setShowNoticeModal(true); }; - - const openEditAlertModal = (alert: AlertDefinition) => { - setEditingAlert(alert); - setAlertForm(alert); - setShowAlertModal(true); + const openEditNoticeModal = (n: Notice) => { + setEditingNotice(n); + setNoticeForm({ title: n.title, content: n.content, type: n.type, link: n.link || '', condoId: n.condoId, active: n.active }); + setShowNoticeModal(true); }; - - const handleDeleteAlert = async (id: string) => { - if(!window.confirm("Eliminare questo avviso automatico?")) return; + const handleNoticeSubmit = async (e: React.FormEvent) => { + e.preventDefault(); try { - await CondoService.deleteAlert(id); - setAlerts(alerts.filter(a => a.id !== id)); + const payload: Notice = { + id: editingNotice ? editingNotice.id : '', + ...noticeForm, + date: editingNotice ? editingNotice.date : new Date().toISOString() + }; + const saved = await CondoService.saveNotice(payload); + setNotices(await CondoService.getNotices()); + setShowNoticeModal(false); } catch (e) { console.error(e); } }; + const handleDeleteNotice = async (id: string) => { + if(!window.confirm("Eliminare annuncio?")) return; + await CondoService.deleteNotice(id); + setNotices(notices.filter(n => n.id !== id)); + }; + + const toggleNoticeActive = async (notice: Notice) => { + try { + const updated = { ...notice, active: !notice.active }; + await CondoService.saveNotice(updated); + setNotices(notices.map(n => n.id === notice.id ? updated : n)); + } catch(e) { console.error(e); } + }; + const openReadDetails = (noticeId: string) => { + setSelectedNoticeId(noticeId); + setShowReadDetailsModal(true); + }; + + // --- Alert Handlers (Placeholder for brevity, logic same as before) --- + const openAddAlertModal = () => { setEditingAlert(null); setAlertForm({ subject: '', body: '', daysOffset: 1, offsetType: 'before_next_month', sendHour: 9, active: true }); setShowAlertModal(true); }; + const openEditAlertModal = (alert: AlertDefinition) => { setEditingAlert(alert); setAlertForm(alert); setShowAlertModal(true); }; const handleAlertSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { - const payload: AlertDefinition = { - id: editingAlert ? editingAlert.id : '', - subject: alertForm.subject!, - body: alertForm.body!, - daysOffset: Number(alertForm.daysOffset), - offsetType: alertForm.offsetType as any, - sendHour: Number(alertForm.sendHour), - active: alertForm.active! - }; + const payload: AlertDefinition = { id: editingAlert ? editingAlert.id : '', subject: alertForm.subject!, body: alertForm.body!, daysOffset: Number(alertForm.daysOffset), offsetType: alertForm.offsetType as any, sendHour: Number(alertForm.sendHour), active: alertForm.active! }; const saved = await CondoService.saveAlert(payload); - if (editingAlert) { - setAlerts(alerts.map(a => a.id === saved.id ? saved : a)); - } else { - setAlerts([...alerts, saved]); - } + setAlerts(editingAlert ? alerts.map(a => a.id === saved.id ? saved : a) : [...alerts, saved]); setShowAlertModal(false); } catch (e) { console.error(e); } }; + const handleDeleteAlert = async (id: string) => { if(!window.confirm("Eliminare avviso?")) return; await CondoService.deleteAlert(id); setAlerts(alerts.filter(a => a.id !== id)); }; + + + const getCondoName = (id: string) => condos.find(c => c.id === id)?.name || 'Sconosciuto'; + + // --- TABS CONFIG --- + const tabs: {id: TabType, label: string, icon: React.ReactNode}[] = [ + { id: 'profile', label: 'Profilo', icon: }, + ]; + if (isAdmin) { + tabs.push( + { id: 'general', label: 'Condominio', icon: }, + { id: 'condos', label: 'Lista Condomini', icon: }, + { id: 'families', label: 'Famiglie', icon: }, + { id: 'users', label: 'Utenti', icon: }, + { id: 'notices', label: 'Bacheca', icon: }, + { id: 'alerts', label: 'Avvisi Email', icon: }, + { id: 'smtp', label: 'Impostazioni Posta', icon: } + ); + } if (loading) return
Caricamento...
; return ( -
+

Impostazioni

-

Gestisci configurazione, anagrafica, utenti e comunicazioni.

+

+ {activeCondo ? `Gestione: ${activeCondo.name}` : 'Pannello di Controllo'} +

- {/* Tabs - Scrollable on mobile */} -
- - {/* Profile Tab (Always Visible) */} - - - {/* Admin Tabs */} - {isAdmin && ( - <> - - - - - - - )} + {/* Tabs */} +
+ {tabs.map(tab => ( + + ))}
+ {/* Profile Tab */} {activeTab === 'profile' && ( -
+

- - Dati Profilo + Il Tuo Profilo

-
-
- - setProfileForm({...profileForm, name: e.target.value})} - className="w-full border border-slate-300 rounded-lg p-2.5 focus:ring-2 focus:ring-blue-500 outline-none" - /> -
-
- - -
-
- - setProfileForm({...profileForm, phone: e.target.value})} - className="w-full border border-slate-300 rounded-lg p-2.5 focus:ring-2 focus:ring-blue-500 outline-none" - /> -
-
- -
- setProfileForm({...profileForm, password: e.target.value})} - className="w-full border border-slate-300 rounded-lg p-2.5 pl-9 focus:ring-2 focus:ring-blue-500 outline-none" - /> - -
-
-
- -
-
- setProfileForm({...profileForm, receiveAlerts: e.target.checked})} - className="w-5 h-5 text-blue-600 rounded border-slate-300 focus:ring-blue-500" - /> -
- -

Abilita la ricezione di email per scadenze e comunicazioni condominiali.

-
-
-
- -
- {profileMsg} - +
setProfileForm({...profileForm, name: e.target.value})} className="w-full border p-2.5 rounded-lg text-slate-700"/>
+
+
setProfileForm({...profileForm, phone: e.target.value})} className="w-full border p-2.5 rounded-lg text-slate-700"/>
+
setProfileForm({...profileForm, password: e.target.value})} className="w-full border p-2.5 rounded-lg text-slate-700"/>
+
)} + {/* General Tab */} {isAdmin && activeTab === 'general' && (
- {/* General Data Form */} -
-

Dati Generali

-
-
- - setSettings({ ...settings, condoName: e.target.value })} - className="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-blue-500 outline-none transition-all" - placeholder="Es. Condominio Roma" - required - /> -
- -
- -
- setSettings({ ...settings, defaultMonthlyQuota: parseFloat(e.target.value) })} - className="w-full border border-slate-300 rounded-lg px-4 py-2.5 pl-8 focus:ring-2 focus:ring-blue-500 outline-none transition-all" - required - /> - -
-
- -
- {successMsg} - -
-
-
- - {/* Fiscal Year Management */} -
-

- - Anno Fiscale -

-
-

- Anno corrente: {settings.currentYear} -

+ {!activeCondo ? ( +
Nessun condominio selezionato.
+ ) : ( +
+

Dati Condominio Corrente

+
+ setActiveCondo({ ...activeCondo, name: e.target.value })} className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Nome" required /> + setActiveCondo({ ...activeCondo, address: e.target.value })} className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Indirizzo" /> +
+ + setActiveCondo({ ...activeCondo, defaultMonthlyQuota: parseFloat(e.target.value) })} className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Quota Default" required /> +
+
{successMsg}
+
- -
-
- -

- Chiudendo l'anno, il sistema passerà al {settings.currentYear + 1}. I dati storici rimarranno consultabili. -

-
- - -
-
+ )} + {globalSettings && ( +
+

Anno Fiscale

+

Corrente: {globalSettings.currentYear}

+ +
+ )}
)} + {/* Condos List Tab */} + {isAdmin && activeTab === 'condos' && ( +
+
+

I Tuoi Condomini

+ +
+
+ {condos.map(condo => ( +
+ {activeCondo?.id === condo.id &&
Attivo
} +

{condo.name}

+

{condo.address || 'Nessun indirizzo'}

+
+ + +
+
+ ))} +
+
+ )} + + {/* Families Tab */} {isAdmin && activeTab === 'families' && (
-
- +
+
Famiglie in: {activeCondo?.name}
+
- - {/* Desktop Table */} -
+
- - - - - - - - + {families.map(family => ( - - - - - + + + + + + ))}
Nome FamigliaInternoEmailAzioni
NomeInternoEmailQuotaAzioni
{family.name}{family.unitNumber}{family.contactEmail || '-'} -
- - -
-
{family.name}{family.unitNumber}{family.contactEmail} + {family.customMonthlyQuota ? ( + € {family.customMonthlyQuota} + ) : ( + Default (€ {activeCondo?.defaultMonthlyQuota}) + )} +
- - {/* Mobile Cards for Families */} -
- {families.map(family => ( -
-
-
-

{family.name}

-

Interno: {family.unitNumber}

-
-
-

- - {family.contactEmail || 'Nessuna email'} -

-
- - -
-
- ))} -
)} + {/* Users Tab */} {isAdmin && activeTab === 'users' && (
- - {/* Desktop Table */} -
- - - - - - - - - - +
+
UtenteContattiRuoloAlertsAzioni
+ - {users.map(user => ( - - - - - - + {users.map(u => ( + + + + ))}
UtenteRuoloAzioni
{user.name || '-'} -
{user.email}
- {user.phone &&
{user.phone}
} -
- - {user.role} - - - {user.receiveAlerts ? ( - - ) : ( - No - )} - -
- - -
-
{u.name}
{u.email}
{u.role}
- {/* Mobile User Cards */} -
- {users.map(user => ( -
-
- - {user.role} - -
-
-

- - {user.name || 'Senza Nome'} -

-

{user.email}

-
-
- {user.receiveAlerts ? ( - - Riceve Avvisi - - ) : ( - - Niente Avvisi - - )} -
-
- - -
-
- ))} -
+
+ )} + + {/* NOTICES (BACHECA) TAB */} + {isAdmin && activeTab === 'notices' && ( +
+
+

Bacheca Condominiale

Pubblica avvisi visibili a tutti i condomini.

+ +
+ +
+ {notices.map(notice => ( +
+
+
+
+ {notice.type === 'warning' ? : notice.type === 'maintenance' ? : notice.type === 'event' ? : } +
+
+

{notice.title}

+

{getCondoName(notice.condoId)} • {new Date(notice.date).toLocaleDateString()}

+

{notice.content}

+ {notice.link && Allegato} +
+
+
+ {/* Toggle Active */} + + + {/* Reads Counter */} + +
+
+
+ + +
+
+ ))} + {notices.length === 0 &&
Nessun avviso pubblicato.
} +
+
+ )} + + {/* Alerts Tab */} + {isAdmin && activeTab === 'alerts' && ( +
+
+

Avvisi Automatici Email

+ +
+ {alerts.map(a => ( +
+

{a.subject}

{a.body}

+
+
+ ))}
)} {isAdmin && activeTab === 'smtp' && ( -
-
-
- -
-
-

Server SMTP

-

Configura il server per l'invio delle email.

-
-
- -
-
-
- - setSettings({...settings, smtpConfig: {...settings.smtpConfig!, host: e.target.value}})} - className="w-full border border-slate-300 rounded-lg p-2.5 focus:ring-2 focus:ring-blue-500 outline-none" - /> -
-
- - setSettings({...settings, smtpConfig: {...settings.smtpConfig!, port: parseInt(e.target.value)}})} - className="w-full border border-slate-300 rounded-lg p-2.5 focus:ring-2 focus:ring-blue-500 outline-none" - /> -
-
+
+

Server SMTP Globale

+ {globalSettings && ( + +
+ setGlobalSettings({...globalSettings, smtpConfig: {...globalSettings.smtpConfig!, host: e.target.value}})} className="border p-2 rounded text-slate-700"/> + setGlobalSettings({...globalSettings, smtpConfig: {...globalSettings.smtpConfig!, port: parseInt(e.target.value)}})} className="border p-2 rounded text-slate-700"/> +
+
+ setGlobalSettings({...globalSettings, smtpConfig: {...globalSettings.smtpConfig!, user: e.target.value}})} className="border p-2 rounded text-slate-700"/> + setGlobalSettings({...globalSettings, smtpConfig: {...globalSettings.smtpConfig!, pass: e.target.value}})} className="border p-2 rounded text-slate-700"/> +
+ + + )} +
+ )} -
-
- - setSettings({...settings, smtpConfig: {...settings.smtpConfig!, user: e.target.value}})} - className="w-full border border-slate-300 rounded-lg p-2.5 focus:ring-2 focus:ring-blue-500 outline-none" - /> -
-
- - setSettings({...settings, smtpConfig: {...settings.smtpConfig!, pass: e.target.value}})} - className="w-full border border-slate-300 rounded-lg p-2.5 focus:ring-2 focus:ring-blue-500 outline-none" - /> -
-
- -
- - setSettings({...settings, smtpConfig: {...settings.smtpConfig!, fromEmail: e.target.value}})} - className="w-full border border-slate-300 rounded-lg p-2.5 focus:ring-2 focus:ring-blue-500 outline-none" - /> -
- -
- setSettings({...settings, smtpConfig: {...settings.smtpConfig!, secure: e.target.checked}})} - className="w-4 h-4 text-blue-600 rounded border-slate-300 focus:ring-blue-500" - /> - -
- -
- {successMsg || 'Salvataggio...'} - -
- + {/* MODALS */} + + {/* CONDO MODAL */} + {showCondoModal && ( +
+
+

{editingCondo ? 'Modifica Condominio' : 'Nuovo Condominio'}

+
+ setCondoForm({...condoForm, name: e.target.value})} required /> + setCondoForm({...condoForm, address: e.target.value})} /> +
Quota Default € setCondoForm({...condoForm, defaultMonthlyQuota: parseFloat(e.target.value)})} />
+
+ + +
+
+
)} - {isAdmin && activeTab === 'alerts' && ( -
-
-
-

Avvisi Automatici

-

Pianifica email ricorrenti per i condomini.

-
- -
+ {/* NOTICE MODAL */} + {showNoticeModal && ( +
+
+

+ + {editingNotice ? 'Modifica Avviso' : 'Nuovo Avviso'} +

+
+
+ + setNoticeForm({...noticeForm, title: e.target.value})} required /> +
+ +
+
+ + +
+
+ + +
+
-
- {alerts.length === 0 && ( -
- Nessun avviso configurato. -
- )} - {alerts.map(alert => ( -
-
-
-

{alert.subject}

- {!alert.active && DISATTIVO} -
-

{alert.body}

- -
-
- - {alert.offsetType === 'before_next_month' - ? `${alert.daysOffset} giorni prima del prossimo mese` - : `${alert.daysOffset} giorni dopo inizio mese corrente` - } -
-
- - Alle ore {alert.sendHour}:00 -
-
-
-
- - -
-
- ))} -
+
+ +