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.
This commit is contained in:
@@ -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"]
|
||||
|
||||
65
server/db.js
65
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 (
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user