Files
CLProject/src/routes/reportRoutes.js
T
2026-04-22 22:39:26 +02:00

276 lines
7.2 KiB
JavaScript

import { Router } from 'express';
import { deleteReport, deleteReportImage, getReport, getReportImages, listReports, submitReport } from '../services/reportService.js';
import { logAuditEvent } from '../services/auditService.js';
import { asyncHandler } from '../utils/asyncHandler.js';
import { validateParam } from '../middleware/validateParams.js';
const router = Router();
/**
* @openapi
* /api/v1/reports:
* get:
* summary: List reports
* tags:
* - Reports
* security:
* - bearerAuth: []
* - cookieAuth: []
* parameters:
* - name: status
* in: query
* required: false
* schema:
* type: string
* - name: templateCode
* in: query
* required: false
* schema:
* type: string
* - name: limit
* in: query
* required: false
* schema:
* type: integer
* default: 100
* maximum: 500
* - name: offset
* in: query
* required: false
* schema:
* type: integer
* default: 0
* responses:
* 200:
* description: List of reports
* post:
* summary: Submit report
* tags:
* - Reports
* security:
* - bearerAuth: []
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - id
* - reportNumber
* - templateCode
* - templateVersion
* - answers
* properties:
* id:
* type: string
* reportNumber:
* type: string
* templateCode:
* type: string
* templateVersion:
* type: integer
* status:
* type: string
* answers:
* type: object
* responses:
* 201:
* description: Report submitted
* 400:
* description: Invalid input
* /api/v1/reports/{reportId}:
* get:
* summary: Get report details
* tags:
* - Reports
* security:
* - bearerAuth: []
* - cookieAuth: []
* parameters:
* - name: reportId
* in: path
* required: true
* schema:
* type: string
* pattern: '^[a-zA-Z0-9_-]{1,100}$'
* responses:
* 200:
* description: Report details
* 404:
* description: Report not found
* delete:
* summary: Delete report
* tags:
* - Reports
* security:
* - bearerAuth: []
* - cookieAuth: []
* parameters:
* - name: reportId
* in: path
* required: true
* schema:
* type: string
* pattern: '^[a-zA-Z0-9_-]{1,100}$'
* responses:
* 200:
* description: Report deleted
* /api/v1/reports/{reportId}/images:
* get:
* summary: Get report images
* tags:
* - Reports
* security:
* - bearerAuth: []
* - cookieAuth: []
* parameters:
* - name: reportId
* in: path
* required: true
* schema:
* type: string
* pattern: '^[a-zA-Z0-9_-]{1,100}$'
* responses:
* 200:
* description: Report images
* /api/v1/reports/{reportId}/images/{recordId}/{fileName}:
* delete:
* summary: Delete report image
* tags:
* - Reports
* security:
* - bearerAuth: []
* - cookieAuth: []
* parameters:
* - name: reportId
* in: path
* required: true
* schema:
* type: string
* pattern: '^[a-zA-Z0-9_-]{1,100}$'
* - name: recordId
* in: path
* required: true
* schema:
* type: string
* - name: fileName
* in: path
* required: true
* schema:
* type: string
* pattern: '^[a-zA-Z0-9_.-]{1,500}$'
* responses:
* 200:
* description: Image deleted
*/
/*
* Report submission accepts the full local report payload (answers, template
* binding, status) and stores it server-side. This bridges the offline-first
* client workflow with centralized storage for review and archival.
*/
router.get(
'/',
asyncHandler(async (req, res) => {
const reports = await listReports({
status: req.query.status || undefined,
templateCode: req.query.templateCode || undefined,
limit: Math.min(Number(req.query.limit) || 100, 500),
offset: Math.max(Number(req.query.offset) || 0, 0)
});
res.json({ items: reports });
})
);
router.get(
'/:reportId',
validateParam('reportId', { pattern: /^[a-zA-Z0-9_-]{1,100}$/ }),
asyncHandler(async (req, res) => {
const report = await getReport(req.params.reportId);
if (!report) {
return res.status(404).json({ message: 'Report not found.' });
}
return res.json(report);
})
);
router.post(
'/',
asyncHandler(async (req, res) => {
const body = req.body;
if (!body?.id || !body?.reportNumber || !body?.templateCode || !body?.templateVersion || !body?.answers) {
return res.status(400).json({
message: 'id, reportNumber, templateCode, templateVersion, and answers are required.'
});
}
const report = await submitReport({
id: String(body.id).trim(),
reportNumber: String(body.reportNumber).trim(),
templateCode: String(body.templateCode).trim(),
templateVersion: Number(body.templateVersion),
status: body.status || 'exported',
answers: body.answers
});
await logAuditEvent({
entityType: 'report',
entityCode: report.id,
action: 'submit',
newValue: { reportNumber: report.reportNumber, templateCode: report.templateCode }
});
return res.status(201).json(report);
})
);
/* Get all images for a report grouped by record */
router.get(
'/:reportId/images',
validateParam('reportId', { pattern: /^[a-zA-Z0-9_-]{1,100}$/ }),
asyncHandler(async (req, res) => {
const images = await getReportImages(req.params.reportId);
return res.json(images);
})
);
/* Delete a report and all associated images */
router.delete(
'/:reportId',
validateParam('reportId', { pattern: /^[a-zA-Z0-9_-]{1,100}$/ }),
asyncHandler(async (req, res) => {
await deleteReport(req.params.reportId);
await logAuditEvent({
entityType: 'report',
entityCode: req.params.reportId,
action: 'delete',
newValue: null
});
return res.json({ message: 'Report and images deleted.' });
})
);
/* Safe pattern for image file names: alphanumeric, underscore, hyphen, dot */
const SAFE_FILENAME_PATTERN = /^[a-zA-Z0-9_.-]{1,500}$/;
/* Delete a specific image from a report */
router.delete(
'/:reportId/images/:recordId/:fileName',
validateParam('reportId', { pattern: /^[a-zA-Z0-9_-]{1,100}$/ }),
validateParam('fileName', { pattern: SAFE_FILENAME_PATTERN }),
asyncHandler(async (req, res) => {
await deleteReportImage(req.params.reportId, req.params.recordId, req.params.fileName);
return res.json({ message: 'Image deleted.' });
})
);
export default router;