fix(worksheets): add borrowNotation and borrowingHints to validation fallback

Add missing subtraction-specific scaffold fields to the fallback displayRules
object in validation.ts to ensure all DisplayRules fields have defaults.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-11-08 14:55:31 -06:00
parent 5a8fc5735d
commit 3f700af643
1 changed files with 66 additions and 56 deletions

View File

@ -1,98 +1,105 @@
// Validation logic for worksheet configuration
import type { WorksheetFormState, WorksheetConfig, ValidationResult } from './types'
import type { DisplayRules } from './displayRules'
import type {
WorksheetFormState,
WorksheetConfig,
ValidationResult,
} from "./types";
import type { DisplayRules } from "./displayRules";
/**
* Get current date formatted as "Month Day, Year"
*/
function getDefaultDate(): string {
const now = new Date()
return now.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
})
const now = new Date();
return now.toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
});
}
/**
* Validate and create complete config from partial form state
*/
export function validateWorksheetConfig(formState: WorksheetFormState): ValidationResult {
const errors: string[] = []
export function validateWorksheetConfig(
formState: WorksheetFormState,
): ValidationResult {
const errors: string[] = [];
// Validate total (must be positive, reasonable limit)
const total = formState.total ?? 20
const total = formState.total ?? 20;
if (total < 1 || total > 100) {
errors.push('Total problems must be between 1 and 100')
errors.push("Total problems must be between 1 and 100");
}
// Validate cols and auto-calculate rows
const cols = formState.cols ?? 4
const cols = formState.cols ?? 4;
if (cols < 1 || cols > 10) {
errors.push('Columns must be between 1 and 10')
errors.push("Columns must be between 1 and 10");
}
// Auto-calculate rows to fit all problems
const rows = Math.ceil(total / cols)
const rows = Math.ceil(total / cols);
// Validate probabilities (0-1 range)
const pAnyStart = formState.pAnyStart ?? 0.75
const pAllStart = formState.pAllStart ?? 0.25
const pAnyStart = formState.pAnyStart ?? 0.75;
const pAllStart = formState.pAllStart ?? 0.25;
if (pAnyStart < 0 || pAnyStart > 1) {
errors.push('pAnyStart must be between 0 and 1')
errors.push("pAnyStart must be between 0 and 1");
}
if (pAllStart < 0 || pAllStart > 1) {
errors.push('pAllStart must be between 0 and 1')
errors.push("pAllStart must be between 0 and 1");
}
if (pAllStart > pAnyStart) {
errors.push('pAllStart cannot be greater than pAnyStart')
errors.push("pAllStart cannot be greater than pAnyStart");
}
// Validate fontSize
const fontSize = formState.fontSize ?? 16
const fontSize = formState.fontSize ?? 16;
if (fontSize < 8 || fontSize > 32) {
errors.push('Font size must be between 8 and 32')
errors.push("Font size must be between 8 and 32");
}
// V4: Validate digitRange (min and max must be 1-5, min <= max)
// Note: Same range applies to both addition and subtraction
const digitRange = formState.digitRange ?? { min: 2, max: 2 }
const digitRange = formState.digitRange ?? { min: 2, max: 2 };
if (!digitRange.min || digitRange.min < 1 || digitRange.min > 5) {
errors.push('Digit range min must be between 1 and 5')
errors.push("Digit range min must be between 1 and 5");
}
if (!digitRange.max || digitRange.max < 1 || digitRange.max > 5) {
errors.push('Digit range max must be between 1 and 5')
errors.push("Digit range max must be between 1 and 5");
}
if (digitRange.min > digitRange.max) {
errors.push('Digit range min cannot be greater than max')
errors.push("Digit range min cannot be greater than max");
}
// V4: Validate operator (addition, subtraction, or mixed)
const operator = formState.operator ?? 'addition'
if (!['addition', 'subtraction', 'mixed'].includes(operator)) {
errors.push('Operator must be "addition", "subtraction", or "mixed"')
const operator = formState.operator ?? "addition";
if (!["addition", "subtraction", "mixed"].includes(operator)) {
errors.push('Operator must be "addition", "subtraction", or "mixed"');
}
// Validate seed (must be positive integer)
const seed = formState.seed ?? Date.now() % 2147483647
const seed = formState.seed ?? Date.now() % 2147483647;
if (!Number.isInteger(seed) || seed < 0) {
errors.push('Seed must be a non-negative integer')
errors.push("Seed must be a non-negative integer");
}
if (errors.length > 0) {
return { isValid: false, errors }
return { isValid: false, errors };
}
// Determine orientation based on columns (portrait = 2-3 cols, landscape = 4-5 cols)
const orientation = formState.orientation || (cols <= 3 ? 'portrait' : 'landscape')
const orientation =
formState.orientation || (cols <= 3 ? "portrait" : "landscape");
// Get primary state values
const problemsPerPage = formState.problemsPerPage ?? total
const pages = formState.pages ?? 1
const problemsPerPage = formState.problemsPerPage ?? total;
const pages = formState.pages ?? 1;
// Determine mode (default to 'smart' if not specified)
const mode = formState.mode ?? 'smart'
const mode = formState.mode ?? "smart";
// Shared fields for both modes
const sharedFields = {
@ -107,7 +114,7 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
rows,
// Other fields
name: formState.name?.trim() || 'Student',
name: formState.name?.trim() || "Student",
date: formState.date?.trim() || getDefaultDate(),
pAnyStart,
pAllStart,
@ -117,12 +124,12 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
digitRange,
// V4: Operator selection (addition, subtraction, or mixed)
operator: formState.operator ?? 'addition',
operator: formState.operator ?? "addition",
// Layout
page: {
wIn: orientation === 'portrait' ? 8.5 : 11,
hIn: orientation === 'portrait' ? 11 : 8.5,
wIn: orientation === "portrait" ? 8.5 : 11,
hIn: orientation === "portrait" ? 11 : 8.5,
},
margins: {
left: 0.6,
@ -133,34 +140,37 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
fontSize,
seed,
}
};
// Build mode-specific config
let config: WorksheetConfig
let config: WorksheetConfig;
if (mode === 'smart') {
if (mode === "smart") {
// Smart mode: Use displayRules for conditional scaffolding
const displayRules: DisplayRules = formState.displayRules ?? {
carryBoxes: 'whenRegrouping',
answerBoxes: 'always',
placeValueColors: 'always',
tenFrames: 'whenRegrouping',
problemNumbers: 'always',
cellBorders: 'always',
}
const displayRules: DisplayRules = {
carryBoxes: "whenRegrouping",
answerBoxes: "always",
placeValueColors: "always",
tenFrames: "whenRegrouping",
problemNumbers: "always",
cellBorders: "always",
borrowNotation: "whenRegrouping", // Subtraction: show when borrowing
borrowingHints: "never", // Subtraction: no hints by default
...((formState.displayRules as any) ?? {}), // Override with provided rules if any
};
config = {
version: 4,
mode: 'smart',
mode: "smart",
displayRules,
difficultyProfile: formState.difficultyProfile,
...sharedFields,
}
};
} else {
// Manual mode: Use boolean flags for uniform display
config = {
version: 4,
mode: 'manual',
mode: "manual",
showCarryBoxes: formState.showCarryBoxes ?? true,
showAnswerBoxes: formState.showAnswerBoxes ?? true,
showPlaceValueColors: formState.showPlaceValueColors ?? true,
@ -172,8 +182,8 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
showBorrowingHints: formState.showBorrowingHints ?? false,
manualPreset: formState.manualPreset,
...sharedFields,
}
};
}
return { isValid: true, config }
return { isValid: true, config };
}