Update server.js
This commit is contained in:
421
server/server.js
421
server/server.js
@@ -322,20 +322,429 @@ app.delete('/api/families/:id', authenticateToken, requireAdmin, async (req, res
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- NOTICES ---
|
// --- NOTICES ---
|
||||||
// ... (Notices API skipped for brevity, unchanged) ...
|
app.get('/api/notices', authenticateToken, async (req, res) => {
|
||||||
// (Assume existing Notices API is here as per previous code)
|
const { condoId } = req.query; // Usually filtered by condo
|
||||||
|
try {
|
||||||
|
let query = 'SELECT * FROM notices';
|
||||||
|
let params = [];
|
||||||
|
if (condoId) {
|
||||||
|
query += ' WHERE condo_id = ?';
|
||||||
|
params.push(condoId);
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
targetFamilyIds: r.target_families || []
|
||||||
|
})));
|
||||||
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/notices/unread', authenticateToken, async (req, res) => {
|
||||||
|
const { userId, condoId } = req.query;
|
||||||
|
try {
|
||||||
|
const [notices] = await pool.query('SELECT * FROM notices WHERE condo_id = ? AND active = TRUE ORDER BY date DESC', [condoId]);
|
||||||
|
const [reads] = await pool.query('SELECT notice_id FROM notice_reads WHERE user_id = ?', [userId]);
|
||||||
|
const readIds = new Set(reads.map(r => r.notice_id));
|
||||||
|
|
||||||
|
const [u] = await pool.query('SELECT family_id FROM users WHERE id = ?', [userId]);
|
||||||
|
const familyId = u[0]?.family_id;
|
||||||
|
|
||||||
|
const unread = notices.filter(n => {
|
||||||
|
if (readIds.has(n.id)) return false;
|
||||||
|
// Target Check
|
||||||
|
if (n.target_families && n.target_families.length > 0) {
|
||||||
|
if (!familyId) return false;
|
||||||
|
// n.target_families is parsed by mysql2 driver if column type is JSON
|
||||||
|
// However, pg might need manual parsing if not automatic.
|
||||||
|
// Let's assume it's array.
|
||||||
|
const targets = (typeof n.target_families === 'string') ? JSON.parse(n.target_families) : n.target_families;
|
||||||
|
return Array.isArray(targets) && targets.includes(familyId);
|
||||||
|
}
|
||||||
|
return true; // Public
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(unread.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
|
||||||
|
})));
|
||||||
|
|
||||||
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/notices/:id/read-status', authenticateToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const [rows] = await pool.query('SELECT * FROM notice_reads WHERE notice_id = ?', [req.params.id]);
|
||||||
|
res.json(rows.map(r => ({ userId: r.user_id, noticeId: r.notice_id, readAt: r.read_at })));
|
||||||
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/notices/:id/read', authenticateToken, async (req, res) => {
|
||||||
|
const { userId } = req.body;
|
||||||
|
try {
|
||||||
|
await pool.query('INSERT IGNORE INTO notice_reads (user_id, notice_id) VALUES (?, ?)', [userId, req.params.id]);
|
||||||
|
res.json({ success: true });
|
||||||
|
} 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, targetFamilyIds } = req.body;
|
||||||
|
const id = uuidv4();
|
||||||
|
try {
|
||||||
|
await pool.query(
|
||||||
|
'INSERT INTO notices (id, condo_id, title, content, type, link, active, target_families) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||||
|
[id, condoId, title, content, type, link, active, JSON.stringify(targetFamilyIds)]
|
||||||
|
);
|
||||||
|
res.json({ success: true });
|
||||||
|
} 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, targetFamilyIds } = req.body;
|
||||||
|
try {
|
||||||
|
await pool.query(
|
||||||
|
'UPDATE notices SET title = ?, content = ?, type = ?, link = ?, active = ?, target_families = ? WHERE id = ?',
|
||||||
|
[title, content, type, link, active, JSON.stringify(targetFamilyIds), 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]);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
// --- PAYMENTS ---
|
// --- PAYMENTS ---
|
||||||
// ... (Payments API skipped for brevity, unchanged) ...
|
app.get('/api/payments', authenticateToken, async (req, res) => {
|
||||||
|
const { familyId, condoId } = req.query;
|
||||||
|
try {
|
||||||
|
let query = 'SELECT p.* FROM payments p JOIN families f ON p.family_id = f.id';
|
||||||
|
let params = [];
|
||||||
|
let conditions = [];
|
||||||
|
|
||||||
|
if (familyId) {
|
||||||
|
conditions.push('p.family_id = ?');
|
||||||
|
params.push(familyId);
|
||||||
|
}
|
||||||
|
if (condoId) {
|
||||||
|
conditions.push('f.condo_id = ?');
|
||||||
|
params.push(condoId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.length > 0) {
|
||||||
|
query += ' WHERE ' + conditions.join(' AND ');
|
||||||
|
}
|
||||||
|
|
||||||
|
query += ' ORDER BY p.date_paid DESC';
|
||||||
|
|
||||||
|
const [rows] = await pool.query(query, params);
|
||||||
|
res.json(rows.map(r => ({
|
||||||
|
id: r.id,
|
||||||
|
familyId: r.family_id,
|
||||||
|
amount: parseFloat(r.amount),
|
||||||
|
datePaid: r.date_paid,
|
||||||
|
forMonth: r.for_month,
|
||||||
|
forYear: r.for_year,
|
||||||
|
notes: r.notes
|
||||||
|
})));
|
||||||
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/payments', authenticateToken, async (req, res) => {
|
||||||
|
const { familyId, amount, datePaid, forMonth, forYear, notes } = req.body;
|
||||||
|
// Allow users to pay for themselves or admin to pay for anyone
|
||||||
|
if (req.user.role !== 'admin' && req.user.role !== 'poweruser' && req.user.familyId !== familyId) {
|
||||||
|
return res.status(403).json({ message: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = uuidv4();
|
||||||
|
try {
|
||||||
|
await pool.query(
|
||||||
|
'INSERT INTO payments (id, family_id, amount, date_paid, for_month, for_year, notes) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||||
|
[id, familyId, amount, datePaid, forMonth, forYear, notes]
|
||||||
|
);
|
||||||
|
res.json({ id, familyId, amount, datePaid, forMonth, forYear, notes });
|
||||||
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
// --- USERS ---
|
// --- USERS ---
|
||||||
// ... (Users API skipped for brevity, unchanged) ...
|
app.get('/api/users', authenticateToken, async (req, res) => {
|
||||||
|
const { condoId } = req.query;
|
||||||
|
try {
|
||||||
|
let query = 'SELECT id, email, name, role, phone, family_id, receive_alerts, created_at FROM users';
|
||||||
|
let params = [];
|
||||||
|
// If condoId provided, we filter users belonging to families in that condo
|
||||||
|
if (condoId) {
|
||||||
|
query = `
|
||||||
|
SELECT u.id, u.email, u.name, u.role, u.phone, u.family_id, u.receive_alerts, u.created_at
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN families f ON u.family_id = f.id
|
||||||
|
WHERE f.condo_id = ? OR u.role IN ('admin', 'poweruser')
|
||||||
|
`;
|
||||||
|
params = [condoId];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rows] = await pool.query(query, params);
|
||||||
|
res.json(rows.map(u => ({
|
||||||
|
id: u.id,
|
||||||
|
email: u.email,
|
||||||
|
name: u.name,
|
||||||
|
role: u.role,
|
||||||
|
phone: u.phone,
|
||||||
|
familyId: u.family_id,
|
||||||
|
receiveAlerts: !!u.receive_alerts
|
||||||
|
})));
|
||||||
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/users', authenticateToken, requireAdmin, async (req, res) => {
|
||||||
|
const { email, password, name, role, phone, familyId, receiveAlerts } = req.body;
|
||||||
|
const id = uuidv4();
|
||||||
|
try {
|
||||||
|
const hashedPassword = await bcrypt.hash(password || 'password', 10);
|
||||||
|
await pool.query(
|
||||||
|
'INSERT INTO users (id, email, password_hash, name, role, phone, family_id, receive_alerts) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||||
|
[id, email, hashedPassword, name, role, phone, familyId || null, receiveAlerts]
|
||||||
|
);
|
||||||
|
res.json({ success: true, id });
|
||||||
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/users/:id', authenticateToken, requireAdmin, async (req, res) => {
|
||||||
|
const { email, name, role, phone, familyId, receiveAlerts, password } = req.body;
|
||||||
|
try {
|
||||||
|
let query = 'UPDATE users SET email = ?, name = ?, role = ?, phone = ?, family_id = ?, receive_alerts = ?';
|
||||||
|
let params = [email, name, role, phone, familyId || null, receiveAlerts];
|
||||||
|
|
||||||
|
if (password) {
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
query += ', password_hash = ?';
|
||||||
|
params.push(hashedPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
query += ' WHERE id = ?';
|
||||||
|
params.push(req.params.id);
|
||||||
|
|
||||||
|
await pool.query(query, params);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/users/:id', authenticateToken, requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
await pool.query('DELETE FROM users WHERE id = ?', [req.params.id]);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
// --- ALERTS ---
|
// --- ALERTS ---
|
||||||
// ... (Alerts API skipped for brevity, unchanged) ...
|
app.get('/api/alerts', authenticateToken, async (req, res) => {
|
||||||
|
const { condoId } = req.query;
|
||||||
|
try {
|
||||||
|
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 { condoId, subject, body, daysOffset, offsetType, sendHour, active } = req.body;
|
||||||
|
const id = uuidv4();
|
||||||
|
try {
|
||||||
|
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) => {
|
||||||
|
const { subject, body, daysOffset, offsetType, sendHour, active } = req.body;
|
||||||
|
try {
|
||||||
|
await pool.query(
|
||||||
|
'UPDATE alerts SET subject = ?, body = ?, days_offset = ?, offset_type = ?, send_hour = ?, active = ? WHERE id = ?',
|
||||||
|
[subject, body, daysOffset, offsetType, sendHour, active, req.params.id]
|
||||||
|
);
|
||||||
|
res.json({ success: true, id: req.params.id });
|
||||||
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/alerts/:id', authenticateToken, requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
await pool.query('DELETE FROM alerts WHERE id = ?', [req.params.id]);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
// --- TICKETS ---
|
// --- TICKETS ---
|
||||||
// ... (Tickets API skipped for brevity, unchanged) ...
|
app.get('/api/tickets', authenticateToken, async (req, res) => {
|
||||||
|
const { condoId } = req.query;
|
||||||
|
try {
|
||||||
|
let query = `
|
||||||
|
SELECT t.*, u.name as user_name, u.email as user_email
|
||||||
|
FROM tickets t
|
||||||
|
JOIN users u ON t.user_id = u.id
|
||||||
|
`;
|
||||||
|
let params = [];
|
||||||
|
|
||||||
|
if (req.user.role !== 'admin' && req.user.role !== 'poweruser') {
|
||||||
|
query += ' WHERE t.user_id = ?';
|
||||||
|
params.push(req.user.id);
|
||||||
|
} else if (condoId) {
|
||||||
|
query += ' WHERE t.condo_id = ?';
|
||||||
|
params.push(condoId);
|
||||||
|
}
|
||||||
|
|
||||||
|
query += ' ORDER BY t.created_at DESC';
|
||||||
|
|
||||||
|
const [rows] = await pool.query(query, params);
|
||||||
|
|
||||||
|
const ticketIds = rows.map(r => r.id);
|
||||||
|
let attachments = [];
|
||||||
|
if (ticketIds.length > 0) {
|
||||||
|
const [atts] = await pool.query(`SELECT id, ticket_id FROM ticket_attachments`);
|
||||||
|
attachments = atts;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = rows.map(r => ({
|
||||||
|
id: r.id,
|
||||||
|
condoId: r.condo_id,
|
||||||
|
userId: r.user_id,
|
||||||
|
title: r.title,
|
||||||
|
description: r.description,
|
||||||
|
status: r.status,
|
||||||
|
priority: r.priority,
|
||||||
|
category: r.category,
|
||||||
|
createdAt: r.created_at,
|
||||||
|
updatedAt: r.updated_at,
|
||||||
|
userName: r.user_name,
|
||||||
|
userEmail: r.user_email,
|
||||||
|
attachments: attachments.filter(a => a.ticket_id === r.id)
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json(result);
|
||||||
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/tickets', authenticateToken, async (req, res) => {
|
||||||
|
const { condoId, title, description, category, priority, attachments } = req.body;
|
||||||
|
const id = uuidv4();
|
||||||
|
const connection = await pool.getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connection.beginTransaction();
|
||||||
|
await connection.query(
|
||||||
|
'INSERT INTO tickets (id, condo_id, user_id, title, description, category, priority) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||||
|
[id, condoId, req.user.id, title, description, category, priority]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (attachments && attachments.length > 0) {
|
||||||
|
for(const att of attachments) {
|
||||||
|
await connection.query(
|
||||||
|
'INSERT INTO ticket_attachments (id, ticket_id, file_name, file_type, data) VALUES (?, ?, ?, ?, ?)',
|
||||||
|
[uuidv4(), id, att.fileName, att.fileType, att.data]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await connection.commit();
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch(e) {
|
||||||
|
await connection.rollback();
|
||||||
|
res.status(500).json({ error: e.message });
|
||||||
|
} finally {
|
||||||
|
connection.release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/tickets/:id', authenticateToken, async (req, res) => {
|
||||||
|
const { status, priority } = req.body;
|
||||||
|
try {
|
||||||
|
await pool.query('UPDATE tickets SET status = ?, priority = ? WHERE id = ?', [status, priority, req.params.id]);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/tickets/:id', authenticateToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
await pool.query('DELETE FROM tickets WHERE id = ?', [req.params.id]);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/tickets/:id/comments', authenticateToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const [rows] = await pool.query(`
|
||||||
|
SELECT c.*, u.name as user_name
|
||||||
|
FROM ticket_comments c
|
||||||
|
JOIN users u ON c.user_id = u.id
|
||||||
|
WHERE c.ticket_id = ?
|
||||||
|
ORDER BY c.created_at ASC
|
||||||
|
`, [req.params.id]);
|
||||||
|
|
||||||
|
res.json(rows.map(r => ({
|
||||||
|
id: r.id,
|
||||||
|
ticketId: r.ticket_id,
|
||||||
|
userId: r.user_id,
|
||||||
|
userName: r.user_name,
|
||||||
|
text: r.text,
|
||||||
|
createdAt: r.created_at
|
||||||
|
})));
|
||||||
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/tickets/:id/comments', authenticateToken, async (req, res) => {
|
||||||
|
const { text } = req.body;
|
||||||
|
const id = uuidv4();
|
||||||
|
try {
|
||||||
|
await pool.query(
|
||||||
|
'INSERT INTO ticket_comments (id, ticket_id, user_id, text) VALUES (?, ?, ?, ?)',
|
||||||
|
[id, req.params.id, req.user.id, text]
|
||||||
|
);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/tickets/:id/attachments/:attId', authenticateToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const [rows] = await pool.query('SELECT * FROM ticket_attachments WHERE id = ?', [req.params.attId]);
|
||||||
|
if (rows.length === 0) return res.status(404).json({ message: 'File not found' });
|
||||||
|
const file = rows[0];
|
||||||
|
res.json({ id: file.id, fileName: file.file_name, fileType: file.file_type, data: file.data });
|
||||||
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
// --- EXTRAORDINARY EXPENSES ---
|
// --- EXTRAORDINARY EXPENSES ---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user