Files
Condopay/pages/Settings.tsx
2025-12-18 21:04:20 +01:00

324 lines
26 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, Palette, Monitor,
Sun, Moon, Check
} from 'lucide-react';
const PALETTES = [
{ name: 'Blue (Standard)', color: '#2563eb' },
{ name: 'Indigo', color: '#4f46e5' },
{ name: 'Emerald', color: '#059669' },
{ name: 'Rose', color: '#e11d48' },
{ name: 'Amber', color: '#d97706' },
{ name: 'Slate', color: '#475569' },
{ name: 'Purple', color: '#9333ea' }
];
export const SettingsPage: React.FC = () => {
const currentUser = CondoService.getCurrentUser();
const isSuperAdmin = currentUser?.role === 'admin';
const isPrivileged = currentUser?.role === 'admin' || currentUser?.role === 'poweruser';
type TabType = 'profile' | 'features' | 'branding' | '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,
theme: localStorage.getItem('app-theme') || 'light'
});
const [profileSaving, setProfileSaving] = useState(false);
const [profileMsg, setProfileMsg] = useState('');
// Branding State
const [brandingForm, setBrandingForm] = useState({
appName: '',
appIcon: '',
loginBg: '',
primaryColor: '#2563eb'
});
const [activeCondo, setActiveCondo] = useState<Condo | undefined>(undefined);
const [globalSettings, setGlobalSettings] = useState<AppSettings | null>(null);
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('');
const [families, setFamilies] = useState<Family[]>([]);
const [showFamilyModal, setShowFamilyModal] = useState(false);
const [editingFamily, setEditingFamily] = useState<Family | null>(null);
const [familyForm, setFamilyForm] = useState({ name: '', unitNumber: '', stair: '', floor: '', notes: '', contactEmail: '', customMonthlyQuota: '' });
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 });
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 });
const [showSmtpModal, setShowSmtpModal] = useState(false);
const [testingSmtp, setTestingSmtp] = useState(false);
const [testSmtpMsg, setTestSmtpMsg] = useState('');
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: 'info' | 'warning' | 'maintenance' | 'event'; link: string; condoId: string; active: boolean; targetFamilyIds: string[]; }>({ title: '', content: '', type: 'info', link: '', condoId: '', active: true, targetFamilyIds: [] });
const [noticeReadStats, setNoticeReadStats] = useState<Record<string, NoticeRead[]>>({});
const [showReadDetailsModal, setShowReadDetailsModal] = useState(false);
const [selectedNoticeId, setSelectedNoticeId] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
const activeC = await CondoService.getActiveCondo();
setActiveCondo(activeC);
if (isPrivileged) {
const condoList = await CondoService.getCondos();
const gSettings = await CondoService.getSettings();
setCondos(condoList);
setGlobalSettings(gSettings);
setBrandingForm({
appName: gSettings.branding?.appName || 'CondoPay',
appIcon: gSettings.branding?.appIcon || '',
loginBg: gSettings.branding?.loginBg || '',
primaryColor: gSettings.branding?.primaryColor || '#2563eb'
});
if (activeC) {
setFamilies(await CondoService.getFamilies(activeC.id));
setUsers(await CondoService.getUsers(activeC.id));
setAlerts(await CondoService.getAlerts(activeC.id));
const allNotices = await CondoService.getNotices(activeC.id);
setNotices(allNotices);
const stats: Record<string, NoticeRead[]> = {};
for (const n of allNotices) {
try { stats[n.id] = await CondoService.getNoticeReadStatus(n.id); } catch(e) {}
}
setNoticeReadStats(stats);
}
}
} catch(e) { console.error(e); } finally { setLoading(false); }
};
fetchData();
}, [isPrivileged]);
const handleProfileSubmit = async (e: React.FormEvent) => {
e.preventDefault(); setProfileSaving(true); setProfileMsg('');
try {
await CondoService.updateProfile(profileForm);
// Apply theme
localStorage.setItem('app-theme', profileForm.theme);
if (profileForm.theme === 'dark') document.documentElement.classList.add('dark');
else document.documentElement.classList.remove('dark');
setProfileMsg('Profilo aggiornato!');
setTimeout(() => setProfileMsg(''), 3000);
} catch (e) { setProfileMsg('Errore'); } finally { setProfileSaving(false); }
};
const handleBrandingSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!globalSettings) return;
setSaving(true);
try {
const updated = { ...globalSettings, branding: brandingForm };
await CondoService.updateSettings(updated);
setGlobalSettings(updated);
setSuccessMsg('Branding aggiornato!');
window.dispatchEvent(new Event('branding-updated'));
// Apply color immediately to UI
document.documentElement.style.setProperty('--primary-color', brandingForm.primaryColor);
setTimeout(() => setSuccessMsg(''), 3000);
} catch(e) { console.error(e); } finally { setSaving(false); }
};
// --- CRUD Handlers (Simplified for brevity as logic is unchanged) ---
const handleGeneralSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!activeCondo) return; setSaving(true); try { await CondoService.saveCondo(activeCondo); setSuccessMsg('Aggiornato!'); setTimeout(() => setSuccessMsg(''), 3000); } catch (e) {} finally { setSaving(false); } };
const handleFeaturesSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!globalSettings) return; setSaving(true); try { await CondoService.updateSettings(globalSettings); setSuccessMsg('Salvato!'); window.location.reload(); } catch (e) {} finally { setSaving(false); } };
const handleSmtpSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!globalSettings) return; setSaving(true); try { await CondoService.updateSettings(globalSettings); setShowSmtpModal(false); } catch (e) {} finally { setSaving(false); } };
const handleStorageSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!globalSettings) return; setSaving(true); try { await CondoService.updateSettings(globalSettings); setSuccessMsg('Salvato!'); } catch (e) {} finally { setSaving(false); } };
const handleCondoSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { await CondoService.saveCondo({ id: editingCondo?.id || '', ...condoForm }); setCondos(await CondoService.getCondos()); setShowCondoModal(false); } catch (e) {} };
const handleDeleteCondo = async (id: string) => { if(!confirm("Eliminare?")) return; await CondoService.deleteCondo(id); setCondos(await CondoService.getCondos()); };
const handleFamilySubmit = async (e: React.FormEvent) => { e.preventDefault(); try { const quota = familyForm.customMonthlyQuota ? parseFloat(familyForm.customMonthlyQuota) : undefined; if (editingFamily) { await CondoService.updateFamily({ ...editingFamily, ...familyForm, customMonthlyQuota: quota }); } else { await CondoService.addFamily({ ...familyForm, customMonthlyQuota: quota }); } setFamilies(await CondoService.getFamilies(activeCondo?.id)); setShowFamilyModal(false); } catch (e) {} };
const handleDeleteFamily = async (id: string) => { if(!confirm("Eliminare?")) return; await CondoService.deleteFamily(id); setFamilies(families.filter(f => f.id !== id)); };
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) {} };
const handleDeleteUser = async (id: string) => { if(!confirm("Eliminare?")) return; await CondoService.deleteUser(id); setUsers(users.filter(u => u.id !== id)); };
const handleNoticeSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { const now = new Date(); const sqlDate = now.toISOString().slice(0, 19).replace('T', ' '); await CondoService.saveNotice({ id: editingNotice?.id || '', ...noticeForm, targetFamilyIds: noticeTargetMode === 'all' ? [] : noticeForm.targetFamilyIds, date: editingNotice ? editingNotice.date : sqlDate }); setNotices(await CondoService.getNotices(activeCondo?.id)); setShowNoticeModal(false); } catch (e) {} };
const handleDeleteNotice = async (id: string) => { if(!confirm("Eliminare?")) return; await CondoService.deleteNotice(id); setNotices(notices.filter(n => n.id !== id)); };
const handleAlertSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { await CondoService.saveAlert({ id: editingAlert?.id || '', ...alertForm } as any); setAlerts(await CondoService.getAlerts(activeCondo?.id)); setShowAlertModal(false); } catch (e) {} };
const handleDeleteAlert = async (id: string) => { if(!confirm("Eliminare?")) return; await CondoService.deleteAlert(id); setAlerts(alerts.filter(a => a.id !== id)); };
const tabs: {id: TabType, label: string, icon: React.ReactNode}[] = [
{ id: 'profile', label: 'Profilo & Tema', icon: <UserIcon className="w-4 h-4"/> },
];
if (isSuperAdmin) {
tabs.push({ id: 'features', label: 'Funzionalità', icon: <LayoutGrid className="w-4 h-4"/> });
tabs.push({ id: 'branding', label: 'Personalizzazione', icon: <Palette className="w-4 h-4"/> });
}
if (isPrivileged) {
tabs.push({ id: 'general', label: 'Condominio', icon: <Building className="w-4 h-4"/> });
if (globalSettings?.features.documents) tabs.push({ id: 'storage', label: 'Storage', icon: <HardDrive className="w-4 h-4"/> });
if (globalSettings?.features.multiCondo) tabs.push({ id: 'condos', label: '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', 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 dark:text-slate-100">Impostazioni</h2>
<p className="text-slate-500 dark:text-slate-400">{activeCondo ? `Gestione: ${activeCondo.name}` : 'Pannello di Controllo'}</p>
</div>
<div className="flex flex-col md:flex-row gap-6">
<div className="md:w-64 flex-shrink-0">
<div className="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden sticky top-4">
<div className="flex md:flex-col overflow-x-auto no-scrollbar 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 ${activeTab === tab.id ? 'bg-primary/10 text-primary shadow-sm' : 'text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-700'}`}>
{tab.icon} <span>{tab.label}</span>
</button>
))}
</div>
</div>
</div>
<div className="flex-1 min-w-0">
{activeTab === 'profile' && (
<div className="animate-fade-in bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 p-6">
<h3 className="text-lg font-bold text-slate-800 dark:text-slate-100 mb-6 flex items-center gap-2"><UserIcon className="w-5 h-5 text-primary" /> Profilo & Preferenze</h3>
<form onSubmit={handleProfileSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div><label className="text-sm font-medium text-slate-700 dark:text-slate-300">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 dark:bg-slate-700 dark:text-slate-100"/></div>
<div><label className="text-sm font-medium text-slate-700 dark:text-slate-300">Tema App</label>
<div className="flex gap-2 mt-1">
<button type="button" onClick={() => setProfileForm({...profileForm, theme: 'light'})} className={`flex-1 py-2 px-3 rounded-lg border flex items-center justify-center gap-2 transition-all ${profileForm.theme === 'light' ? 'bg-white border-primary text-primary ring-1 ring-primary' : 'bg-slate-50 dark:bg-slate-700 border-slate-200 dark:border-slate-600 text-slate-500'}`}><Sun className="w-4 h-4"/> Chiaro</button>
<button type="button" onClick={() => setProfileForm({...profileForm, theme: 'dark'})} className={`flex-1 py-2 px-3 rounded-lg border flex items-center justify-center gap-2 transition-all ${profileForm.theme === 'dark' ? 'bg-slate-800 border-primary text-primary ring-1 ring-primary' : 'bg-slate-50 dark:bg-slate-700 border-slate-200 dark:border-slate-600 text-slate-500'}`}><Moon className="w-4 h-4"/> Scuro</button>
</div>
</div>
<div><label className="text-sm font-medium text-slate-700 dark:text-slate-300">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 dark:bg-slate-700 dark:text-slate-100"/></div>
<div><label className="text-sm font-medium text-slate-700 dark:text-slate-300">Password</label><input type="password" placeholder="Nuova password" value={profileForm.password} onChange={e => setProfileForm({...profileForm, password: e.target.value})} className="w-full border p-2.5 rounded-lg text-slate-700 dark:bg-slate-700 dark:text-slate-100"/></div>
</div>
<button type="submit" disabled={profileSaving} className="bg-primary text-white px-6 py-2.5 rounded-lg font-bold hover:opacity-90 flex gap-2"><Save className="w-4 h-4" /> Aggiorna Impostazioni</button>
{profileMsg && <p className="text-sm text-green-600 font-medium">{profileMsg}</p>}
</form>
</div>
)}
{activeTab === 'branding' && isSuperAdmin && (
<div className="animate-fade-in bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 p-6">
<h3 className="text-lg font-bold text-slate-800 dark:text-slate-100 mb-6 flex items-center gap-2"><Palette className="w-5 h-5 text-primary" /> Personalizzazione Piattaforma</h3>
<form onSubmit={handleBrandingSubmit} className="space-y-6">
<div className="space-y-4">
<div>
<label className="text-sm font-bold text-slate-700 dark:text-slate-300 mb-1 block">Nome Applicazione</label>
<input type="text" className="w-full border p-2.5 rounded-lg dark:bg-slate-700 dark:text-slate-100" value={brandingForm.appName} onChange={e => setBrandingForm({...brandingForm, appName: e.target.value})} placeholder="Es: MyCondo Manager" />
</div>
<div>
<label className="text-sm font-bold text-slate-700 dark:text-slate-300 mb-1 block">Icona App (Link URL Pubblico)</label>
<input type="url" className="w-full border p-2.5 rounded-lg dark:bg-slate-700 dark:text-slate-100" value={brandingForm.appIcon} onChange={e => setBrandingForm({...brandingForm, appIcon: e.target.value})} placeholder="https://image.com/logo.png" />
</div>
<div>
<label className="text-sm font-bold text-slate-700 dark:text-slate-300 mb-1 block">Sfondo Pagina Login (Link URL Pubblico)</label>
<input type="url" className="w-full border p-2.5 rounded-lg dark:bg-slate-700 dark:text-slate-100" value={brandingForm.loginBg} onChange={e => setBrandingForm({...brandingForm, loginBg: e.target.value})} placeholder="https://image.com/bg.jpg" />
</div>
<div>
<label className="text-sm font-bold text-slate-700 dark:text-slate-300 mb-3 block">Colore Primario / Palette</label>
<div className="grid grid-cols-4 sm:grid-cols-7 gap-3">
{PALETTES.map(p => (
<button
key={p.color}
type="button"
onClick={() => setBrandingForm({...brandingForm, primaryColor: p.color})}
className={`h-12 rounded-lg border-2 flex items-center justify-center transition-all ${brandingForm.primaryColor === p.color ? 'border-primary ring-2 ring-primary/20 scale-105' : 'border-transparent'}`}
style={{ backgroundColor: p.color }}
title={p.name}
>
{brandingForm.primaryColor === p.color && <Check className="w-6 h-6 text-white"/>}
</button>
))}
<div className="col-span-full flex items-center gap-3 mt-2">
<input type="color" value={brandingForm.primaryColor} onChange={e => setBrandingForm({...brandingForm, primaryColor: e.target.value})} className="h-10 w-10 border-none bg-transparent cursor-pointer" />
<span className="text-sm text-slate-500 font-medium">Scegli un colore personalizzato ({brandingForm.primaryColor})</span>
</div>
</div>
</div>
</div>
<div className="pt-4 border-t dark:border-slate-700">
<button type="submit" disabled={saving} className="bg-primary text-white px-8 py-3 rounded-lg font-bold shadow-lg hover:opacity-90 flex gap-2 items-center"><Save className="w-5 h-5"/> Salva Personalizzazioni</button>
{successMsg && <p className="text-sm text-green-600 mt-2 font-bold">{successMsg}</p>}
</div>
</form>
</div>
)}
{activeTab === 'features' && isSuperAdmin && globalSettings && (
<div className="animate-fade-in bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-bold text-slate-800 dark:text-slate-100 flex items-center gap-2"><LayoutGrid className="w-5 h-5 text-primary" /> Funzionalità Piattaforma</h3>
</div>
<form onSubmit={handleFeaturesSubmit} className="space-y-6">
<div className="space-y-4">
{Object.keys(globalSettings.features).map((key) => (
<div key={key} className="flex items-center justify-between p-4 bg-slate-50 dark:bg-slate-700/50 rounded-xl border border-slate-100 dark:border-slate-700">
<div><p className="font-bold text-slate-800 dark:text-slate-100 capitalize">{key.replace(/([A-Z])/g, ' $1')}</p></div>
<button type="button" onClick={() => {
const newFeats = { ...globalSettings.features, [key]: !globalSettings.features[key as keyof AppSettings['features']] };
setGlobalSettings({ ...globalSettings, features: newFeats });
}} className={`${globalSettings.features[key as keyof AppSettings['features']] ? 'bg-green-500' : 'bg-slate-300 dark:bg-slate-600'} relative inline-flex h-6 w-11 items-center rounded-full transition-colors`}><span className={`${globalSettings.features[key as keyof AppSettings['features']] ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}/></button>
</div>
))}
</div>
<button type="submit" className="bg-primary text-white px-6 py-2.5 rounded-lg font-bold"><Save className="w-4 h-4 inline mr-2"/> Salva Funzionalità</button>
</form>
</div>
)}
{activeTab === 'general' && isPrivileged && activeCondo && (
<div className="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 p-6">
<h3 className="text-lg font-bold text-slate-800 dark:text-slate-100 mb-4 flex items-center gap-2"><Building className="w-5 h-5 text-primary" /> Dati Condominio</h3>
<form onSubmit={handleGeneralSubmit} className="space-y-5">
<input className="w-full border p-2.5 rounded-lg dark:bg-slate-700 dark:text-slate-100" value={activeCondo.name} onChange={e => setActiveCondo({...activeCondo, name: e.target.value})} placeholder="Nome" required />
<div className="grid grid-cols-2 gap-4">
<input className="w-full border p-2.5 rounded-lg dark:bg-slate-700 dark:text-slate-100" value={activeCondo.address || ''} onChange={e => setActiveCondo({...activeCondo, address: e.target.value})} placeholder="Indirizzo" required />
<input className="w-full border p-2.5 rounded-lg dark:bg-slate-700 dark:text-slate-100" value={activeCondo.city || ''} onChange={e => setActiveCondo({...activeCondo, city: e.target.value})} placeholder="Città" required />
</div>
<div className="flex gap-4">
<div className="flex-1">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300">Quota Mensile </label>
<input type="number" className="w-full border p-2.5 rounded-lg dark:bg-slate-700 dark:text-slate-100" value={activeCondo.defaultMonthlyQuota} onChange={e => setActiveCondo({...activeCondo, defaultMonthlyQuota: parseFloat(e.target.value)})} required />
</div>
<div className="flex-1">
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300">Scadenza Giorno</label>
<input type="number" className="w-full border p-2.5 rounded-lg dark:bg-slate-700 dark:text-slate-100" value={activeCondo.dueDay || 10} onChange={e => setActiveCondo({...activeCondo, dueDay: parseInt(e.target.value)})} required />
</div>
</div>
<button type="submit" className="bg-primary text-white px-6 py-2.5 rounded-lg font-bold">Salva Modifiche</button>
</form>
</div>
)}
</div>
</div>
</div>
);
};