Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const { doubleCsrf } = require('csrf-csrf');
const sessionConfig = require('./config/session');
const errorHandler = require('./middleware/errorHandler');
const { i18next, middleware: i18nMiddleware } = require('./config/i18n');
const { calculateTicketAge } = require('./utils/dateHelpers');

const publicRoutes = require('./routes/public');
const authRoutes = require('./routes/auth');
Expand Down Expand Up @@ -92,7 +93,7 @@ app.use(flash());
// Apply CSRF protection to all routes
app.use(doubleCsrfProtection);

// Make CSRF token, flash messages, and i18n available to all views
// Make CSRF token, flash messages, i18n, and utilities available to all views
app.use((req, res, next) => {
res.locals.success_msg = req.flash('success_msg');
res.locals.error_msg = req.flash('error_msg');
Expand All @@ -104,6 +105,9 @@ app.use((req, res, next) => {
res.locals.t = req.t;
res.locals.language = req.language || 'el';

// Make date helpers available for ticket age calculation
res.locals.calculateTicketAge = calculateTicketAge;

// Add request timing for debugging
req.startTime = new Date();

Expand Down
27 changes: 26 additions & 1 deletion locales/el/tickets.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,37 @@
"actions": "Ενέργειες",
"anonymous": "Ανώνυμος",
"allTickets": "Όλα τα Αιτήματα",
"myTickets": "Τα Αιτήματά μου"
"myTickets": "Τα Αιτήματά μου",
"select": "Επιλογή",
"age": "Ηλικία",
"ageNew": "Νέο",
"ageDays": "{{count}} ημέρα",
"ageDays_plural": "{{count}} ημέρες",
"ageWeeks": "{{count}} εβδομάδα",
"ageWeeks_plural": "{{count}} εβδομάδες",
"quickAssign": "Γρήγορη Ανάθεση"
},
"filters": {
"title": "Φίλτρα"
},
"search": {
"placeholder": "Αναζήτηση αιτημάτων..."
},
"bulkUpdate": {
"label": "Μαζική ενημέρωση επιλεγμένων:",
"statusNoChange": "Κατάσταση (χωρίς αλλαγή)",
"priorityNoChange": "Προτεραιότητα (χωρίς αλλαγή)",
"assignmentNoChange": "Ανάθεση (χωρίς αλλαγή)",
"unassign": "-- Χωρίς Ανάθεση --",
"updateSelected": "Ενημέρωση Επιλεγμένων",
"selectAtLeastOneTicket": "Παρακαλώ επιλέξτε τουλάχιστον ένα αίτημα",
"selectAtLeastOneField": "Παρακαλώ επιλέξτε τουλάχιστον ένα πεδίο για ενημέρωση (κατάσταση, προτεραιότητα ή ανάθεση)",
"confirmUpdate": "Ενημέρωση {{count}} επιλεγμένων αιτημάτων;"
},
"pagination": {
"previous": "Προηγούμενη",
"next": "Επόμενη",
"pageInfo": "Σελίδα {{current}} από {{total}}",
"showingOfTotal": "Εμφάνιση {{count}} από {{total}} αιτημάτων"
}
}
27 changes: 26 additions & 1 deletion locales/en/tickets.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,37 @@
"actions": "Actions",
"anonymous": "Anonymous",
"allTickets": "All Tickets",
"myTickets": "My Tickets"
"myTickets": "My Tickets",
"select": "Select",
"age": "Age",
"ageNew": "New",
"ageDays": "{{count}} day old",
"ageDays_plural": "{{count}} days old",
"ageWeeks": "{{count}} week old",
"ageWeeks_plural": "{{count}} weeks old",
"quickAssign": "Quick Assign"
},
"filters": {
"title": "Filters"
},
"search": {
"placeholder": "Search tickets..."
},
"bulkUpdate": {
"label": "Bulk update selected:",
"statusNoChange": "Status (no change)",
"priorityNoChange": "Priority (no change)",
"assignmentNoChange": "Assignment (no change)",
"unassign": "-- Unassign --",
"updateSelected": "Update Selected",
"selectAtLeastOneTicket": "Please select at least one ticket",
"selectAtLeastOneField": "Please select at least one field to update (status, priority, or assignment)",
"confirmUpdate": "Update {{count}} selected ticket(s)?"
},
"pagination": {
"previous": "Previous",
"next": "Next",
"pageInfo": "Page {{current}} of {{total}}",
"showingOfTotal": "Showing {{count}} of {{total}} tickets"
}
}
20 changes: 20 additions & 0 deletions migrations/029_add_dashboard_indexes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- Migration 029: Add dashboard performance indexes
-- Purpose: Optimize dashboard queries for counts, pagination, and last comment retrieval

-- Index for efficient last comment queries
-- Supports DISTINCT ON queries to get most recent comment per ticket
CREATE INDEX IF NOT EXISTS idx_comments_ticket_created
ON comments(ticket_id, created_at DESC);

-- Indexes for count aggregations by status and priority
-- These speed up the GROUP BY queries for filter badge counts
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
CREATE INDEX IF NOT EXISTS idx_tickets_priority ON tickets(priority);

-- Composite index for filtered pagination queries
-- Supports WHERE clauses with status/priority filters plus ORDER BY created_at
CREATE INDEX IF NOT EXISTS idx_tickets_status_created
ON tickets(status, created_at DESC);

CREATE INDEX IF NOT EXISTS idx_tickets_priority_created
ON tickets(priority, created_at DESC);
52 changes: 52 additions & 0 deletions models/Comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,58 @@ class Comment {
throw error;
}
}

static async getLastCommentsByTicketIds(ticketIds) {
const startTime = Date.now();
try {
if (!ticketIds || ticketIds.length === 0) {
logger.debug('Comment.getLastCommentsByTicketIds: Empty ticket IDs array');
return [];
}

logger.debug('Comment.getLastCommentsByTicketIds: Starting query', {
ticketCount: ticketIds.length,
});
const result = await pool.query(
`
SELECT DISTINCT ON (c.ticket_id)
c.ticket_id,
c.content,
u.username,
c.created_at
FROM comments c
JOIN users u ON c.user_id = u.id
WHERE c.ticket_id = ANY($1::int[])
ORDER BY c.ticket_id, c.created_at DESC
`,
[ticketIds]
);

const duration = Date.now() - startTime;

if (duration > 500) {
logger.warn('Comment.getLastCommentsByTicketIds: Slow query detected', {
ticketCount: ticketIds.length,
duration,
});
}

logger.debug('Comment.getLastCommentsByTicketIds: Query completed', {
ticketCount: ticketIds.length,
resultCount: result.rows.length,
duration,
});
return result.rows;
} catch (error) {
logger.error('Comment.getLastCommentsByTicketIds: Database error', {
ticketCount: ticketIds?.length,
error: error.message,
stack: error.stack,
code: error.code,
});
throw error;
}
}
}

module.exports = Comment;
Loading
Loading