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.
897 lines
52 KiB
TypeScript
897 lines
52 KiB
TypeScript
|
|
import React, { useEffect, useState } from 'react';
|
|
import { CondoService } from '../services/mockDb';
|
|
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';
|
|
|
|
// Tab configuration
|
|
type TabType = 'profile' | 'general' | 'condos' | 'families' | 'users' | 'notices' | 'alerts' | 'smtp';
|
|
|
|
const [activeTab, setActiveTab] = useState<TabType>(isAdmin ? 'general' : 'profile');
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// Profile State
|
|
const [profileForm, setProfileForm] = useState({
|
|
name: currentUser?.name || '',
|
|
phone: currentUser?.phone || '',
|
|
password: '',
|
|
receiveAlerts: currentUser?.receiveAlerts ?? true
|
|
});
|
|
const [profileSaving, setProfileSaving] = useState(false);
|
|
const [profileMsg, setProfileMsg] = useState('');
|
|
|
|
// General Settings State
|
|
const [activeCondo, setActiveCondo] = useState<Condo | undefined>(undefined);
|
|
const [globalSettings, setGlobalSettings] = useState<AppSettings | null>(null);
|
|
|
|
// Condos Management State
|
|
const [condos, setCondos] = useState<Condo[]>([]);
|
|
const [showCondoModal, setShowCondoModal] = useState(false);
|
|
const [editingCondo, setEditingCondo] = useState<Condo | null>(null);
|
|
const [condoForm, setCondoForm] = useState({ name: '', address: '', defaultMonthlyQuota: 100 });
|
|
|
|
const [saving, setSaving] = useState(false);
|
|
const [successMsg, setSuccessMsg] = useState('');
|
|
|
|
// Families State
|
|
const [families, setFamilies] = useState<Family[]>([]);
|
|
const [showFamilyModal, setShowFamilyModal] = useState(false);
|
|
const [editingFamily, setEditingFamily] = useState<Family | null>(null);
|
|
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<User[]>([]);
|
|
const [showUserModal, setShowUserModal] = useState(false);
|
|
const [editingUser, setEditingUser] = useState<User | null>(null);
|
|
const [userForm, setUserForm] = useState({
|
|
name: '',
|
|
email: '',
|
|
password: '',
|
|
phone: '',
|
|
role: 'user',
|
|
familyId: '',
|
|
receiveAlerts: true
|
|
});
|
|
|
|
// Alerts State
|
|
const [alerts, setAlerts] = useState<AlertDefinition[]>([]);
|
|
const [showAlertModal, setShowAlertModal] = useState(false);
|
|
const [editingAlert, setEditingAlert] = useState<AlertDefinition | null>(null);
|
|
const [alertForm, setAlertForm] = useState<Partial<AlertDefinition>>({
|
|
subject: '',
|
|
body: '',
|
|
daysOffset: 1,
|
|
offsetType: 'before_next_month',
|
|
sendHour: 9,
|
|
active: true
|
|
});
|
|
|
|
// Notices (Bacheca) State
|
|
const [notices, setNotices] = useState<Notice[]>([]);
|
|
const [showNoticeModal, setShowNoticeModal] = useState(false);
|
|
const [editingNotice, setEditingNotice] = useState<Notice | null>(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<Record<string, NoticeRead[]>>({});
|
|
|
|
// Notice Details Modal
|
|
const [showReadDetailsModal, setShowReadDetailsModal] = useState(false);
|
|
const [selectedNoticeId, setSelectedNoticeId] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
try {
|
|
if (isAdmin) {
|
|
const [condoList, activeC, gSettings, fams, usrs, alrts, allNotices] = await Promise.all([
|
|
CondoService.getCondos(),
|
|
CondoService.getActiveCondo(),
|
|
CondoService.getSettings(),
|
|
CondoService.getFamilies(),
|
|
CondoService.getUsers(),
|
|
CondoService.getAlerts(),
|
|
CondoService.getNotices()
|
|
]);
|
|
setCondos(condoList);
|
|
setActiveCondo(activeC);
|
|
setGlobalSettings(gSettings);
|
|
setFamilies(fams);
|
|
setUsers(usrs);
|
|
setAlerts(alrts);
|
|
setNotices(allNotices);
|
|
|
|
// Fetch read stats for notices
|
|
const stats: Record<string, NoticeRead[]> = {};
|
|
for (const n of allNotices) {
|
|
const reads = await CondoService.getNoticeReadStatus(n.id);
|
|
stats[n.id] = reads;
|
|
}
|
|
setNoticeReadStats(stats);
|
|
|
|
} else {
|
|
const activeC = await CondoService.getActiveCondo();
|
|
setActiveCondo(activeC);
|
|
}
|
|
} catch(e) {
|
|
console.error(e);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchData();
|
|
}, [isAdmin]);
|
|
|
|
// --- Profile Handlers ---
|
|
const handleProfileSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setProfileSaving(true);
|
|
setProfileMsg('');
|
|
try {
|
|
await CondoService.updateProfile(profileForm);
|
|
setProfileMsg('Profilo aggiornato con successo!');
|
|
setTimeout(() => setProfileMsg(''), 3000);
|
|
setProfileForm(prev => ({ ...prev, password: '' }));
|
|
} catch (e) {
|
|
setProfileMsg('Errore aggiornamento profilo');
|
|
} finally {
|
|
setProfileSaving(false);
|
|
}
|
|
};
|
|
|
|
|
|
// --- General Handlers ---
|
|
const handleGeneralSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!activeCondo) return;
|
|
setSaving(true);
|
|
setSuccessMsg('');
|
|
try {
|
|
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 () => {
|
|
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 = { ...globalSettings, currentYear: nextYear };
|
|
await CondoService.updateSettings(newSettings);
|
|
setGlobalSettings(newSettings);
|
|
setSuccessMsg(`Anno ${nextYear} aperto!`);
|
|
} catch(e) { console.error(e); } finally { setSaving(false); }
|
|
}
|
|
};
|
|
|
|
// --- 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: '', customMonthlyQuota: '' });
|
|
setShowFamilyModal(true);
|
|
};
|
|
|
|
const openEditFamilyModal = (family: Family) => {
|
|
setEditingFamily(family);
|
|
setFamilyForm({
|
|
name: family.name,
|
|
unitNumber: family.unitNumber,
|
|
contactEmail: family.contactEmail || '',
|
|
customMonthlyQuota: family.customMonthlyQuota ? family.customMonthlyQuota.toString() : ''
|
|
});
|
|
setShowFamilyModal(true);
|
|
};
|
|
|
|
const handleDeleteFamily = async (id: string) => {
|
|
if (!window.confirm('Eliminare questa famiglia?')) return;
|
|
try {
|
|
await CondoService.deleteFamily(id);
|
|
setFamilies(families.filter(f => f.id !== id));
|
|
} 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,
|
|
customMonthlyQuota: quota
|
|
};
|
|
await CondoService.updateFamily(updatedFamily);
|
|
setFamilies(families.map(f => f.id === updatedFamily.id ? updatedFamily : f));
|
|
} else {
|
|
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); }
|
|
};
|
|
|
|
// --- 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({
|
|
name: user.name || '',
|
|
email: user.email,
|
|
password: '',
|
|
phone: user.phone || '',
|
|
role: user.role || 'user',
|
|
familyId: user.familyId || '',
|
|
receiveAlerts: user.receiveAlerts ?? true
|
|
});
|
|
setShowUserModal(true);
|
|
};
|
|
const handleUserSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
try {
|
|
if (editingUser) {
|
|
await CondoService.updateUser(editingUser.id, userForm);
|
|
} else {
|
|
await CondoService.createUser(userForm);
|
|
}
|
|
setUsers(await CondoService.getUsers());
|
|
setShowUserModal(false);
|
|
} 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));
|
|
};
|
|
|
|
// --- Notice Handlers ---
|
|
const openAddNoticeModal = () => {
|
|
setEditingNotice(null);
|
|
setNoticeForm({ title: '', content: '', type: 'info', link: '', condoId: activeCondo?.id || '', active: true });
|
|
setShowNoticeModal(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 handleNoticeSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
try {
|
|
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 saved = await CondoService.saveAlert(payload);
|
|
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: <UserIcon className="w-4 h-4"/> },
|
|
];
|
|
if (isAdmin) {
|
|
tabs.push(
|
|
{ id: 'general', label: 'Condominio', icon: <Building className="w-4 h-4"/> },
|
|
{ id: 'condos', label: 'Lista Condomini', icon: <List className="w-4 h-4"/> },
|
|
{ id: 'families', label: 'Famiglie', icon: <Coins className="w-4 h-4"/> },
|
|
{ id: 'users', label: 'Utenti', icon: <UserCog className="w-4 h-4"/> },
|
|
{ id: 'notices', label: 'Bacheca', icon: <Megaphone className="w-4 h-4"/> },
|
|
{ id: 'alerts', label: 'Avvisi Email', icon: <Bell className="w-4 h-4"/> },
|
|
{ id: 'smtp', label: 'Impostazioni Posta', icon: <Mail className="w-4 h-4"/> }
|
|
);
|
|
}
|
|
|
|
if (loading) return <div className="p-8 text-center text-slate-400">Caricamento...</div>;
|
|
|
|
return (
|
|
<div className="max-w-5xl mx-auto space-y-6 pb-20">
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-slate-800">Impostazioni</h2>
|
|
<p className="text-slate-500 text-sm md:text-base">
|
|
{activeCondo ? `Gestione: ${activeCondo.name}` : 'Pannello di Controllo'}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="flex border-b border-slate-200 overflow-x-auto no-scrollbar pb-1 gap-1">
|
|
{tabs.map(tab => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={`px-4 py-3 font-medium text-sm whitespace-nowrap flex items-center gap-2 rounded-t-lg transition-colors ${activeTab === tab.id ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50/50' : 'text-slate-500 hover:text-slate-700 hover:bg-slate-50'}`}
|
|
>
|
|
{tab.icon}
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Profile Tab */}
|
|
{activeTab === 'profile' && (
|
|
<div className="animate-fade-in bg-white rounded-xl shadow-sm border border-slate-200 p-6 max-w-2xl">
|
|
<h3 className="text-lg font-bold text-slate-800 mb-6 flex items-center gap-2">
|
|
<UserIcon className="w-5 h-5 text-blue-600" /> Il Tuo Profilo
|
|
</h3>
|
|
<form onSubmit={handleProfileSubmit} className="space-y-5">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
<div><label className="text-sm font-medium text-slate-700">Nome</label><input type="text" value={profileForm.name} onChange={(e) => setProfileForm({...profileForm, name: e.target.value})} className="w-full border p-2.5 rounded-lg text-slate-700"/></div>
|
|
<div><label className="text-sm font-medium text-slate-700">Email</label><input type="email" value={currentUser?.email || ''} disabled className="w-full border bg-slate-50 p-2.5 rounded-lg text-slate-500"/></div>
|
|
<div><label className="text-sm font-medium text-slate-700">Telefono</label><input type="tel" value={profileForm.phone} onChange={(e) => setProfileForm({...profileForm, phone: e.target.value})} className="w-full border p-2.5 rounded-lg text-slate-700"/></div>
|
|
<div><label className="text-sm font-medium text-slate-700">Password</label><input type="password" placeholder="Opzionale" value={profileForm.password} onChange={(e) => setProfileForm({...profileForm, password: e.target.value})} className="w-full border p-2.5 rounded-lg text-slate-700"/></div>
|
|
</div>
|
|
<button type="submit" disabled={profileSaving} className="bg-blue-600 text-white px-6 py-2.5 rounded-lg font-medium hover:bg-blue-700 flex gap-2"><Save className="w-4 h-4" /> Aggiorna</button>
|
|
</form>
|
|
</div>
|
|
)}
|
|
|
|
{/* General Tab */}
|
|
{isAdmin && activeTab === 'general' && (
|
|
<div className="space-y-6 animate-fade-in">
|
|
{!activeCondo ? (
|
|
<div className="bg-amber-50 border border-amber-200 text-amber-800 p-4 rounded-lg">Nessun condominio selezionato.</div>
|
|
) : (
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 md:p-8 max-w-2xl">
|
|
<h3 className="text-lg font-bold text-slate-800 mb-4 flex items-center gap-2"><Building className="w-5 h-5 text-blue-600" /> Dati Condominio Corrente</h3>
|
|
<form onSubmit={handleGeneralSubmit} className="space-y-5">
|
|
<input type="text" value={activeCondo.name} onChange={(e) => setActiveCondo({ ...activeCondo, name: e.target.value })} className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Nome" required />
|
|
<input type="text" value={activeCondo.address || ''} onChange={(e) => setActiveCondo({ ...activeCondo, address: e.target.value })} className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Indirizzo" />
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Quota Mensile Standard (€)</label>
|
|
<input type="number" value={activeCondo.defaultMonthlyQuota} onChange={(e) => setActiveCondo({ ...activeCondo, defaultMonthlyQuota: parseFloat(e.target.value) })} className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Quota Default" required />
|
|
</div>
|
|
<div className="pt-2 flex justify-between"><span className="text-green-600">{successMsg}</span><button type="submit" className="bg-blue-600 text-white px-6 py-2.5 rounded-lg hover:bg-blue-700 flex gap-2"><Save className="w-4 h-4"/> Salva</button></div>
|
|
</form>
|
|
</div>
|
|
)}
|
|
{globalSettings && (
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 md:p-8 max-w-2xl">
|
|
<h3 className="font-bold text-slate-800 mb-2">Anno Fiscale</h3>
|
|
<p className="text-slate-600 mb-4">Corrente: <strong>{globalSettings.currentYear}</strong></p>
|
|
<button type="button" onClick={handleNewYear} className="bg-slate-800 text-white px-4 py-2 rounded-lg text-sm">Chiudi Anno {globalSettings.currentYear}</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Condos List Tab */}
|
|
{isAdmin && activeTab === 'condos' && (
|
|
<div className="space-y-4 animate-fade-in">
|
|
<div className="flex justify-between items-center bg-blue-50 p-4 rounded-xl border border-blue-100">
|
|
<div><h3 className="font-bold text-blue-800">I Tuoi Condomini</h3></div>
|
|
<button onClick={openAddCondoModal} className="bg-blue-600 text-white px-4 py-2 rounded-lg font-medium flex gap-2"><Plus className="w-4 h-4" /> Aggiungi</button>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{condos.map(condo => (
|
|
<div key={condo.id} className={`bg-white p-5 rounded-xl border shadow-sm relative ${activeCondo?.id === condo.id ? 'border-blue-500 ring-1 ring-blue-500' : 'border-slate-200'}`}>
|
|
{activeCondo?.id === condo.id && <div className="absolute top-3 right-3 text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded font-bold uppercase">Attivo</div>}
|
|
<h4 className="font-bold text-slate-800 text-lg mb-1">{condo.name}</h4>
|
|
<p className="text-sm text-slate-500 mb-3">{condo.address || 'Nessun indirizzo'}</p>
|
|
<div className="border-t pt-3 flex gap-2">
|
|
<button onClick={() => openEditCondoModal(condo)} className="flex-1 py-1.5 bg-slate-50 text-slate-700 rounded text-sm font-medium hover:bg-slate-100">Modifica</button>
|
|
<button onClick={() => handleDeleteCondo(condo.id)} className="flex-1 py-1.5 bg-red-50 text-red-600 rounded text-sm font-medium hover:bg-red-100">Elimina</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Families Tab */}
|
|
{isAdmin && activeTab === 'families' && (
|
|
<div className="space-y-4 animate-fade-in">
|
|
<div className="flex justify-between items-center">
|
|
<div className="text-sm text-slate-500">Famiglie in: <span className="font-bold text-slate-800">{activeCondo?.name}</span></div>
|
|
<button onClick={openAddFamilyModal} className="bg-blue-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2"><Plus className="w-4 h-4" /> Aggiungi</button>
|
|
</div>
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
|
<table className="w-full text-left text-sm text-slate-600">
|
|
<thead className="bg-slate-50 text-slate-700 font-semibold border-b"><tr><th className="px-6 py-4">Nome</th><th className="px-6 py-4">Interno</th><th className="px-6 py-4">Email</th><th className="px-6 py-4">Quota</th><th className="px-6 py-4 text-right">Azioni</th></tr></thead>
|
|
<tbody className="divide-y divide-slate-100">
|
|
{families.map(family => (
|
|
<tr key={family.id} className="hover:bg-slate-50">
|
|
<td className="px-6 py-4 font-medium text-slate-900">{family.name}</td>
|
|
<td className="px-6 py-4">{family.unitNumber}</td>
|
|
<td className="px-6 py-4 text-slate-400">{family.contactEmail}</td>
|
|
<td className="px-6 py-4">
|
|
{family.customMonthlyQuota ? (
|
|
<span className="font-bold text-blue-600">€ {family.customMonthlyQuota}</span>
|
|
) : (
|
|
<span className="text-slate-400 italic">Default (€ {activeCondo?.defaultMonthlyQuota})</span>
|
|
)}
|
|
</td>
|
|
<td className="px-6 py-4 text-right"><div className="flex justify-end gap-2"><button onClick={() => openEditFamilyModal(family)} className="text-blue-600"><Pencil className="w-4 h-4" /></button><button onClick={() => handleDeleteFamily(family.id)} className="text-red-600"><Trash2 className="w-4 h-4" /></button></div></td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Users Tab */}
|
|
{isAdmin && activeTab === 'users' && (
|
|
<div className="space-y-4 animate-fade-in">
|
|
<div className="flex justify-end">
|
|
<button
|
|
onClick={openAddUserModal}
|
|
className="bg-blue-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 hover:bg-blue-700 transition-colors"
|
|
type="button"
|
|
>
|
|
<Plus className="w-4 h-4" /> Nuovo Utente
|
|
</button>
|
|
</div>
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
|
<table className="w-full text-left text-sm text-slate-600">
|
|
<thead className="bg-slate-50 text-slate-700 font-semibold border-b"><tr><th className="px-6 py-4">Utente</th><th className="px-6 py-4">Ruolo</th><th className="px-6 py-4 text-right">Azioni</th></tr></thead>
|
|
<tbody className="divide-y divide-slate-100">
|
|
{users.map(u => (
|
|
<tr key={u.id} className="hover:bg-slate-50">
|
|
<td className="px-6 py-4"><div className="font-medium text-slate-900">{u.name}</div><div className="text-xs text-slate-400">{u.email}</div></td>
|
|
<td className="px-6 py-4"><span className="bg-slate-100 px-2 py-1 rounded text-xs font-bold uppercase">{u.role}</span></td>
|
|
<td className="px-6 py-4 text-right"><div className="flex justify-end gap-2"><button onClick={() => openEditUserModal(u)} className="text-blue-600"><Pencil className="w-4 h-4"/></button><button onClick={() => handleDeleteUser(u.id)} className="text-red-600"><Trash2 className="w-4 h-4"/></button></div></td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* NOTICES (BACHECA) TAB */}
|
|
{isAdmin && activeTab === 'notices' && (
|
|
<div className="space-y-4 animate-fade-in">
|
|
<div className="flex justify-between items-center bg-blue-50 p-4 rounded-xl border border-blue-100">
|
|
<div><h3 className="font-bold text-blue-800">Bacheca Condominiale</h3><p className="text-sm text-blue-600">Pubblica avvisi visibili a tutti i condomini.</p></div>
|
|
<button onClick={openAddNoticeModal} className="bg-blue-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 hover:bg-blue-700 transition-colors"><Plus className="w-4 h-4" /> Nuovo Avviso</button>
|
|
</div>
|
|
|
|
<div className="grid gap-4">
|
|
{notices.map(notice => (
|
|
<div key={notice.id} className={`bg-white p-5 rounded-xl border shadow-sm relative transition-all ${notice.active ? 'border-slate-200' : 'border-slate-100 opacity-60 bg-slate-50'}`}>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-start gap-3">
|
|
<div className={`p-2 rounded-lg ${notice.type === 'warning' ? 'bg-amber-100 text-amber-600' : notice.type === 'maintenance' ? 'bg-orange-100 text-orange-600' : 'bg-blue-100 text-blue-600'}`}>
|
|
{notice.type === 'warning' ? <AlertTriangle className="w-5 h-5"/> : notice.type === 'maintenance' ? <Hammer className="w-5 h-5"/> : notice.type === 'event' ? <Calendar className="w-5 h-5"/> : <Info className="w-5 h-5"/>}
|
|
</div>
|
|
<div>
|
|
<h4 className="font-bold text-slate-800">{notice.title}</h4>
|
|
<p className="text-xs text-slate-400 font-medium uppercase tracking-wide mb-1">{getCondoName(notice.condoId)} • {new Date(notice.date).toLocaleDateString()}</p>
|
|
<p className="text-sm text-slate-600 line-clamp-2">{notice.content}</p>
|
|
{notice.link && <a href={notice.link} className="text-xs text-blue-600 underline mt-1 flex items-center gap-1"><LinkIcon className="w-3 h-3"/> Allegato</a>}
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col items-end gap-3">
|
|
{/* Toggle Active */}
|
|
<button
|
|
onClick={() => toggleNoticeActive(notice)}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${notice.active ? 'bg-green-500' : 'bg-slate-300'}`}
|
|
title={notice.active ? "Attivo" : "Disattivato"}
|
|
>
|
|
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${notice.active ? 'translate-x-6' : 'translate-x-1'}`} />
|
|
</button>
|
|
|
|
{/* Reads Counter */}
|
|
<button
|
|
onClick={() => openReadDetails(notice.id)}
|
|
className="text-center group"
|
|
title="Vedi dettaglio letture"
|
|
>
|
|
<span className="block text-lg font-bold text-slate-700 group-hover:text-blue-600">{noticeReadStats[notice.id]?.length || 0}</span>
|
|
<span className="text-[10px] text-slate-400 uppercase font-bold flex items-center gap-1 group-hover:text-blue-500"><Eye className="w-3 h-3"/> Letture</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="mt-4 pt-3 border-t border-slate-100 flex justify-end gap-2">
|
|
<button onClick={() => openEditNoticeModal(notice)} className="text-sm text-blue-600 font-medium px-3 py-1 hover:bg-blue-50 rounded">Modifica</button>
|
|
<button onClick={() => handleDeleteNotice(notice.id)} className="text-sm text-red-600 font-medium px-3 py-1 hover:bg-red-50 rounded">Elimina</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{notices.length === 0 && <div className="text-center p-8 text-slate-400">Nessun avviso pubblicato.</div>}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Alerts Tab */}
|
|
{isAdmin && activeTab === 'alerts' && (
|
|
<div className="space-y-4 animate-fade-in">
|
|
<div className="flex justify-between items-center">
|
|
<h3 className="font-bold text-slate-800">Avvisi Automatici Email</h3>
|
|
<button onClick={openAddAlertModal} className="bg-blue-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-blue-700 transition-colors"><Plus className="w-4 h-4"/> Nuovo</button>
|
|
</div>
|
|
{alerts.map(a => (
|
|
<div key={a.id} className="bg-white p-4 rounded-xl border flex justify-between items-center">
|
|
<div><h4 className="font-bold text-slate-800">{a.subject}</h4><p className="text-sm text-slate-500">{a.body}</p></div>
|
|
<div className="flex gap-2"><button onClick={() => openEditAlertModal(a)} className="text-blue-600 p-2 hover:bg-blue-50 rounded"><Pencil className="w-4 h-4"/></button><button onClick={() => handleDeleteAlert(a.id)} className="text-red-600 p-2 hover:bg-red-50 rounded"><Trash2 className="w-4 h-4"/></button></div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{isAdmin && activeTab === 'smtp' && (
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 max-w-2xl animate-fade-in">
|
|
<h3 className="text-lg font-bold text-slate-800 mb-4 flex items-center gap-2"><Server className="w-5 h-5"/> Server SMTP Globale</h3>
|
|
{globalSettings && (
|
|
<form onSubmit={handleSmtpSubmit} className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<input type="text" placeholder="Host" value={globalSettings.smtpConfig?.host} onChange={e => setGlobalSettings({...globalSettings, smtpConfig: {...globalSettings.smtpConfig!, host: e.target.value}})} className="border p-2 rounded text-slate-700"/>
|
|
<input type="number" placeholder="Port" value={globalSettings.smtpConfig?.port} onChange={e => setGlobalSettings({...globalSettings, smtpConfig: {...globalSettings.smtpConfig!, port: parseInt(e.target.value)}})} className="border p-2 rounded text-slate-700"/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<input type="text" placeholder="User" value={globalSettings.smtpConfig?.user} onChange={e => setGlobalSettings({...globalSettings, smtpConfig: {...globalSettings.smtpConfig!, user: e.target.value}})} className="border p-2 rounded text-slate-700"/>
|
|
<input type="password" placeholder="Pass" value={globalSettings.smtpConfig?.pass} onChange={e => setGlobalSettings({...globalSettings, smtpConfig: {...globalSettings.smtpConfig!, pass: e.target.value}})} className="border p-2 rounded text-slate-700"/>
|
|
</div>
|
|
<button type="submit" className="bg-blue-600 text-white px-6 py-2 rounded-lg w-full">Salva Configurazione SMTP</button>
|
|
</form>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* MODALS */}
|
|
|
|
{/* CONDO MODAL */}
|
|
{showCondoModal && (
|
|
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
|
|
<div className="bg-white rounded-xl shadow-xl w-full max-w-md p-6 animate-in fade-in zoom-in duration-200">
|
|
<h3 className="font-bold text-lg mb-4 text-slate-800">{editingCondo ? 'Modifica Condominio' : 'Nuovo Condominio'}</h3>
|
|
<form onSubmit={handleCondoSubmit} className="space-y-4">
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Nome Condominio" value={condoForm.name} onChange={e => setCondoForm({...condoForm, name: e.target.value})} required />
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Indirizzo" value={condoForm.address} onChange={e => setCondoForm({...condoForm, address: e.target.value})} />
|
|
<div className="flex items-center gap-2"><span className="text-sm font-medium text-slate-600">Quota Default €</span><input type="number" className="border p-2 rounded w-24 text-slate-700" value={condoForm.defaultMonthlyQuota} onChange={e => setCondoForm({...condoForm, defaultMonthlyQuota: parseFloat(e.target.value)})} /></div>
|
|
<div className="flex gap-2 pt-2">
|
|
<button type="button" onClick={() => setShowCondoModal(false)} className="flex-1 border p-2.5 rounded-lg text-slate-600 hover:bg-slate-50">Annulla</button>
|
|
<button type="submit" className="flex-1 bg-blue-600 text-white p-2.5 rounded-lg hover:bg-blue-700">Salva</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* NOTICE MODAL */}
|
|
{showNoticeModal && (
|
|
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
|
|
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg p-6 animate-in fade-in zoom-in duration-200">
|
|
<h3 className="font-bold text-lg mb-4 text-slate-800 flex items-center gap-2">
|
|
<Megaphone className="w-5 h-5 text-blue-600"/>
|
|
{editingNotice ? 'Modifica Avviso' : 'Nuovo Avviso'}
|
|
</h3>
|
|
<form onSubmit={handleNoticeSubmit} className="space-y-4">
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-500 uppercase">Titolo</label>
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700" value={noticeForm.title} onChange={e => setNoticeForm({...noticeForm, title: e.target.value})} required />
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-500 uppercase">Tipo</label>
|
|
<select className="w-full border p-2.5 rounded-lg text-slate-700 bg-white" value={noticeForm.type} onChange={e => setNoticeForm({...noticeForm, type: e.target.value as any})}>
|
|
<option value="info">Informazione</option>
|
|
<option value="warning">Avviso / Pericolo</option>
|
|
<option value="maintenance">Manutenzione</option>
|
|
<option value="event">Evento</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-500 uppercase">Condominio</label>
|
|
<select className="w-full border p-2.5 rounded-lg text-slate-700 bg-white" value={noticeForm.condoId} onChange={e => setNoticeForm({...noticeForm, condoId: e.target.value})}>
|
|
{condos.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-500 uppercase">Contenuto</label>
|
|
<textarea className="w-full border p-2.5 rounded-lg text-slate-700 h-24" value={noticeForm.content} onChange={e => setNoticeForm({...noticeForm, content: e.target.value})} required />
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-500 uppercase">Link Documento (Opzionale)</label>
|
|
<div className="relative">
|
|
<LinkIcon className="absolute left-3 top-3 w-4 h-4 text-slate-400"/>
|
|
<input className="w-full border p-2.5 pl-9 rounded-lg text-slate-700" placeholder="https://..." value={noticeForm.link} onChange={e => setNoticeForm({...noticeForm, link: e.target.value})} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<input type="checkbox" checked={noticeForm.active} onChange={e => setNoticeForm({...noticeForm, active: e.target.checked})} className="w-4 h-4 text-blue-600"/>
|
|
<span className="text-sm font-medium text-slate-700">Visibile ai condomini</span>
|
|
</div>
|
|
|
|
<div className="flex gap-2 pt-2">
|
|
<button type="button" onClick={() => setShowNoticeModal(false)} className="flex-1 border p-2.5 rounded-lg text-slate-600 hover:bg-slate-50">Annulla</button>
|
|
<button type="submit" className="flex-1 bg-blue-600 text-white p-2.5 rounded-lg hover:bg-blue-700">Pubblica</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* FAMILY MODAL */}
|
|
{showFamilyModal && (
|
|
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
|
|
<div className="bg-white rounded-2xl shadow-xl w-full max-w-md p-6">
|
|
<h3 className="font-bold text-lg mb-4 text-slate-800">{editingFamily ? 'Modifica Famiglia' : 'Nuova Famiglia'}</h3>
|
|
<form onSubmit={handleFamilySubmit} className="space-y-4">
|
|
<input type="text" required value={familyForm.name} onChange={(e) => setFamilyForm({...familyForm, name: e.target.value})} className="w-full border rounded-lg p-3 text-slate-700" placeholder="Nome Famiglia"/>
|
|
<input type="text" required value={familyForm.unitNumber} onChange={(e) => setFamilyForm({...familyForm, unitNumber: e.target.value})} className="w-full border rounded-lg p-3 text-slate-700" placeholder="Interno"/>
|
|
<input type="email" value={familyForm.contactEmail} onChange={(e) => setFamilyForm({...familyForm, contactEmail: e.target.value})} className="w-full border rounded-lg p-3 text-slate-700" placeholder="Email"/>
|
|
|
|
<div className="pt-2 border-t border-slate-100">
|
|
<label className="block text-xs font-bold text-slate-500 uppercase mb-1">Quota Mensile Personalizzata</label>
|
|
<p className="text-xs text-slate-400 mb-2">Lasciare vuoto per usare il default del condominio (€ {activeCondo?.defaultMonthlyQuota})</p>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
value={familyForm.customMonthlyQuota}
|
|
onChange={(e) => setFamilyForm({...familyForm, customMonthlyQuota: e.target.value})}
|
|
className="w-full border rounded-lg p-3 text-slate-700"
|
|
placeholder="Es. 120.00"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-3 pt-4">
|
|
<button type="button" onClick={() => setShowFamilyModal(false)} className="flex-1 p-3 border rounded-lg text-slate-600">Annulla</button>
|
|
<button type="submit" className="flex-1 p-3 bg-blue-600 text-white rounded-lg">Salva</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ALERT MODAL */}
|
|
{showAlertModal && (
|
|
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
|
|
<div className="bg-white rounded-xl shadow-xl w-full max-w-md p-6 animate-in fade-in zoom-in duration-200">
|
|
<h3 className="font-bold text-lg mb-4 text-slate-800">{editingAlert ? 'Modifica Avviso' : 'Nuovo Avviso Automatico'}</h3>
|
|
<form onSubmit={handleAlertSubmit} className="space-y-4">
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Oggetto Email" value={alertForm.subject} onChange={e => setAlertForm({...alertForm, subject: e.target.value})} required />
|
|
<textarea className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Corpo Email" value={alertForm.body} onChange={e => setAlertForm({...alertForm, body: e.target.value})} required />
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="text-xs text-slate-500 font-bold uppercase">Giorni Offset</label>
|
|
<input type="number" className="w-full border p-2.5 rounded-lg text-slate-700" value={alertForm.daysOffset} onChange={e => setAlertForm({...alertForm, daysOffset: parseInt(e.target.value)})} />
|
|
</div>
|
|
<div>
|
|
<label className="text-xs text-slate-500 font-bold uppercase">Ora Invio</label>
|
|
<input type="number" min="0" max="23" className="w-full border p-2.5 rounded-lg text-slate-700" value={alertForm.sendHour} onChange={e => setAlertForm({...alertForm, sendHour: parseInt(e.target.value)})} />
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-xs text-slate-500 font-bold uppercase">Tipo Offset</label>
|
|
<select className="w-full border p-2.5 rounded-lg text-slate-700 bg-white" value={alertForm.offsetType} onChange={e => setAlertForm({...alertForm, offsetType: e.target.value as any})}>
|
|
<option value="before_next_month">Giorni prima del prossimo mese</option>
|
|
<option value="after_current_month">Giorni dopo inizio mese</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="flex gap-2 pt-2">
|
|
<button type="button" onClick={() => setShowAlertModal(false)} className="flex-1 border p-2.5 rounded-lg text-slate-600">Annulla</button>
|
|
<button type="submit" className="flex-1 bg-blue-600 text-white p-2.5 rounded-lg">Salva</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* READ DETAILS MODAL */}
|
|
{showReadDetailsModal && selectedNoticeId && (
|
|
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
|
|
<div className="bg-white rounded-xl shadow-xl w-full max-w-md p-6 animate-in fade-in zoom-in duration-200">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="font-bold text-lg text-slate-800">Dettaglio Letture</h3>
|
|
<button onClick={() => setShowReadDetailsModal(false)} className="p-1 rounded hover:bg-slate-100 text-slate-500"><X className="w-5 h-5"/></button>
|
|
</div>
|
|
|
|
<div className="max-h-64 overflow-y-auto pr-2">
|
|
{noticeReadStats[selectedNoticeId] && noticeReadStats[selectedNoticeId].length > 0 ? (
|
|
<div className="space-y-3">
|
|
{noticeReadStats[selectedNoticeId].map((read, idx) => {
|
|
const user = users.find(u => u.id === read.userId);
|
|
return (
|
|
<div key={idx} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg border border-slate-100">
|
|
<div className="flex items-center gap-3">
|
|
<div className="bg-blue-100 p-2 rounded-full"><UserIcon className="w-4 h-4 text-blue-600"/></div>
|
|
<div>
|
|
<p className="text-sm font-bold text-slate-800">{user?.name || 'Utente Sconosciuto'}</p>
|
|
<p className="text-xs text-slate-500">{user?.email}</p>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-xs text-slate-500">{new Date(read.readAt).toLocaleDateString()}</p>
|
|
<p className="text-xs font-mono text-slate-400">{new Date(read.readAt).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<p className="text-center text-slate-400 py-8">Nessuna lettura registrata.</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* USER MODAL */}
|
|
{showUserModal && (
|
|
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
|
|
<div className="bg-white rounded-xl shadow-xl w-full max-w-md p-6 animate-in fade-in zoom-in duration-200">
|
|
<h3 className="font-bold text-lg mb-4 text-slate-800">{editingUser ? 'Modifica Utente' : 'Nuovo Utente'}</h3>
|
|
<form onSubmit={handleUserSubmit} className="space-y-4">
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Nome Completo" value={userForm.name} onChange={e => setUserForm({...userForm, name: e.target.value})} required />
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700" type="email" placeholder="Email" value={userForm.email} onChange={e => setUserForm({...userForm, email: e.target.value})} required />
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700" type="tel" placeholder="Telefono" value={userForm.phone} onChange={e => setUserForm({...userForm, phone: e.target.value})} />
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700" type="password" placeholder={editingUser ? "Nuova Password (opzionale)" : "Password"} value={userForm.password} onChange={e => setUserForm({...userForm, password: e.target.value})} required={!editingUser} />
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="text-xs text-slate-500 font-bold uppercase mb-1 block">Ruolo</label>
|
|
<select className="w-full border p-2.5 rounded-lg text-slate-700 bg-white" value={userForm.role} onChange={e => setUserForm({...userForm, role: e.target.value as any})}>
|
|
<option value="user">Utente</option>
|
|
<option value="admin">Admin</option>
|
|
<option value="poweruser">Power User</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs text-slate-500 font-bold uppercase mb-1 block">Famiglia</label>
|
|
<select className="w-full border p-2.5 rounded-lg text-slate-700 bg-white" value={userForm.familyId} onChange={e => setUserForm({...userForm, familyId: e.target.value})}>
|
|
<option value="">Nessuna</option>
|
|
{families.map(f => <option key={f.id} value={f.id}>{f.name}</option>)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<input type="checkbox" checked={userForm.receiveAlerts} onChange={e => setUserForm({...userForm, receiveAlerts: e.target.checked})} className="w-4 h-4 text-blue-600"/>
|
|
<span className="text-sm font-medium text-slate-700">Ricevi avvisi email</span>
|
|
</div>
|
|
|
|
<div className="flex gap-2 pt-2">
|
|
<button type="button" onClick={() => setShowUserModal(false)} className="flex-1 border p-2.5 rounded-lg text-slate-600 hover:bg-slate-50">Annulla</button>
|
|
<button type="submit" className="flex-1 bg-blue-600 text-white p-2.5 rounded-lg hover:bg-blue-700">Salva</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|