import type { UserInputQuestion } from "@t3tools/contracts"; export interface PendingUserInputDraftAnswer { selectedOptionValues?: string[]; customAnswer?: string; attachmentCount?: number; attachmentsBlocked?: boolean; } export interface PendingUserInputProgress { questionIndex: number; activeQuestion: UserInputQuestion | null; activeDraft: PendingUserInputDraftAnswer | undefined; selectedOptionValues: string[]; customAnswer: string; resolvedAnswer: string | string[] | null; usingCustomAnswer: boolean; answeredQuestionCount: number; isLastQuestion: boolean; isComplete: boolean; canAdvance: boolean; } function normalizeDraftAnswer(value: string | undefined): string | null { if (typeof value !== "string") { return null; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } function normalizeSelectedOptionValues(value: string[] | undefined): string[] { if (!Array.isArray(value)) { return []; } // Provider option IDs must stay unchanged, including whitespace. return Array.from(new Set(value.filter((entry) => typeof entry === "string"))); } export function resolvePendingUserInputAnswer( question: UserInputQuestion, draft: PendingUserInputDraftAnswer | undefined, ): string | string[] | null { if (draft?.attachmentsBlocked) return null; const customAnswer = question.allowCustomAnswer === false ? null : normalizeDraftAnswer(draft?.customAnswer); if (customAnswer) { return customAnswer; } const selectedOptionValues = normalizeSelectedOptionValues(draft?.selectedOptionValues).filter( (value) => question.options.some((option) => (option.value ?? option.label) === value), ); if (question.multiSelect) { return selectedOptionValues.length > 0 ? selectedOptionValues : question.allowCustomAnswer !== false && (draft?.attachmentCount ?? 0) > 0 ? "" : null; } return ( selectedOptionValues[0] ?? (question.allowCustomAnswer !== false && (draft?.attachmentCount ?? 0) > 0 ? "" : null) ); } export function setPendingUserInputCustomAnswer( draft: PendingUserInputDraftAnswer | undefined, customAnswer: string, ): PendingUserInputDraftAnswer { const selectedOptionValues = customAnswer.trim().length > 0 ? undefined : normalizeSelectedOptionValues(draft?.selectedOptionValues); return { customAnswer, ...(selectedOptionValues && selectedOptionValues.length > 0 ? { selectedOptionValues } : {}), }; } export function togglePendingUserInputOptionSelection( question: UserInputQuestion, draft: PendingUserInputDraftAnswer | undefined, optionValue: string, ): PendingUserInputDraftAnswer { if (question.multiSelect) { const selectedOptionValues = normalizeSelectedOptionValues(draft?.selectedOptionValues); const nextSelectedOptionValues = selectedOptionValues.includes(optionValue) ? selectedOptionValues.filter((value) => value !== optionValue) : [...selectedOptionValues, optionValue]; return { customAnswer: "", ...(nextSelectedOptionValues.length > 0 ? { selectedOptionValues: nextSelectedOptionValues } : {}), }; } return { customAnswer: "", selectedOptionValues: [optionValue], }; } export function buildPendingUserInputAnswers( questions: ReadonlyArray, draftAnswers: Record, ): Record | null { const answers: Record = {}; for (const question of questions) { const answer = resolvePendingUserInputAnswer(question, draftAnswers[question.id]); if (answer === null) { return null; } answers[question.id] = answer; } return answers; } export function countAnsweredPendingUserInputQuestions( questions: ReadonlyArray, draftAnswers: Record, ): number { return questions.reduce((count, question) => { return resolvePendingUserInputAnswer(question, draftAnswers[question.id]) !== null ? count + 1 : count; }, 0); } export function derivePendingUserInputProgress( questions: ReadonlyArray, draftAnswers: Record, questionIndex: number, ): PendingUserInputProgress { const normalizedQuestionIndex = questions.length === 0 ? 0 : Math.max(0, Math.min(questionIndex, questions.length - 1)); const activeQuestion = questions[normalizedQuestionIndex] ?? null; const activeDraft = activeQuestion ? draftAnswers[activeQuestion.id] : undefined; const resolvedAnswer = activeQuestion ? resolvePendingUserInputAnswer(activeQuestion, activeDraft) : null; const customAnswer = activeQuestion?.allowCustomAnswer === false ? "" : (activeDraft?.customAnswer ?? ""); const answeredQuestionCount = countAnsweredPendingUserInputQuestions(questions, draftAnswers); const isLastQuestion = questions.length === 0 ? true : normalizedQuestionIndex >= questions.length - 1; return { questionIndex: normalizedQuestionIndex, activeQuestion, activeDraft, selectedOptionValues: normalizeSelectedOptionValues(activeDraft?.selectedOptionValues), customAnswer, resolvedAnswer, usingCustomAnswer: customAnswer.trim().length > 0, answeredQuestionCount, isLastQuestion, isComplete: buildPendingUserInputAnswers(questions, draftAnswers) !== null, canAdvance: resolvedAnswer !== null, }; }