276 lines
7.2 KiB
JavaScript
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;
|