From fd107c1ef88e3dd373a0bdb38d5f5ff4b763c659 Mon Sep 17 00:00:00 2001 From: frakarr Date: Sun, 7 Dec 2025 16:10:33 +0100 Subject: [PATCH] feat: Enhance condo and family data models Adds new fields for detailed address information and notes to the Condo and Family types. Updates database schema and server API endpoints to support these new fields, improving data richness for location and specific family/condo details. --- .dockerignore | Bin 81 -> 105 bytes Dockerfile | 22 --- nginx.conf | 29 +-- pages/Settings.tsx | 442 +++++++++++++++++++++++++-------------------- server/Dockerfile | 13 -- server/db.js | 65 ++++++- server/server.js | 96 ++++++++-- services/mockDb.ts | 39 ++-- types.ts | 10 +- 9 files changed, 422 insertions(+), 294 deletions(-) diff --git a/.dockerignore b/.dockerignore index 10c5075f1f010ae3f81113205ea3014be510d60b..b3e90c29e43ec3e8cab67b74444927db2400e19e 100644 GIT binary patch literal 105 zcmaFAfA9PKd*gsOOBP6k196$QZXS>-;ZOpS>_7}e3=m2?147F|C { 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