diff --git a/.dockerignore b/.dockerignore index 10c5075..b3e90c2 100644 Binary files a/.dockerignore and b/.dockerignore differ diff --git a/Dockerfile b/Dockerfile index ae68a73..e69de29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +0,0 @@ -# Stage 1: Build dell'applicazione React -FROM node:20-alpine as build -WORKDIR /app -# Copia i file di dipendenze -COPY package*.json ./ -# Usa npm install per installare le dipendenze (più sicuro di ci se manca lockfile) -RUN npm install -# Copia tutto il codice sorgente -COPY . . -# Esegue la build di produzione (crea la cartella dist) -RUN npm run build - -# Stage 2: Server Nginx per servire i file statici -FROM nginx:alpine -# Copia i file compilati dallo stage precedente alla cartella di Nginx -COPY --from=build /app/dist /usr/share/nginx/html -# Copia la configurazione custom di Nginx -COPY nginx.conf /etc/nginx/conf.d/default.conf -# Espone la porta 80 -EXPOSE 80 -# Avvia Nginx -CMD ["nginx", "-g", "daemon off;"] diff --git a/nginx.conf b/nginx.conf index 177eaa5..97684f0 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,28 +1 @@ -server { - listen 80; - - # Serve i file statici del frontend React - location / { - root /usr/share/nginx/html; - index index.html index.htm; - # Fondamentale per il routing client-side (Single Page Application) - # Se il file non esiste, serve index.html invece di dare 404 - try_files $uri $uri/ /index.html; - } - - # Proxy per le chiamate API verso il container del backend - location /api { - # 'backend' deve corrispondere al nome del servizio nel docker-compose.yml - proxy_pass http://backend:3001; - - # Configurazioni standard per proxy HTTP 1.1 e WebSocket - 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; - - # IMPORTANTE: Assicura che l'header Authorization (il token JWT) passi al backend - proxy_set_header Authorization $http_authorization; - } -} +���z \ No newline at end of file diff --git a/pages/Settings.tsx b/pages/Settings.tsx index 15c8141..32f57de 100644 --- a/pages/Settings.tsx +++ b/pages/Settings.tsx @@ -2,7 +2,7 @@ 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 } from 'lucide-react'; +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 } from 'lucide-react'; export const SettingsPage: React.FC = () => { const currentUser = CondoService.getCurrentUser(); @@ -32,7 +32,16 @@ export const SettingsPage: React.FC = () => { const [condos, setCondos] = useState([]); const [showCondoModal, setShowCondoModal] = useState(false); const [editingCondo, setEditingCondo] = useState(null); - const [condoForm, setCondoForm] = useState({ name: '', address: '', defaultMonthlyQuota: 100 }); + const [condoForm, setCondoForm] = useState({ + name: '', + address: '', + streetNumber: '', + city: '', + province: '', + zipCode: '', + notes: '', + defaultMonthlyQuota: 100 + }); const [saving, setSaving] = useState(false); const [successMsg, setSuccessMsg] = useState(''); @@ -44,9 +53,12 @@ export const SettingsPage: React.FC = () => { const [familyForm, setFamilyForm] = useState<{ name: string; unitNumber: string; + stair: string; + floor: string; + notes: string; contactEmail: string; - customMonthlyQuota: string; // Use string for input handling, parse to number on save - }>({ name: '', unitNumber: '', contactEmail: '', customMonthlyQuota: '' }); + customMonthlyQuota: string; + }>({ name: '', unitNumber: '', stair: '', floor: '', notes: '', contactEmail: '', customMonthlyQuota: '' }); // Users State const [users, setUsers] = useState([]); @@ -104,30 +116,42 @@ export const SettingsPage: React.FC = () => { const fetchData = async () => { try { if (isAdmin) { - const [condoList, activeC, gSettings, fams, usrs, alrts, allNotices] = await Promise.all([ - CondoService.getCondos(), - CondoService.getActiveCondo(), - CondoService.getSettings(), - CondoService.getFamilies(), - CondoService.getUsers(), - CondoService.getAlerts(), - CondoService.getNotices() - ]); + // 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); - setFamilies(fams); - setUsers(usrs); - setAlerts(alrts); - setNotices(allNotices); - // Fetch read stats for notices - const stats: Record = {}; - for (const n of allNotices) { - const reads = await CondoService.getNoticeReadStatus(n.id); - stats[n.id] = reads; + // 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 = {}; + for (const n of allNotices) { + const reads = await CondoService.getNoticeReadStatus(n.id); + stats[n.id] = reads; + } + setNoticeReadStats(stats); + } else { + setFamilies([]); + setUsers([]); + setAlerts([]); + setNotices([]); } - setNoticeReadStats(stats); } else { const activeC = await CondoService.getActiveCondo(); @@ -211,24 +235,37 @@ export const SettingsPage: React.FC = () => { // --- Condo Management Handlers --- const openAddCondoModal = () => { setEditingCondo(null); - setCondoForm({ name: '', address: '', defaultMonthlyQuota: 100 }); + setCondoForm({ name: '', address: '', streetNumber: '', city: '', province: '', zipCode: '', notes: '', defaultMonthlyQuota: 100 }); setShowCondoModal(true); }; const openEditCondoModal = (c: Condo) => { setEditingCondo(c); - setCondoForm({ name: c.name, address: c.address || '', defaultMonthlyQuota: c.defaultMonthlyQuota }); + setCondoForm({ + name: c.name, + address: c.address || '', + streetNumber: c.streetNumber || '', + city: c.city || '', + province: c.province || '', + zipCode: c.zipCode || '', + notes: c.notes || '', + defaultMonthlyQuota: c.defaultMonthlyQuota + }); setShowCondoModal(true); }; const handleCondoSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { - // If editingCondo exists, use its ID. If not, empty string tells service to create new. 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, defaultMonthlyQuota: condoForm.defaultMonthlyQuota }; @@ -265,7 +302,7 @@ export const SettingsPage: React.FC = () => { // --- Family Handlers --- const openAddFamilyModal = () => { setEditingFamily(null); - setFamilyForm({ name: '', unitNumber: '', contactEmail: '', customMonthlyQuota: '' }); + setFamilyForm({ name: '', unitNumber: '', stair: '', floor: '', notes: '', contactEmail: '', customMonthlyQuota: '' }); setShowFamilyModal(true); }; @@ -274,6 +311,9 @@ export const SettingsPage: React.FC = () => { 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() : '' }); @@ -296,23 +336,22 @@ export const SettingsPage: React.FC = () => { ? 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, - name: familyForm.name, - unitNumber: familyForm.unitNumber, - contactEmail: familyForm.contactEmail, - customMonthlyQuota: quota - }; + const updatedFamily = { ...editingFamily, ...payload }; await CondoService.updateFamily(updatedFamily); setFamilies(families.map(f => f.id === updatedFamily.id ? updatedFamily : f)); } else { - const newFamily = await CondoService.addFamily({ - name: familyForm.name, - unitNumber: familyForm.unitNumber, - contactEmail: familyForm.contactEmail, - customMonthlyQuota: quota - }); + const newFamily = await CondoService.addFamily(payload); setFamilies([...families, newFamily]); } setShowFamilyModal(false); @@ -349,7 +388,8 @@ export const SettingsPage: React.FC = () => { } else { await CondoService.createUser(userForm); } - setUsers(await CondoService.getUsers()); + // Refresh user list for active condo + setUsers(await CondoService.getUsers(activeCondo?.id)); setShowUserModal(false); } catch (e) { alert("Errore nel salvataggio utente"); } }; @@ -379,7 +419,8 @@ export const SettingsPage: React.FC = () => { date: editingNotice ? editingNotice.date : new Date().toISOString() }; await CondoService.saveNotice(payload); - setNotices(await CondoService.getNotices()); + // Refresh notices for active condo + setNotices(await CondoService.getNotices(activeCondo?.id)); setShowNoticeModal(false); } catch (e) { console.error(e); } }; @@ -409,6 +450,7 @@ export const SettingsPage: React.FC = () => { 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); @@ -488,7 +530,39 @@ export const SettingsPage: React.FC = () => {

