feat: Add email alerts tab to settings

Introduces a new tab in the settings section dedicated to configuring email alerts. This allows administrators to manage automatic email notifications for various events within the Condopay application.
This commit is contained in:
2025-12-07 16:29:20 +01:00
parent 5cb7037128
commit 3a2532805a
5 changed files with 93 additions and 42 deletions

Binary file not shown.

View File

@@ -1,14 +0,0 @@
# Stage 1: Build the React application
FROM node:18-alpine as build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2: Serve with Nginx
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,20 +1 @@
server {
listen 80;
# Serve React App (SPA)
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# Proxy API requests to Backend Service
location /api {
proxy_pass http://backend:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
<EFBFBD><EFBFBD><EFBFBD>z

View File

@@ -473,7 +473,7 @@ export const SettingsPage: React.FC = () => {
{ id: 'users', label: 'Utenti', icon: <UserCog className="w-4 h-4"/> },
{ id: 'notices', label: 'Bacheca', icon: <Megaphone className="w-4 h-4"/> },
{ id: 'alerts', label: 'Avvisi Email', icon: <Bell className="w-4 h-4"/> },
{ id: 'smtp', label: 'Impostazioni Posta', icon: <Mail className="w-4 h-4"/> }
{ id: 'smtp', label: 'SMTP', icon: <Mail className="w-4 h-4"/> }
);
}
@@ -741,6 +741,54 @@ export const SettingsPage: React.FC = () => {
</div>
)}
{/* ALERTS TAB */}
{isAdmin && activeTab === 'alerts' && (
<div className="space-y-4 animate-fade-in">
<div className="flex justify-between items-center bg-blue-50 p-4 rounded-xl border border-blue-100">
<div><h3 className="font-bold text-blue-800">Avvisi Automatici</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>
)}
{/* SMTP TAB */}
{isAdmin && activeTab === 'smtp' && (
<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"><Server className="w-5 h-5 text-blue-600"/> Configurazione SMTP</h3>
<form onSubmit={handleSmtpSubmit} className="space-y-5">
<div className="grid grid-cols-2 gap-4">
<div><label className="text-sm font-bold text-slate-700">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"/></div>
<div><label className="text-sm font-bold text-slate-700">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"/></div>
</div>
<div className="grid grid-cols-2 gap-4">
<div><label className="text-sm font-bold text-slate-700">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-sm font-bold text-slate-700">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-sm font-bold text-slate-700">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"/></div>
<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"/><label className="text-sm text-slate-700">Usa SSL/TLS (Secure)</label></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">Salva Configurazione</button></div>
</form>
</div>
)}
{/* ALERT MODAL */}
{showAlertModal && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
@@ -778,6 +826,48 @@ export const SettingsPage: React.FC = () => {
</div>
)}
{/* NOTICE MODAL */}
{showNoticeModal && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg p-6 animate-in fade-in zoom-in duration-200">
<h3 className="font-bold text-lg mb-4 text-slate-800">{editingNotice ? 'Modifica Avviso' : 'Nuovo Avviso'}</h3>
<form 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-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>
<div className="flex gap-2 pt-2">
<button type="button" onClick={() => setShowNoticeModal(false)} className="flex-1 border p-2.5 rounded-lg text-slate-600 hover:bg-slate-50">Annulla</button>
<button type="submit" className="flex-1 bg-blue-600 text-white p-2.5 rounded-lg hover:bg-blue-700">Salva</button>
</div>
</form>
</div>
</div>
)}
{/* READ DETAILS MODAL */}
{showReadDetailsModal && selectedNoticeId && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">

View File

@@ -1,7 +1 @@
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3001
CMD ["node", "server.js"]
<13><><EFBFBD>^