101 lines
3.2 KiB
JavaScript
101 lines
3.2 KiB
JavaScript
/*
|
|
* Report and image-rules validation. These are pure functions with no DOM or
|
|
* state dependencies so they can be tested independently and kept in sync with
|
|
* server-side validation in src/routes/configRoutes.js (A7).
|
|
*/
|
|
|
|
/* ── Report validation ──────────────────────────────────────────────────── */
|
|
|
|
/*
|
|
* Returns an array of human-readable issue strings. The caller decides whether
|
|
* issues are informational or blocking.
|
|
*/
|
|
export function validateReport(report, template, attachments) {
|
|
const issues = [];
|
|
|
|
for (const section of template.definition.sections || []) {
|
|
for (const field of section.fields || []) {
|
|
const value = report.answers[field.id];
|
|
const required = Boolean(field.required || evaluateRequiredWhen(field.requiredWhen, report.answers));
|
|
|
|
if (field.type === 'attachment') {
|
|
const fieldAttachments = attachments.filter((item) => item.fieldId === field.id);
|
|
|
|
if (required && fieldAttachments.length === 0) {
|
|
issues.push(`${field.label}: at least one image is required.`);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (required && isBlankValue(value, field.type)) {
|
|
issues.push(`${field.label}: value is required.`);
|
|
}
|
|
|
|
if (field.type === 'number' && value !== '' && value != null) {
|
|
if (Number.isNaN(Number(value))) {
|
|
issues.push(`${field.label}: number is invalid.`);
|
|
}
|
|
|
|
if (field.validation?.min !== undefined && Number(value) < field.validation.min) {
|
|
issues.push(`${field.label}: must be at least ${field.validation.min}.`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
/* ── Image-rules validation (mirrors server-side logic in configRoutes) ── */
|
|
|
|
export function validateImageRulesPayload(payload) {
|
|
if (!payload.name) {
|
|
return 'Policy name is required';
|
|
}
|
|
|
|
if (!payload.allowedMimeTypes.length) {
|
|
return 'Add at least one MIME type';
|
|
}
|
|
|
|
if (!Number.isFinite(payload.maxFileSizeBytes) || payload.maxFileSizeBytes <= 0) {
|
|
return 'Max file size must be greater than 0';
|
|
}
|
|
|
|
if (!Number.isInteger(payload.maxWidthPx) || payload.maxWidthPx <= 0) {
|
|
return 'Max width must be a positive number';
|
|
}
|
|
|
|
if (!Number.isInteger(payload.maxHeightPx) || payload.maxHeightPx <= 0) {
|
|
return 'Max height must be a positive number';
|
|
}
|
|
|
|
if (!Number.isInteger(payload.jpegQuality) || payload.jpegQuality < 1 || payload.jpegQuality > 100) {
|
|
return 'JPEG quality must be between 1 and 100';
|
|
}
|
|
|
|
if (!Number.isInteger(payload.maxAttachmentsPerField) || payload.maxAttachmentsPerField <= 0) {
|
|
return 'Max attachments must be a positive number';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/* ── Helpers ────────────────────────────────────────────────────────────── */
|
|
|
|
export function evaluateRequiredWhen(requiredWhen, answers) {
|
|
if (!requiredWhen?.field) {
|
|
return false;
|
|
}
|
|
|
|
return answers[requiredWhen.field] === requiredWhen.equals;
|
|
}
|
|
|
|
export function isBlankValue(value, type) {
|
|
if (type === 'checkbox') {
|
|
return false;
|
|
}
|
|
|
|
return value === '' || value == null;
|
|
}
|