Dati Condominio Corrente

setActiveCondo({ ...activeCondo, name: e.target.value })} className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Nome" required /> - setActiveCondo({ ...activeCondo, address: e.target.value })} className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Indirizzo" /> + +
+ + setActiveCondo({ ...activeCondo, address: e.target.value })} className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Via/Piazza..." required/> +
+ +
+
+ + setActiveCondo({ ...activeCondo, streetNumber: e.target.value })} className="w-full border p-2.5 rounded-lg text-slate-700" required/> +
+
+ + setActiveCondo({ ...activeCondo, zipCode: e.target.value })} className="w-full border p-2.5 rounded-lg text-slate-700"/> +
+
+ +
+
+ + setActiveCondo({ ...activeCondo, city: e.target.value })} className="w-full border p-2.5 rounded-lg text-slate-700" required/> +
+
+ + setActiveCondo({ ...activeCondo, province: e.target.value })} className="w-full border p-2.5 rounded-lg text-slate-700" required/> +
+
+ +
+ + +
+
setActiveCondo({ ...activeCondo, defaultMonthlyQuota: parseFloat(e.target.value) })} className="w-full border p-2.5 rounded-lg text-slate-700" placeholder="Quota Default" required /> @@ -519,7 +593,10 @@ export const SettingsPage: React.FC = () => {
{activeCondo?.id === condo.id &&
Attivo
}

{condo.name}

-

{condo.address || 'Nessun indirizzo'}

+
+

{condo.address} {condo.streetNumber}

+ {condo.city &&

{condo.zipCode} {condo.city} ({condo.province})

} +
@@ -545,12 +622,22 @@ export const SettingsPage: React.FC = () => {
- + {families.map(family => ( - +
NomeInternoEmailQuotaAzioni
NomeDettagliEmailQuotaAzioni
{family.name}{family.unitNumber} +
+ Int: {family.unitNumber || '-'} + {(family.stair || family.floor) && ( + + {family.stair ? `Scala: ${family.stair} ` : ''} + {family.floor ? `Piano: ${family.floor}` : ''} + + )} +
+
{family.contactEmail} {family.customMonthlyQuota ? ( @@ -654,152 +741,6 @@ export const SettingsPage: React.FC = () => { )} - {/* Alerts Tab */} - {isAdmin && activeTab === 'alerts' && ( -
-
-

Avvisi Automatici Email

- -
- {alerts.map(a => ( -
-

{a.subject}

{a.body}

-
-
- ))} -
- )} - - {isAdmin && activeTab === 'smtp' && ( -
-

Server SMTP Globale

- {globalSettings && ( - -
- setGlobalSettings({...globalSettings, smtpConfig: {...globalSettings.smtpConfig!, host: e.target.value}})} className="border p-2 rounded text-slate-700"/> - setGlobalSettings({...globalSettings, smtpConfig: {...globalSettings.smtpConfig!, port: parseInt(e.target.value)}})} className="border p-2 rounded text-slate-700"/> -
-
- setGlobalSettings({...globalSettings, smtpConfig: {...globalSettings.smtpConfig!, user: e.target.value}})} className="border p-2 rounded text-slate-700"/> - setGlobalSettings({...globalSettings, smtpConfig: {...globalSettings.smtpConfig!, pass: e.target.value}})} className="border p-2 rounded text-slate-700"/> -
- - - )} -
- )} - - {/* MODALS */} - - {/* CONDO MODAL */} - {showCondoModal && ( -
-
-

{editingCondo ? 'Modifica Condominio' : 'Nuovo Condominio'}

-
- setCondoForm({...condoForm, name: e.target.value})} required /> - setCondoForm({...condoForm, address: e.target.value})} /> -
Quota Default € setCondoForm({...condoForm, defaultMonthlyQuota: parseFloat(e.target.value)})} />
-
- - -
-
-
-
- )} - - {/* NOTICE MODAL */} - {showNoticeModal && ( -
-
-

- - {editingNotice ? 'Modifica Avviso' : 'Nuovo Avviso'} -

-
-
- - setNoticeForm({...noticeForm, title: e.target.value})} required /> -
- -
-
- - -
-
- - -
-
- -
- - +
+ +
+ +

Lasciare vuoto per usare il default del condominio (€ {activeCondo?.defaultMonthlyQuota})

+ setFamilyForm({...familyForm, customMonthlyQuota: e.target.value})} + className="w-full border rounded-lg p-2.5 text-slate-700" + placeholder="Es. 120.00" + /> +
+ +
+ + +
+
+
+
+ )} ); }; diff --git a/server/Dockerfile b/server/Dockerfile index dbe9c07..e69de29 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,13 +0,0 @@ -# Usa Node.js 20 come base -FROM node:20-alpine -WORKDIR /app -# Copia i file di dipendenze del server -COPY package*.json ./ -# Installa le dipendenze -RUN npm install -# Copia il codice sorgente del server -COPY . . -# Espone la porta del backend -EXPOSE 3001 -# Avvia il server -CMD ["npm", "start"] diff --git a/server/db.js b/server/db.js index 7918b44..a7f24f7 100644 --- a/server/db.js +++ b/server/db.js @@ -76,12 +76,39 @@ const initDb = async () => { id VARCHAR(36) PRIMARY KEY, name VARCHAR(255) NOT NULL, address VARCHAR(255), + street_number VARCHAR(20), + city VARCHAR(100), + province VARCHAR(100), + zip_code VARCHAR(20), + notes TEXT, iban VARCHAR(50), default_monthly_quota DECIMAL(10, 2) DEFAULT 100.00, image VARCHAR(255), created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP ) `); + + // Migration for condos: Add new address fields + try { + let hasCity = false; + if (DB_CLIENT === 'postgres') { + const [cols] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='condos'"); + hasCity = cols.some(c => c.column_name === 'city'); + } else { + const [cols] = await connection.query("SHOW COLUMNS FROM condos"); + hasCity = cols.some(c => c.Field === 'city'); + } + + if (!hasCity) { + console.log('Migrating: Adding address fields to condos...'); + await connection.query("ALTER TABLE condos ADD COLUMN street_number VARCHAR(20)"); + await connection.query("ALTER TABLE condos ADD COLUMN city VARCHAR(100)"); + await connection.query("ALTER TABLE condos ADD COLUMN province VARCHAR(100)"); + await connection.query("ALTER TABLE condos ADD COLUMN zip_code VARCHAR(20)"); + await connection.query("ALTER TABLE condos ADD COLUMN notes TEXT"); + } + } catch(e) { console.warn("Condos migration warning:", e.message); } + // 2. Families Table await connection.query(` @@ -90,6 +117,9 @@ const initDb = async () => { condo_id VARCHAR(36), name VARCHAR(255) NOT NULL, unit_number VARCHAR(50), + stair VARCHAR(50), + floor VARCHAR(50), + notes TEXT, contact_email VARCHAR(255), custom_monthly_quota DECIMAL(10, 2) NULL, created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, @@ -97,18 +127,22 @@ const initDb = async () => { ) `); - // Migration for families: Add condo_id and custom_monthly_quota if missing + // Migration for families: Add condo_id, custom_monthly_quota, stair, floor, notes try { let hasCondoId = false; let hasQuota = false; + let hasStair = false; + if (DB_CLIENT === 'postgres') { const [cols] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='families'"); hasCondoId = cols.some(c => c.column_name === 'condo_id'); hasQuota = cols.some(c => c.column_name === 'custom_monthly_quota'); + hasStair = cols.some(c => c.column_name === 'stair'); } else { const [cols] = await connection.query("SHOW COLUMNS FROM families"); hasCondoId = cols.some(c => c.Field === 'condo_id'); hasQuota = cols.some(c => c.Field === 'custom_monthly_quota'); + hasStair = cols.some(c => c.Field === 'stair'); } if (!hasCondoId) { @@ -119,6 +153,13 @@ const initDb = async () => { console.log('Migrating: Adding custom_monthly_quota to families...'); await connection.query("ALTER TABLE families ADD COLUMN custom_monthly_quota DECIMAL(10, 2) NULL"); } + if (!hasStair) { + console.log('Migrating: Adding extended fields to families...'); + await connection.query("ALTER TABLE families ADD COLUMN stair VARCHAR(50)"); + await connection.query("ALTER TABLE families ADD COLUMN floor VARCHAR(50)"); + await connection.query("ALTER TABLE families ADD COLUMN notes TEXT"); + } + } catch(e) { console.warn("Families migration warning:", e.message); } // 3. Payments Table @@ -156,6 +197,7 @@ const initDb = async () => { await connection.query(` CREATE TABLE IF NOT EXISTS alerts ( id VARCHAR(36) PRIMARY KEY, + condo_id VARCHAR(36) NULL, subject VARCHAR(255) NOT NULL, body TEXT, days_offset INT DEFAULT 1, @@ -163,10 +205,29 @@ const initDb = async () => { send_hour INT DEFAULT 9, active BOOLEAN DEFAULT TRUE, last_sent ${TIMESTAMP_TYPE} NULL, - created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP + created_at ${TIMESTAMP_TYPE} DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE ) `); + // Migration for alerts: Add condo_id if missing + try { + let hasCondoId = false; + if (DB_CLIENT === 'postgres') { + const [cols] = await connection.query("SELECT column_name FROM information_schema.columns WHERE table_name='alerts'"); + hasCondoId = cols.some(c => c.column_name === 'condo_id'); + } else { + const [cols] = await connection.query("SHOW COLUMNS FROM alerts"); + hasCondoId = cols.some(c => c.Field === 'condo_id'); + } + + if (!hasCondoId) { + console.log('Migrating: Adding condo_id to alerts...'); + await connection.query("ALTER TABLE alerts ADD COLUMN condo_id VARCHAR(36)"); + await connection.query("ALTER TABLE alerts ADD CONSTRAINT fk_alerts_condo FOREIGN KEY (condo_id) REFERENCES condos(id) ON DELETE CASCADE"); + } + } catch(e) { console.warn("Alerts migration warning:", e.message); } + // 6. Notices Table await connection.query(` CREATE TABLE IF NOT EXISTS notices ( diff --git a/server/server.js b/server/server.js index 20145d6..efcf836 100644 --- a/server/server.js +++ b/server/server.js @@ -144,22 +144,38 @@ app.get('/api/condos', authenticateToken, async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM condos'); res.json(rows.map(r => ({ - id: r.id, name: r.name, address: r.address, iban: r.iban, defaultMonthlyQuota: parseFloat(r.default_monthly_quota), image: r.image + id: r.id, + name: r.name, + address: r.address, + streetNumber: r.street_number, + city: r.city, + province: r.province, + zipCode: r.zip_code, + notes: r.notes, + iban: r.iban, + defaultMonthlyQuota: parseFloat(r.default_monthly_quota), + image: r.image }))); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/api/condos', authenticateToken, requireAdmin, async (req, res) => { - const { name, address, defaultMonthlyQuota } = req.body; + const { name, address, streetNumber, city, province, zipCode, notes, defaultMonthlyQuota } = req.body; const id = uuidv4(); try { - await pool.query('INSERT INTO condos (id, name, address, default_monthly_quota) VALUES (?, ?, ?, ?)', [id, name, address, defaultMonthlyQuota]); - res.json({ id, name, address, defaultMonthlyQuota }); + await pool.query( + 'INSERT INTO condos (id, name, address, street_number, city, province, zip_code, notes, default_monthly_quota) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', + [id, name, address, streetNumber, city, province, zipCode, notes, defaultMonthlyQuota] + ); + res.json({ id, name, address, streetNumber, city, province, zipCode, notes, defaultMonthlyQuota }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.put('/api/condos/:id', authenticateToken, requireAdmin, async (req, res) => { - const { name, address, defaultMonthlyQuota } = req.body; + const { name, address, streetNumber, city, province, zipCode, notes, defaultMonthlyQuota } = req.body; try { - await pool.query('UPDATE condos SET name = ?, address = ?, default_monthly_quota = ? WHERE id = ?', [name, address, defaultMonthlyQuota, req.params.id]); + await pool.query( + 'UPDATE condos SET name = ?, address = ?, street_number = ?, city = ?, province = ?, zip_code = ?, notes = ?, default_monthly_quota = ? WHERE id = ?', + [name, address, streetNumber, city, province, zipCode, notes, defaultMonthlyQuota, req.params.id] + ); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); @@ -172,32 +188,58 @@ app.delete('/api/condos/:id', authenticateToken, requireAdmin, async (req, res) // --- FAMILIES --- app.get('/api/families', authenticateToken, async (req, res) => { + const { condoId } = req.query; try { let query = `SELECT f.* FROM families f`; let params = []; + + // Authorization/Filtering logic if (req.user.role !== 'admin' && req.user.role !== 'poweruser') { + // Regular user: can only see their own family if (!req.user.familyId) return res.json([]); query += ' WHERE f.id = ?'; params.push(req.user.familyId); + } else { + // Admin: If condoId provided, filter by it. + if (condoId) { + query += ' WHERE f.condo_id = ?'; + params.push(condoId); + } } + const [rows] = await pool.query(query, params); res.json(rows.map(r => ({ - id: r.id, condoId: r.condo_id, name: r.name, unitNumber: r.unit_number, contactEmail: r.contact_email, customMonthlyQuota: r.custom_monthly_quota ? parseFloat(r.custom_monthly_quota) : undefined, balance: 0 + id: r.id, + condoId: r.condo_id, + name: r.name, + unitNumber: r.unit_number, + stair: r.stair, + floor: r.floor, + notes: r.notes, + contactEmail: r.contact_email, + customMonthlyQuota: r.custom_monthly_quota ? parseFloat(r.custom_monthly_quota) : undefined, + balance: 0 }))); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/api/families', authenticateToken, requireAdmin, async (req, res) => { - const { name, unitNumber, contactEmail, condoId, customMonthlyQuota } = req.body; + const { name, unitNumber, stair, floor, notes, contactEmail, condoId, customMonthlyQuota } = req.body; const id = uuidv4(); try { - await pool.query('INSERT INTO families (id, condo_id, name, unit_number, contact_email, custom_monthly_quota) VALUES (?, ?, ?, ?, ?, ?)', [id, condoId, name, unitNumber, contactEmail, customMonthlyQuota || null]); - res.json({ id, condoId, name, unitNumber, contactEmail, customMonthlyQuota }); + await pool.query( + 'INSERT INTO families (id, condo_id, name, unit_number, stair, floor, notes, contact_email, custom_monthly_quota) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', + [id, condoId, name, unitNumber, stair, floor, notes, contactEmail, customMonthlyQuota || null] + ); + res.json({ id, condoId, name, unitNumber, stair, floor, notes, contactEmail, customMonthlyQuota }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.put('/api/families/:id', authenticateToken, requireAdmin, async (req, res) => { - const { name, unitNumber, contactEmail, customMonthlyQuota } = req.body; + const { name, unitNumber, stair, floor, notes, contactEmail, customMonthlyQuota } = req.body; try { - await pool.query('UPDATE families SET name = ?, unit_number = ?, contact_email = ?, custom_monthly_quota = ? WHERE id = ?', [name, unitNumber, contactEmail, customMonthlyQuota || null, req.params.id]); + await pool.query( + 'UPDATE families SET name = ?, unit_number = ?, stair = ?, floor = ?, notes = ?, contact_email = ?, custom_monthly_quota = ? WHERE id = ?', + [name, unitNumber, stair, floor, notes, contactEmail, customMonthlyQuota || null, req.params.id] + ); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); @@ -305,8 +347,19 @@ app.post('/api/payments', authenticateToken, async (req, res) => { // --- USERS --- app.get('/api/users', authenticateToken, requireAdmin, async (req, res) => { + const { condoId } = req.query; try { - const [rows] = await pool.query('SELECT id, email, name, role, phone, family_id, receive_alerts FROM users'); + let query = 'SELECT u.id, u.email, u.name, u.role, u.phone, u.family_id, u.receive_alerts FROM users u'; + let params = []; + + // Filter users by condo. + // Logic: Users belong to families, families belong to condos. + if (condoId) { + query += ' LEFT JOIN families f ON u.family_id = f.id WHERE f.condo_id = ?'; + params.push(condoId); + } + + const [rows] = await pool.query(query, params); res.json(rows.map(r => ({ id: r.id, email: r.email, name: r.name, role: r.role, phone: r.phone, familyId: r.family_id, receiveAlerts: !!r.receive_alerts }))); } catch (e) { res.status(500).json({ error: e.message }); } }); @@ -344,17 +397,24 @@ app.delete('/api/users/:id', authenticateToken, requireAdmin, async (req, res) = // --- ALERTS --- app.get('/api/alerts', authenticateToken, requireAdmin, async (req, res) => { + const { condoId } = req.query; try { - const [rows] = await pool.query('SELECT * FROM alerts'); - res.json(rows.map(r => ({ id: r.id, subject: r.subject, body: r.body, daysOffset: r.days_offset, offsetType: r.offset_type, sendHour: r.send_hour, active: !!r.active, lastSent: r.last_sent }))); + let query = 'SELECT * FROM alerts'; + let params = []; + if (condoId) { + query += ' WHERE condo_id = ?'; + params.push(condoId); + } + const [rows] = await pool.query(query, params); + res.json(rows.map(r => ({ id: r.id, condoId: r.condo_id, subject: r.subject, body: r.body, daysOffset: r.days_offset, offsetType: r.offset_type, sendHour: r.send_hour, active: !!r.active, lastSent: r.last_sent }))); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/api/alerts', authenticateToken, requireAdmin, async (req, res) => { - const { subject, body, daysOffset, offsetType, sendHour, active } = req.body; + const { condoId, subject, body, daysOffset, offsetType, sendHour, active } = req.body; const id = uuidv4(); try { - await pool.query('INSERT INTO alerts (id, subject, body, days_offset, offset_type, send_hour, active) VALUES (?, ?, ?, ?, ?, ?, ?)', [id, subject, body, daysOffset, offsetType, sendHour, active]); - res.json({ id, subject, body, daysOffset, offsetType, sendHour, active }); + await pool.query('INSERT INTO alerts (id, condo_id, subject, body, days_offset, offset_type, send_hour, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [id, condoId, subject, body, daysOffset, offsetType, sendHour, active]); + res.json({ id, condoId, subject, body, daysOffset, offsetType, sendHour, active }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.put('/api/alerts/:id', authenticateToken, requireAdmin, async (req, res) => { diff --git a/services/mockDb.ts b/services/mockDb.ts index 6acaf78..bd7b787 100644 --- a/services/mockDb.ts +++ b/services/mockDb.ts @@ -92,7 +92,8 @@ export const CondoService = { getNotices: async (condoId?: string): Promise => { let url = '/notices'; - if (condoId) url += `?condoId=${condoId}`; + const activeId = condoId || CondoService.getActiveCondoId(); + if (activeId) url += `?condoId=${activeId}`; return request(url); }, @@ -142,11 +143,8 @@ export const CondoService = { // Set active condo if user belongs to a family if (data.user.familyId) { - // We need to fetch family to get condoId. - // For simplicity, we trust the flow or fetch families next. - // In a real app, login might return condoId directly. try { - const families = await CondoService.getFamilies(); // This will filter by user perms + const families = await CondoService.getFamilies(); // This will filter by user perms automatically on server const fam = families.find(f => f.id === data.user.familyId); if (fam) { localStorage.setItem(STORAGE_KEYS.ACTIVE_CONDO_ID, fam.condoId); @@ -194,13 +192,11 @@ export const CondoService = { // --- FAMILIES --- - getFamilies: async (): Promise => { - const activeCondoId = CondoService.getActiveCondoId(); - // Pass condoId to filter on server if needed, or filter client side. - // The server `getFamilies` endpoint handles filtering based on user role. - // However, if we are admin, we want ALL families, but usually filtered by the UI for the active condo. - // Let's get all allowed families from server. - return request('/families'); + getFamilies: async (condoId?: string): Promise => { + let url = '/families'; + const activeId = condoId || CondoService.getActiveCondoId(); + if (activeId) url += `?condoId=${activeId}`; + return request(url); }, addFamily: async (familyData: Omit): Promise => { @@ -239,8 +235,11 @@ export const CondoService = { // --- USERS --- - getUsers: async (): Promise => { - return request('/users'); + getUsers: async (condoId?: string): Promise => { + let url = '/users'; + const activeId = condoId || CondoService.getActiveCondoId(); + if (activeId) url += `?condoId=${activeId}`; + return request(url); }, createUser: async (userData: any) => { @@ -263,15 +262,19 @@ export const CondoService = { // --- ALERTS --- - getAlerts: async (): Promise => { - return request('/alerts'); + getAlerts: async (condoId?: string): Promise => { + let url = '/alerts'; + const activeId = condoId || CondoService.getActiveCondoId(); + if (activeId) url += `?condoId=${activeId}`; + return request(url); }, - saveAlert: async (alert: AlertDefinition): Promise => { + saveAlert: async (alert: AlertDefinition & { condoId?: string }): Promise => { + const activeCondoId = CondoService.getActiveCondoId(); if (!alert.id) { return request('/alerts', { method: 'POST', - body: JSON.stringify(alert) + body: JSON.stringify({ ...alert, condoId: activeCondoId }) }); } else { return request(`/alerts/${alert.id}`, { diff --git a/types.ts b/types.ts index 1d06d0b..45f7525 100644 --- a/types.ts +++ b/types.ts @@ -3,6 +3,11 @@ export interface Condo { id: string; name: string; address?: string; + streetNumber?: string; // Civico + city?: string; // Città + province?: string; // Provincia + zipCode?: string; // CAP + notes?: string; // Note iban?: string; defaultMonthlyQuota: number; image?: string; // Optional placeholder for logo @@ -12,7 +17,10 @@ export interface Family { id: string; condoId: string; // Link to specific condo name: string; - unitNumber: string; // Internal apartment number + unitNumber: string; // Internal apartment number (Interno) + stair?: string; // Scala + floor?: string; // Piano + notes?: string; // Note contactEmail?: string; balance: number; // Calculated balance (positive = credit, negative = debt) customMonthlyQuota?: number; // Optional override for default quota