Working version before modification.

This commit is contained in:
Stan
2026-04-20 21:04:54 +02:00
parent 28d167f11f
commit e7127f3215
30 changed files with 7046 additions and 1201 deletions
+130 -5
View File
@@ -2,13 +2,14 @@ import { query } from '../db/pool.js';
import { parseJsonColumn } from '../utils/json.js';
/*
* The report service handles server-side storage of submitted reports. In
* phase 1, reports are created locally in the browser and only uploaded when
* the operator explicitly submits. This keeps the offline-first workflow intact
* while giving the backend a durable copy for review, export, or archival.
* The report service handles server-side storage of submitted reports.
* Images are stored as BLOBs in the report_images table alongside metadata.
*/
export async function submitReport(report) {
/* Strip image dataUrls from answers before storing in JSON column */
const answersForJson = stripImagesFromAnswers(report.answers);
await query(
`
INSERT INTO reports (report_uuid, report_number, template_code, template_version, status, answers_json, submitted_at)
@@ -25,10 +26,15 @@ export async function submitReport(report) {
report.templateCode,
report.templateVersion,
report.status,
JSON.stringify(report.answers)
JSON.stringify(answersForJson)
]
);
/* Store images as BLOBs in DB */
if (report.answers?.records) {
await storeReportImages(report.id, report.answers.records);
}
return getReport(report.id);
}
@@ -106,3 +112,122 @@ function mapReportRow(row) {
updatedAt: row.updatedAt
};
}
/* ═══════════════════════════════════════════════════════════════════════════
* Image storage helpers
* ═══════════════════════════════════════════════════════════════════════════ */
/**
* Strips dataUrl from image objects in answers so the JSON column stays lean.
* The full image data is stored separately in report_images.
*/
function stripImagesFromAnswers(answers) {
if (!answers?.records) return answers;
const clean = { ...answers, records: {} };
for (const [recId, rd] of Object.entries(answers.records)) {
clean.records[recId] = {
...rd,
images: (rd.images || []).map(img => ({
name: img.name,
size: img.size,
width: img.width,
height: img.height,
exif: img.exif || null
}))
};
}
return clean;
}
/**
* Stores image binary data as BLOBs in the report_images table.
* Replaces existing images for the report on re-submit.
*/
async function storeReportImages(reportUuid, records) {
/* Clear existing images for this report to avoid duplicates */
await query('DELETE FROM report_images WHERE report_uuid = ?', [reportUuid]);
for (const [recId, rd] of Object.entries(records)) {
if (!rd.images?.length) continue;
for (let i = 0; i < rd.images.length; i++) {
const img = rd.images[i];
if (!img.dataUrl) continue;
/* Convert base64 dataUrl to Buffer */
const matches = img.dataUrl.match(/^data:([^;]+);base64,(.+)$/);
if (!matches) continue;
const mimeType = matches[1];
const buffer = Buffer.from(matches[2], 'base64');
const fileName = img.name || `image_${i}.jpg`;
await query(
`INSERT INTO report_images (report_uuid, record_id, image_index, file_name, file_size, mime_type, width_px, height_px, exif_json, image_data)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
reportUuid,
recId,
i,
fileName,
img.size || buffer.length,
mimeType,
img.width || null,
img.height || null,
img.exif ? JSON.stringify(img.exif) : null,
buffer
]
);
}
}
}
/**
* Retrieves all images for a given report, grouped by record ID.
* Returns base64 dataUrls constructed from the stored BLOBs.
*/
export async function getReportImages(reportUuid) {
const rows = await query(
`SELECT record_id AS recordId, image_index AS imageIndex, file_name AS fileName,
file_size AS fileSize, mime_type AS mimeType, width_px AS widthPx,
height_px AS heightPx, exif_json AS exifJson, image_data AS imageData
FROM report_images
WHERE report_uuid = ?
ORDER BY record_id, image_index`,
[reportUuid]
);
const grouped = {};
for (const row of rows) {
if (!grouped[row.recordId]) grouped[row.recordId] = [];
const base64 = row.imageData.toString('base64');
grouped[row.recordId].push({
index: row.imageIndex,
name: row.fileName,
size: row.fileSize,
mimeType: row.mimeType,
width: row.widthPx,
height: row.heightPx,
exif: parseJsonColumn(row.exifJson, null),
dataUrl: `data:${row.mimeType};base64,${base64}`
});
}
return grouped;
}
/**
* Deletes a report and all its associated images from DB.
*/
export async function deleteReport(reportUuid) {
/* CASCADE will remove report_images rows automatically */
await query('DELETE FROM reports WHERE report_uuid = ?', [reportUuid]);
}
/**
* Deletes a specific image for a record in a report.
*/
export async function deleteReportImage(reportUuid, recordId, fileName) {
await query(
'DELETE FROM report_images WHERE report_uuid = ? AND record_id = ? AND file_name = ?',
[reportUuid, recordId, fileName]
);
}