feat: Add email configuration and alert system
Introduces SMTP configuration settings and alert definitions to enable automated email notifications. This includes new types for `SmtpConfig` and `AlertDefinition`, and integrates these into the settings page and mock database. Adds styling for select elements and scrollbar hiding in the main HTML. Updates mock database logic to potentially support local development without a backend.
This commit is contained in:
@@ -1,17 +1,33 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { CondoService } from '../services/mockDb';
|
||||
import { AppSettings, Family, User } from '../types';
|
||||
import { Save, Building, Coins, Plus, Pencil, Trash2, X, CalendarCheck, AlertTriangle, UserCog, Mail, Phone, Lock, Shield, User as UserIcon } from 'lucide-react';
|
||||
import { AppSettings, Family, User, AlertDefinition } from '../types';
|
||||
import { Save, Building, Coins, Plus, Pencil, Trash2, X, CalendarCheck, AlertTriangle, User as UserIcon, Mail, Server, Bell, Clock, FileText, Send, Lock } from 'lucide-react';
|
||||
|
||||
export const SettingsPage: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<'general' | 'families' | 'users'>('general');
|
||||
const currentUser = CondoService.getCurrentUser();
|
||||
const isAdmin = currentUser?.role === 'admin';
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'profile' | 'general' | 'families' | 'users' | 'smtp' | 'alerts'>(isAdmin ? 'general' : 'profile');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Profile State
|
||||
const [profileForm, setProfileForm] = useState({
|
||||
name: currentUser?.name || '',
|
||||
phone: currentUser?.phone || '',
|
||||
password: '',
|
||||
receiveAlerts: currentUser?.receiveAlerts ?? true
|
||||
});
|
||||
const [profileSaving, setProfileSaving] = useState(false);
|
||||
const [profileMsg, setProfileMsg] = useState('');
|
||||
|
||||
// General Settings State
|
||||
const [settings, setSettings] = useState<AppSettings>({
|
||||
defaultMonthlyQuota: 0,
|
||||
condoName: '',
|
||||
currentYear: new Date().getFullYear()
|
||||
currentYear: new Date().getFullYear(),
|
||||
smtpConfig: {
|
||||
host: '', port: 587, user: '', pass: '', secure: false, fromEmail: ''
|
||||
}
|
||||
});
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [successMsg, setSuccessMsg] = useState('');
|
||||
@@ -32,27 +48,69 @@ export const SettingsPage: React.FC = () => {
|
||||
password: '',
|
||||
phone: '',
|
||||
role: 'user',
|
||||
familyId: ''
|
||||
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: 0,
|
||||
offsetType: 'before_next_month',
|
||||
sendHour: 9,
|
||||
active: true
|
||||
});
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [s, f, u] = await Promise.all([
|
||||
CondoService.getSettings(),
|
||||
CondoService.getFamilies(),
|
||||
CondoService.getUsers()
|
||||
]);
|
||||
setSettings(s);
|
||||
setFamilies(f);
|
||||
setUsers(u);
|
||||
if (isAdmin) {
|
||||
const [s, f, u, a] = await Promise.all([
|
||||
CondoService.getSettings(),
|
||||
CondoService.getFamilies(),
|
||||
CondoService.getUsers(),
|
||||
CondoService.getAlerts()
|
||||
]);
|
||||
setSettings(s);
|
||||
setFamilies(f);
|
||||
setUsers(u);
|
||||
setAlerts(a);
|
||||
} else {
|
||||
// If not admin, we might only need limited data, or nothing if we rely on session
|
||||
// For now, nothing extra to fetch for the profile as it's in session
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
}, [isAdmin]);
|
||||
|
||||
// --- Profile Handlers ---
|
||||
const handleProfileSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setProfileSaving(true);
|
||||
setProfileMsg('');
|
||||
try {
|
||||
await CondoService.updateProfile(profileForm);
|
||||
setProfileMsg('Profilo aggiornato con successo!');
|
||||
setTimeout(() => setProfileMsg(''), 3000);
|
||||
setProfileForm(prev => ({ ...prev, password: '' })); // clear password
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setProfileMsg('Errore aggiornamento profilo');
|
||||
} finally {
|
||||
setProfileSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// --- Settings Handlers ---
|
||||
|
||||
@@ -145,7 +203,7 @@ export const SettingsPage: React.FC = () => {
|
||||
|
||||
const openAddUserModal = () => {
|
||||
setEditingUser(null);
|
||||
setUserForm({ name: '', email: '', password: '', phone: '', role: 'user', familyId: '' });
|
||||
setUserForm({ name: '', email: '', password: '', phone: '', role: 'user', familyId: '', receiveAlerts: true });
|
||||
setShowUserModal(true);
|
||||
};
|
||||
|
||||
@@ -157,7 +215,8 @@ export const SettingsPage: React.FC = () => {
|
||||
password: '',
|
||||
phone: user.phone || '',
|
||||
role: user.role || 'user',
|
||||
familyId: user.familyId || ''
|
||||
familyId: user.familyId || '',
|
||||
receiveAlerts: user.receiveAlerts ?? true
|
||||
});
|
||||
setShowUserModal(true);
|
||||
};
|
||||
@@ -191,9 +250,55 @@ export const SettingsPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const getFamilyName = (id: string | null | undefined) => {
|
||||
if (!id) return '-';
|
||||
return families.find(f => f.id === id)?.name || 'Sconosciuta';
|
||||
// --- 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 handleDeleteAlert = async (id: string) => {
|
||||
if(!window.confirm("Eliminare questo avviso automatico?")) return;
|
||||
try {
|
||||
await CondoService.deleteAlert(id);
|
||||
setAlerts(alerts.filter(a => a.id !== id));
|
||||
} catch (e) { console.error(e); }
|
||||
};
|
||||
|
||||
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);
|
||||
if (editingAlert) {
|
||||
setAlerts(alerts.map(a => a.id === saved.id ? saved : a));
|
||||
} else {
|
||||
setAlerts([...alerts, saved]);
|
||||
}
|
||||
setShowAlertModal(false);
|
||||
} catch (e) { console.error(e); }
|
||||
};
|
||||
|
||||
if (loading) return <div className="p-8 text-center text-slate-400">Caricamento...</div>;
|
||||
@@ -202,41 +307,153 @@ export const SettingsPage: React.FC = () => {
|
||||
<div className="max-w-4xl 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">Gestisci configurazione, anagrafica e utenti.</p>
|
||||
<p className="text-slate-500 text-sm md:text-base">Gestisci configurazione, anagrafica, utenti e comunicazioni.</p>
|
||||
</div>
|
||||
|
||||
{/* Tabs - Scrollable on mobile */}
|
||||
<div className="flex border-b border-slate-200 overflow-x-auto no-scrollbar pb-1">
|
||||
|
||||
{/* Profile Tab (Always Visible) */}
|
||||
<button
|
||||
onClick={() => setActiveTab('general')}
|
||||
onClick={() => setActiveTab('profile')}
|
||||
className={`px-4 md:px-6 py-3 font-medium text-sm transition-all relative whitespace-nowrap flex-shrink-0 ${
|
||||
activeTab === 'general' ? 'text-blue-600' : 'text-slate-500 hover:text-slate-700'
|
||||
activeTab === 'profile' ? 'text-blue-600' : 'text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
Generale
|
||||
{activeTab === 'general' && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"></div>}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('families')}
|
||||
className={`px-4 md:px-6 py-3 font-medium text-sm transition-all relative whitespace-nowrap flex-shrink-0 ${
|
||||
activeTab === 'families' ? 'text-blue-600' : 'text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
Famiglie
|
||||
{activeTab === 'families' && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"></div>}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('users')}
|
||||
className={`px-4 md:px-6 py-3 font-medium text-sm transition-all relative whitespace-nowrap flex-shrink-0 ${
|
||||
activeTab === 'users' ? 'text-blue-600' : 'text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
Utenti
|
||||
{activeTab === 'users' && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"></div>}
|
||||
Il Mio Profilo
|
||||
{activeTab === 'profile' && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"></div>}
|
||||
</button>
|
||||
|
||||
{/* Admin Tabs */}
|
||||
{isAdmin && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setActiveTab('general')}
|
||||
className={`px-4 md:px-6 py-3 font-medium text-sm transition-all relative whitespace-nowrap flex-shrink-0 ${
|
||||
activeTab === 'general' ? 'text-blue-600' : 'text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
Generale
|
||||
{activeTab === 'general' && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"></div>}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('families')}
|
||||
className={`px-4 md:px-6 py-3 font-medium text-sm transition-all relative whitespace-nowrap flex-shrink-0 ${
|
||||
activeTab === 'families' ? 'text-blue-600' : 'text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
Famiglie
|
||||
{activeTab === 'families' && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"></div>}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('users')}
|
||||
className={`px-4 md:px-6 py-3 font-medium text-sm transition-all relative whitespace-nowrap flex-shrink-0 ${
|
||||
activeTab === 'users' ? 'text-blue-600' : 'text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
Utenti
|
||||
{activeTab === 'users' && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"></div>}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('smtp')}
|
||||
className={`px-4 md:px-6 py-3 font-medium text-sm transition-all relative whitespace-nowrap flex-shrink-0 ${
|
||||
activeTab === 'smtp' ? 'text-blue-600' : 'text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
SMTP
|
||||
{activeTab === 'smtp' && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"></div>}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('alerts')}
|
||||
className={`px-4 md:px-6 py-3 font-medium text-sm transition-all relative whitespace-nowrap flex-shrink-0 ${
|
||||
activeTab === 'alerts' ? 'text-blue-600' : 'text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
Avvisi Automatici
|
||||
{activeTab === 'alerts' && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"></div>}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{activeTab === 'general' && (
|
||||
{activeTab === 'profile' && (
|
||||
<div className="animate-fade-in 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-6 flex items-center gap-2">
|
||||
<UserIcon className="w-5 h-5 text-blue-600" />
|
||||
Dati Profilo
|
||||
</h3>
|
||||
|
||||
<form onSubmit={handleProfileSubmit} className="space-y-5">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Il tuo Nome</label>
|
||||
<input
|
||||
type="text"
|
||||
value={profileForm.name}
|
||||
onChange={(e) => setProfileForm({...profileForm, name: e.target.value})}
|
||||
className="w-full border border-slate-300 rounded-lg p-2.5 focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Email (Login)</label>
|
||||
<input
|
||||
type="email"
|
||||
value={currentUser?.email || ''}
|
||||
disabled
|
||||
className="w-full border border-slate-200 bg-slate-50 text-slate-500 rounded-lg p-2.5 outline-none cursor-not-allowed"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Telefono</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={profileForm.phone}
|
||||
onChange={(e) => setProfileForm({...profileForm, phone: e.target.value})}
|
||||
className="w-full border border-slate-300 rounded-lg p-2.5 focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Nuova Password</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Lascia vuoto per non cambiare"
|
||||
value={profileForm.password}
|
||||
onChange={(e) => setProfileForm({...profileForm, password: e.target.value})}
|
||||
className="w-full border border-slate-300 rounded-lg p-2.5 pl-9 focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
/>
|
||||
<Lock className="w-4 h-4 text-slate-400 absolute left-3 top-3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-slate-100">
|
||||
<div className="flex items-center gap-3 bg-blue-50 p-4 rounded-lg border border-blue-100">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="myAlerts"
|
||||
checked={profileForm.receiveAlerts}
|
||||
onChange={(e) => setProfileForm({...profileForm, receiveAlerts: e.target.checked})}
|
||||
className="w-5 h-5 text-blue-600 rounded border-slate-300 focus:ring-blue-500"
|
||||
/>
|
||||
<div>
|
||||
<label htmlFor="myAlerts" className="text-sm font-bold text-slate-800 cursor-pointer block">Ricevi Avvisi Automatici</label>
|
||||
<p className="text-xs text-slate-600">Abilita la ricezione di email per scadenze e comunicazioni condominiali.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 flex items-center justify-between">
|
||||
<span className={`text-sm font-medium ${profileMsg.includes('Errore') ? 'text-red-600' : 'text-green-600'}`}>{profileMsg}</span>
|
||||
<button type="submit" disabled={profileSaving} className="bg-blue-600 text-white px-6 py-2.5 rounded-lg font-medium hover:bg-blue-700 transition-all flex items-center gap-2 disabled:opacity-70">
|
||||
<Save className="w-4 h-4" /> Aggiorna Profilo
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAdmin && activeTab === 'general' && (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{/* General Data Form */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 md:p-8 max-w-2xl">
|
||||
@@ -322,7 +539,7 @@ export const SettingsPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'families' && (
|
||||
{isAdmin && activeTab === 'families' && (
|
||||
<div className="space-y-4 animate-fade-in">
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
@@ -391,7 +608,7 @@ export const SettingsPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'users' && (
|
||||
{isAdmin && activeTab === 'users' && (
|
||||
<div className="space-y-4 animate-fade-in">
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
@@ -411,7 +628,7 @@ export const SettingsPage: React.FC = () => {
|
||||
<th className="px-6 py-4">Utente</th>
|
||||
<th className="px-6 py-4">Contatti</th>
|
||||
<th className="px-6 py-4">Ruolo</th>
|
||||
<th className="px-6 py-4">Famiglia</th>
|
||||
<th className="px-6 py-4">Alerts</th>
|
||||
<th className="px-6 py-4 text-right">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -432,7 +649,13 @@ export const SettingsPage: React.FC = () => {
|
||||
{user.role}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">{getFamilyName(user.familyId)}</td>
|
||||
<td className="px-6 py-4">
|
||||
{user.receiveAlerts ? (
|
||||
<span className="text-green-600 flex items-center gap-1"><Bell className="w-3 h-3"/> Sì</span>
|
||||
) : (
|
||||
<span className="text-slate-400 flex items-center gap-1"><Bell className="w-3 h-3"/> No</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button onClick={() => openEditUserModal(user)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg"><Pencil className="w-4 h-4" /></button>
|
||||
@@ -444,8 +667,7 @@ export const SettingsPage: React.FC = () => {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Mobile Cards for Users */}
|
||||
{/* Mobile User Cards */}
|
||||
<div className="md:hidden space-y-3">
|
||||
{users.map(user => (
|
||||
<div key={user.id} className="bg-white p-5 rounded-xl border border-slate-200 shadow-sm relative overflow-hidden">
|
||||
@@ -465,21 +687,18 @@ export const SettingsPage: React.FC = () => {
|
||||
</h4>
|
||||
<p className="text-sm text-slate-500 mt-1">{user.email}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm text-slate-600 bg-slate-50 p-3 rounded-lg mb-4">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-400">Famiglia:</span>
|
||||
<span className="font-medium">{getFamilyName(user.familyId)}</span>
|
||||
</div>
|
||||
{user.phone && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-400">Telefono:</span>
|
||||
<span className="font-medium">{user.phone}</span>
|
||||
</div>
|
||||
<div className="flex gap-2 mb-2">
|
||||
{user.receiveAlerts ? (
|
||||
<span className="text-xs bg-green-50 text-green-600 px-2 py-1 rounded border border-green-100 flex items-center gap-1">
|
||||
<Bell className="w-3 h-3"/> Riceve Avvisi
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs bg-slate-50 text-slate-400 px-2 py-1 rounded border border-slate-100 flex items-center gap-1">
|
||||
<Bell className="w-3 h-3"/> Niente Avvisi
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid grid-cols-2 gap-3 mt-3">
|
||||
<button onClick={() => openEditUserModal(user)} className="flex items-center justify-center gap-2 py-2 text-blue-600 bg-blue-50 rounded-lg text-sm font-bold">
|
||||
<Pencil className="w-4 h-4" /> Modifica
|
||||
</button>
|
||||
@@ -493,6 +712,150 @@ export const SettingsPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAdmin && activeTab === 'smtp' && (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 md:p-8 max-w-2xl animate-fade-in">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="p-2 bg-blue-50 text-blue-600 rounded-lg">
|
||||
<Server className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-slate-800">Server SMTP</h3>
|
||||
<p className="text-sm text-slate-500">Configura il server per l'invio delle email.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSettingsSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Host SMTP</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="es. smtp.gmail.com"
|
||||
value={settings.smtpConfig?.host || ''}
|
||||
onChange={(e) => setSettings({...settings, smtpConfig: {...settings.smtpConfig!, host: e.target.value}})}
|
||||
className="w-full border border-slate-300 rounded-lg p-2.5 focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Porta</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="587"
|
||||
value={settings.smtpConfig?.port}
|
||||
onChange={(e) => setSettings({...settings, smtpConfig: {...settings.smtpConfig!, port: parseInt(e.target.value)}})}
|
||||
className="w-full border border-slate-300 rounded-lg p-2.5 focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Utente</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.smtpConfig?.user || ''}
|
||||
onChange={(e) => setSettings({...settings, smtpConfig: {...settings.smtpConfig!, user: e.target.value}})}
|
||||
className="w-full border border-slate-300 rounded-lg p-2.5 focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={settings.smtpConfig?.pass || ''}
|
||||
onChange={(e) => setSettings({...settings, smtpConfig: {...settings.smtpConfig!, pass: e.target.value}})}
|
||||
className="w-full border border-slate-300 rounded-lg p-2.5 focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Email Mittente</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="noreply@condominio.it"
|
||||
value={settings.smtpConfig?.fromEmail || ''}
|
||||
onChange={(e) => setSettings({...settings, smtpConfig: {...settings.smtpConfig!, fromEmail: e.target.value}})}
|
||||
className="w-full border border-slate-300 rounded-lg p-2.5 focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="secure"
|
||||
checked={settings.smtpConfig?.secure}
|
||||
onChange={(e) => setSettings({...settings, smtpConfig: {...settings.smtpConfig!, secure: e.target.checked}})}
|
||||
className="w-4 h-4 text-blue-600 rounded border-slate-300 focus:ring-blue-500"
|
||||
/>
|
||||
<label htmlFor="secure" className="text-sm text-slate-700">Usa connessione sicura (SSL/TLS)</label>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex items-center justify-between">
|
||||
<span className={`text-sm font-medium ${successMsg ? 'text-green-600' : 'text-transparent'}`}>{successMsg || 'Salvataggio...'}</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 transition-all flex items-center gap-2 disabled:opacity-70">
|
||||
<Save className="w-4 h-4" /> Salva Configurazione
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAdmin && activeTab === 'alerts' && (
|
||||
<div className="space-y-4 animate-fade-in">
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-slate-800">Avvisi Automatici</h3>
|
||||
<p className="text-sm text-slate-500">Pianifica email ricorrenti per i condomini.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={openAddAlertModal}
|
||||
className="flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-700 text-white px-5 py-2.5 rounded-lg font-medium shadow-sm transition-colors"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
Nuovo Avviso
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{alerts.length === 0 && (
|
||||
<div className="text-center p-8 bg-white rounded-xl border border-slate-200 text-slate-400">
|
||||
Nessun avviso configurato.
|
||||
</div>
|
||||
)}
|
||||
{alerts.map(alert => (
|
||||
<div key={alert.id} className={`bg-white p-5 rounded-xl border shadow-sm flex flex-col md:flex-row justify-between gap-4 ${alert.active ? 'border-slate-200' : 'border-slate-100 opacity-70'}`}>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<h4 className="font-bold text-slate-800">{alert.subject}</h4>
|
||||
{!alert.active && <span className="bg-slate-100 text-slate-500 text-xs px-2 py-0.5 rounded font-bold">DISATTIVO</span>}
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 line-clamp-2 mb-3">{alert.body}</p>
|
||||
|
||||
<div className="flex flex-wrap gap-3 text-xs font-medium text-slate-600">
|
||||
<div className="flex items-center gap-1 bg-slate-100 px-2 py-1 rounded">
|
||||
<Clock className="w-3.5 h-3.5" />
|
||||
{alert.offsetType === 'before_next_month'
|
||||
? `${alert.daysOffset} giorni prima del prossimo mese`
|
||||
: `${alert.daysOffset} giorni dopo inizio mese corrente`
|
||||
}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 bg-slate-100 px-2 py-1 rounded">
|
||||
<Bell className="w-3.5 h-3.5" />
|
||||
Alle ore {alert.sendHour}:00
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 md:border-l md:pl-4 border-slate-100">
|
||||
<button onClick={() => openEditAlertModal(alert)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg"><Pencil className="w-5 h-5" /></button>
|
||||
<button onClick={() => handleDeleteAlert(alert.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg"><Trash2 className="w-5 h-5" /></button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</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">
|
||||
@@ -631,6 +994,19 @@ export const SettingsPage: React.FC = () => {
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 pt-2 bg-slate-50 p-3 rounded-lg border border-slate-100">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="receiveAlerts"
|
||||
checked={userForm.receiveAlerts}
|
||||
onChange={(e) => setUserForm({...userForm, receiveAlerts: e.target.checked})}
|
||||
className="w-5 h-5 text-blue-600 rounded border-slate-300 focus:ring-blue-500"
|
||||
/>
|
||||
<label htmlFor="receiveAlerts" className="text-sm font-medium text-slate-700 select-none cursor-pointer">
|
||||
Ricevi avvisi email automatici
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex gap-3">
|
||||
@@ -641,6 +1017,117 @@ export const SettingsPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Alert Config 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-2xl shadow-xl w-full max-w-lg overflow-hidden animate-in fade-in zoom-in duration-200">
|
||||
<div className="bg-slate-50 px-6 py-4 border-b border-slate-100 flex justify-between items-center">
|
||||
<h3 className="font-bold text-lg text-slate-800">
|
||||
{editingAlert ? 'Modifica Avviso' : 'Nuovo Avviso'}
|
||||
</h3>
|
||||
<button onClick={() => setShowAlertModal(false)} className="text-slate-400 hover:text-slate-600 p-1">
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleAlertSubmit} className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Oggetto Email</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={alertForm.subject}
|
||||
onChange={(e) => setAlertForm({...alertForm, subject: e.target.value})}
|
||||
className="w-full border border-slate-300 rounded-lg p-3 focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
placeholder="Es. Promemoria Scadenza Rata"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Corpo del Messaggio</label>
|
||||
<textarea
|
||||
required
|
||||
rows={6}
|
||||
value={alertForm.body}
|
||||
onChange={(e) => setAlertForm({...alertForm, body: e.target.value})}
|
||||
className="w-full border border-slate-300 rounded-lg p-3 focus:ring-2 focus:ring-blue-500 outline-none resize-none"
|
||||
placeholder="Gentile condomino, si ricorda che..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<FileText className="w-4 h-4 text-slate-400" />
|
||||
<span className="text-xs text-slate-500">Allegati: Funzionalità in arrivo.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 bg-slate-50 p-4 rounded-lg border border-slate-200">
|
||||
<div className="md:col-span-2">
|
||||
<h4 className="font-bold text-sm text-slate-700 mb-2 flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" /> Schedulazione Invio
|
||||
</h4>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-500 mb-1">Quando</label>
|
||||
<select
|
||||
value={alertForm.offsetType}
|
||||
onChange={(e) => setAlertForm({...alertForm, offsetType: e.target.value as any})}
|
||||
className="w-full border border-slate-300 rounded-lg p-2 text-sm bg-white"
|
||||
>
|
||||
<option value="before_next_month">Prima del mese successivo</option>
|
||||
<option value="after_current_month">Dopo inizio mese corrente</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-500 mb-1">Giorni di differenza</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="30"
|
||||
value={alertForm.daysOffset}
|
||||
onChange={(e) => setAlertForm({...alertForm, daysOffset: parseInt(e.target.value)})}
|
||||
className="w-full border border-slate-300 rounded-lg p-2 text-sm"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">gg</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-500 mb-1">Orario Invio</label>
|
||||
<select
|
||||
value={alertForm.sendHour}
|
||||
onChange={(e) => setAlertForm({...alertForm, sendHour: parseInt(e.target.value)})}
|
||||
className="w-full border border-slate-300 rounded-lg p-2 text-sm bg-white"
|
||||
>
|
||||
{Array.from({length: 24}, (_, i) => (
|
||||
<option key={i} value={i}>{i.toString().padStart(2, '0')}:00</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<div className="flex items-center gap-2 mb-2 w-full">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="alertActive"
|
||||
checked={alertForm.active}
|
||||
onChange={(e) => setAlertForm({...alertForm, active: e.target.checked})}
|
||||
className="w-4 h-4 text-blue-600 rounded border-slate-300"
|
||||
/>
|
||||
<label htmlFor="alertActive" className="text-sm font-medium text-slate-700">Attivo</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex gap-3">
|
||||
<button type="button" onClick={() => setShowAlertModal(false)} className="flex-1 px-4 py-3 border border-slate-300 rounded-lg font-medium text-slate-700">Annulla</button>
|
||||
<button type="submit" className="flex-1 px-4 py-3 bg-blue-600 text-white rounded-lg font-medium">Salva Avviso</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user