feat: Add targeted notices to Condopay

Implements the ability to send notices to specific families within a condominium, rather than broadcasting to all. This includes:
- Updating the `Notice` type with `targetFamilyIds`.
- Adding a `target_families` JSON column to the `notices` table in the database, with a migration for existing installations.
- Modifying the API to handle the new `targetFamilyIds` field during notice creation and retrieval.
- Updating the UI to allow users to select specific families for notices.
This commit is contained in:
2025-12-09 14:04:49 +01:00
parent bd6fce6f51
commit ca38e891c9
6 changed files with 209 additions and 62 deletions

View File

@@ -297,24 +297,45 @@ app.get('/api/notices', authenticateToken, async (req, res) => {
}
query += ' ORDER BY date DESC';
const [rows] = await pool.query(query, params);
res.json(rows.map(r => ({ id: r.id, condoId: r.condo_id, title: r.title, content: r.content, type: r.type, link: r.link, date: r.date, active: !!r.active })));
res.json(rows.map(r => ({
id: r.id,
condoId: r.condo_id,
title: r.title,
content: r.content,
type: r.type,
link: r.link,
date: r.date,
active: !!r.active,
targetFamilyIds: r.target_families ? (typeof r.target_families === 'string' ? JSON.parse(r.target_families) : r.target_families) : []
})));
} catch (e) { res.status(500).json({ error: e.message }); }
});
app.post('/api/notices', authenticateToken, requireAdmin, async (req, res) => {
const { condoId, title, content, type, link, active } = req.body;
const { condoId, title, content, type, link, active, targetFamilyIds } = req.body;
const id = uuidv4();
try {
await pool.query('INSERT INTO notices (id, condo_id, title, content, type, link, active, date) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())', [id, condoId, title, content, type, link, active]);
res.json({ id, condoId, title, content, type, link, active });
const targetFamiliesJson = targetFamilyIds && targetFamilyIds.length > 0 ? JSON.stringify(targetFamilyIds) : null;
await pool.query(
'INSERT INTO notices (id, condo_id, title, content, type, link, active, target_families, date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())',
[id, condoId, title, content, type, link, active, targetFamiliesJson]
);
res.json({ id, condoId, title, content, type, link, active, targetFamilyIds });
} catch (e) { res.status(500).json({ error: e.message }); }
});
app.put('/api/notices/:id', authenticateToken, requireAdmin, async (req, res) => {
const { title, content, type, link, active } = req.body;
const { title, content, type, link, active, targetFamilyIds } = req.body;
try {
await pool.query('UPDATE notices SET title = ?, content = ?, type = ?, link = ?, active = ? WHERE id = ?', [title, content, type, link, active, req.params.id]);
const targetFamiliesJson = targetFamilyIds && targetFamilyIds.length > 0 ? JSON.stringify(targetFamilyIds) : null;
await pool.query(
'UPDATE notices SET title = ?, content = ?, type = ?, link = ?, active = ?, target_families = ? WHERE id = ?',
[title, content, type, link, active, targetFamiliesJson, req.params.id]
);
res.json({ success: true });
} catch (e) { res.status(500).json({ error: e.message }); }
});
app.delete('/api/notices/:id', authenticateToken, requireAdmin, async (req, res) => {
try {
await pool.query('DELETE FROM notices WHERE id = ?', [req.params.id]);
@@ -337,13 +358,40 @@ app.get('/api/notices/:id/reads', authenticateToken, async (req, res) => {
app.get('/api/notices/unread', authenticateToken, async (req, res) => {
const { userId, condoId } = req.query;
try {
// First get user's family ID to filter targeted notices
const [users] = await pool.query('SELECT family_id FROM users WHERE id = ?', [userId]);
const userFamilyId = users.length > 0 ? users[0].family_id : null;
const [rows] = await pool.query(`
SELECT n.* FROM notices n
LEFT JOIN notice_reads nr ON n.id = nr.notice_id AND nr.user_id = ?
WHERE n.condo_id = ? AND n.active = TRUE AND nr.read_at IS NULL
ORDER BY n.date DESC
`, [userId, condoId]);
res.json(rows.map(r => ({ id: r.id, condoId: r.condo_id, title: r.title, content: r.content, type: r.type, link: r.link, date: r.date, active: !!r.active })));
// Filter in JS for simplicity across DBs (handling JSON field logic)
const filtered = rows.filter(n => {
if (!n.target_families) return true; // Public to all
let targets = n.target_families;
if (typeof targets === 'string') {
try { targets = JSON.parse(targets); } catch(e) { return true; }
}
if (!Array.isArray(targets) || targets.length === 0) return true; // Empty array = Public
// If explicit targets are set, user MUST belong to one of the families
return userFamilyId && targets.includes(userFamilyId);
});
res.json(filtered.map(r => ({
id: r.id,
condoId: r.condo_id,
title: r.title,
content: r.content,
type: r.type,
link: r.link,
date: r.date,
active: !!r.active
})));
} catch (e) { res.status(500).json({ error: e.message }); }
});