feat: Implement ticket commenting functionality
Adds the ability for users to comment on tickets, view comments, and distinguish between user and admin responses. Also introduces a new 'SUSPENDED' status for tickets and refactors database schema and API endpoints to support comments.
This commit is contained in:
@@ -617,6 +617,81 @@ app.get('/api/tickets/:id/attachments/:attachmentId', authenticateToken, async (
|
||||
} 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;
|
||||
@@ -709,23 +784,32 @@ app.put('/api/tickets/:id', authenticateToken, async (req, res) => {
|
||||
|
||||
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) {
|
||||
query += ' AND user_id = ? AND status = "OPEN"'; // Users can only delete their own OPEN tickets
|
||||
params.push(userId);
|
||||
}
|
||||
|
||||
const [result] = await pool.query(query, params);
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(403).json({ message: 'Cannot delete ticket (Permission denied or not found)' });
|
||||
// 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 }); }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user