2 * @file portalCustomer.js
3 * @description Customer Portal Data Routes
5 * Provides data access endpoints for authenticated customer portal users.
6 * All routes require valid JWT authentication token.
9 * - Profile management (view/update)
10 * - Invoice viewing (list/details)
11 * - Ticket management (list/create/comment)
15 * - Dashboard statistics
18 * - All routes protected by authenticateCustomer middleware
19 * - Tenant isolation via setCustomerContext middleware
20 * - Customer data filtered by customer_id from JWT
21 * - Audit logging for data modifications
23 * @module routes/portalCustomer
25 * @requires services/db
26 * @requires middleware/customerAuth
27 * @author Independent Business Group
29 * @see module:routes/portalAuth
30 * @see module:middleware/customerAuth
33const express = require('express');
34const router = express.Router();
35const pool = require('../services/db');
36const { authenticateCustomer, setCustomerContext, logCustomerAudit } = require('../middleware/customerAuth');
38// Apply authentication to all routes
39router.use(authenticateCustomer);
40router.use(setCustomerContext);
43 * @api {get} /portal/customer/profile Get Customer Profile
44 * @apiName GetCustomerProfile
45 * @apiGroup CustomerPortalData
49 * Returns customer profile information including portal user details,
50 * billing address, and account status.
52 * @apiPermission customer
54 * @apiHeader {String} Authorization Bearer JWT token
56 * @apiSuccess {Number} customer_id Customer ID
57 * @apiSuccess {String} customer_name Customer business name
58 * @apiSuccess {String} contact_email Customer contact email
59 * @apiSuccess {String} contact_phone Customer contact phone
60 * @apiSuccess {String} billing_address Billing street address
61 * @apiSuccess {String} billing_city Billing city
62 * @apiSuccess {String} billing_state Billing state/province
63 * @apiSuccess {String} billing_postcode Billing postal code
64 * @apiSuccess {String} billing_country Billing country
65 * @apiSuccess {String} status Account status
66 * @apiSuccess {Date} created_at Account creation date
67 * @apiSuccess {String} portal_email Portal login email
68 * @apiSuccess {String} first_name User first name
69 * @apiSuccess {String} last_name User last name
70 * @apiSuccess {String} portal_phone User phone number
72 * @apiError (401) {String} error Unauthorized
73 * @apiError (404) {String} error Customer not found
74 * @apiError (500) {String} error Failed to get profile
76router.get('/profile', async (req, res) => {
78 const result = await pool.query(
80 c.customer_id, c.name as customer_name, c.email as contact_email, c.phone as contact_phone,
81 c.billing_address, c.billing_city, c.billing_state, c.billing_postcode,
82 c.billing_country, c.status, c.created_at,
83 cu.email as portal_email, cu.first_name, cu.last_name, cu.phone as portal_phone
85 JOIN customer_users cu ON c.customer_id = cu.customer_id
86 WHERE c.customer_id = $1 AND cu.customer_user_id = $2`,
87 [req.customerId, req.customerUserId]
90 if (result.rows.length === 0) {
91 return res.status(404).json({ error: 'Customer not found.' });
94 res.json(result.rows[0]);
96 console.error('Get customer profile error:', error);
97 res.status(500).json({ error: 'Failed to get profile.', details: error.message });
102 * @api {put} /portal/customer/profile Update Customer Profile
103 * @apiName UpdateCustomerProfile
104 * @apiGroup CustomerPortalData
108 * Updates customer contact information and billing address.
109 * Logs audit event for profile modifications.
111 * @apiPermission customer
113 * @apiHeader {String} Authorization Bearer JWT token
114 * @apiHeader {String} Content-Type application/json
116 * @apiParam {String} [firstName] User first name
117 * @apiParam {String} [lastName] User last name
118 * @apiParam {String} [phone] User phone number
119 * @apiParam {String} [contactEmail] Customer contact email
120 * @apiParam {String} [contactPhone] Customer contact phone
121 * @apiParam {String} [billingAddress] Billing street address
122 * @apiParam {String} [billingCity] Billing city
123 * @apiParam {String} [billingState] Billing state/province
124 * @apiParam {String} [billingPostcode] Billing postal code
125 * @apiParam {String} [billingCountry] Billing country
127 * @apiSuccess {String} message Success message
129 * @apiError (401) {String} error Unauthorized
130 * @apiError (500) {String} error Failed to update profile
132router.put('/profile', async (req, res) => {
147 // Update customer_users
149 `UPDATE customer_users
150 SET first_name = $1, last_name = $2, phone = $3, updated_at = NOW()
151 WHERE customer_user_id = $4`,
152 [firstName, lastName, phone, req.customerUserId]
158 SET email = $1, phone = $2,
159 billing_address = $3, billing_city = $4, billing_state = $5,
160 billing_postcode = $6, billing_country = $7
161 WHERE customer_id = $8`,
175 await logCustomerAudit(
181 { fields: Object.keys(req.body) },
182 req.ip || req.connection.remoteAddress
185 res.json({ message: 'Profile updated successfully.' });
187 console.error('Update customer profile error:', error);
188 res.status(500).json({ error: 'Failed to update profile.', details: error.message });
193 * @api {get} /portal/customer/invoices List Customer Invoices
194 * @apiName ListCustomerInvoices
195 * @apiGroup CustomerPortalData
199 * Returns paginated list of customer invoices with optional status filtering.
201 * @apiPermission customer
203 * @apiHeader {String} Authorization Bearer JWT token
205 * @apiParam {String} [status] Filter by status (Paid, Unpaid, Overdue, etc.)
206 * @apiParam {Number} [limit=50] Results per page
207 * @apiParam {Number} [offset=0] Pagination offset
209 * @apiSuccess {Object[]} invoices Array of invoices
210 * @apiSuccess {Number} invoices.invoice_id Invoice ID
211 * @apiSuccess {String} invoices.invoice_number Invoice number
212 * @apiSuccess {Date} invoices.invoice_date Invoice date
213 * @apiSuccess {Date} invoices.due_date Due date
214 * @apiSuccess {Number} invoices.subtotal Subtotal amount
215 * @apiSuccess {Number} invoices.tax Tax amount
216 * @apiSuccess {Number} invoices.total Total amount
217 * @apiSuccess {String} invoices.status Status
218 * @apiSuccess {String} invoices.payment_method Payment method
219 * @apiSuccess {Date} invoices.payment_date Payment date
220 * @apiSuccess {Date} invoices.created_at Creation date
221 * @apiSuccess {Number} total Total count of invoices
222 * @apiSuccess {Number} limit Page size
223 * @apiSuccess {Number} offset Current offset
225 * @apiError (401) {String} error Unauthorized
226 * @apiError (500) {String} error Failed to get invoices
228router.get('/invoices', async (req, res) => {
230 const { status, limit = 50, offset = 0 } = req.query;
234 invoice_id, invoice_number, invoice_date, due_date,
235 subtotal, tax, total, status, payment_method, payment_date,
238 WHERE customer_id = $1 AND tenant_id = $2
241 const params = [req.customerId, req.tenantId];
244 query += ` AND status = $${params.length + 1}`;
248 query += ` ORDER BY invoice_date DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
249 params.push(parseInt(limit), parseInt(offset));
251 const result = await pool.query(query, params);
254 const countResult = await pool.query(
255 'SELECT COUNT(*) FROM invoices WHERE customer_id = $1 AND tenant_id = $2',
256 [req.customerId, req.tenantId]
260 invoices: result.rows,
261 total: parseInt(countResult.rows[0].count),
262 limit: parseInt(limit),
263 offset: parseInt(offset)
266 console.error('Get customer invoices error:', error);
267 res.status(500).json({ error: 'Failed to get invoices.', details: error.message });
272 * @api {get} /portal/customer/invoices/:id Get Invoice Details
273 * @apiName GetInvoiceDetails
274 * @apiGroup CustomerPortalData
278 * Returns invoice details including line items.
280 * @apiPermission customer
282 * @apiHeader {String} Authorization Bearer JWT token
284 * @apiParam {Number} id Invoice ID (URL parameter)
286 * @apiSuccess {Object} invoice Invoice object with line_items array
287 * @apiSuccess {Number} invoice.invoice_id Invoice ID
288 * @apiSuccess {String} invoice.invoice_number Invoice number
289 * @apiSuccess {Object[]} invoice.line_items Array of line items
291 * @apiError (401) {String} error Unauthorized
292 * @apiError (404) {String} error Invoice not found
293 * @apiError (500) {String} error Failed to get invoice details
295router.get('/invoices/:id', async (req, res) => {
297 const invoiceId = parseInt(req.params.id);
300 const invoiceResult = await pool.query(
301 `SELECT * FROM invoices
302 WHERE invoice_id = $1 AND customer_id = $2 AND tenant_id = $3`,
303 [invoiceId, req.customerId, req.tenantId]
306 if (invoiceResult.rows.length === 0) {
307 return res.status(404).json({ error: 'Invoice not found.' });
311 const lineItemsResult = await pool.query(
312 `SELECT * FROM invoice_line_items
313 WHERE invoice_id = $1
314 ORDER BY line_item_id`,
319 ...invoiceResult.rows[0],
320 line_items: lineItemsResult.rows
323 console.error('Get invoice details error:', error);
324 res.status(500).json({ error: 'Failed to get invoice details.', details: error.message });
329 * @api {get} /portal/customer/tickets List Customer Tickets
330 * @apiName ListCustomerTickets
331 * @apiGroup CustomerPortalData
335 * Returns paginated list of customer support tickets with optional status filtering.
337 * @apiPermission customer
339 * @apiHeader {String} Authorization Bearer JWT token
341 * @apiParam {String} [status] Filter by status (Open, In Progress, Resolved, Closed)
342 * @apiParam {Number} [limit=50] Results per page
343 * @apiParam {Number} [offset=0] Pagination offset
345 * @apiSuccess {Object[]} tickets Array of tickets
346 * @apiSuccess {Number} tickets.ticket_id Ticket ID
347 * @apiSuccess {String} tickets.ticket_number Ticket number
348 * @apiSuccess {String} tickets.subject Ticket subject
349 * @apiSuccess {String} tickets.description Ticket description
350 * @apiSuccess {String} tickets.status Status
351 * @apiSuccess {String} tickets.priority Priority (Low, Normal, High, Critical)
352 * @apiSuccess {Number} tickets.assigned_to Assigned user ID
353 * @apiSuccess {Date} tickets.created_at Creation date
354 * @apiSuccess {Date} tickets.updated_at Last update date
355 * @apiSuccess {Date} tickets.resolved_at Resolution date
356 * @apiSuccess {Number} total Total count of tickets
357 * @apiSuccess {Number} limit Page size
358 * @apiSuccess {Number} offset Current offset
360 * @apiError (401) {String} error Unauthorized
361 * @apiError (500) {String} error Failed to get tickets
363router.get('/tickets', async (req, res) => {
365 const { status, limit = 50, offset = 0 } = req.query;
369 ticket_id, ticket_number, subject, description, status, priority,
370 assigned_to, created_at, updated_at, resolved_at
372 WHERE customer_id = $1 AND tenant_id = $2
375 const params = [req.customerId, req.tenantId];
378 query += ` AND status = $${params.length + 1}`;
382 query += ` ORDER BY created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
383 params.push(parseInt(limit), parseInt(offset));
385 const result = await pool.query(query, params);
388 const countResult = await pool.query(
389 'SELECT COUNT(*) FROM tickets WHERE customer_id = $1 AND tenant_id = $2',
390 [req.customerId, req.tenantId]
394 tickets: result.rows,
395 total: parseInt(countResult.rows[0].count),
396 limit: parseInt(limit),
397 offset: parseInt(offset)
400 console.error('Get customer tickets error:', error);
401 res.status(500).json({ error: 'Failed to get tickets.', details: error.message });
406 * @api {get} /portal/customer/tickets/:id Get Ticket Details
407 * @apiName GetTicketDetails
408 * @apiGroup CustomerPortalData
412 * Returns ticket details including all comments.
414 * @apiPermission customer
416 * @apiHeader {String} Authorization Bearer JWT token
418 * @apiParam {Number} id Ticket ID (URL parameter)
420 * @apiSuccess {Object} ticket Ticket object with comments array
421 * @apiSuccess {Number} ticket.ticket_id Ticket ID
422 * @apiSuccess {String} ticket.ticket_number Ticket number
423 * @apiSuccess {String} ticket.subject Subject
424 * @apiSuccess {String} ticket.description Description
425 * @apiSuccess {String} ticket.status Status
426 * @apiSuccess {String} ticket.priority Priority
427 * @apiSuccess {String} ticket.assigned_to_name Assigned technician name
428 * @apiSuccess {Object[]} ticket.comments Array of comments
430 * @apiError (401) {String} error Unauthorized
431 * @apiError (404) {String} error Ticket not found
432 * @apiError (500) {String} error Failed to get ticket details
434router.get('/tickets/:id', async (req, res) => {
436 const ticketId = parseInt(req.params.id);
439 const ticketResult = await pool.query(
440 `SELECT t.*, u.user_name as assigned_to_name
442 LEFT JOIN users u ON t.assigned_to = u.user_id
443 WHERE t.ticket_id = $1 AND t.customer_id = $2 AND t.tenant_id = $3`,
444 [ticketId, req.customerId, req.tenantId]
447 if (ticketResult.rows.length === 0) {
448 return res.status(404).json({ error: 'Ticket not found.' });
452 const commentsResult = await pool.query(
453 `SELECT tc.*, u.user_name as author_name
454 FROM ticket_comments tc
455 LEFT JOIN users u ON tc.user_id = u.user_id
456 WHERE tc.ticket_id = $1
457 ORDER BY tc.created_at ASC`,
462 ...ticketResult.rows[0],
463 comments: commentsResult.rows
466 console.error('Get ticket details error:', error);
467 res.status(500).json({ error: 'Failed to get ticket details.', details: error.message });
472 * @api {post} /portal/customer/tickets Create Support Ticket
473 * @apiName CreateSupportTicket
474 * @apiGroup CustomerPortalData
478 * Creates new support ticket. Auto-generates ticket number.
479 * Logs audit event for ticket creation.
481 * @apiPermission customer
483 * @apiHeader {String} Authorization Bearer JWT token
484 * @apiHeader {String} Content-Type application/json
486 * @apiParam {String} subject Ticket subject (required)
487 * @apiParam {String} description Ticket description (required)
488 * @apiParam {String} [priority=Normal] Priority (Low, Normal, High, Critical)
490 * @apiSuccess (201) {Object} ticket Created ticket object
491 * @apiSuccess (201) {Number} ticket.ticket_id Ticket ID
492 * @apiSuccess (201) {String} ticket.ticket_number Generated ticket number
493 * @apiSuccess (201) {String} ticket.subject Subject
494 * @apiSuccess (201) {String} ticket.status Status (always Open initially)
496 * @apiError (400) {String} error Subject and description are required
497 * @apiError (401) {String} error Unauthorized
498 * @apiError (500) {String} error Failed to create ticket
500router.post('/tickets', async (req, res) => {
502 const { subject, description, priority = 'Normal' } = req.body;
504 if (!subject || !description) {
505 return res.status(400).json({ error: 'Subject and description are required.' });
508 // Generate ticket number
509 const ticketNumberResult = await pool.query(
510 `SELECT COALESCE(MAX(CAST(SUBSTRING(ticket_number FROM '[0-9]+') AS INTEGER)), 0) + 1 as next_num
511 FROM tickets WHERE tenant_id = $1`,
514 const ticketNumber = `TICKET-${ticketNumberResult.rows[0].next_num}`;
517 const result = await pool.query(
519 (tenant_id, customer_id, ticket_number, subject, description, priority, status, created_at, updated_at)
520 VALUES ($1, $2, $3, $4, $5, $6, 'Open', NOW(), NOW())
522 [req.tenantId, req.customerId, ticketNumber, subject, description, priority]
526 await logCustomerAudit(
531 result.rows[0].ticket_id,
532 { subject, priority },
533 req.ip || req.connection.remoteAddress
536 res.status(201).json(result.rows[0]);
538 console.error('Create ticket error:', error);
539 res.status(500).json({ error: 'Failed to create ticket.', details: error.message });
544 * @api {post} /portal/customer/tickets/:id/comments Add Ticket Comment
545 * @apiName AddTicketComment
546 * @apiGroup CustomerPortalData
550 * Adds comment to existing ticket. Updates ticket's updated_at timestamp.
553 * @apiPermission customer
555 * @apiHeader {String} Authorization Bearer JWT token
556 * @apiHeader {String} Content-Type application/json
558 * @apiParam {Number} id Ticket ID (URL parameter)
559 * @apiParam {String} comment Comment text (required)
561 * @apiSuccess (201) {Object} comment Created comment object
562 * @apiSuccess (201) {Number} comment.comment_id Comment ID
563 * @apiSuccess (201) {Number} comment.ticket_id Ticket ID
564 * @apiSuccess (201) {String} comment.comment Comment text
565 * @apiSuccess (201) {Boolean} comment.is_internal Internal flag (always false)
566 * @apiSuccess (201) {Date} comment.created_at Creation date
568 * @apiError (400) {String} error Comment is required
569 * @apiError (401) {String} error Unauthorized
570 * @apiError (404) {String} error Ticket not found
571 * @apiError (500) {String} error Failed to add comment
573router.post('/tickets/:id/comments', async (req, res) => {
575 const ticketId = parseInt(req.params.id);
576 const { comment } = req.body;
579 return res.status(400).json({ error: 'Comment is required.' });
582 // Verify ticket belongs to customer
583 const ticketResult = await pool.query(
584 'SELECT ticket_id FROM tickets WHERE ticket_id = $1 AND customer_id = $2 AND tenant_id = $3',
585 [ticketId, req.customerId, req.tenantId]
588 if (ticketResult.rows.length === 0) {
589 return res.status(404).json({ error: 'Ticket not found.' });
593 const result = await pool.query(
594 `INSERT INTO ticket_comments
595 (ticket_id, user_id, comment, is_internal, created_at)
596 VALUES ($1, NULL, $2, false, NOW())
601 // Update ticket updated_at
603 'UPDATE tickets SET updated_at = NOW() WHERE ticket_id = $1',
608 await logCustomerAudit(
615 req.ip || req.connection.remoteAddress
618 res.status(201).json(result.rows[0]);
620 console.error('Add ticket comment error:', error);
621 res.status(500).json({ error: 'Failed to add comment.', details: error.message });
626 * @api {get} /portal/customer/contracts List Customer Contracts
627 * @apiName ListCustomerContracts
628 * @apiGroup CustomerPortalData
632 * Returns list of all customer contracts with billing information.
634 * @apiPermission customer
636 * @apiHeader {String} Authorization Bearer JWT token
638 * @apiSuccess {Object[]} contracts Array of contracts
639 * @apiSuccess {Number} contracts.contract_id Contract ID
640 * @apiSuccess {String} contracts.contract_name Contract name
641 * @apiSuccess {String} contracts.description Description
642 * @apiSuccess {String} contracts.billing_interval Billing interval (Monthly, Quarterly, Annually)
643 * @apiSuccess {Number} contracts.amount Billing amount
644 * @apiSuccess {Date} contracts.start_date Start date
645 * @apiSuccess {Date} contracts.end_date End date
646 * @apiSuccess {Date} contracts.next_billing_date Next billing date
647 * @apiSuccess {String} contracts.status Status (Active, Expired, Cancelled)
648 * @apiSuccess {Date} contracts.created_at Creation date
650 * @apiError (401) {String} error Unauthorized
651 * @apiError (500) {String} error Failed to get contracts
653router.get('/contracts', async (req, res) => {
655 const result = await pool.query(
657 contract_id, contract_name, description, billing_interval, amount,
658 start_date, end_date, next_billing_date, status, created_at
660 WHERE customer_id = $1 AND tenant_id = $2
661 ORDER BY created_at DESC`,
662 [req.customerId, req.tenantId]
665 res.json({ contracts: result.rows });
667 console.error('Get customer contracts error:', error);
668 res.status(500).json({ error: 'Failed to get contracts.', details: error.message });
673 * @api {get} /portal/customer/contracts/:id Get Contract Details
674 * @apiName GetContractDetails
675 * @apiGroup CustomerPortalData
679 * Returns detailed information for specific contract.
681 * @apiPermission customer
683 * @apiHeader {String} Authorization Bearer JWT token
685 * @apiParam {Number} id Contract ID (URL parameter)
687 * @apiSuccess {Object} contract Contract object with all fields
689 * @apiError (401) {String} error Unauthorized
690 * @apiError (404) {String} error Contract not found
691 * @apiError (500) {String} error Failed to get contract details
693router.get('/contracts/:id', async (req, res) => {
695 const contractId = parseInt(req.params.id);
697 const result = await pool.query(
698 `SELECT * FROM contracts
699 WHERE contract_id = $1 AND customer_id = $2 AND tenant_id = $3`,
700 [contractId, req.customerId, req.tenantId]
703 if (result.rows.length === 0) {
704 return res.status(404).json({ error: 'Contract not found.' });
707 res.json(result.rows[0]);
709 console.error('Get contract details error:', error);
710 res.status(500).json({ error: 'Failed to get contract details.', details: error.message });
715 * @api {get} /portal/customer/assets List Customer Assets
716 * @apiName ListCustomerAssets
717 * @apiGroup CustomerPortalData
721 * Returns list of all customer assets/equipment.
723 * @apiPermission customer
725 * @apiHeader {String} Authorization Bearer JWT token
727 * @apiSuccess {Object[]} assets Array of assets
728 * @apiSuccess {Number} assets.asset_id Asset ID
729 * @apiSuccess {String} assets.asset_name Asset name
730 * @apiSuccess {String} assets.asset_type Asset type
731 * @apiSuccess {String} assets.serial_number Serial number
732 * @apiSuccess {String} assets.location Physical location
733 * @apiSuccess {Date} assets.purchase_date Purchase date
734 * @apiSuccess {Date} assets.warranty_expiry Warranty expiration date
735 * @apiSuccess {String} assets.status Status (Active, Retired, etc.)
736 * @apiSuccess {Date} assets.created_at Creation date
738 * @apiError (401) {String} error Unauthorized
739 * @apiError (500) {String} error Failed to get assets
741router.get('/assets', async (req, res) => {
743 const result = await pool.query(
745 asset_id, asset_name, asset_type, serial_number, location,
746 purchase_date, warranty_expiry, status, created_at
748 WHERE customer_id = $1 AND tenant_id = $2
749 ORDER BY created_at DESC`,
750 [req.customerId, req.tenantId]
753 res.json({ assets: result.rows });
755 console.error('Get customer assets error:', error);
756 res.status(500).json({ error: 'Failed to get assets.', details: error.message });
761 * @api {get} /portal/customer/documents List Customer Documents
762 * @apiName ListCustomerDocuments
763 * @apiGroup CustomerPortalData
767 * Returns list of all customer documents.
769 * @apiPermission customer
771 * @apiHeader {String} Authorization Bearer JWT token
773 * @apiSuccess {Object[]} documents Array of documents
774 * @apiSuccess {Number} documents.document_id Document ID
775 * @apiSuccess {String} documents.document_name Document name
776 * @apiSuccess {String} documents.document_type Document type
777 * @apiSuccess {String} documents.file_path File path
778 * @apiSuccess {Number} documents.file_size File size (bytes)
779 * @apiSuccess {String} documents.uploaded_by Uploader name
780 * @apiSuccess {Date} documents.created_at Upload date
782 * @apiError (401) {String} error Unauthorized
783 * @apiError (500) {String} error Failed to get documents
785router.get('/documents', async (req, res) => {
787 const result = await pool.query(
789 document_id, document_name, document_type, file_path, file_size,
790 uploaded_by, created_at
792 WHERE customer_id = $1 AND tenant_id = $2
793 ORDER BY created_at DESC`,
794 [req.customerId, req.tenantId]
797 res.json({ documents: result.rows });
799 console.error('Get customer documents error:', error);
800 res.status(500).json({ error: 'Failed to get documents.', details: error.message });
805 * @api {get} /portal/customer/dashboard Get Dashboard Overview
806 * @apiName GetCustomerDashboard
807 * @apiGroup CustomerPortalData
811 * Returns dashboard overview with statistics and recent activity.
812 * Aggregates data from invoices, contracts, tickets, and assets.
814 * @apiPermission customer
816 * @apiHeader {String} Authorization Bearer JWT token
818 * @apiSuccess {Object} stats Statistics object
819 * @apiSuccess {Number} stats.outstandingBalance Total unpaid invoices
820 * @apiSuccess {Number} stats.activeContracts Count of active contracts
821 * @apiSuccess {Number} stats.openTickets Count of open/in-progress tickets
822 * @apiSuccess {Number} stats.totalAssets Count of total assets
823 * @apiSuccess {Object[]} recentInvoices 5 most recent invoices
824 * @apiSuccess {Object[]} recentTickets 5 most recent tickets
826 * @apiError (401) {String} error Unauthorized
827 * @apiError (500) {String} error Failed to get dashboard data
829router.get('/dashboard', async (req, res) => {
831 // Get outstanding balance
832 const balanceResult = await pool.query(
833 `SELECT COALESCE(SUM(total), 0) as outstanding_balance
835 WHERE customer_id = $1 AND tenant_id = $2 AND status = 'Unpaid'`,
836 [req.customerId, req.tenantId]
839 // Get active contracts count
840 const contractsResult = await pool.query(
841 `SELECT COUNT(*) as active_contracts
843 WHERE customer_id = $1 AND tenant_id = $2 AND status = 'Active'`,
844 [req.customerId, req.tenantId]
847 // Get open tickets count
848 const ticketsResult = await pool.query(
849 `SELECT COUNT(*) as open_tickets
851 WHERE customer_id = $1 AND tenant_id = $2 AND status IN ('Open', 'In Progress')`,
852 [req.customerId, req.tenantId]
855 // Get total assets count
856 const assetsResult = await pool.query(
857 `SELECT COUNT(*) as total_assets
859 WHERE customer_id = $1 AND tenant_id = $2`,
860 [req.customerId, req.tenantId]
863 // Get recent invoices
864 const recentInvoicesResult = await pool.query(
865 `SELECT invoice_id, invoice_number, invoice_date, total, status
867 WHERE customer_id = $1 AND tenant_id = $2
868 ORDER BY invoice_date DESC
870 [req.customerId, req.tenantId]
873 // Get recent tickets
874 const recentTicketsResult = await pool.query(
875 `SELECT ticket_id, ticket_number, subject, status, created_at
877 WHERE customer_id = $1 AND tenant_id = $2
878 ORDER BY created_at DESC
880 [req.customerId, req.tenantId]
885 outstandingBalance: parseFloat(balanceResult.rows[0].outstanding_balance),
886 activeContracts: parseInt(contractsResult.rows[0].active_contracts),
887 openTickets: parseInt(ticketsResult.rows[0].open_tickets),
888 totalAssets: parseInt(assetsResult.rows[0].total_assets)
890 recentInvoices: recentInvoicesResult.rows,
891 recentTickets: recentTicketsResult.rows
894 console.error('Get customer dashboard error:', error);
895 res.status(500).json({ error: 'Failed to get dashboard data.', details: error.message });
899module.exports = router;