/* * 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; }