922 lines
79 KiB
TypeScript
922 lines
79 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, MapPin, CreditCard,
|
|
ToggleLeft, ToggleRight, LayoutGrid, PieChart, Users, Send, Cloud, HardDrive, ChevronRight
|
|
} from 'lucide-react';
|
|
|
|
export const SettingsPage: React.FC = () => {
|
|
const currentUser = CondoService.getCurrentUser();
|
|
const isSuperAdmin = currentUser?.role === 'admin';
|
|
const isPrivileged = currentUser?.role === 'admin' || currentUser?.role === 'poweruser';
|
|
|
|
// Tab configuration
|
|
type TabType = 'profile' | 'features' | 'general' | 'storage' | 'condos' | 'families' | 'users' | 'notices' | 'alerts';
|
|
|
|
const [activeTab, setActiveTab] = useState<TabType>(isPrivileged ? '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: '',
|
|
streetNumber: '',
|
|
city: '',
|
|
province: '',
|
|
zipCode: '',
|
|
notes: '',
|
|
paypalClientId: '',
|
|
defaultMonthlyQuota: 100,
|
|
dueDay: 10
|
|
});
|
|
|
|
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;
|
|
stair: string;
|
|
floor: string;
|
|
notes: string;
|
|
contactEmail: string;
|
|
customMonthlyQuota: string;
|
|
}>({ name: '', unitNumber: '', stair: '', floor: '', notes: '', 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
|
|
});
|
|
|
|
// SMTP Modal State
|
|
const [showSmtpModal, setShowSmtpModal] = useState(false);
|
|
const [testingSmtp, setTestingSmtp] = useState(false);
|
|
const [testSmtpMsg, setTestSmtpMsg] = useState('');
|
|
|
|
// Notices (Bacheca) State
|
|
const [notices, setNotices] = useState<Notice[]>([]);
|
|
const [showNoticeModal, setShowNoticeModal] = useState(false);
|
|
const [editingNotice, setEditingNotice] = useState<Notice | null>(null);
|
|
const [noticeTargetMode, setNoticeTargetMode] = useState<'all' | 'specific'>('all');
|
|
const [noticeForm, setNoticeForm] = useState<{
|
|
title: string;
|
|
content: string;
|
|
type: NoticeIconType;
|
|
link: string;
|
|
condoId: string;
|
|
active: boolean;
|
|
targetFamilyIds: string[];
|
|
}>({
|
|
title: '',
|
|
content: '',
|
|
type: 'info',
|
|
link: '',
|
|
condoId: '',
|
|
active: true,
|
|
targetFamilyIds: []
|
|
});
|
|
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 (isPrivileged) {
|
|
// First fetch global/structural data
|
|
const condoList = await CondoService.getCondos();
|
|
const activeC = await CondoService.getActiveCondo();
|
|
const gSettings = await CondoService.getSettings();
|
|
|
|
// Ensure storageConfig exists locally even if API missed it
|
|
if (!gSettings.storageConfig) {
|
|
gSettings.storageConfig = { provider: 'local_db' };
|
|
}
|
|
|
|
setCondos(condoList);
|
|
setActiveCondo(activeC);
|
|
setGlobalSettings(gSettings);
|
|
|
|
// Fetch condo-specific data individually to prevent one failure from blocking others
|
|
if (activeC) {
|
|
// Families
|
|
try {
|
|
const fams = await CondoService.getFamilies(activeC.id);
|
|
setFamilies(fams);
|
|
} catch(e) { console.error("Error fetching families", e); }
|
|
|
|
// Users
|
|
try {
|
|
const usrs = await CondoService.getUsers(activeC.id);
|
|
setUsers(usrs);
|
|
} catch(e) { console.error("Error fetching users", e); }
|
|
|
|
// Alerts
|
|
try {
|
|
const alrts = await CondoService.getAlerts(activeC.id);
|
|
setAlerts(alrts);
|
|
} catch(e) { console.error("Error fetching alerts", e); }
|
|
|
|
// Notices
|
|
try {
|
|
const allNotices = await CondoService.getNotices(activeC.id);
|
|
setNotices(allNotices);
|
|
|
|
// Fetch read stats for notices
|
|
const stats: Record<string, NoticeRead[]> = {};
|
|
for (const n of allNotices) {
|
|
try {
|
|
const reads = await CondoService.getNoticeReadStatus(n.id);
|
|
stats[n.id] = reads;
|
|
} catch(e) { console.warn("Error reading notice status", e); }
|
|
}
|
|
setNoticeReadStats(stats);
|
|
} catch(e) { console.error("Error fetching notices", e); }
|
|
|
|
} else {
|
|
setFamilies([]);
|
|
setUsers([]);
|
|
setAlerts([]);
|
|
setNotices([]);
|
|
}
|
|
|
|
} else {
|
|
const activeC = await CondoService.getActiveCondo();
|
|
setActiveCondo(activeC);
|
|
}
|
|
} catch(e) {
|
|
console.error("Global fetch error", e);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchData();
|
|
}, [isPrivileged]);
|
|
|
|
// --- HANDLERS (Omitted details for brevity as they are unchanged) ---
|
|
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); }
|
|
};
|
|
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 handleFeaturesSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault(); if (!globalSettings) return; setSaving(true);
|
|
try { await CondoService.updateSettings(globalSettings); setSuccessMsg('Configurazione salvata!'); setTimeout(() => setSuccessMsg(''), 3000); window.location.reload(); } 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(''); setShowSmtpModal(false); }, 2000); } catch (e) { console.error(e); } finally { setSaving(false); }
|
|
};
|
|
const handleSmtpTest = async () => {
|
|
if (!globalSettings?.smtpConfig) return; setTestingSmtp(true); setTestSmtpMsg('');
|
|
try { await CondoService.testSmtpConfig(globalSettings.smtpConfig); setTestSmtpMsg('Successo! Email di prova inviata.'); } catch(e: any) { setTestSmtpMsg('Errore: ' + (e.message || "Impossibile connettersi")); } finally { setTestingSmtp(false); }
|
|
};
|
|
const handleStorageSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault(); if (!globalSettings) return; setSaving(true);
|
|
try { await CondoService.updateSettings(globalSettings); setSuccessMsg('Configurazione Storage 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); } }
|
|
};
|
|
|
|
// CRUD Handlers
|
|
const openAddCondoModal = () => { setEditingCondo(null); setCondoForm({ name: '', address: '', streetNumber: '', city: '', province: '', zipCode: '', notes: '', paypalClientId: '', defaultMonthlyQuota: 100, dueDay: 10 }); setShowCondoModal(true); };
|
|
const openEditCondoModal = (c: Condo) => { setEditingCondo(c); setCondoForm({ name: c.name, address: c.address || '', streetNumber: c.streetNumber || '', city: c.city || '', province: c.province || '', zipCode: c.zipCode || '', notes: c.notes || '', paypalClientId: c.paypalClientId || '', defaultMonthlyQuota: c.defaultMonthlyQuota, dueDay: c.dueDay || 10 }); setShowCondoModal(true); };
|
|
const handleCondoSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault(); try { const payload: Condo = { id: editingCondo ? editingCondo.id : '', name: condoForm.name, address: condoForm.address, streetNumber: condoForm.streetNumber, city: condoForm.city, province: condoForm.province, zipCode: condoForm.zipCode, notes: condoForm.notes, paypalClientId: condoForm.paypalClientId, defaultMonthlyQuota: condoForm.defaultMonthlyQuota, dueDay: condoForm.dueDay }; const savedCondo = await CondoService.saveCondo(payload); const list = await CondoService.getCondos(); setCondos(list); if (activeCondo?.id === savedCondo.id) { setActiveCondo(savedCondo); } if (!activeCondo && list.length === 1) { CondoService.setActiveCondo(savedCondo.id); } setShowCondoModal(false); window.dispatchEvent(new Event('condo-updated')); } catch (e) { console.error(e); alert("Errore nel salvataggio del condominio. Assicurati di essere amministratore."); }
|
|
};
|
|
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); } };
|
|
const openAddFamilyModal = () => { setEditingFamily(null); setFamilyForm({ name: '', unitNumber: '', stair: '', floor: '', notes: '', contactEmail: '', customMonthlyQuota: '' }); setShowFamilyModal(true); };
|
|
const openEditFamilyModal = (family: Family) => { setEditingFamily(family); setFamilyForm({ name: family.name, unitNumber: family.unitNumber, stair: family.stair || '', floor: family.floor || '', notes: family.notes || '', 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 && familyForm.customMonthlyQuota.trim() !== '' ? parseFloat(familyForm.customMonthlyQuota) : undefined; const payload: any = { name: familyForm.name, unitNumber: familyForm.unitNumber, stair: familyForm.stair, floor: familyForm.floor, notes: familyForm.notes, contactEmail: familyForm.contactEmail, customMonthlyQuota: quota }; if (editingFamily) { const updatedFamily = { ...editingFamily, ...payload }; await CondoService.updateFamily(updatedFamily); setFamilies(families.map(f => f.id === updatedFamily.id ? updatedFamily : f)); } else { const newFamily = await CondoService.addFamily(payload); setFamilies([...families, newFamily]); } setShowFamilyModal(false); } catch (e: any) { console.error(e); alert(`Errore: ${e.message || "Impossibile salvare la famiglia."}`); } };
|
|
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(activeCondo?.id)); setShowUserModal(false); } catch (e) { alert("Errore nel salvataggio utente"); } };
|
|
const handleDeleteUser = async (id: string) => { if(!window.confirm("Eliminare utente?")) return; await CondoService.deleteUser(id); setUsers(users.filter(u => u.id !== id)); };
|
|
const openAddNoticeModal = () => { setEditingNotice(null); setNoticeTargetMode('all'); setNoticeForm({ title: '', content: '', type: 'info', link: '', condoId: activeCondo?.id || '', active: true, targetFamilyIds: [] }); setShowNoticeModal(true); };
|
|
const openEditNoticeModal = (n: Notice) => { setEditingNotice(n); const isTargeted = n.targetFamilyIds && n.targetFamilyIds.length > 0; setNoticeTargetMode(isTargeted ? 'specific' : 'all'); setNoticeForm({ title: n.title, content: n.content, type: n.type, link: n.link || '', condoId: n.condoId, active: n.active, targetFamilyIds: n.targetFamilyIds || [] }); setShowNoticeModal(true); };
|
|
const handleNoticeSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { const payload: Notice = { id: editingNotice ? editingNotice.id : '', ...noticeForm, targetFamilyIds: noticeTargetMode === 'all' ? [] : noticeForm.targetFamilyIds, date: editingNotice ? editingNotice.date : new Date().toISOString() }; await CondoService.saveNotice(payload); setNotices(await CondoService.getNotices(activeCondo?.id)); 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 toggleNoticeFamilyTarget = (familyId: string) => { setNoticeForm(prev => { const current = prev.targetFamilyIds; if (current.includes(familyId)) { return { ...prev, targetFamilyIds: current.filter(id => id !== familyId) }; } else { return { ...prev, targetFamilyIds: [...current, familyId] }; } }); };
|
|
const openReadDetails = (noticeId: string) => { setSelectedNoticeId(noticeId); setShowReadDetailsModal(true); };
|
|
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 toggleFeature = (key: keyof AppSettings['features']) => {
|
|
if (!globalSettings) return;
|
|
setGlobalSettings({
|
|
...globalSettings,
|
|
features: {
|
|
...globalSettings.features,
|
|
[key]: !globalSettings.features[key]
|
|
}
|
|
});
|
|
};
|
|
|
|
const tabs: {id: TabType, label: string, icon: React.ReactNode}[] = [
|
|
{ id: 'profile', label: 'Profilo', icon: <UserIcon className="w-4 h-4"/> },
|
|
];
|
|
if (isSuperAdmin) {
|
|
tabs.push({ id: 'features', label: 'Funzionalità', icon: <LayoutGrid className="w-4 h-4"/> });
|
|
}
|
|
if (isPrivileged) {
|
|
tabs.push({ id: 'general', label: 'Condominio', icon: <Building className="w-4 h-4"/> });
|
|
|
|
// Only show Storage tab if Documents feature is enabled
|
|
if (globalSettings?.features.documents) {
|
|
tabs.push({ id: 'storage', label: 'Cloud & Storage', icon: <HardDrive className="w-4 h-4"/> });
|
|
}
|
|
|
|
if (globalSettings?.features.multiCondo) {
|
|
tabs.push({ id: 'condos', label: 'Lista Condomini', icon: <List className="w-4 h-4"/> });
|
|
}
|
|
tabs.push(
|
|
{ id: 'families', label: 'Famiglie', icon: <Coins className="w-4 h-4"/> },
|
|
{ id: 'users', label: 'Utenti', icon: <UserCog className="w-4 h-4"/> }
|
|
);
|
|
if (globalSettings?.features.notices) {
|
|
tabs.push({ id: 'notices', label: 'Bacheca', icon: <Megaphone className="w-4 h-4"/> });
|
|
}
|
|
tabs.push(
|
|
{ id: 'alerts', label: 'Avvisi Email', icon: <Bell className="w-4 h-4"/> }
|
|
);
|
|
}
|
|
|
|
if (loading) return <div className="p-8 text-center text-slate-400">Caricamento...</div>;
|
|
|
|
return (
|
|
<div className="max-w-6xl mx-auto pb-20">
|
|
<div className="mb-6">
|
|
<h2 className="text-2xl font-bold text-slate-800">Impostazioni</h2>
|
|
<p className="text-slate-500">{activeCondo ? `Gestione: ${activeCondo.name}` : 'Pannello di Controllo'}</p>
|
|
</div>
|
|
|
|
<div className="flex flex-col md:flex-row gap-6">
|
|
{/* NAVIGATION SIDEBAR / MOBILE SCROLL */}
|
|
<div className="md:w-64 flex-shrink-0">
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden sticky top-4">
|
|
<div className="p-4 border-b border-slate-100 md:block hidden">
|
|
<h3 className="font-bold text-slate-700 text-sm uppercase tracking-wider">Menu</h3>
|
|
</div>
|
|
{/* Mobile: Horizontal Scroll, Desktop: Vertical List */}
|
|
<div className="flex md:flex-col overflow-x-auto md:overflow-visible no-scrollbar p-2 md:p-2 gap-2">
|
|
{tabs.map(tab => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={`
|
|
flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium transition-all whitespace-nowrap flex-shrink-0
|
|
${activeTab === tab.id
|
|
? 'bg-blue-50 text-blue-700 shadow-sm'
|
|
: 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'}
|
|
`}
|
|
>
|
|
{tab.icon}
|
|
<span>{tab.label}</span>
|
|
{activeTab === tab.id && <ChevronRight className="w-4 h-4 ml-auto hidden md:block opacity-50"/>}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* CONTENT AREA */}
|
|
<div className="flex-1 min-w-0">
|
|
{/* PROFILE TAB */}
|
|
{activeTab === 'profile' && (
|
|
<div className="animate-fade-in bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
|
<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>
|
|
{profileMsg && <p className={`text-sm ${profileMsg.includes('Errore') ? 'text-red-500' : 'text-green-600'}`}>{profileMsg}</p>}
|
|
</form>
|
|
</div>
|
|
)}
|
|
|
|
{/* FEATURES TAB */}
|
|
{isSuperAdmin && activeTab === 'features' && globalSettings && (
|
|
<div className="animate-fade-in bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h3 className="text-lg font-bold text-slate-800 flex items-center gap-2"><LayoutGrid className="w-5 h-5 text-blue-600" /> Funzionalità Piattaforma</h3>
|
|
</div>
|
|
<form onSubmit={handleFeaturesSubmit} className="space-y-6">
|
|
<div className="space-y-4">
|
|
{/* Toggles */}
|
|
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-xl border border-slate-100">
|
|
<div><p className="font-bold text-slate-800">Gestione Multicondominio</p><p className="text-sm text-slate-500">Abilita la gestione di più stabili.</p></div>
|
|
<button type="button" onClick={() => toggleFeature('multiCondo')} className={`${globalSettings.features.multiCondo ? 'bg-green-500' : 'bg-slate-300'} relative inline-flex h-6 w-11 items-center rounded-full transition-colors`}><span className={`${globalSettings.features.multiCondo ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}/></button>
|
|
</div>
|
|
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-xl border border-slate-100">
|
|
<div><p className="font-bold text-slate-800">Gestione Tickets</p><p className="text-sm text-slate-500">Abilita il sistema di segnalazione guasti.</p></div>
|
|
<button type="button" onClick={() => toggleFeature('tickets')} className={`${globalSettings.features.tickets ? 'bg-green-500' : 'bg-slate-300'} relative inline-flex h-6 w-11 items-center rounded-full transition-colors`}><span className={`${globalSettings.features.tickets ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}/></button>
|
|
</div>
|
|
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-xl border border-slate-100">
|
|
<div><p className="font-bold text-slate-800">Gestione Documenti</p><p className="text-sm text-slate-500">Archivio digitale con tag e upload.</p></div>
|
|
<button type="button" onClick={() => toggleFeature('documents')} className={`${globalSettings.features.documents ? 'bg-green-500' : 'bg-slate-300'} relative inline-flex h-6 w-11 items-center rounded-full transition-colors`}><span className={`${globalSettings.features.documents ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}/></button>
|
|
</div>
|
|
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-xl border border-slate-100">
|
|
<div><p className="font-bold text-slate-800">Pagamenti PayPal</p><p className="text-sm text-slate-500">Permetti ai condomini di pagare online.</p></div>
|
|
<button type="button" onClick={() => toggleFeature('payPal')} className={`${globalSettings.features.payPal ? 'bg-green-500' : 'bg-slate-300'} relative inline-flex h-6 w-11 items-center rounded-full transition-colors`}><span className={`${globalSettings.features.payPal ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}/></button>
|
|
</div>
|
|
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-xl border border-slate-100">
|
|
<div><p className="font-bold text-slate-800">Bacheca Avvisi</p><p className="text-sm text-slate-500">Mostra la bacheca digitale.</p></div>
|
|
<button type="button" onClick={() => toggleFeature('notices')} className={`${globalSettings.features.notices ? 'bg-green-500' : 'bg-slate-300'} relative inline-flex h-6 w-11 items-center rounded-full transition-colors`}><span className={`${globalSettings.features.notices ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}/></button>
|
|
</div>
|
|
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-xl border border-slate-100">
|
|
<div><p className="font-bold text-slate-800">Reportistica Avanzata</p><p className="text-sm text-slate-500">Abilita grafici incassi e bilancio.</p></div>
|
|
<button type="button" onClick={() => toggleFeature('reports')} className={`${globalSettings.features.reports ? 'bg-green-500' : 'bg-slate-300'} relative inline-flex h-6 w-11 items-center rounded-full transition-colors`}><span className={`${globalSettings.features.reports ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}/></button>
|
|
</div>
|
|
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-xl border border-slate-100">
|
|
<div><p className="font-bold text-slate-800">Spese Straordinarie</p><p className="text-sm text-slate-500">Gestione lavori e preventivi.</p></div>
|
|
<button type="button" onClick={() => toggleFeature('extraordinaryExpenses')} className={`${globalSettings.features.extraordinaryExpenses !== false ? 'bg-green-500' : 'bg-slate-300'} relative inline-flex h-6 w-11 items-center rounded-full transition-colors`}><span className={`${globalSettings.features.extraordinaryExpenses !== false ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}/></button>
|
|
</div>
|
|
<div className="flex items-center justify-between p-4 bg-slate-50 rounded-xl border border-slate-100">
|
|
<div>
|
|
<p className="font-bold text-slate-800">Visualizza Spese Condominiali agli Utenti</p>
|
|
<p className="text-sm text-slate-500">Se attivo, gli utenti potranno vedere (sola lettura) il registro delle spese condominiali.</p>
|
|
</div>
|
|
<button type="button" onClick={() => toggleFeature('condoFinancialsView')} className={`${globalSettings.features.condoFinancialsView ? 'bg-green-500' : 'bg-slate-300'} relative inline-flex h-6 w-11 items-center rounded-full transition-colors`}>
|
|
<span className={`${globalSettings.features.condoFinancialsView ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}/>
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
<div className="pt-2 flex justify-between items-center"><span className="text-green-600 font-medium">{successMsg}</span><button type="submit" 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" /> Salva Configurazione</button></div>
|
|
</form>
|
|
</div>
|
|
)}
|
|
|
|
{/* STORAGE CONFIG TAB - CONDITIONAL */}
|
|
{isPrivileged && activeTab === 'storage' && globalSettings?.features.documents && (
|
|
<div className="animate-fade-in bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
|
<h3 className="text-lg font-bold text-slate-800 mb-4 flex items-center gap-2"><Cloud className="w-5 h-5 text-blue-600" /> Configurazione Storage</h3>
|
|
<p className="text-sm text-slate-500 mb-6">Scegli dove salvare i documenti caricati.</p>
|
|
|
|
<form onSubmit={handleStorageSubmit} className="space-y-6">
|
|
{globalSettings?.storageConfig ? (
|
|
<>
|
|
<div>
|
|
<label className="text-sm font-bold text-slate-700 mb-2 block">Provider Attivo</label>
|
|
<select
|
|
className="w-full border p-2.5 rounded-lg bg-slate-50 font-medium text-slate-700"
|
|
value={globalSettings.storageConfig.provider}
|
|
onChange={e => setGlobalSettings({
|
|
...globalSettings,
|
|
storageConfig: { ...globalSettings.storageConfig!, provider: e.target.value as any }
|
|
})}
|
|
>
|
|
<option value="local_db">Database Locale (Solo Demo)</option>
|
|
<option value="s3">AWS S3 / Compatible</option>
|
|
<option value="google_drive">Google Drive</option>
|
|
<option value="dropbox">Dropbox</option>
|
|
<option value="onedrive">OneDrive</option>
|
|
</select>
|
|
</div>
|
|
|
|
{globalSettings.storageConfig.provider === 'local_db' && (
|
|
<div className="bg-amber-50 text-amber-800 p-4 rounded-lg text-sm border border-amber-200">
|
|
<p className="font-bold">Modalità Demo Attiva</p>
|
|
<p>I file vengono salvati direttamente nel database (Base64). Non raccomandato per produzione o file grandi.</p>
|
|
</div>
|
|
)}
|
|
|
|
{globalSettings.storageConfig.provider === 's3' && (
|
|
<div className="space-y-3 border-l-4 border-blue-500 pl-4">
|
|
<h4 className="font-bold text-sm text-slate-700">Configurazione S3</h4>
|
|
<input placeholder="Bucket Name" className="w-full border p-2 rounded" value={globalSettings.storageConfig.bucket || ''} onChange={e => setGlobalSettings({...globalSettings, storageConfig: {...globalSettings.storageConfig!, bucket: e.target.value}})}/>
|
|
<input placeholder="Region (es. eu-central-1)" className="w-full border p-2 rounded" value={globalSettings.storageConfig.region || ''} onChange={e => setGlobalSettings({...globalSettings, storageConfig: {...globalSettings.storageConfig!, region: e.target.value}})}/>
|
|
<input placeholder="Access Key ID" className="w-full border p-2 rounded" value={globalSettings.storageConfig.apiKey || ''} onChange={e => setGlobalSettings({...globalSettings, storageConfig: {...globalSettings.storageConfig!, apiKey: e.target.value}})}/>
|
|
<input type="password" placeholder="Secret Access Key" className="w-full border p-2 rounded" value={globalSettings.storageConfig.apiSecret || ''} onChange={e => setGlobalSettings({...globalSettings, storageConfig: {...globalSettings.storageConfig!, apiSecret: e.target.value}})}/>
|
|
</div>
|
|
)}
|
|
|
|
{(globalSettings.storageConfig.provider === 'google_drive' || globalSettings.storageConfig.provider === 'dropbox' || globalSettings.storageConfig.provider === 'onedrive') && (
|
|
<div className="space-y-3 border-l-4 border-purple-500 pl-4">
|
|
<h4 className="font-bold text-sm text-slate-700">Autenticazione API</h4>
|
|
<p className="text-xs text-slate-500">Inserisci le credenziali dell'applicazione sviluppatore.</p>
|
|
<input placeholder="Client ID / App Key" className="w-full border p-2 rounded" value={globalSettings.storageConfig.apiKey || ''} onChange={e => setGlobalSettings({...globalSettings, storageConfig: {...globalSettings.storageConfig!, apiKey: e.target.value}})}/>
|
|
<input type="password" placeholder="Client Secret / App Secret" className="w-full border p-2 rounded" value={globalSettings.storageConfig.apiSecret || ''} onChange={e => setGlobalSettings({...globalSettings, storageConfig: {...globalSettings.storageConfig!, apiSecret: e.target.value}})}/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="pt-2 flex justify-between items-center">
|
|
<span className="text-green-600 font-medium">{successMsg}</span>
|
|
<button type="submit" disabled={saving} 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" /> Salva Impostazioni
|
|
</button>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="text-center p-4 text-slate-500">Inizializzazione configurazione storage...</div>
|
|
)}
|
|
</form>
|
|
</div>
|
|
)}
|
|
|
|
{/* GENERAL TAB */}
|
|
{isPrivileged && 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">
|
|
<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 />
|
|
<div><label className="block text-xs font-bold text-slate-500 uppercase mb-1">Indirizzo</label><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="Via/Piazza..." required/></div>
|
|
<div className="grid grid-cols-2 gap-4"><div><label className="block text-xs font-bold text-slate-500 uppercase mb-1">Civico</label><input type="text" value={activeCondo.streetNumber || ''} onChange={(e) => setActiveCondo({ ...activeCondo, streetNumber: e.target.value })} className="w-full border p-2.5 rounded-lg text-slate-700" required/></div><div><label className="block text-xs font-bold text-slate-500 uppercase mb-1">CAP</label><input type="text" value={activeCondo.zipCode || ''} onChange={(e) => setActiveCondo({ ...activeCondo, zipCode: e.target.value })} className="w-full border p-2.5 rounded-lg text-slate-700"/></div></div>
|
|
<div className="grid grid-cols-2 gap-4"><div><label className="block text-xs font-bold text-slate-500 uppercase mb-1">Città</label><input type="text" value={activeCondo.city || ''} onChange={(e) => setActiveCondo({ ...activeCondo, city: e.target.value })} className="w-full border p-2.5 rounded-lg text-slate-700" required/></div><div><label className="block text-xs font-bold text-slate-500 uppercase mb-1">Provincia</label><input type="text" value={activeCondo.province || ''} onChange={(e) => setActiveCondo({ ...activeCondo, province: e.target.value })} className="w-full border p-2.5 rounded-lg text-slate-700" required/></div></div>
|
|
<div><label className="block text-xs font-bold text-slate-500 uppercase mb-1">Note (Opzionali)</label><textarea value={activeCondo.notes || ''} onChange={(e) => setActiveCondo({ ...activeCondo, notes: e.target.value })} className="w-full border p-2.5 rounded-lg text-slate-700 h-24"></textarea></div>
|
|
{globalSettings?.features.payPal && (<div className="bg-blue-50 p-4 rounded-lg border border-blue-100"><div className="flex items-center gap-2 mb-2 text-blue-800"><CreditCard className="w-4 h-4"/><span className="text-xs font-bold uppercase">Configurazione Pagamenti</span></div><div><label className="text-xs font-semibold text-slate-600 block mb-1">PayPal Client ID</label><input type="password" className="w-full border p-2 rounded text-slate-700 text-sm" placeholder="Es: Afg..." value={activeCondo.paypalClientId || ''} onChange={e => setActiveCondo({...activeCondo, paypalClientId: e.target.value})} /><p className="text-[10px] text-slate-500 mt-1">Necessario per abilitare i pagamenti online delle rate.</p></div></div>)}
|
|
|
|
<div className="flex gap-4">
|
|
<div className="flex-1">
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Quota Mensile (€)</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="flex-1">
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Giorno Scadenza (1-31)</label>
|
|
<input type="number" min="1" max="31" value={activeCondo.dueDay || 10} onChange={(e) => setActiveCondo({ ...activeCondo, dueDay: parseInt(e.target.value) })} className="w-full border p-2.5 rounded-lg text-slate-700" required />
|
|
</div>
|
|
</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"><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 */}
|
|
{isPrivileged && 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 lg: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>
|
|
<div className="text-sm text-slate-500 mb-3 space-y-1"><p className="flex items-center gap-1"><MapPin className="w-3 h-3"/> {condo.address} {condo.streetNumber}</p>{condo.city && <p className="pl-4">{condo.zipCode} {condo.city} ({condo.province})</p>}</div>
|
|
<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 */}
|
|
{isPrivileged && activeTab === 'families' && (
|
|
<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">Elenco Famiglie</h3><p className="text-xs text-blue-600">Condominio: {activeCondo?.name}</p></div><button onClick={openAddFamilyModal} 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="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 border-slate-200"><tr><th className="px-4 py-3">Nominativo</th><th className="px-4 py-3">Interno</th><th className="px-4 py-3 hidden md:table-cell">Email</th><th className="px-4 py-3 text-right">Azioni</th></tr></thead>
|
|
<tbody className="divide-y divide-slate-100">
|
|
{families.map(f => (
|
|
<tr key={f.id} className="hover:bg-slate-50"><td className="px-4 py-3 font-medium text-slate-800">{f.name}</td><td className="px-4 py-3">{f.unitNumber}</td><td className="px-4 py-3 hidden md:table-cell text-slate-500">{f.contactEmail}</td><td className="px-4 py-3 text-right"><button onClick={() => openEditFamilyModal(f)} className="text-blue-600 hover:underline mr-3">Modifica</button><button onClick={() => handleDeleteFamily(f.id)} className="text-red-600 hover:underline">Elimina</button></td></tr>
|
|
))}
|
|
{families.length === 0 && <tr><td colSpan={4} className="p-8 text-center text-slate-400">Nessuna famiglia registrata.</td></tr>}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* USERS TAB */}
|
|
{isPrivileged && activeTab === 'users' && (
|
|
<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">Utenti & Accessi</h3><p className="text-xs text-blue-600">Gestione account di accesso</p></div><button onClick={openAddUserModal} 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="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 border-slate-200"><tr><th className="px-4 py-3">Email</th><th className="px-4 py-3">Ruolo</th><th className="px-4 py-3 hidden md:table-cell">Famiglia</th><th className="px-4 py-3 text-right">Azioni</th></tr></thead>
|
|
<tbody className="divide-y divide-slate-100">
|
|
{users.map(u => {
|
|
const fam = families.find(f => f.id === u.familyId);
|
|
return (
|
|
<tr key={u.id} className="hover:bg-slate-50"><td className="px-4 py-3 font-medium text-slate-800">{u.email}</td><td className="px-4 py-3"><span className={`px-2 py-1 rounded text-xs font-bold uppercase ${u.role === 'admin' ? 'bg-purple-100 text-purple-700' : u.role === 'poweruser' ? 'bg-orange-100 text-orange-700' : 'bg-slate-100 text-slate-600'}`}>{u.role}</span></td><td className="px-4 py-3 hidden md:table-cell">{fam ? `${fam.name} (${fam.unitNumber})` : '-'}</td><td className="px-4 py-3 text-right"><button onClick={() => openEditUserModal(u)} className="text-blue-600 hover:underline mr-3">Modifica</button><button onClick={() => handleDeleteUser(u.id)} className="text-red-600 hover:underline">Elimina</button></td></tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* NOTICES TAB */}
|
|
{isPrivileged && 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 Avvisi</h3></div><button onClick={openAddNoticeModal} className="bg-blue-600 text-white px-4 py-2 rounded-lg font-medium flex gap-2"><Plus className="w-4 h-4" /> Nuovo Avviso</button></div>
|
|
<div className="space-y-3">
|
|
{notices.map(notice => {
|
|
const reads = noticeReadStats[notice.id] || [];
|
|
const targetCount = notice.targetFamilyIds?.length || 0;
|
|
return (
|
|
<div key={notice.id} className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm flex flex-col md:flex-row justify-between gap-4">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<h4 className="font-bold text-slate-800">{notice.title}</h4>
|
|
<span className={`px-2 py-0.5 text-[10px] font-bold uppercase rounded ${notice.active ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-500'}`}>{notice.active ? 'Attivo' : 'Archiviato'}</span>
|
|
<span className="text-xs text-slate-400">• {new Date(notice.date).toLocaleDateString()}</span>
|
|
</div>
|
|
<p className="text-sm text-slate-600 line-clamp-1">{notice.content}</p>
|
|
<div className="mt-2 flex gap-3 text-xs">
|
|
<span className="text-blue-600 font-medium bg-blue-50 px-2 py-0.5 rounded">
|
|
Destinatari: {targetCount === 0 ? 'Tutti' : `${targetCount} Famiglie`}
|
|
</span>
|
|
<button onClick={() => openReadDetails(notice.id)} className="text-slate-500 hover:text-blue-600 flex items-center gap-1">
|
|
<Eye className="w-3 h-3"/> Letto da: <strong>{reads.length}</strong>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button onClick={() => toggleNoticeActive(notice)} className="p-2 text-slate-500 hover:bg-slate-100 rounded" title={notice.active ? "Archivia" : "Attiva"}><Power className="w-4 h-4"/></button>
|
|
<button onClick={() => openEditNoticeModal(notice)} className="p-2 text-blue-600 hover:bg-blue-50 rounded" title="Modifica"><Pencil className="w-4 h-4"/></button>
|
|
<button onClick={() => handleDeleteNotice(notice.id)} className="p-2 text-red-600 hover:bg-red-50 rounded" title="Elimina"><Trash2 className="w-4 h-4"/></button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
{notices.length === 0 && <div className="text-center p-8 text-slate-400">Nessun avviso in bacheca.</div>}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ALERTS TAB */}
|
|
{isPrivileged && activeTab === 'alerts' && (
|
|
<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">Avvisi Automatici Email</h3><p className="text-xs text-blue-600">Configura email periodiche</p></div>
|
|
<div className="flex gap-2">
|
|
<button onClick={() => setShowSmtpModal(true)} className="bg-white text-blue-600 border border-blue-200 px-4 py-2 rounded-lg font-medium hover:bg-blue-50 flex gap-2"><Server className="w-4 h-4"/> Configura SMTP</button>
|
|
<button onClick={openAddAlertModal} className="bg-blue-600 text-white px-4 py-2 rounded-lg font-medium flex gap-2"><Plus className="w-4 h-4" /> Nuovo Alert</button>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{alerts.map(alert => (
|
|
<div key={alert.id} className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm flex justify-between items-center">
|
|
<div>
|
|
<h4 className="font-bold text-slate-800">{alert.subject}</h4>
|
|
<p className="text-sm text-slate-500">Invia {alert.daysOffset} giorni {alert.offsetType === 'before_next_month' ? 'prima del prossimo mese' : 'dopo il mese corrente'} alle {alert.sendHour}:00</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button onClick={() => openEditAlertModal(alert)} className="text-blue-600 hover:underline text-sm">Modifica</button>
|
|
<button onClick={() => handleDeleteAlert(alert.id)} className="text-red-600 hover:underline text-sm">Elimina</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{alerts.length === 0 && <div className="text-center p-8 text-slate-400">Nessun alert configurato.</div>}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 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-lg p-6 animate-in fade-in zoom-in duration-200 overflow-y-auto max-h-[90vh]">
|
|
<h3 className="font-bold text-lg mb-4 text-slate-800">{editingCondo ? 'Modifica Condominio' : 'Nuovo Condominio'}</h3>
|
|
<form onSubmit={handleCondoSubmit} className="space-y-4">
|
|
{/* Form fields same as before... omitted for brevity */}
|
|
<div>
|
|
<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 />
|
|
</div>
|
|
<div className="grid grid-cols-12 gap-3">
|
|
<div className="col-span-9">
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Via/Piazza..." value={condoForm.address} onChange={e => setCondoForm({...condoForm, address: e.target.value})} required />
|
|
</div>
|
|
<div className="col-span-3">
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Civico" value={condoForm.streetNumber} onChange={e => setCondoForm({...condoForm, streetNumber: e.target.value})} required />
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-12 gap-3">
|
|
<div className="col-span-3">
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="CAP" value={condoForm.zipCode} onChange={e => setCondoForm({...condoForm, zipCode: e.target.value})} />
|
|
</div>
|
|
<div className="col-span-5">
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Città" value={condoForm.city} onChange={e => setCondoForm({...condoForm, city: e.target.value})} required />
|
|
</div>
|
|
<div className="col-span-4">
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Provincia" value={condoForm.province} onChange={e => setCondoForm({...condoForm, province: e.target.value})} required />
|
|
</div>
|
|
</div>
|
|
{globalSettings?.features.payPal && (
|
|
<div className="bg-blue-50 p-3 rounded-lg border border-blue-100">
|
|
<div className="flex items-center gap-2 mb-2 text-blue-800">
|
|
<CreditCard className="w-4 h-4"/>
|
|
<span className="text-xs font-bold uppercase">Configurazione Pagamenti</span>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs font-semibold text-slate-600 block mb-1">PayPal Client ID (REST API App)</label>
|
|
<input type="password" className="w-full border p-2 rounded text-slate-700 text-sm" placeholder="Es: Afg..." value={condoForm.paypalClientId} onChange={e => setCondoForm({...condoForm, paypalClientId: e.target.value})} />
|
|
<p className="text-[10px] text-slate-500 mt-1">Necessario per abilitare i pagamenti online delle rate.</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div>
|
|
<textarea className="w-full border p-2.5 rounded-lg text-slate-700 h-16" placeholder="Note (opzionali)" value={condoForm.notes} onChange={e => setCondoForm({...condoForm, notes: e.target.value})} />
|
|
</div>
|
|
<div className="flex gap-4">
|
|
<div className="flex-1">
|
|
<label className="text-sm font-medium text-slate-600 block mb-1">Quota Default €</label>
|
|
<input type="number" className="border p-2 rounded w-full text-slate-700" value={condoForm.defaultMonthlyQuota} onChange={e => setCondoForm({...condoForm, defaultMonthlyQuota: parseFloat(e.target.value)})} />
|
|
</div>
|
|
<div className="flex-1">
|
|
<label className="text-sm font-medium text-slate-600 block mb-1">Giorno Scadenza (1-31)</label>
|
|
<input type="number" min="1" max="31" className="border p-2 rounded w-full text-slate-700" value={condoForm.dueDay} onChange={e => setCondoForm({...condoForm, dueDay: parseInt(e.target.value)})} />
|
|
</div>
|
|
</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>
|
|
)}
|
|
|
|
{/* 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-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">{editingFamily ? 'Modifica Famiglia' : 'Nuova Famiglia'}</h3>
|
|
<form onSubmit={handleFamilySubmit} className="space-y-4">
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Cognome / Nome Famiglia" value={familyForm.name} onChange={e => setFamilyForm({...familyForm, name: e.target.value})} required />
|
|
<div className="grid grid-cols-3 gap-3">
|
|
<input className="border p-2.5 rounded-lg text-slate-700" placeholder="Interno" value={familyForm.unitNumber} onChange={e => setFamilyForm({...familyForm, unitNumber: e.target.value})} required />
|
|
<input className="border p-2.5 rounded-lg text-slate-700" placeholder="Scala" value={familyForm.stair} onChange={e => setFamilyForm({...familyForm, stair: e.target.value})} />
|
|
<input className="border p-2.5 rounded-lg text-slate-700" placeholder="Piano" value={familyForm.floor} onChange={e => setFamilyForm({...familyForm, floor: e.target.value})} />
|
|
</div>
|
|
<input type="email" className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Email Contatto Principal" value={familyForm.contactEmail} onChange={e => setFamilyForm({...familyForm, contactEmail: e.target.value})} />
|
|
<input type="number" className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Quota Personalizzata (Opzionale)" value={familyForm.customMonthlyQuota} onChange={e => setFamilyForm({...familyForm, customMonthlyQuota: e.target.value})} />
|
|
<textarea className="w-full border p-2.5 rounded-lg text-slate-700 h-20" placeholder="Note..." value={familyForm.notes} onChange={e => setFamilyForm({...familyForm, notes: e.target.value})} />
|
|
<div className="flex gap-2 pt-2">
|
|
<button type="button" onClick={() => setShowFamilyModal(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>
|
|
)}
|
|
|
|
{/* 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 type="email" className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Email" value={userForm.email} onChange={e => setUserForm({...userForm, email: e.target.value})} required />
|
|
<input type="text" className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Nome" value={userForm.name} onChange={e => setUserForm({...userForm, name: e.target.value})} />
|
|
<input type="password" className="w-full border p-2.5 rounded-lg text-slate-700" placeholder={editingUser ? "Password (lascia vuoto per mantenere)" : "Password"} value={userForm.password} onChange={e => setUserForm({...userForm, password: e.target.value})} />
|
|
<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})}>
|
|
<option value="user">Utente (Condomino)</option>
|
|
<option value="poweruser">Power User (Gestore)</option>
|
|
<option value="admin">Amministratore</option>
|
|
</select>
|
|
<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="">-- Associa a Famiglia --</option>
|
|
{families.map(f => <option key={f.id} value={f.id}>{f.name} (Int. {f.unitNumber})</option>)}
|
|
</select>
|
|
<div className="flex items-center gap-2">
|
|
<input type="checkbox" checked={userForm.receiveAlerts} onChange={e => setUserForm({...userForm, receiveAlerts: e.target.checked})} />
|
|
<span className="text-sm text-slate-600">Ricevi notifiche 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">Annulla</button>
|
|
<button type="submit" className="flex-1 bg-blue-600 text-white p-2.5 rounded-lg">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-2xl p-6 animate-in fade-in zoom-in duration-200 flex flex-col max-h-[90vh]">
|
|
<h3 className="font-bold text-lg mb-4 text-slate-800">{editingNotice ? 'Modifica Avviso' : 'Nuovo Avviso in Bacheca'}</h3>
|
|
<div className="flex-1 overflow-y-auto pr-2">
|
|
<form id="noticeForm" onSubmit={handleNoticeSubmit} className="space-y-4">
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700 font-bold" placeholder="Titolo Avviso" value={noticeForm.title} onChange={e => setNoticeForm({...noticeForm, title: e.target.value})} required />
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<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 Urgente</option>
|
|
<option value="maintenance">Manutenzione</option>
|
|
<option value="event">Evento / Assemblea</option>
|
|
</select>
|
|
<div className="flex items-center gap-2 border p-2.5 rounded-lg bg-slate-50">
|
|
<span className="text-sm text-slate-600">Stato:</span>
|
|
<div className="flex gap-2">
|
|
<button type="button" onClick={() => setNoticeForm({...noticeForm, active: true})} className={`px-2 py-1 text-xs rounded ${noticeForm.active ? 'bg-green-500 text-white' : 'bg-slate-200'}`}>Attivo</button>
|
|
<button type="button" onClick={() => setNoticeForm({...noticeForm, active: false})} className={`px-2 py-1 text-xs rounded ${!noticeForm.active ? 'bg-slate-500 text-white' : 'bg-slate-200'}`}>Bozza</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<textarea className="w-full border p-2.5 rounded-lg text-slate-700 h-32" placeholder="Testo dell'avviso..." value={noticeForm.content} onChange={e => setNoticeForm({...noticeForm, content: e.target.value})} required />
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Link esterno (es. PDF verbale)" value={noticeForm.link} onChange={e => setNoticeForm({...noticeForm, link: e.target.value})} />
|
|
|
|
{/* Targeting */}
|
|
<div className="border-t pt-4">
|
|
<label className="block text-sm font-bold text-slate-700 mb-2">Visibilità</label>
|
|
<div className="flex gap-4 mb-3">
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input type="radio" name="targetMode" checked={noticeTargetMode === 'all'} onChange={() => setNoticeTargetMode('all')} className="text-blue-600"/>
|
|
<span className="text-sm">Pubblico (Tutti)</span>
|
|
</label>
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input type="radio" name="targetMode" checked={noticeTargetMode === 'specific'} onChange={() => setNoticeTargetMode('specific')} className="text-blue-600"/>
|
|
<span className="text-sm">Seleziona Famiglie</span>
|
|
</label>
|
|
</div>
|
|
{noticeTargetMode === 'specific' && (
|
|
<div className="border rounded-lg p-2 max-h-40 overflow-y-auto bg-slate-50 grid grid-cols-2 gap-2">
|
|
{families.map(f => (
|
|
<label key={f.id} className="flex items-center gap-2 text-sm p-1 hover:bg-white rounded cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={noticeForm.targetFamilyIds.includes(f.id)}
|
|
onChange={() => toggleNoticeFamilyTarget(f.id)}
|
|
className="rounded text-blue-600"
|
|
/>
|
|
<span className="truncate">{f.name} ({f.unitNumber})</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div className="pt-4 border-t mt-4 flex gap-2">
|
|
<button onClick={() => setShowNoticeModal(false)} className="flex-1 border p-2.5 rounded-lg text-slate-600">Annulla</button>
|
|
<button type="submit" form="noticeForm" className="flex-1 bg-blue-600 text-white p-2.5 rounded-lg">Salva e Pubblica</button>
|
|
</div>
|
|
</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-lg p-6 animate-in fade-in zoom-in duration-200">
|
|
<h3 className="font-bold text-lg mb-4 text-slate-800">Configura Alert Email</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 h-24" placeholder="Corpo del messaggio..." value={alertForm.body} onChange={e => setAlertForm({...alertForm, body: e.target.value})} required />
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-500 block mb-1">Offset Giorni</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)})} required />
|
|
</div>
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-500 block mb-1">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 prox mese</option>
|
|
<option value="after_current_month">Giorni dopo inizio mese</option>
|
|
</select>
|
|
</div>
|
|
</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 Configurazione</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* SMTP Modal */}
|
|
{showSmtpModal && globalSettings && (
|
|
<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"><Server className="w-5 h-5"/> Configurazione SMTP</h3>
|
|
<form onSubmit={handleSmtpSubmit} className="space-y-4">
|
|
<div className="grid grid-cols-3 gap-3">
|
|
<div className="col-span-2"><input className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Host (es. smtp.gmail.com)" value={globalSettings.smtpConfig?.host || ''} onChange={e => setGlobalSettings({...globalSettings, smtpConfig: {...globalSettings.smtpConfig!, host: e.target.value}})} /></div>
|
|
<div><input type="number" className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Porta" value={globalSettings.smtpConfig?.port || 587} onChange={e => setGlobalSettings({...globalSettings, smtpConfig: {...globalSettings.smtpConfig!, port: parseInt(e.target.value)}})} /></div>
|
|
</div>
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Utente SMTP" value={globalSettings.smtpConfig?.user || ''} onChange={e => setGlobalSettings({...globalSettings, smtpConfig: {...globalSettings.smtpConfig!, user: e.target.value}})} />
|
|
<input type="password" className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Password SMTP" value={globalSettings.smtpConfig?.pass || ''} onChange={e => setGlobalSettings({...globalSettings, smtpConfig: {...globalSettings.smtpConfig!, pass: e.target.value}})} />
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Email Mittente (From)" value={globalSettings.smtpConfig?.fromEmail || ''} onChange={e => setGlobalSettings({...globalSettings, smtpConfig: {...globalSettings.smtpConfig!, fromEmail: e.target.value}})} />
|
|
<div className="flex items-center gap-2">
|
|
<input type="checkbox" checked={globalSettings.smtpConfig?.secure || false} onChange={e => setGlobalSettings({...globalSettings, smtpConfig: {...globalSettings.smtpConfig!, secure: e.target.checked}})} />
|
|
<span className="text-sm text-slate-600">Usa SSL/TLS (Secure)</span>
|
|
</div>
|
|
{testSmtpMsg && <div className={`text-xs p-2 rounded ${testSmtpMsg.includes('Successo') ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>{testSmtpMsg}</div>}
|
|
<div className="flex gap-2 pt-2">
|
|
<button type="button" onClick={handleSmtpTest} disabled={testingSmtp} className="flex-1 border border-slate-300 text-slate-600 p-2.5 rounded-lg hover:bg-slate-50">{testingSmtp ? 'Test...' : 'Test Connessione'}</button>
|
|
<button type="submit" className="flex-1 bg-blue-600 text-white p-2.5 rounded-lg hover:bg-blue-700">Salva</button>
|
|
</div>
|
|
<button type="button" onClick={() => setShowSmtpModal(false)} className="w-full text-center text-sm text-slate-400 mt-2 hover:text-slate-600">Chiudi</button>
|
|
</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-sm p-6 animate-in fade-in zoom-in duration-200 flex flex-col max-h-[80vh]">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="font-bold text-slate-800">Dettaglio Letture</h3>
|
|
<button onClick={() => setShowReadDetailsModal(false)}><X className="w-5 h-5 text-slate-400"/></button>
|
|
</div>
|
|
<div className="flex-1 overflow-y-auto">
|
|
{(noticeReadStats[selectedNoticeId] || []).length === 0 ? (
|
|
<p className="text-slate-500 text-sm text-center py-4">Nessuna lettura registrata.</p>
|
|
) : (
|
|
<ul className="divide-y">
|
|
{(noticeReadStats[selectedNoticeId] || []).map((read, idx) => {
|
|
const u = users.find(usr => usr.id === read.userId);
|
|
return (
|
|
<li key={idx} className="py-2 text-sm">
|
|
<div className="font-medium text-slate-700">{u?.name || u?.email || 'Utente sconosciuto'}</div>
|
|
<div className="text-xs text-slate-400">{new Date(read.readAt).toLocaleString()}</div>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|