Introduces a new module to manage and track extraordinary expenses within condominiums. This includes defining expense items, sharing arrangements, and attaching relevant documents. The module adds new types for `ExpenseItem`, `ExpenseShare`, and `ExtraordinaryExpense`. Mock database functions are updated to support fetching, creating, and managing these expenses. UI components in `Layout.tsx` and `Settings.tsx` are modified to include navigation and feature toggling for extraordinary expenses. Additionally, new routes are added in `App.tsx` for both administrative and user-facing views of these expenses.
1068 lines
44 KiB
JavaScript
1068 lines
44 KiB
JavaScript
|
|
const express = require('express');
|
|
const cors = require('cors');
|
|
const bodyParser = require('body-parser');
|
|
const bcrypt = require('bcryptjs');
|
|
const jwt = require('jsonwebtoken');
|
|
const { pool, initDb } = require('./db');
|
|
const { v4: uuidv4 } = require('uuid');
|
|
const nodemailer = require('nodemailer');
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3001;
|
|
const JWT_SECRET = process.env.JWT_SECRET || 'dev_secret_key_123';
|
|
|
|
app.use(cors());
|
|
// Increased limit to support base64 file uploads for tickets
|
|
app.use(bodyParser.json({ limit: '50mb' }));
|
|
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
|
|
|
// --- EMAIL HELPERS ---
|
|
|
|
async function getTransporter() {
|
|
const [settings] = await pool.query('SELECT smtp_config FROM settings WHERE id = 1');
|
|
if (!settings.length || !settings[0].smtp_config) return null;
|
|
|
|
const config = settings[0].smtp_config;
|
|
if (!config.host || !config.user || !config.pass) return null;
|
|
|
|
return {
|
|
transporter: nodemailer.createTransport({
|
|
host: config.host,
|
|
port: config.port,
|
|
secure: config.secure,
|
|
auth: { user: config.user, pass: config.pass },
|
|
}),
|
|
from: config.fromEmail || config.user
|
|
};
|
|
}
|
|
|
|
async function sendEmailToUsers(subject, body) {
|
|
try {
|
|
const setup = await getTransporter();
|
|
if (!setup) return;
|
|
|
|
const [users] = await pool.query('SELECT email FROM users WHERE receive_alerts = TRUE AND email IS NOT NULL AND email != ""');
|
|
if (users.length === 0) return;
|
|
const bccList = users.map(u => u.email).join(',');
|
|
|
|
await setup.transporter.sendMail({
|
|
from: setup.from,
|
|
bcc: bccList,
|
|
subject: subject,
|
|
text: body,
|
|
});
|
|
} catch (error) { console.error('Email error:', error.message); }
|
|
}
|
|
|
|
async function sendDirectEmail(to, subject, body) {
|
|
try {
|
|
const setup = await getTransporter();
|
|
if (!setup) return;
|
|
|
|
await setup.transporter.sendMail({
|
|
from: setup.from,
|
|
to: to,
|
|
subject: subject,
|
|
text: body
|
|
});
|
|
console.log(`Direct email sent to ${to}`);
|
|
} catch (error) { console.error('Direct Email error:', error.message); }
|
|
}
|
|
|
|
// --- MIDDLEWARE ---
|
|
const authenticateToken = (req, res, next) => {
|
|
const authHeader = req.headers['authorization'];
|
|
const token = authHeader && authHeader.split(' ')[1];
|
|
|
|
if (!token) return res.sendStatus(401);
|
|
|
|
jwt.verify(token, JWT_SECRET, (err, user) => {
|
|
// Return 401 for token errors (expired/invalid) to trigger frontend logout
|
|
if (err) {
|
|
console.error("Token verification failed:", err.message);
|
|
return res.sendStatus(401);
|
|
}
|
|
req.user = user;
|
|
next();
|
|
});
|
|
};
|
|
|
|
const requireAdmin = (req, res, next) => {
|
|
// Allow both 'admin' and 'poweruser' to access administrative routes
|
|
if (req.user && (req.user.role === 'admin' || req.user.role === 'poweruser')) {
|
|
next();
|
|
} else {
|
|
console.warn(`Access denied for user ${req.user?.email} with role ${req.user?.role}`);
|
|
res.status(403).json({ message: 'Access denied: Privileged users only' });
|
|
}
|
|
};
|
|
|
|
// --- AUTH ---
|
|
app.post('/api/auth/login', async (req, res) => {
|
|
const { email, password } = req.body;
|
|
try {
|
|
const [users] = await pool.query('SELECT * FROM users WHERE email = ?', [email]);
|
|
if (users.length === 0) return res.status(401).json({ message: 'Invalid credentials' });
|
|
const user = users[0];
|
|
const validPassword = await bcrypt.compare(password, user.password_hash);
|
|
if (!validPassword) return res.status(401).json({ message: 'Invalid credentials' });
|
|
|
|
// Ensure role is captured correctly from DB
|
|
const token = jwt.sign(
|
|
{ id: user.id, email: user.email, role: user.role, familyId: user.family_id },
|
|
JWT_SECRET, { expiresIn: '24h' }
|
|
);
|
|
res.json({
|
|
token,
|
|
user: { id: user.id, email: user.email, name: user.name, role: user.role, familyId: user.family_id, receiveAlerts: !!user.receive_alerts }
|
|
});
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
// --- PROFILE ---
|
|
app.put('/api/profile', authenticateToken, async (req, res) => {
|
|
const userId = req.user.id;
|
|
const { name, phone, password, receiveAlerts } = req.body;
|
|
try {
|
|
let query = 'UPDATE users SET name = ?, phone = ?, receive_alerts = ?';
|
|
let params = [name, phone, receiveAlerts];
|
|
if (password && password.trim() !== '') {
|
|
const hashedPassword = await bcrypt.hash(password, 10);
|
|
query += ', password_hash = ?';
|
|
params.push(hashedPassword);
|
|
}
|
|
query += ' WHERE id = ?';
|
|
params.push(userId);
|
|
await pool.query(query, params);
|
|
const [updatedUser] = await pool.query('SELECT id, email, name, role, phone, family_id, receive_alerts FROM users WHERE id = ?', [userId]);
|
|
res.json({ success: true, user: { ...updatedUser[0], receiveAlerts: !!updatedUser[0].receive_alerts } });
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
// --- SETTINGS ---
|
|
app.get('/api/settings', authenticateToken, async (req, res) => {
|
|
try {
|
|
const [rows] = await pool.query('SELECT * FROM settings WHERE id = 1');
|
|
if (rows.length > 0) {
|
|
res.json({
|
|
currentYear: rows[0].current_year,
|
|
smtpConfig: rows[0].smtp_config || {},
|
|
features: rows[0].features || { multiCondo: true, tickets: true, payPal: true, notices: true, reports: true }
|
|
});
|
|
} else { res.status(404).json({ message: 'Settings not found' }); }
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
app.put('/api/settings', authenticateToken, requireAdmin, async (req, res) => {
|
|
const { currentYear, smtpConfig, features } = req.body;
|
|
try {
|
|
await pool.query(
|
|
'UPDATE settings SET current_year = ?, smtp_config = ?, features = ? WHERE id = 1',
|
|
[currentYear, JSON.stringify(smtpConfig), JSON.stringify(features)]
|
|
);
|
|
res.json({ success: true });
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
// SMTP TEST
|
|
app.post('/api/settings/smtp-test', authenticateToken, requireAdmin, async (req, res) => {
|
|
const config = req.body; // Expects SmtpConfig object
|
|
const userEmail = req.user.email;
|
|
|
|
try {
|
|
if (!config.host || !config.user || !config.pass) {
|
|
return res.status(400).json({ message: 'Parametri SMTP incompleti' });
|
|
}
|
|
|
|
const transporter = nodemailer.createTransport({
|
|
host: config.host,
|
|
port: config.port,
|
|
secure: config.secure,
|
|
auth: { user: config.user, pass: config.pass },
|
|
});
|
|
|
|
// Verify connection
|
|
await transporter.verify();
|
|
|
|
// Send Test Email
|
|
await transporter.sendMail({
|
|
from: config.fromEmail || config.user,
|
|
to: userEmail,
|
|
subject: 'CondoPay - Test Configurazione SMTP',
|
|
text: 'Se leggi questo messaggio, la configurazione SMTP è corretta.',
|
|
});
|
|
|
|
res.json({ success: true });
|
|
} catch (e) {
|
|
console.error("SMTP Test Error", e);
|
|
res.status(400).json({ message: e.message });
|
|
}
|
|
});
|
|
|
|
app.get('/api/years', authenticateToken, async (req, res) => {
|
|
try {
|
|
const [rows] = await pool.query('SELECT DISTINCT for_year FROM payments ORDER BY for_year DESC');
|
|
const [settings] = await pool.query('SELECT current_year FROM settings WHERE id = 1');
|
|
const years = new Set(rows.map(r => r.for_year));
|
|
if (settings.length > 0) years.add(settings[0].current_year);
|
|
res.json(Array.from(years).sort((a, b) => b - a));
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
// --- CONDOS ---
|
|
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,
|
|
streetNumber: r.street_number,
|
|
city: r.city,
|
|
province: r.province,
|
|
zipCode: r.zip_code,
|
|
notes: r.notes,
|
|
iban: r.iban,
|
|
paypalClientId: r.paypal_client_id, // PayPal
|
|
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, streetNumber, city, province, zipCode, notes, defaultMonthlyQuota, paypalClientId } = req.body;
|
|
const id = uuidv4();
|
|
try {
|
|
await pool.query(
|
|
'INSERT INTO condos (id, name, address, street_number, city, province, zip_code, notes, default_monthly_quota, paypal_client_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
[id, name, address, streetNumber, city, province, zipCode, notes, defaultMonthlyQuota, paypalClientId]
|
|
);
|
|
res.json({ id, name, address, streetNumber, city, province, zipCode, notes, defaultMonthlyQuota, paypalClientId });
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
app.put('/api/condos/:id', authenticateToken, requireAdmin, async (req, res) => {
|
|
const { name, address, streetNumber, city, province, zipCode, notes, defaultMonthlyQuota, paypalClientId } = req.body;
|
|
try {
|
|
await pool.query(
|
|
'UPDATE condos SET name = ?, address = ?, street_number = ?, city = ?, province = ?, zip_code = ?, notes = ?, default_monthly_quota = ?, paypal_client_id = ? WHERE id = ?',
|
|
[name, address, streetNumber, city, province, zipCode, notes, defaultMonthlyQuota, paypalClientId, req.params.id]
|
|
);
|
|
res.json({ success: true });
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
app.delete('/api/condos/:id', authenticateToken, requireAdmin, async (req, res) => {
|
|
try {
|
|
await pool.query('DELETE FROM condos WHERE id = ?', [req.params.id]);
|
|
res.json({ success: true });
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
// --- 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,
|
|
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, stair, floor, notes, contactEmail, condoId, customMonthlyQuota } = req.body;
|
|
const id = uuidv4();
|
|
try {
|
|
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, stair, floor, notes, contactEmail, customMonthlyQuota } = req.body;
|
|
try {
|
|
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 }); }
|
|
});
|
|
app.delete('/api/families/:id', authenticateToken, requireAdmin, async (req, res) => {
|
|
try {
|
|
await pool.query('DELETE FROM families WHERE id = ?', [req.params.id]);
|
|
res.json({ success: true });
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
// --- NOTICES ---
|
|
app.get('/api/notices', authenticateToken, async (req, res) => {
|
|
const { condoId } = req.query;
|
|
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 ? (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, targetFamilyIds } = req.body;
|
|
const id = uuidv4();
|
|
try {
|
|
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, targetFamilyIds } = req.body;
|
|
try {
|
|
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]);
|
|
res.json({ success: true });
|
|
} 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, read_at) VALUES (?, ?, NOW())', [userId, req.params.id]);
|
|
res.json({ success: true });
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
app.get('/api/notices/:id/reads', 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.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]);
|
|
|
|
// 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 }); }
|
|
});
|
|
|
|
// --- PAYMENTS ---
|
|
app.get('/api/payments', authenticateToken, async (req, res) => {
|
|
const { familyId, condoId } = req.query;
|
|
try {
|
|
const isPrivileged = req.user.role === 'admin' || req.user.role === 'poweruser';
|
|
if (!isPrivileged) {
|
|
if (familyId && familyId !== req.user.familyId) return res.status(403).json({ message: 'Forbidden' });
|
|
if (!familyId) {
|
|
const [rows] = await pool.query('SELECT * FROM payments WHERE family_id = ?', [req.user.familyId]);
|
|
return res.json(rows.map(mapPaymentRow));
|
|
}
|
|
}
|
|
|
|
let query = 'SELECT p.* FROM payments p';
|
|
let params = [];
|
|
|
|
// If condoId provided, we need to JOIN with families to filter
|
|
if (condoId) {
|
|
query += ' JOIN families f ON p.family_id = f.id WHERE f.condo_id = ?';
|
|
params.push(condoId);
|
|
|
|
if (familyId) {
|
|
query += ' AND p.family_id = ?';
|
|
params.push(familyId);
|
|
}
|
|
} else if (familyId) {
|
|
query += ' WHERE p.family_id = ?';
|
|
params.push(familyId);
|
|
}
|
|
|
|
// Sort by date (newest first)
|
|
query += ' ORDER BY p.date_paid DESC';
|
|
|
|
const [rows] = await pool.query(query, params);
|
|
res.json(rows.map(mapPaymentRow));
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
function mapPaymentRow(r) { return { 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 }; }
|
|
|
|
app.post('/api/payments', authenticateToken, async (req, res) => {
|
|
const { familyId, amount, datePaid, forMonth, forYear, notes } = req.body;
|
|
|
|
// Security Check:
|
|
// Admin can post for anyone.
|
|
// Regular users can only post for their own family (e.g. PayPal automated callback)
|
|
const isPrivileged = req.user.role === 'admin' || req.user.role === 'poweruser';
|
|
if (!isPrivileged) {
|
|
if (familyId !== req.user.familyId) {
|
|
return res.status(403).json({message: "Forbidden: You can only record payments for your own family."});
|
|
}
|
|
}
|
|
|
|
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, new Date(datePaid), forMonth, forYear, notes]);
|
|
res.json({ id, familyId, amount, datePaid, forMonth, forYear, notes });
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
// --- USERS ---
|
|
app.get('/api/users', authenticateToken, requireAdmin, async (req, res) => {
|
|
const { condoId } = req.query;
|
|
try {
|
|
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 }); }
|
|
});
|
|
app.post('/api/users', authenticateToken, requireAdmin, async (req, res) => {
|
|
const { email, password, name, role, familyId, phone, receiveAlerts } = req.body;
|
|
try {
|
|
const hashedPassword = await bcrypt.hash(password, 10);
|
|
const id = uuidv4();
|
|
await pool.query('INSERT INTO users (id, email, password_hash, name, role, family_id, phone, receive_alerts) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [id, email, hashedPassword, name, role || 'user', familyId || null, phone, 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, role, familyId, name, phone, password, receiveAlerts } = req.body;
|
|
try {
|
|
let query = 'UPDATE users SET email = ?, role = ?, family_id = ?, name = ?, phone = ?, receive_alerts = ?';
|
|
let params = [email, role, familyId || null, name, phone, receiveAlerts];
|
|
if (password && password.trim() !== '') {
|
|
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 ---
|
|
app.get('/api/alerts', authenticateToken, requireAdmin, 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 });
|
|
} 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 (SEGNALAZIONI) ---
|
|
app.get('/api/tickets', authenticateToken, async (req, res) => {
|
|
const { condoId } = req.query;
|
|
const userId = req.user.id;
|
|
const isAdmin = req.user.role === 'admin' || req.user.role === 'poweruser';
|
|
|
|
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
|
|
WHERE t.condo_id = ?
|
|
`;
|
|
let params = [condoId];
|
|
|
|
// If not admin, restrict to own tickets
|
|
if (!isAdmin) {
|
|
query += ' AND t.user_id = ?';
|
|
params.push(userId);
|
|
}
|
|
|
|
query += ' ORDER BY t.created_at DESC';
|
|
|
|
const [rows] = await pool.query(query, params);
|
|
|
|
// Fetch attachments for these tickets
|
|
const ticketIds = rows.map(r => r.id);
|
|
let attachmentsMap = {};
|
|
|
|
if (ticketIds.length > 0) {
|
|
const placeholders = ticketIds.map(() => '?').join(',');
|
|
// Exclude 'data' column to keep listing light
|
|
const [attRows] = await pool.query(`SELECT id, ticket_id, file_name, file_type FROM ticket_attachments WHERE ticket_id IN (${placeholders})`, ticketIds);
|
|
|
|
attRows.forEach(a => {
|
|
if (!attachmentsMap[a.ticket_id]) attachmentsMap[a.ticket_id] = [];
|
|
attachmentsMap[a.ticket_id].push({ id: a.id, fileName: a.file_name, fileType: a.file_type });
|
|
});
|
|
}
|
|
|
|
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: attachmentsMap[r.id] || []
|
|
}));
|
|
|
|
res.json(result);
|
|
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
app.get('/api/tickets/:id/attachments/:attachmentId', authenticateToken, async (req, res) => {
|
|
// Serve file content
|
|
try {
|
|
const [rows] = await pool.query('SELECT * FROM ticket_attachments WHERE id = ? AND ticket_id = ?', [req.params.attachmentId, req.params.id]);
|
|
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 }); }
|
|
});
|
|
|
|
app.get('/api/tickets/:id/comments', authenticateToken, async (req, res) => {
|
|
try {
|
|
const [rows] = await pool.query(`
|
|
SELECT c.*, u.name as user_name, u.role as user_role
|
|
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,
|
|
isAdminResponse: r.user_role === 'admin' || r.user_role === 'poweruser'
|
|
})));
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
app.post('/api/tickets/:id/comments', authenticateToken, async (req, res) => {
|
|
const { text } = req.body;
|
|
const userId = req.user.id;
|
|
const ticketId = req.params.id;
|
|
const commentId = uuidv4();
|
|
const isAdmin = req.user.role === 'admin' || req.user.role === 'poweruser';
|
|
|
|
try {
|
|
await pool.query(
|
|
'INSERT INTO ticket_comments (id, ticket_id, user_id, text) VALUES (?, ?, ?, ?)',
|
|
[commentId, ticketId, userId, text]
|
|
);
|
|
|
|
// --- EMAIL NOTIFICATION LOGIC ---
|
|
// 1. Get ticket info to know who to notify
|
|
const [ticketRows] = await pool.query(`
|
|
SELECT t.title, t.user_id, u.email as creator_email, u.receive_alerts as creator_alerts
|
|
FROM tickets t
|
|
JOIN users u ON t.user_id = u.id
|
|
WHERE t.id = ?
|
|
`, [ticketId]);
|
|
|
|
if (ticketRows.length > 0) {
|
|
const ticket = ticketRows[0];
|
|
const subject = `Nuovo commento sul ticket: ${ticket.title}`;
|
|
|
|
// If ADMIN replied -> Notify Creator
|
|
if (isAdmin && ticket.creator_email && ticket.creator_alerts) {
|
|
const body = `Salve,\n\nÈ stato aggiunto un nuovo commento al tuo ticket "${ticket.title}".\n\nCommento:\n${text}\n\nAccedi alla piattaforma per rispondere.`;
|
|
sendDirectEmail(ticket.creator_email, subject, body);
|
|
}
|
|
// If CREATOR replied -> Notify Admins (logic similar to new ticket)
|
|
else if (!isAdmin) {
|
|
const [admins] = await pool.query(`
|
|
SELECT u.email FROM users u
|
|
LEFT JOIN families f ON u.family_id = f.id
|
|
JOIN tickets t ON t.id = ?
|
|
WHERE (u.role = 'admin' OR u.role = 'poweruser')
|
|
AND (f.condo_id = t.condo_id OR u.family_id IS NULL)
|
|
AND u.receive_alerts = TRUE
|
|
`, [ticketId]);
|
|
|
|
const body = `Salve,\n\nNuova risposta dall'utente sul ticket "${ticket.title}".\n\nCommento:\n${text}\n\nAccedi alla piattaforma per gestire.`;
|
|
for(const admin of admins) {
|
|
if (admin.email) sendDirectEmail(admin.email, subject, body);
|
|
}
|
|
}
|
|
}
|
|
|
|
res.json({ success: true, id: commentId });
|
|
} 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 userId = req.user.id;
|
|
const ticketId = uuidv4();
|
|
|
|
// Begin transaction
|
|
const connection = await pool.getConnection();
|
|
try {
|
|
await connection.beginTransaction();
|
|
|
|
await connection.query(
|
|
'INSERT INTO tickets (id, condo_id, user_id, title, description, category, priority, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
|
[ticketId, condoId, userId, title, description, category, priority || 'MEDIUM', 'OPEN']
|
|
);
|
|
|
|
if (attachments && Array.isArray(attachments)) {
|
|
for (const att of attachments) {
|
|
const attId = uuidv4();
|
|
await connection.query(
|
|
'INSERT INTO ticket_attachments (id, ticket_id, file_name, file_type, data) VALUES (?, ?, ?, ?, ?)',
|
|
[attId, ticketId, att.fileName, att.fileType, att.data]
|
|
);
|
|
}
|
|
}
|
|
|
|
await connection.commit();
|
|
|
|
// --- EMAIL NOTIFICATION TO ADMINS ---
|
|
// Find Admins/PowerUsers for this condo (or global) who want alerts
|
|
const [admins] = await connection.query(`
|
|
SELECT u.email FROM users u
|
|
LEFT JOIN families f ON u.family_id = f.id
|
|
WHERE (u.role = 'admin' OR u.role = 'poweruser')
|
|
AND (f.condo_id = ? OR u.family_id IS NULL)
|
|
AND u.receive_alerts = TRUE
|
|
`, [condoId]);
|
|
|
|
const adminEmails = admins.map(a => a.email).filter(e => e);
|
|
if (adminEmails.length > 0) {
|
|
// Fetch user name for clearer email
|
|
const [uRows] = await connection.query('SELECT name FROM users WHERE id = ?', [userId]);
|
|
const userName = uRows[0]?.name || 'Un condomino';
|
|
|
|
const subject = `Nuova Segnalazione: ${title}`;
|
|
const body = `Salve,\n\n${userName} ha aperto una nuova segnalazione.\n\nOggetto: ${title}\nCategoria: ${category}\nPriorità: ${priority || 'MEDIUM'}\n\nDescrizione:\n${description}\n\nAccedi alla piattaforma per gestire il ticket.`;
|
|
|
|
// Loop to send individually or use BCC
|
|
for(const email of adminEmails) {
|
|
sendDirectEmail(email, subject, body);
|
|
}
|
|
}
|
|
|
|
res.json({ success: true, id: ticketId });
|
|
|
|
} 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;
|
|
const isAdmin = req.user.role === 'admin' || req.user.role === 'poweruser';
|
|
|
|
// Only admins/powerusers can change status/priority for now
|
|
if (!isAdmin) return res.status(403).json({ message: 'Forbidden' });
|
|
|
|
try {
|
|
await pool.query(
|
|
'UPDATE tickets SET status = ?, priority = ? WHERE id = ?',
|
|
[status, priority, req.params.id]
|
|
);
|
|
|
|
// --- EMAIL NOTIFICATION TO USER ---
|
|
const [tRows] = await pool.query('SELECT t.title, t.user_id, u.email, u.receive_alerts FROM tickets t JOIN users u ON t.user_id = u.id WHERE t.id = ?', [req.params.id]);
|
|
if (tRows.length > 0) {
|
|
const ticket = tRows[0];
|
|
if (ticket.email && ticket.receive_alerts) {
|
|
const subject = `Aggiornamento Ticket: ${ticket.title}`;
|
|
const body = `Salve,\n\nIl tuo ticket "${ticket.title}" è stato aggiornato.\n\nNuovo Stato: ${status}\nPriorità: ${priority}\n\nAccedi alla piattaforma per i dettagli.`;
|
|
sendDirectEmail(ticket.email, subject, body);
|
|
}
|
|
}
|
|
|
|
res.json({ success: true });
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
app.delete('/api/tickets/:id', authenticateToken, async (req, res) => {
|
|
// Only delete own ticket if open, or admin can delete any
|
|
// MODIFIED: Prevent deletion if status is CLOSED or RESOLVED (Archived)
|
|
const isAdmin = req.user.role === 'admin' || req.user.role === 'poweruser';
|
|
const userId = req.user.id;
|
|
|
|
try {
|
|
// Check status first
|
|
const [rows] = await pool.query('SELECT status, user_id FROM tickets WHERE id = ?', [req.params.id]);
|
|
if (rows.length === 0) return res.status(404).json({ message: 'Ticket not found' });
|
|
|
|
const ticket = rows[0];
|
|
|
|
// Block deletion of Archived tickets
|
|
if (ticket.status === 'CLOSED' || ticket.status === 'RESOLVED') {
|
|
return res.status(403).json({ message: 'Cannot delete archived tickets. They are kept for history.' });
|
|
}
|
|
|
|
let query = 'DELETE FROM tickets WHERE id = ?';
|
|
let params = [req.params.id];
|
|
|
|
if (!isAdmin) {
|
|
// Additional check for user ownership
|
|
if (ticket.user_id !== userId) return res.status(403).json({ message: 'Forbidden' });
|
|
if (ticket.status !== 'OPEN') return res.status(403).json({ message: 'Can only delete OPEN tickets' });
|
|
}
|
|
|
|
await pool.query(query, params);
|
|
res.json({ success: true });
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
// --- EXTRAORDINARY EXPENSES ---
|
|
|
|
app.get('/api/expenses', authenticateToken, async (req, res) => {
|
|
const { condoId } = req.query;
|
|
try {
|
|
const [expenses] = await pool.query('SELECT * FROM extraordinary_expenses WHERE condo_id = ? ORDER BY created_at DESC', [condoId]);
|
|
res.json(expenses.map(e => ({
|
|
id: e.id,
|
|
condoId: e.condo_id,
|
|
title: e.title,
|
|
description: e.description,
|
|
startDate: e.start_date,
|
|
endDate: e.end_date,
|
|
contractorName: e.contractor_name,
|
|
totalAmount: parseFloat(e.total_amount),
|
|
createdAt: e.created_at
|
|
})));
|
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
app.get('/api/expenses/:id', authenticateToken, async (req, res) => {
|
|
try {
|
|
const [expenses] = await pool.query('SELECT * FROM extraordinary_expenses WHERE id = ?', [req.params.id]);
|
|
if (expenses.length === 0) return res.status(404).json({ message: 'Expense not found' });
|
|
const expense = expenses[0];
|
|
|
|
// Fetch Items
|
|
const [items] = await pool.query('SELECT id, description, amount FROM expense_items WHERE expense_id = ?', [expense.id]);
|
|
|
|
// Fetch Shares
|
|
const [shares] = await pool.query(`
|
|
SELECT s.id, s.family_id, f.name as family_name, s.percentage, s.amount_due, s.amount_paid, s.status
|
|
FROM expense_shares s
|
|
JOIN families f ON s.family_id = f.id
|
|
WHERE s.expense_id = ?
|
|
`, [expense.id]);
|
|
|
|
// Fetch Attachments (light)
|
|
const [attachments] = await pool.query('SELECT id, file_name, file_type FROM expense_attachments WHERE expense_id = ?', [expense.id]);
|
|
|
|
res.json({
|
|
id: expense.id,
|
|
condoId: expense.condo_id,
|
|
title: expense.title,
|
|
description: expense.description,
|
|
startDate: expense.start_date,
|
|
endDate: expense.end_date,
|
|
contractorName: expense.contractor_name,
|
|
totalAmount: parseFloat(expense.total_amount),
|
|
createdAt: expense.created_at,
|
|
items: items.map(i => ({ id: i.id, description: i.description, amount: parseFloat(i.amount) })),
|
|
shares: shares.map(s => ({
|
|
id: s.id,
|
|
familyId: s.family_id,
|
|
familyName: s.family_name,
|
|
percentage: parseFloat(s.percentage),
|
|
amountDue: parseFloat(s.amount_due),
|
|
amountPaid: parseFloat(s.amount_paid),
|
|
status: s.status
|
|
})),
|
|
attachments: attachments.map(a => ({ id: a.id, fileName: a.file_name, fileType: a.file_type }))
|
|
});
|
|
} catch(e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
app.post('/api/expenses', authenticateToken, requireAdmin, async (req, res) => {
|
|
const { condoId, title, description, startDate, endDate, contractorName, items, shares, attachments } = req.body;
|
|
const expenseId = uuidv4();
|
|
const connection = await pool.getConnection();
|
|
|
|
try {
|
|
await connection.beginTransaction();
|
|
|
|
// Calculate total
|
|
const totalAmount = items.reduce((sum, i) => sum + parseFloat(i.amount), 0);
|
|
|
|
// Insert Expense
|
|
await connection.query(
|
|
'INSERT INTO extraordinary_expenses (id, condo_id, title, description, start_date, end_date, contractor_name, total_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
|
[expenseId, condoId, title, description, startDate, endDate, contractorName, totalAmount]
|
|
);
|
|
|
|
// Insert Items
|
|
for (const item of items) {
|
|
await connection.query(
|
|
'INSERT INTO expense_items (id, expense_id, description, amount) VALUES (?, ?, ?, ?)',
|
|
[uuidv4(), expenseId, item.description, item.amount]
|
|
);
|
|
}
|
|
|
|
// Insert Shares
|
|
for (const share of shares) {
|
|
await connection.query(
|
|
'INSERT INTO expense_shares (id, expense_id, family_id, percentage, amount_due, status) VALUES (?, ?, ?, ?, ?, ?)',
|
|
[uuidv4(), expenseId, share.familyId, share.percentage, share.amountDue, 'UNPAID']
|
|
);
|
|
}
|
|
|
|
// Insert Attachments
|
|
if (attachments && attachments.length > 0) {
|
|
for (const att of attachments) {
|
|
await connection.query(
|
|
'INSERT INTO expense_attachments (id, expense_id, file_name, file_type, data) VALUES (?, ?, ?, ?, ?)',
|
|
[uuidv4(), expenseId, att.fileName, att.fileType, att.data]
|
|
);
|
|
}
|
|
}
|
|
|
|
await connection.commit();
|
|
res.json({ success: true, id: expenseId });
|
|
} catch (e) {
|
|
await connection.rollback();
|
|
console.error(e);
|
|
res.status(500).json({ error: e.message });
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
});
|
|
|
|
app.get('/api/expenses/:id/attachments/:attachmentId', authenticateToken, async (req, res) => {
|
|
try {
|
|
const [rows] = await pool.query('SELECT * FROM expense_attachments WHERE id = ? AND expense_id = ?', [req.params.attachmentId, req.params.id]);
|
|
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 }); }
|
|
});
|
|
|
|
app.post('/api/expenses/:id/pay', authenticateToken, async (req, res) => {
|
|
const { amount, notes } = req.body;
|
|
const expenseId = req.params.id;
|
|
const userId = req.user.id;
|
|
const connection = await pool.getConnection();
|
|
|
|
try {
|
|
// Find user's family
|
|
const [users] = await connection.query('SELECT family_id FROM users WHERE id = ?', [userId]);
|
|
if (users.length === 0 || !users[0].family_id) return res.status(400).json({ message: 'User has no family assigned' });
|
|
const familyId = users[0].family_id;
|
|
|
|
// Find share
|
|
const [shares] = await connection.query('SELECT * FROM expense_shares WHERE expense_id = ? AND family_id = ?', [expenseId, familyId]);
|
|
if (shares.length === 0) return res.status(404).json({ message: 'No share found for this expense' });
|
|
|
|
const share = shares[0];
|
|
const newPaid = parseFloat(share.amount_paid) + parseFloat(amount);
|
|
const due = parseFloat(share.amount_due);
|
|
|
|
let status = 'PARTIAL';
|
|
if (newPaid >= due - 0.01) status = 'PAID'; // Tolerance for float
|
|
|
|
await connection.beginTransaction();
|
|
|
|
// Update Share
|
|
await connection.query('UPDATE expense_shares SET amount_paid = ?, status = ? WHERE id = ?', [newPaid, status, share.id]);
|
|
|
|
// Also record in global payments for visibility in reports (optional but requested to track family payments)
|
|
// We use a special month/year or notes to distinguish
|
|
await connection.query(
|
|
'INSERT INTO payments (id, family_id, amount, date_paid, for_month, for_year, notes) VALUES (?, ?, ?, NOW(), 13, YEAR(NOW()), ?)',
|
|
[uuidv4(), familyId, amount, `Spesa Straordinaria: ${notes || 'PayPal'}`]
|
|
);
|
|
|
|
await connection.commit();
|
|
res.json({ success: true });
|
|
|
|
} catch (e) {
|
|
await connection.rollback();
|
|
res.status(500).json({ error: e.message });
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
});
|
|
|
|
// Get User's Expenses
|
|
app.get('/api/my-expenses', authenticateToken, async (req, res) => {
|
|
const userId = req.user.id;
|
|
const { condoId } = req.query; // Optional filter if user belongs to multiple condos (unlikely in current logic but safe)
|
|
|
|
try {
|
|
const [users] = await pool.query('SELECT family_id FROM users WHERE id = ?', [userId]);
|
|
if (!users[0]?.family_id) return res.json([]);
|
|
const familyId = users[0].family_id;
|
|
|
|
const [rows] = await pool.query(`
|
|
SELECT e.id, e.title, e.total_amount, e.start_date, e.end_date, s.amount_due, s.amount_paid, s.status, s.percentage
|
|
FROM expense_shares s
|
|
JOIN extraordinary_expenses e ON s.expense_id = e.id
|
|
WHERE s.family_id = ? AND e.condo_id = ?
|
|
ORDER BY e.created_at DESC
|
|
`, [familyId, condoId]); // Ensure we only get expenses for the active condo context if needed
|
|
|
|
res.json(rows.map(r => ({
|
|
id: r.id,
|
|
title: r.title,
|
|
totalAmount: parseFloat(r.total_amount),
|
|
startDate: r.start_date,
|
|
endDate: r.end_date,
|
|
myShare: {
|
|
percentage: parseFloat(r.percentage),
|
|
amountDue: parseFloat(r.amount_due),
|
|
amountPaid: parseFloat(r.amount_paid),
|
|
status: r.status
|
|
}
|
|
})));
|
|
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
|
|
initDb().then(() => {
|
|
app.listen(PORT, () => {
|
|
console.log(`Server running on port ${PORT}`);
|
|
});
|
|
});
|