stage 1
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user