Introduce a new feature to test SMTP configuration directly from the settings page. This involves adding a new API endpoint and corresponding UI elements to trigger and display the results of an SMTP test. Additionally, this commit refactors the Docker setup by consolidating Dockerfiles and removing unnecessary configuration files. The goal is to streamline the build process and reduce image size and complexity.
1403 lines
82 KiB
TypeScript
1403 lines
82 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 } 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' | '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
|
|
});
|
|
|
|
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();
|
|
|
|
setCondos(condoList);
|
|
setActiveCondo(activeC);
|
|
setGlobalSettings(gSettings);
|
|
|
|
// Fetch condo-specific data ONLY if there is an active condo
|
|
if (activeC) {
|
|
const [fams, usrs, alrts, allNotices] = await Promise.all([
|
|
CondoService.getFamilies(activeC.id),
|
|
CondoService.getUsers(activeC.id),
|
|
CondoService.getAlerts(activeC.id),
|
|
CondoService.getNotices(activeC.id)
|
|
]);
|
|
|
|
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 {
|
|
setFamilies([]);
|
|
setUsers([]);
|
|
setAlerts([]);
|
|
setNotices([]);
|
|
}
|
|
|
|
} else {
|
|
const activeC = await CondoService.getActiveCondo();
|
|
setActiveCondo(activeC);
|
|
}
|
|
} catch(e) {
|
|
console.error(e);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchData();
|
|
}, [isPrivileged]);
|
|
|
|
// --- 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 handleFeaturesSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!globalSettings) return;
|
|
setSaving(true);
|
|
try {
|
|
await CondoService.updateSettings(globalSettings);
|
|
setSuccessMsg('Funzionalità salvate!');
|
|
setTimeout(() => setSuccessMsg(''), 3000);
|
|
window.location.reload(); // Refresh to apply changes to layout
|
|
} 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 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: '', streetNumber: '', city: '', province: '', zipCode: '', notes: '', paypalClientId: '', defaultMonthlyQuota: 100 });
|
|
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
|
|
});
|
|
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
|
|
};
|
|
|
|
const savedCondo = await CondoService.saveCondo(payload);
|
|
const list = await CondoService.getCondos();
|
|
setCondos(list);
|
|
|
|
if (activeCondo?.id === savedCondo.id) {
|
|
setActiveCondo(savedCondo);
|
|
}
|
|
|
|
// Auto-select if it's the first one or none selected
|
|
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); }
|
|
};
|
|
|
|
// --- Family Handlers ---
|
|
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 {
|
|
// Handle parsing safely
|
|
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. Controlla i permessi."}`);
|
|
}
|
|
};
|
|
|
|
// --- 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);
|
|
}
|
|
// Refresh user list for active condo
|
|
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));
|
|
};
|
|
|
|
// --- Notice Handlers ---
|
|
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);
|
|
// Refresh notices for active condo
|
|
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);
|
|
};
|
|
|
|
// --- Alert Handlers ---
|
|
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! };
|
|
// Save alert with current condoId
|
|
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';
|
|
|
|
// Helpers for Toggle Feature
|
|
const toggleFeature = (key: keyof AppSettings['features']) => {
|
|
if (!globalSettings) return;
|
|
setGlobalSettings({
|
|
...globalSettings,
|
|
features: {
|
|
...globalSettings.features,
|
|
[key]: !globalSettings.features[key]
|
|
}
|
|
});
|
|
};
|
|
|
|
// --- TABS CONFIG ---
|
|
const tabs: {id: TabType, label: string, icon: React.ReactNode}[] = [
|
|
{ id: 'profile', label: 'Profilo', icon: <UserIcon className="w-4 h-4"/> },
|
|
];
|
|
|
|
// "Funzionalità" visible ONLY to SuperAdmin
|
|
if (isSuperAdmin) {
|
|
tabs.push({ id: 'features', label: 'Funzionalità', icon: <LayoutGrid className="w-4 h-4"/> });
|
|
}
|
|
|
|
// Other tabs visible to Privileged (Admin + PowerUser)
|
|
if (isPrivileged) {
|
|
tabs.push(
|
|
{ id: 'general', label: 'Condominio', icon: <Building 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-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>
|
|
)}
|
|
|
|
{/* Features Tab (SUPER ADMIN ONLY) */}
|
|
{isSuperAdmin && activeTab === 'features' && globalSettings && (
|
|
<div className="animate-fade-in bg-white rounded-xl shadow-sm border border-slate-200 p-6 max-w-3xl">
|
|
<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">
|
|
{/* Multi Condo */}
|
|
<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. Se disattivo, il sistema gestirà un solo condominio.</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>
|
|
|
|
{/* Tickets */}
|
|
<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 e richieste (Segnalazioni).</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>
|
|
|
|
{/* PayPal */}
|
|
<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 le rate tramite PayPal.</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>
|
|
|
|
{/* Notices */}
|
|
<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 per comunicazioni ai condomini.</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>
|
|
|
|
{/* Reports */}
|
|
<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 e tabelle dettagliate sui pagamenti per amministratori.</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>
|
|
|
|
<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>
|
|
)}
|
|
|
|
{/* 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 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 />
|
|
|
|
<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>
|
|
|
|
{/* PayPal Configuration for Single Condo Mode */}
|
|
{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
|
|
className="w-full border p-2 rounded text-slate-700 text-sm"
|
|
placeholder="Es: Afg... (Ottienilo da developer.paypal.com)"
|
|
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>
|
|
<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 */}
|
|
{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 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>
|
|
<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">
|
|
{!activeCondo ? (
|
|
<div className="p-8 text-center bg-slate-50 rounded-xl border border-dashed border-slate-300">
|
|
<p className="text-slate-500">Seleziona o crea un condominio per gestire le famiglie.</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<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">Dettagli</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 text-xs">
|
|
<div className="space-y-0.5">
|
|
<span className="block text-slate-700 font-medium">Int: {family.unitNumber || '-'}</span>
|
|
{(family.stair || family.floor) && (
|
|
<span className="block text-slate-500">
|
|
{family.stair ? `Scala: ${family.stair} ` : ''}
|
|
{family.floor ? `Piano: ${family.floor}` : ''}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</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 */}
|
|
{isPrivileged && 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 */}
|
|
{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 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 => {
|
|
const isTargeted = notice.targetFamilyIds && notice.targetFamilyIds.length > 0;
|
|
return (
|
|
<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 flex items-center gap-2">
|
|
{notice.title}
|
|
{isTargeted && (
|
|
<span className="text-[10px] bg-slate-100 text-slate-500 border border-slate-200 px-2 py-0.5 rounded-full uppercase flex items-center gap-1">
|
|
<Users className="w-3 h-3" /> Privato
|
|
</span>
|
|
)}
|
|
</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>}
|
|
{isTargeted && (
|
|
<p className="text-[10px] text-slate-400 mt-2">
|
|
Visibile a: {notice.targetFamilyIds!.length} famiglie
|
|
</p>
|
|
)}
|
|
</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 */}
|
|
{isPrivileged && activeTab === 'alerts' && (
|
|
<div className="space-y-6 animate-fade-in">
|
|
|
|
{/* SMTP Config Button */}
|
|
<div className="flex justify-end">
|
|
<button
|
|
onClick={() => setShowSmtpModal(true)}
|
|
className="flex items-center gap-2 bg-slate-100 hover:bg-slate-200 text-slate-700 px-4 py-2 rounded-lg font-medium border border-slate-200 transition-colors"
|
|
>
|
|
<Mail className="w-4 h-4" />
|
|
Impostazioni SMTP
|
|
</button>
|
|
</div>
|
|
|
|
<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</h3><p className="text-sm text-blue-600">Configura email automatiche per scadenze.</p></div>
|
|
<button onClick={openAddAlertModal} 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" /> Nuovo Avviso</button>
|
|
</div>
|
|
<div className="grid gap-4">
|
|
{alerts.map(alert => (
|
|
<div key={alert.id} className="bg-white p-5 rounded-xl border border-slate-200 shadow-sm relative">
|
|
<h4 className="font-bold text-slate-800">{alert.subject}</h4>
|
|
<p className="text-sm text-slate-600 mt-1">{alert.body}</p>
|
|
<div className="mt-3 flex gap-2 text-xs font-bold text-slate-500 uppercase">
|
|
<span className="bg-slate-100 px-2 py-1 rounded">Offset: {alert.daysOffset} giorni ({alert.offsetType})</span>
|
|
<span className="bg-slate-100 px-2 py-1 rounded">Ore: {alert.sendHour}:00</span>
|
|
<span className={`px-2 py-1 rounded ${alert.active ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>{alert.active ? 'Attivo' : 'Inattivo'}</span>
|
|
</div>
|
|
<div className="mt-4 pt-3 border-t border-slate-100 flex justify-end gap-2">
|
|
<button onClick={() => openEditAlertModal(alert)} className="text-blue-600 text-sm font-medium">Modifica</button>
|
|
<button onClick={() => handleDeleteAlert(alert.id)} className="text-red-600 text-sm font-medium">Elimina</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{alerts.length === 0 && <div className="text-center p-8 text-slate-400">Nessun alert configurato.</div>}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ALERT MODAL (Existing) */}
|
|
{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>
|
|
)}
|
|
|
|
{/* SMTP CONFIG MODAL */}
|
|
{showSmtpModal && (
|
|
<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">
|
|
<div className="flex justify-between items-center mb-6">
|
|
<h3 className="text-lg font-bold text-slate-800 flex items-center gap-2">
|
|
<Server className="w-5 h-5 text-blue-600"/> Configurazione SMTP
|
|
</h3>
|
|
<button onClick={() => setShowSmtpModal(false)} className="text-slate-400 hover:text-slate-600"><X className="w-6 h-6"/></button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSmtpSubmit} className="space-y-5">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Host</label>
|
|
<input type="text" value={globalSettings?.smtpConfig?.host || ''} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), host: e.target.value} as any} : null)} className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="smtp.gmail.com"/>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Porta</label>
|
|
<input type="number" value={globalSettings?.smtpConfig?.port || 0} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), port: parseInt(e.target.value)} as any} : null)} className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="587"/>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Utente</label>
|
|
<input type="text" value={globalSettings?.smtpConfig?.user || ''} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), user: e.target.value} as any} : null)} className="w-full border p-2.5 rounded-lg text-slate-700"/>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Password</label>
|
|
<input type="password" value={globalSettings?.smtpConfig?.pass || ''} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), pass: e.target.value} as any} : null)} className="w-full border p-2.5 rounded-lg text-slate-700"/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Email Mittente</label>
|
|
<input type="email" value={globalSettings?.smtpConfig?.fromEmail || ''} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), fromEmail: e.target.value} as any} : null)} className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="no-reply@condominio.it"/>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<input type="checkbox" checked={globalSettings?.smtpConfig?.secure || false} onChange={(e) => setGlobalSettings(prev => prev ? {...prev, smtpConfig: {...(prev.smtpConfig || {}), secure: e.target.checked} as any} : null)} className="w-4 h-4 text-blue-600"/>
|
|
<label className="text-sm text-slate-700 font-medium">Usa SSL/TLS (Secure)</label>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={handleSmtpTest}
|
|
disabled={testingSmtp}
|
|
className="text-xs font-bold bg-amber-100 text-amber-700 px-3 py-1.5 rounded hover:bg-amber-200 transition-colors flex items-center gap-1 disabled:opacity-50"
|
|
>
|
|
{testingSmtp ? <span className="animate-pulse">Test...</span> : <><Send className="w-3 h-3"/> Test Configurazione</>}
|
|
</button>
|
|
</div>
|
|
{testSmtpMsg && <p className={`text-xs font-medium text-center ${testSmtpMsg.startsWith('Errore') ? 'text-red-500' : 'text-green-600'}`}>{testSmtpMsg}</p>}
|
|
|
|
<div className="pt-2 flex justify-between items-center border-t border-slate-100 mt-2">
|
|
<span className="text-green-600 text-sm font-medium">{successMsg}</span>
|
|
<div className="flex gap-2">
|
|
<button type="button" onClick={() => setShowSmtpModal(false)} className="px-4 py-2 text-slate-600 border rounded-lg hover:bg-slate-50">Chiudi</button>
|
|
<button type="submit" className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 font-medium">Salva</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* NOTICE MODAL (Updated with Target Selector) */}
|
|
{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 flex flex-col max-h-[90vh]">
|
|
<h3 className="font-bold text-lg mb-4 text-slate-800 flex-shrink-0">{editingNotice ? 'Modifica Avviso' : 'Nuovo Avviso'}</h3>
|
|
<div className="overflow-y-auto flex-1 pr-2">
|
|
<form id="noticeForm" onSubmit={handleNoticeSubmit} className="space-y-4">
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Titolo" value={noticeForm.title} onChange={e => setNoticeForm({...noticeForm, title: e.target.value})} required />
|
|
<textarea className="w-full border p-2.5 rounded-lg text-slate-700 h-24" placeholder="Contenuto avviso..." value={noticeForm.content} onChange={e => setNoticeForm({...noticeForm, content: e.target.value})} required />
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="text-xs text-slate-500 font-bold uppercase mb-1 block">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">Info</option>
|
|
<option value="warning">Avviso</option>
|
|
<option value="maintenance">Manutenzione</option>
|
|
<option value="event">Evento</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs text-slate-500 font-bold uppercase mb-1 block">Stato</label>
|
|
<select className="w-full border p-2.5 rounded-lg text-slate-700 bg-white" value={noticeForm.active ? 'true' : 'false'} onChange={e => setNoticeForm({...noticeForm, active: e.target.value === 'true'})}>
|
|
<option value="true">Attivo</option>
|
|
<option value="false">Bozza / Nascosto</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-xs text-slate-500 font-bold uppercase mb-2 block">A chi è rivolto?</label>
|
|
<div className="flex gap-4 mb-3">
|
|
<label className="flex items-center gap-2 text-sm text-slate-700 cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
name="targetMode"
|
|
value="all"
|
|
checked={noticeTargetMode === 'all'}
|
|
onChange={() => setNoticeTargetMode('all')}
|
|
className="w-4 h-4 text-blue-600"
|
|
/>
|
|
Tutti i condomini
|
|
</label>
|
|
<label className="flex items-center gap-2 text-sm text-slate-700 cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
name="targetMode"
|
|
value="specific"
|
|
checked={noticeTargetMode === 'specific'}
|
|
onChange={() => setNoticeTargetMode('specific')}
|
|
className="w-4 h-4 text-blue-600"
|
|
/>
|
|
Seleziona Famiglie
|
|
</label>
|
|
</div>
|
|
|
|
{/* Family Selector */}
|
|
{noticeTargetMode === 'specific' && (
|
|
<div className="border border-slate-200 rounded-lg p-2 max-h-40 overflow-y-auto bg-slate-50">
|
|
{families.length === 0 ? (
|
|
<p className="text-xs text-slate-400 text-center py-2">Nessuna famiglia disponibile.</p>
|
|
) : (
|
|
<div className="space-y-1">
|
|
{families.map(fam => (
|
|
<label key={fam.id} className="flex items-center gap-2 p-1.5 hover:bg-white rounded cursor-pointer transition-colors">
|
|
<input
|
|
type="checkbox"
|
|
checked={noticeForm.targetFamilyIds.includes(fam.id)}
|
|
onChange={() => toggleNoticeFamilyTarget(fam.id)}
|
|
className="w-4 h-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
<span className="text-sm text-slate-700">{fam.name} <span className="text-slate-400 text-xs">(Int. {fam.unitNumber})</span></span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-xs text-slate-500 font-bold uppercase mb-1 block">Link Esterno (Opzionale)</label>
|
|
<input className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="https://..." value={noticeForm.link} onChange={e => setNoticeForm({...noticeForm, link: e.target.value})} />
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div className="flex gap-2 pt-4 border-t border-slate-100 flex-shrink-0">
|
|
<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" form="noticeForm" className="flex-1 bg-blue-600 text-white p-2.5 rounded-lg hover:bg-blue-700">Salva</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* READ DETAILS MODAL (Existing) */}
|
|
{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 (Existing) */}
|
|
{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>
|
|
)}
|
|
|
|
{/* CONDO MODAL (UPDATED) */}
|
|
{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">
|
|
|
|
{/* Name */}
|
|
<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>
|
|
|
|
{/* Address Line 1 */}
|
|
<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>
|
|
|
|
{/* Address Line 2 */}
|
|
<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>
|
|
|
|
{/* PayPal Integration Section */}
|
|
{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
|
|
className="w-full border p-2 rounded text-slate-700 text-sm"
|
|
placeholder="Es: Afg... (Ottienilo da developer.paypal.com)"
|
|
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>
|
|
)}
|
|
|
|
{/* Notes */}
|
|
<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>
|
|
|
|
{/* Quota */}
|
|
<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>
|
|
)}
|
|
|
|
{/* FAMILY MODAL (Existing) */}
|
|
{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-lg 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">
|
|
{/* Name & Email */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="md:col-span-2">
|
|
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Nome Famiglia</label>
|
|
<input type="text" required value={familyForm.name} onChange={(e) => setFamilyForm({...familyForm, name: e.target.value})} className="w-full border rounded-lg p-2.5 text-slate-700" placeholder="Es. Rossi"/>
|
|
</div>
|
|
<div className="md:col-span-2">
|
|
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Email Contatto (Obbligatoria)</label>
|
|
<input type="email" required value={familyForm.contactEmail} onChange={(e) => setFamilyForm({...familyForm, contactEmail: e.target.value})} className="w-full border rounded-lg p-2.5 text-slate-700" placeholder="email@esempio.it"/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Location Details */}
|
|
<div className="grid grid-cols-3 gap-3">
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Interno</label>
|
|
<input type="text" value={familyForm.unitNumber} onChange={(e) => setFamilyForm({...familyForm, unitNumber: e.target.value})} className="w-full border rounded-lg p-2.5 text-slate-700" />
|
|
</div>
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Scala</label>
|
|
<input type="text" value={familyForm.stair} onChange={(e) => setFamilyForm({...familyForm, stair: e.target.value})} className="w-full border rounded-lg p-2.5 text-slate-700" />
|
|
</div>
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Piano</label>
|
|
<input type="text" value={familyForm.floor} onChange={(e) => setFamilyForm({...familyForm, floor: e.target.value})} className="w-full border rounded-lg p-2.5 text-slate-700" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Notes */}
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-500 uppercase mb-1 block">Note</label>
|
|
<textarea value={familyForm.notes} onChange={(e) => setFamilyForm({...familyForm, notes: e.target.value})} className="w-full border rounded-lg p-2.5 text-slate-700 h-16" placeholder="Note opzionali..."></textarea>
|
|
</div>
|
|
|
|
<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-2.5 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>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|