2026-06-18 10:09:11 +02:00

159 lines
4.8 KiB
TypeScript

import type { SafetyQuiz } from '@/shared/types/app';
import type {
SafetyQuizCompletionSummary,
SafetyQuizComplianceRow,
} from '@/business/safety-quiz/types';
import { toWeekStartIso } from '@/shared/business/week';
export function getCurrentSafetyQuizWeek(date: Date): string {
// Shared American (Sunday-start) canonicalization — same util as the dashboard
// hero and F.R.A.M.E. (behavior unchanged: this was already Sunday-based).
return toWeekStartIso(date);
}
export function calculateSafetyQuizScore(
quiz: SafetyQuiz,
answers: readonly number[],
): number {
return answers.filter((answer, index) => answer === quiz.questions[index]?.correctIndex).length;
}
export function calculateSafetyQuizProgress(
currentQuestionIndex: number,
questionCount: number,
): number {
if (questionCount <= 0) {
return 0;
}
return Math.round(((currentQuestionIndex + 1) / questionCount) * 100);
}
export function getSafetyQuizFeedback(
score: number,
totalQuestions: number,
): string {
if (score === totalQuestions) {
return 'Perfect score! Outstanding safety knowledge.';
}
if (score >= 3) {
return 'Great job! Review the explanations for any missed questions.';
}
return 'Please review the material and retake when ready.';
}
export function calculateSafetyQuizCompletionSummary(
complianceRows: readonly SafetyQuizComplianceRow[],
): SafetyQuizCompletionSummary {
const completedCount = complianceRows.filter((row) => row.status === 'complete').length;
const totalStaff = complianceRows.length;
return {
completedCount,
totalStaff,
pendingCount: Math.max(totalStaff - completedCount, 0),
completionRate: totalStaff > 0 ? Math.round((completedCount / totalStaff) * 100) : 0,
};
}
export function serializeSafetyQuizPayload(payload: SafetyQuiz): string {
return JSON.stringify(payload, null, 2);
}
export function parseSafetyQuizPayload(draft: string): SafetyQuiz | string {
try {
const parsed: unknown = JSON.parse(draft);
return getSafetyQuizPayloadValidationResult(parsed);
} catch (error) {
return error instanceof Error && error.message ? error.message : 'Safety quiz JSON is invalid.';
}
}
export function validateSafetyQuizPayload(payload: SafetyQuiz): string | null {
const result = getSafetyQuizPayloadValidationResult(payload);
return typeof result === 'string' ? result : null;
}
function getSafetyQuizPayloadValidationResult(value: unknown): SafetyQuiz | string {
if (!isRecord(value)) {
return 'Safety quiz payload must be a JSON object.';
}
if (!isNonEmptyString(value.id) || !isNonEmptyString(value.title)) {
return 'Safety quiz payload must include id and title.';
}
if (!isSafetyQuizFocus(value.focus)) {
return 'Safety quiz focus must be physical-management, de-escalation, or safety-reminders.';
}
if (!isRecord(value.weeklyFocus)
|| !isNonEmptyString(value.weeklyFocus.title)
|| !isNonEmptyString(value.weeklyFocus.description)) {
return 'Weekly focus must include title and description.';
}
const keyReminders = value.keyReminders;
if (!Array.isArray(keyReminders) || !keyReminders.every(isNonEmptyString)) {
return 'Key reminders must be an array of non-empty strings.';
}
const questions = value.questions;
if (!Array.isArray(questions) || questions.length === 0) {
return 'Questions must be a non-empty array.';
}
for (const question of questions) {
if (!isValidSafetyQuizQuestion(question)) {
return 'Each question must include id, question, options, correctIndex, and explanation.';
}
}
return {
id: value.id,
title: value.title,
focus: value.focus,
weeklyFocus: {
title: value.weeklyFocus.title,
description: value.weeklyFocus.description,
},
keyReminders,
questions,
};
}
function isValidSafetyQuizQuestion(value: unknown): value is SafetyQuiz['questions'][number] {
if (!isRecord(value)) {
return false;
}
const correctIndex = value.correctIndex;
return isNonEmptyString(value.id)
&& isNonEmptyString(value.question)
&& Array.isArray(value.options)
&& value.options.length > 0
&& value.options.every(isNonEmptyString)
&& typeof correctIndex === 'number'
&& Number.isInteger(correctIndex)
&& correctIndex >= 0
&& correctIndex < value.options.length
&& isNonEmptyString(value.explanation);
}
function isSafetyQuizFocus(value: unknown): value is SafetyQuiz['focus'] {
return value === 'physical-management'
|| value === 'de-escalation'
|| value === 'safety-reminders';
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0;
}