From e1310badef3145b3f2dcb92b0cddf2bb41b48337 Mon Sep 17 00:00:00 2001 From: ghorbani Date: Mon, 10 Aug 2026 16:53:04 +0330 Subject: [PATCH] feat: implement question detail client logic with automated test flow and answer storage integration --- .../[slug]/question-detail-client.tsx | 275 +--------- src/app/questions-list/page.tsx | 66 +-- src/components/Componentes/progress-helper.ts | 476 +----------------- .../Componentes/question-answer-storage.tsx | 17 +- .../Componentes/required-steps-card.tsx | 76 +-- src/hooks/marriage/use-section-data.ts | 91 +--- src/lib/schema-adapter.ts | 130 +++++ 7 files changed, 186 insertions(+), 945 deletions(-) create mode 100644 src/lib/schema-adapter.ts diff --git a/src/app/questions-list/[slug]/question-detail-client.tsx b/src/app/questions-list/[slug]/question-detail-client.tsx index e044906..4d7976f 100644 --- a/src/app/questions-list/[slug]/question-detail-client.tsx +++ b/src/app/questions-list/[slug]/question-detail-client.tsx @@ -21,10 +21,7 @@ import TestQuestionsFlow, { import { cattellFallbackQuestions } from "@/data/cattell-fallback"; import { glasserFallbackQuestions } from "@/data/glasser-fallback"; import { - getQuestionListItemBySlug, isQuestionListItemVisibleForProfile, - isQuestionRequiredForProfile, - isQuestionVisibleForProfile, type QuestionField, } from "@/data/question-data"; import type { MarriageGender } from "@/hooks/marriage/types"; @@ -37,6 +34,8 @@ import { useSubmitGlasserAssessmentMutation, } from "@/hooks/marriage/use-glasser"; import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; +import { useFormSchemaQuery } from "@/hooks/marriage/use-form-schema"; +import { convertSchemaToFrontendItems } from "@/lib/schema-adapter"; import { defaultLocale, type Locale } from "@/translations/config"; import { useI18n } from "@/translations/provider"; @@ -172,251 +171,8 @@ function QuestionFlowWrapper({ }) { const { getAnswerValue } = useQuestionAnswers(); - const dynamicQuestions = useMemo(() => { - // Find employment status question by title or by choices length (10) - const employmentQuestionIndex = visibleQuestions.findIndex((q) => { - const enTitle = q.englishTitle || q.title; - return ( - enTitle === "Employment Status" || - (q.type === "dropdown" && q.extras.options?.length === 10) - ); - }); - - let employmentSelectedOptionIndex = -1; - if (employmentQuestionIndex !== -1) { - const employmentQuestion = visibleQuestions[employmentQuestionIndex]; - const ans = getAnswerValue(employmentQuestion, employmentQuestionIndex); - if (ans) { - employmentSelectedOptionIndex = - employmentQuestion.extras.options?.indexOf(String(ans)) ?? -1; - } - } - - // Find Parents' Survival Status question index and check selected index - const survivalStatusQuestionIndex = visibleQuestions.findIndex((q) => { - const enTitle = q.englishTitle || q.title; - return enTitle === "Parents' Survival Status"; - }); - - let survivalSelectedOptionIndex = -1; - if (survivalStatusQuestionIndex !== -1) { - const survivalQuestion = visibleQuestions[survivalStatusQuestionIndex]; - const ans = getAnswerValue(survivalQuestion, survivalStatusQuestionIndex); - if (ans) { - survivalSelectedOptionIndex = - survivalQuestion.extras.options?.indexOf(String(ans)) ?? -1; - } - } - - // Find Parents' Marital Status question index and check selected index - const maritalStatusQuestionIndex = visibleQuestions.findIndex((q) => { - const enTitle = q.englishTitle || q.title; - return enTitle === "Parents' Marital Status"; - }); - - let isCircumstancesSelected = false; - if (maritalStatusQuestionIndex !== -1) { - const maritalQuestion = visibleQuestions[maritalStatusQuestionIndex]; - const ans = getAnswerValue(maritalQuestion, maritalStatusQuestionIndex); - if (ans) { - const selectedIdx = - maritalQuestion.extras.options?.indexOf(String(ans)) ?? -1; - isCircumstancesSelected = selectedIdx === 2; - } - } - - // Find Current Marital Status question index and check selected index - const currentMaritalQuestionIndex = visibleQuestions.findIndex((q) => { - const enTitle = q.englishTitle || q.title; - return enTitle === "Current Marital Status"; - }); - - let maritalSelectedIndex = -1; - if (currentMaritalQuestionIndex !== -1) { - const maritalQuestion = visibleQuestions[currentMaritalQuestionIndex]; - const ans = getAnswerValue(maritalQuestion, currentMaritalQuestionIndex); - if (ans) { - maritalSelectedIndex = - maritalQuestion.extras.options?.indexOf(String(ans)) ?? -1; - } - } - - // Find Children and Guardianship Status question index - const custodyQuestionIndex = visibleQuestions.findIndex((q) => { - const enTitle = q.englishTitle || q.title; - return enTitle === "Children and Guardianship Status"; - }); - - let hasChildrenSelected = false; - let hasAnyGuardianshipSelected = false; - - if (custodyQuestionIndex !== -1) { - const custodyQuestion = visibleQuestions[custodyQuestionIndex]; - const ans = getAnswerValue(custodyQuestion, custodyQuestionIndex); - if (ans) { - const ansList = Array.isArray(ans) ? ans.map(String) : [String(ans)]; - const selectedIndices = ansList.map((val) => - custodyQuestion.extras.options?.indexOf(val), - ); - hasChildrenSelected = selectedIndices.some( - (idx) => idx === 1 || idx === 2, - ); - hasAnyGuardianshipSelected = selectedIndices.some( - (idx) => idx === 2 || idx === 3, - ); - } - } - - const filtered = visibleQuestions.filter((question, index) => { - const enTitle = question.englishTitle || question.title; - // Check if this is one of the marital status/children questions - if (currentMaritalQuestionIndex !== -1) { - const isDuration = - index === currentMaritalQuestionIndex + 1 || - enTitle === "Previous Marriage Duration"; - - const isSeparation = - index === currentMaritalQuestionIndex + 2 || - enTitle === "Reason for Separation"; - - const isCustody = - index === currentMaritalQuestionIndex + 3 || - enTitle === "Children and Guardianship Status"; - - const isChildrenCount = - index === currentMaritalQuestionIndex + 4 || - enTitle === "Number of Children"; - - const isChildrenExplanation = - index === currentMaritalQuestionIndex + 5 || - enTitle === "Short Children/Guardianship Explanation"; - - if (isDuration) { - return [1, 2, 3].includes(maritalSelectedIndex); - } - if (isSeparation) { - return [1, 2].includes(maritalSelectedIndex); - } - if (isCustody) { - return [2, 3].includes(maritalSelectedIndex); - } - if (isChildrenCount) { - return [2, 3].includes(maritalSelectedIndex) && hasChildrenSelected; - } - if (isChildrenExplanation) { - return ( - [2, 3].includes(maritalSelectedIndex) && hasAnyGuardianshipSelected - ); - } - } - - // Check if this is one of the three job-related questions - if (employmentQuestionIndex !== -1) { - const isJobTitle = - index === employmentQuestionIndex + 1 || enTitle === "Job Title"; - - const isWorkLocation = - index === employmentQuestionIndex + 2 || enTitle === "Work Location"; - - const isMonthlyIncome = - index === employmentQuestionIndex + 3 || enTitle === "Monthly Income"; - - if (isJobTitle || isWorkLocation || isMonthlyIncome) { - // If no employment status is selected yet, hide them by default - if (employmentSelectedOptionIndex === -1) { - return false; - } - - // Options logic: - // Show all 3 for: index 0 (Full-time), 1 (Part-time), 2 (Self-employed), 3 (Entrepreneur), 5 (Working Student) - const showAll = [0, 1, 2, 3, 5].includes( - employmentSelectedOptionIndex, - ); - // Hide all 3 for: index 4 (Student), 6 (Student & Job Seeking), 7 (Job Seeking / Unemployed), 8 (Homemaker) - const hideAll = [4, 6, 7, 8].includes(employmentSelectedOptionIndex); - // Special Retired logic: index 9 (Retired) - const isRetired = employmentSelectedOptionIndex === 9; - - if (showAll) { - return true; - } - if (hideAll) { - return false; - } - if (isRetired) { - if (isJobTitle || isMonthlyIncome) { - return true; - } - if (isWorkLocation) { - return false; - } - } - return false; - } - } - - // Check Parents' Survival Status to decide if Parents' Marital Status is visible - if (survivalStatusQuestionIndex !== -1) { - const isParentsMaritalStatus = enTitle === "Parents' Marital Status"; - - if (isParentsMaritalStatus) { - return survivalSelectedOptionIndex === 0; - } - } - - // Default dependsOn logic - if (question.logic?.dependsOn) { - const { title, values } = question.logic.dependsOn; - const dependentQuestionIndex = visibleQuestions.findIndex( - (q) => q.title === title, - ); - - if (dependentQuestionIndex !== -1) { - const dependentQuestion = visibleQuestions[dependentQuestionIndex]; - const answer = getAnswerValue( - dependentQuestion, - dependentQuestionIndex, - ); - if (Array.isArray(answer)) { - return answer.some((ans) => values.includes(String(ans))); - } - return values.includes(String(answer)); - } - return false; - } - return true; - }); - - return filtered.map((question) => { - const enTitle = question.englishTitle || question.title; - if (enTitle === "Short Family Description") { - return { - ...question, - required: isCircumstancesSelected, - }; - } - - if ( - enTitle === "Previous Marriage Duration" || - enTitle === "Number of Children" || - enTitle === "Short Children/Guardianship Explanation" || - enTitle === "Additional details about family responsibility" || - enTitle === "Do the supported individual(s) live with you?" || - enTitle === "What is the custody status of your child(ren)?" || - enTitle === - "Does the custody, visitation, or relocation schedule impact your residence or immigration?" || - enTitle === "What is the payment or receipt status of child support?" || - enTitle === - "Acceptance of necessary communication between future spouse and the other parent" - ) { - return { - ...question, - required: true, - }; - } - return question; - }); - }, [visibleQuestions, getAnswerValue]); + // dynamicQuestions is now exactly what the backend gives as visible + const dynamicQuestions = visibleQuestions; const requiredCount = useMemo( () => dynamicQuestions.filter((q) => q.required).length, @@ -437,7 +193,6 @@ function QuestionFlowWrapper({ {dynamicQuestions.map((question, index) => { let originalIndex = visibleQuestions.indexOf(question); if (originalIndex === -1) { - // Spread-copied questions lose reference equality; fall back to title originalIndex = visibleQuestions.findIndex( (q) => (q.englishTitle || q.title) === @@ -529,7 +284,9 @@ export default function QuestionDetailClient({ useMarriageProfileQuery(); const profileGender = profile?.gender; const age = getStoredAge(); - const item = getQuestionListItemBySlug(itemSlug, locale); + const { data: schema } = useFormSchemaQuery("profile", locale); + const items = useMemo(() => convertSchemaToFrontendItems(schema, locale), [schema, locale]); + const item = items.find((i) => i.slug === itemSlug); const isCattellSlug = itemSlug === "personality_test"; const isGlasserSlug = itemSlug === "glasser_5_needs_test"; @@ -617,25 +374,13 @@ export default function QuestionDetailClient({ return []; } - const hasDobQuestion = item.questions.some( - (q) => q.title === "Date of Birth" || q.title === "طھط§ط±غŒط® طھظˆظ„ط¯", - ); - return item.questions - .filter((question) => { - if ( - hasDobQuestion && - (question.title === "Age" || question.title === "ط³ظ†") - ) { - return false; - } - return isQuestionVisibleForProfile(question, profileContext); - }) + .filter((question) => (question as any).isVisible !== false) .map((question) => ({ ...question, - required: isQuestionRequiredForProfile(question, profileContext), + required: Boolean(question.required), })); - }, [item, profileContext]); + }, [item]); const requiredQuestionsCount = useMemo( () => visibleQuestions.filter((q) => q.required).length, diff --git a/src/app/questions-list/page.tsx b/src/app/questions-list/page.tsx index cf4c0d1..892c528 100644 --- a/src/app/questions-list/page.tsx +++ b/src/app/questions-list/page.tsx @@ -18,16 +18,13 @@ import { FixToTheEnd } from "@/components/Componentes/fix-to-the-end"; import { LoadingSkeleton } from "@/components/Componentes/loading-skeleton"; import { PageBackground } from "@/components/Componentes/page-background"; import ErrorToast from "@/components/Componentes/error-toast"; -import { - getQuestionListItems, - isQuestionListItemVisibleForProfile, - type QuestionListItem, -} from "@/data/question-data"; -import { toFrontendSlug } from "@/data/section-slug-map"; import { useQueryClient } from "@tanstack/react-query"; import { useStartMarriageMatchMutation } from "@/hooks/marriage/use-match-start"; import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { useMarriageSectionsQuery } from "@/hooks/marriage/use-sections"; +import { useFormSchemaQuery } from "@/hooks/marriage/use-form-schema"; +import { convertSchemaToFrontendItems } from "@/lib/schema-adapter"; +import type { QuestionListItem } from "@/data/question-data"; import { useCloseServiceOnBack } from "@/hooks/use-close-service-on-back"; import { clearMatchStartGrace, @@ -47,6 +44,8 @@ export default function QuestionsListPage() { useMarriageProfileQuery(); const { data: sections, isLoading: isSectionsLoading } = useMarriageSectionsQuery(); + const { data: schema, isLoading: isSchemaLoading } = + useFormSchemaQuery("profile", locale); useEffect(() => { triggerSilentReload(queryClient); @@ -68,59 +67,26 @@ export default function QuestionsListPage() { const [selectedSection, setSelectedSection] = useState(null); const questionListItems = useMemo( - () => - getQuestionListItems(locale).filter((item) => - isQuestionListItemVisibleForProfile(item, { - gender: profile?.gender, - }), - ), - [locale, profile?.gender], + () => convertSchemaToFrontendItems(schema, locale), + [schema, locale], ); const sectionProgressBySlug = useMemo(() => { const progressBySlug = new Map(); - const age = getStoredAge(); - - sections?.forEach((section) => { - const frontendSlug = toFrontendSlug(section.slug); - const progress = Math.max( - 0, - Math.min(100, Math.round(section.completion_percent)), - ); - progressBySlug.set(frontendSlug, progress); - progressBySlug.set(section.slug, progress); - }); - const fbSec = sections?.find((s) => s.slug === "family_background"); - const mhSec = sections?.find((s) => s.slug === "marital_history"); - if (fbSec || mhSec) { - const fbTotal = fbSec?.total_steps ?? 6; - const fbCurrent = fbSec - ? Math.round((fbSec.completion_percent / 100) * fbTotal) - : 0; - const mhTotal = mhSec?.total_steps ?? 6; - const mhCurrent = mhSec - ? Math.round((mhSec.completion_percent / 100) * mhTotal) - : 0; - const combinedProgress = - fbTotal + mhTotal > 0 - ? ((fbCurrent + mhCurrent) / (fbTotal + mhTotal)) * 100 - : 0; - progressBySlug.set( - "family_marital_history", - Math.max(0, Math.min(100, Math.round(combinedProgress))), - ); + if (schema?.progress?.sections_progress) { + Object.entries(schema.progress.sections_progress).forEach(([slug, prog]) => { + progressBySlug.set(slug, Math.max(0, Math.min(100, Math.round(prog.completion_percent)))); + }); } - + + // Add fallback for combined section from the schema adapter which attaches it to questionListItems directly questionListItems.forEach((item) => { - const localProgress = getLocalSectionProgress(item, profile, age); - if (localProgress !== null) { - progressBySlug.set(item.slug, localProgress); - } + progressBySlug.set(item.slug, item.progress); }); return progressBySlug; - }, [sections, questionListItems, profile]); + }, [schema, questionListItems]); const requiredQuestionListItems = useMemo( () => questionListItems.filter((item) => Boolean(item.required)), @@ -235,7 +201,7 @@ export default function QuestionsListPage() { } }; - if ((!profile || !sections) && (isProfileLoading || isSectionsLoading)) { + if (isProfileLoading || isSectionsLoading || isSchemaLoading) { return ( <> diff --git a/src/components/Componentes/progress-helper.ts b/src/components/Componentes/progress-helper.ts index 06ad5c1..1ab1b54 100644 --- a/src/components/Componentes/progress-helper.ts +++ b/src/components/Componentes/progress-helper.ts @@ -1,13 +1,5 @@ -import { - isQuestionRequiredForProfile, - isQuestionVisibleForProfile, - type QuestionListItem, -} from "@/data/question-data"; -import type { - MarriageFieldValue, - MarriageGender, -} from "@/hooks/marriage/types"; -import { hasQuestionAnswerValue } from "./question-answer-storage"; +import { type QuestionListItem } from "@/data/question-data"; +import type { MarriageGender } from "@/hooks/marriage/types"; export function getStoredAge(): number | null { try { @@ -53,472 +45,10 @@ export function getStoredAge(): number | null { return null; } -function isFieldAnswered( - q: Record, - field: Record | undefined, -): boolean { - if (!field || !hasQuestionAnswerValue(field.value as MarriageFieldValue)) { - return false; - } - if (q.type === "birthplace") { - const strVal = String(field.value); - const parts = strVal.split(",").map((p) => p.trim()); - return parts.length >= 2 && parts[0].length > 0 && parts[1].length > 0; - } - return true; -} - -function hashString(value: string) { - let hash = 0; - for (let index = 0; index < value.length; index += 1) { - hash = (hash * 31 + value.charCodeAt(index)) >>> 0; - } - return hash.toString(36); -} - -function slugifyTitle(title: string) { - const slug = title - .normalize("NFKD") - .replace(/[\u0300-\u036f]/g, "") - .toLowerCase() - .replace(/[^a-z0-9]+/g, "_") - .replace(/^_+|_+$/g, ""); - return slug || `field_${hashString(title)}`; -} - export function getLocalSectionProgress( item: QuestionListItem, profile: { gender?: MarriageGender | null } | null | undefined, age: number | null, ): number | null { - try { - if (typeof window === "undefined") return null; - - if (item.slug === "personality_test" || item.slug === "glasser_5_needs_test") { - const draftKey = `marriage:tests:${item.slug}:draft`; - const draftRaw = window.localStorage.getItem(draftKey); - if (draftRaw) { - try { - const parsed = JSON.parse(draftRaw); - if (parsed && typeof parsed.answers === "object" && parsed.answers !== null) { - const answeredCount = Object.keys(parsed.answers).length; - const totalCount = item.slug === "personality_test" ? 187 : 35; - return Math.max( - 0, - Math.min( - 100, - Math.round((answeredCount / totalCount) * 100), - ), - ); - } - } catch {} - } - return null; - } - - const fields: Record[] = []; - let hasFoundStorage = false; - - const mainKey = `marriage:sections:${item.slug}:answers`; - const mainRaw = window.localStorage.getItem(mainKey); - if (mainRaw) { - try { - const parsed = JSON.parse(mainRaw); - if ( - parsed && - Array.isArray(parsed.fields) && - parsed.fields.length > 0 - ) { - fields.push(...parsed.fields); - hasFoundStorage = true; - } - } catch {} - } - - if (item.slug === "family_marital_history") { - const fbRaw = window.localStorage.getItem( - "marriage:sections:family_background:answers", - ); - if (fbRaw) { - try { - const parsed = JSON.parse(fbRaw); - if (parsed && Array.isArray(parsed.fields)) { - fields.push(...parsed.fields); - hasFoundStorage = true; - } - } catch {} - } - - const mhRaw = window.localStorage.getItem( - "marriage:sections:marital_history_children:answers", - ); - if (mhRaw) { - try { - const parsed = JSON.parse(mhRaw); - if (parsed && Array.isArray(parsed.fields)) { - fields.push(...parsed.fields); - hasFoundStorage = true; - } - } catch {} - } - } - - if (!hasFoundStorage || fields.length === 0) { - return null; - } - - const profileContext = { - age, - gender: profile?.gender, - }; - - const hasDobQuestion = item.questions.some( - (q) => q.title === "Date of Birth" || q.title === "تاریخ تولد", - ); - - const profileVisible = item.questions - .filter((question) => { - if ( - hasDobQuestion && - (question.title === "Age" || question.title === "سن") - ) { - return false; - } - return isQuestionVisibleForProfile(question, profileContext); - }) - .map((question) => ({ - ...question, - required: isQuestionRequiredForProfile(question, profileContext), - })); - - const getQuestionFieldKey = (question: any, questionIndex: number) => { - const index = - question.originalIndex !== undefined - ? question.originalIndex - : questionIndex; - return `q${index + 1}_${slugifyTitle(question.englishTitle || question.title)}`; - }; - - const findAnswer = (question: any, questionIndex: number) => { - const key = getQuestionFieldKey(question, questionIndex); - const engSlug = slugifyTitle(question.englishTitle || question.title); - const field = fields.find( - (f) => - f && - (f.key === key || - f.label === question.title || - f.label === question.englishTitle || - (typeof f.key === "string" && - (f.key.endsWith(`.${engSlug}`) || f.key.endsWith(`_${engSlug}`)))), - ); - return field?.value; - }; - - // Find employment status question by title or by choices length (10) - const employmentQuestionIndex = profileVisible.findIndex((q) => { - return ( - q.title === "Employment Status" || - q.title === "وضعیت اشتغال" || - (q.type === "dropdown" && q.extras?.options?.length === 10) - ); - }); - - let employmentSelectedOptionIndex = -1; - if (employmentQuestionIndex !== -1) { - const employmentQuestion = profileVisible[employmentQuestionIndex]; - const ans = findAnswer(employmentQuestion, employmentQuestionIndex); - if (ans) { - employmentSelectedOptionIndex = - employmentQuestion.extras?.options?.indexOf(String(ans)) ?? -1; - } - } - - // Find Parents' Survival Status question index and check selected index - const survivalStatusQuestionIndex = profileVisible.findIndex((q) => { - return ( - q.title === "Parents' Survival Status" || - q.title === "وضعیت حیات والدین" - ); - }); - - let survivalSelectedOptionIndex = -1; - if (survivalStatusQuestionIndex !== -1) { - const survivalQuestion = profileVisible[survivalStatusQuestionIndex]; - const ans = findAnswer(survivalQuestion, survivalStatusQuestionIndex); - if (ans) { - survivalSelectedOptionIndex = - survivalQuestion.extras?.options?.indexOf(String(ans)) ?? -1; - } - } - - // Find Parents' Marital Status question index and check selected index - const maritalStatusQuestionIndex = profileVisible.findIndex((q) => { - return ( - q.title === "Parents' Marital Status" || q.title === "وضعیت تأهل والدین" - ); - }); - - let isCircumstancesSelected = false; - if (maritalStatusQuestionIndex !== -1) { - const maritalQuestion = profileVisible[maritalStatusQuestionIndex]; - const ans = findAnswer(maritalQuestion, maritalStatusQuestionIndex); - if (ans) { - const selectedIdx = - maritalQuestion.extras?.options?.indexOf(String(ans)) ?? -1; - isCircumstancesSelected = selectedIdx === 2; - } - } - - // Find Current Marital Status question index and check selected index - const currentMaritalQuestionIndex = profileVisible.findIndex((q) => { - return ( - q.title === "Current Marital Status" || q.title === "وضعیت تأهل فعلی" - ); - }); - - let maritalSelectedIndex = -1; - if (currentMaritalQuestionIndex !== -1) { - const maritalQuestion = profileVisible[currentMaritalQuestionIndex]; - const ans = findAnswer(maritalQuestion, currentMaritalQuestionIndex); - if (ans) { - maritalSelectedIndex = - maritalQuestion.extras?.options?.indexOf(String(ans)) ?? -1; - } - } - - // Find Children and Guardianship Status question index - const custodyQuestionIndex = profileVisible.findIndex((q) => { - return ( - q.title === "Children and Guardianship Status" || - q.title === "وضعیت فرزند و تکفل" - ); - }); - - let hasChildrenSelected = false; - let hasAnyGuardianshipSelected = false; - - if (custodyQuestionIndex !== -1) { - const custodyQuestion = profileVisible[custodyQuestionIndex]; - const ans = findAnswer(custodyQuestion, custodyQuestionIndex); - if (ans) { - const ansList = Array.isArray(ans) ? ans.map(String) : [String(ans)]; - hasChildrenSelected = ansList.some( - (val) => val.includes("Have children") || val.includes("فرزند دارم"), - ); - hasAnyGuardianshipSelected = ansList.some( - (val) => - val.includes("Have children") || - val.includes("فرزند دارم") || - val.includes("under my guardianship") || - val.includes("تحت تکفل"), - ); - } - } - - const filtered = profileVisible.filter((question, index) => { - // Check if this is one of the marital status/children questions - if (currentMaritalQuestionIndex !== -1) { - const isDuration = - index === currentMaritalQuestionIndex + 1 || - question.title === "Previous Marriage Duration" || - question.title === "مدت ازدواج یا عقد قبلی"; - - const isSeparation = - index === currentMaritalQuestionIndex + 2 || - question.title === "Reason for Separation" || - question.title === "علت جدایی، در صورت وجود"; - - const isCustody = - index === currentMaritalQuestionIndex + 3 || - question.title === "Children and Guardianship Status" || - question.title === "وضعیت فرزند و تکفل"; - - const isChildrenCount = - index === currentMaritalQuestionIndex + 4 || - question.title === "Number of Children" || - question.title === "تعداد فرزندان"; - - const isChildrenExplanation = - index === currentMaritalQuestionIndex + 5 || - question.title === "Short Children/Guardianship Explanation" || - question.title === "توضیح کوتاه درباره شرایط فرزند یا تکفل"; - - if (isDuration) { - return [1, 2, 3].includes(maritalSelectedIndex); - } - if (isSeparation) { - return [1, 2].includes(maritalSelectedIndex); - } - if (isCustody) { - return [2, 3].includes(maritalSelectedIndex); - } - if (isChildrenCount) { - return [2, 3].includes(maritalSelectedIndex) && hasChildrenSelected; - } - if (isChildrenExplanation) { - return ( - [2, 3].includes(maritalSelectedIndex) && hasAnyGuardianshipSelected - ); - } - } - - // Check if this is one of the three job-related questions - if (employmentQuestionIndex !== -1) { - const isJobTitle = - index === employmentQuestionIndex + 1 || - question.title === "Job Title" || - question.title === "عنوان شغلی"; - - const isWorkLocation = - index === employmentQuestionIndex + 2 || - question.title === "Work Location" || - question.title === "محل فعالیت"; - - const isMonthlyIncome = - index === employmentQuestionIndex + 3 || - question.title === "Monthly Income" || - question.title === "میزان درآمد ماهانه"; - - if (isJobTitle || isWorkLocation || isMonthlyIncome) { - // If no employment status is selected yet, hide them by default - if (employmentSelectedOptionIndex === -1) { - return false; - } - - // Options logic: - // Show all 3 for: index 0 (Full-time), 1 (Part-time), 2 (Self-employed), 3 (Entrepreneur), 5 (Working Student) - const showAll = [0, 1, 2, 3, 5].includes( - employmentSelectedOptionIndex, - ); - // Hide all 3 for: index 4 (Student), 6 (Student & Job Seeking), 7 (Job Seeking / Unemployed), 8 (Homemaker) - const hideAll = [4, 6, 7, 8].includes(employmentSelectedOptionIndex); - // Special Retired logic: index 9 (Retired) - const isRetired = employmentSelectedOptionIndex === 9; - - if (showAll) { - return true; - } - if (hideAll) { - return false; - } - if (isRetired) { - if (isJobTitle || isMonthlyIncome) { - return true; - } - if (isWorkLocation) { - return false; - } - } - return false; - } - } - - // Check Parents' Survival Status to decide if Parents' Marital Status is visible - if (survivalStatusQuestionIndex !== -1) { - const isParentsMaritalStatus = - question.title === "Parents' Marital Status" || - question.title === "وضعیت تأهل والدین"; - - if (isParentsMaritalStatus) { - return survivalSelectedOptionIndex === 0; - } - } - - // Default dependsOn logic - if (question.logic?.dependsOn) { - const { title, values } = question.logic.dependsOn; - const dependentQuestionIndex = profileVisible.findIndex( - (q) => q.title === title, - ); - - if (dependentQuestionIndex !== -1) { - const dependentQuestion = profileVisible[dependentQuestionIndex]; - const answer = findAnswer(dependentQuestion, dependentQuestionIndex); - if (Array.isArray(answer)) { - return answer.some((ans) => values.includes(String(ans))); - } - return values.includes(String(answer)); - } - return false; - } - return true; - }); - - const activeQuestions = filtered.map((question) => { - if ( - question.title === "Short Family Description" || - question.title === "توضیح کوتاه درباره خانواده" - ) { - return { - ...question, - required: isCircumstancesSelected, - }; - } - - if ( - question.title === "Previous Marriage Duration" || - question.title === "مدت ازدواج یا عقد قبلی" || - question.title === "Number of Children" || - question.title === "تعداد فرزندان" || - question.title === "Short Children/Guardianship Explanation" || - question.title === "توضیح کوتاه درباره شرایط فرزند یا تکفل" || - question.title === "Additional details about family responsibility" || - question.title === "توضیحات تکمیلی درباره مسئولیت خانوادگی" || - question.title === "Do the supported individual(s) live with you?" || - question.title === "آیا فرد یا افراد تحت حمایت با شما زندگی میکنند؟" || - question.title === "What is the custody status of your child(ren)?" || - question.title === "وضعیت حضانت فرزند یا فرزندان شما چگونه است؟" || - question.title === - "Does the custody, visitation, or relocation schedule impact your residence or immigration?" || - question.title === - "آیا برنامه حضانت، ملاقات یا جابهجایی فرزند بر محل زندگی یا امکان مهاجرت شما تأثیر میگذارد؟" || - question.title === - "What is the payment or receipt status of child support?" || - question.title === - "وضعیت پرداخت یا دریافت نفقه و حمایت مالی فرزند چگونه است؟" || - question.title === - "Acceptance of necessary communication between future spouse and the other parent" || - question.title === "پذیرش ارتباط ضروری همسر آینده با والد دیگرِ فرزند" || - question.title === "پذیرش ارتباط ضروری همسر آینده با والد دیگر فرزند" - ) { - return { - ...question, - required: true, - }; - } - return question; - }); - - const requiredQuestions = activeQuestions.filter((q) => q.required); - - if (requiredQuestions.length === 0) { - return 100; - } - - const answeredCount = requiredQuestions.filter((q) => { - const idx = profileVisible.indexOf(q); - const key = getQuestionFieldKey(q, idx); - const engSlug = slugifyTitle(q.englishTitle || q.title); - const field = fields.find( - (f) => - f && - (f.key === key || - f.label === q.title || - f.label === q.englishTitle || - (typeof f.key === "string" && - (f.key.endsWith(`.${engSlug}`) || f.key.endsWith(`_${engSlug}`)))), - ); - return isFieldAnswered(q, field); - }).length; - - return Math.max( - 0, - Math.min( - 100, - Math.round((answeredCount / requiredQuestions.length) * 100), - ), - ); - } catch (_e) { - return null; - } + return null; } diff --git a/src/components/Componentes/question-answer-storage.tsx b/src/components/Componentes/question-answer-storage.tsx index d025ea7..2fbf591 100644 --- a/src/components/Componentes/question-answer-storage.tsx +++ b/src/components/Componentes/question-answer-storage.tsx @@ -203,14 +203,27 @@ function createQuestionField( currentAnswers?: QuestionAnswersByKey, backendFields?: MarriageField[], ): MarriageField { - const key = findQuestionFieldKey(question, questionIndex, currentAnswers, backendFields); + const backendId = (question as any).backendId; + const backendOptions = (question as any).backendOptions; + + let option_id = undefined; + if (backendOptions && Array.isArray(backendOptions)) { + const selectedOpt = backendOptions.find((opt: any) => opt.label === value || opt.id === value || opt.value === value); + if (selectedOpt) { + option_id = selectedOpt.id; + } + } + + const key = backendId || findQuestionFieldKey(question, questionIndex, currentAnswers, backendFields); + return { key, label: question.title, type: question.type, value, private: question.private, - }; + option_id: option_id, + } as MarriageField; } function getOrderedFields( diff --git a/src/components/Componentes/required-steps-card.tsx b/src/components/Componentes/required-steps-card.tsx index bfea451..f943252 100644 --- a/src/components/Componentes/required-steps-card.tsx +++ b/src/components/Componentes/required-steps-card.tsx @@ -2,16 +2,11 @@ import { useMemo } from "react"; import { IoAlert, IoCheckmark } from "react-icons/io5"; -import { getLocalSectionProgress, getStoredAge } from "./progress-helper"; -import { - getQuestionListItems, - isQuestionListItemVisibleForProfile, - type QuestionListItem, -} from "@/data/question-data"; -import { toFrontendSlug } from "@/data/section-slug-map"; import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; -import { useMarriageSectionsQuery } from "@/hooks/marriage/use-sections"; +import { useFormSchemaQuery } from "@/hooks/marriage/use-form-schema"; +import { convertSchemaToFrontendItems } from "@/lib/schema-adapter"; import { useI18n } from "@/translations/provider"; +import type { QuestionListItem } from "@/data/question-data"; type RequiredStepsCardProps = { items?: QuestionListItem[]; @@ -40,72 +35,19 @@ export default function RequiredStepsCard({ }: RequiredStepsCardProps = {}) { const { dictionary: t, locale } = useI18n(); const { data: profile } = useMarriageProfileQuery(); - const { data: sections } = useMarriageSectionsQuery(); + const { data: schema } = useFormSchemaQuery("profile", locale); const questionListItems = useMemo( - () => - items ?? - getQuestionListItems(locale).filter((item) => - isQuestionListItemVisibleForProfile(item, { - gender: profile?.gender, - }), - ), - [items, locale, profile?.gender], + () => items ?? convertSchemaToFrontendItems(schema, locale), + [items, schema, locale], ); const steps: RequiredStep[] = useMemo(() => { - const age = getStoredAge(); - type SectionType = NonNullable[number]; - const sectionMap = new Map(); - sections?.forEach((s) => { - sectionMap.set(toFrontendSlug(s.slug), s); - sectionMap.set(s.slug, s); - }); - return questionListItems.map((item) => { - const localProgress = getLocalSectionProgress(item, profile, age); - const section = sectionMap.get(item.slug); + let progress = item.progress; - let progress = 0; - if (localProgress !== null) { - progress = localProgress; - } else if ( - progressBySlug && - typeof progressBySlug.get(item.slug) === "number" - ) { + if (progressBySlug && typeof progressBySlug.get(item.slug) === "number") { progress = progressBySlug.get(item.slug) ?? 0; - } else if (item.slug === "family_marital_history" && sections) { - const fbSec = sections.find((s) => s.slug === "family_background"); - const mhSec = sections.find((s) => s.slug === "marital_history"); - if (fbSec || mhSec) { - const fbTotal = fbSec?.total_steps ?? 6; - const fbCurrent = fbSec - ? Math.round((fbSec.completion_percent / 100) * fbTotal) - : 0; - const mhTotal = mhSec?.total_steps ?? 6; - const mhCurrent = mhSec - ? Math.round((mhSec.completion_percent / 100) * mhTotal) - : 0; - progress = - fbTotal + mhTotal > 0 - ? Math.max( - 0, - Math.min( - 100, - Math.round( - ((fbCurrent + mhCurrent) / (fbTotal + mhTotal)) * 100, - ), - ), - ) - : 0; - } - } else if (section) { - progress = Math.max( - 0, - Math.min(100, Math.round(section.completion_percent)), - ); - } else { - progress = item.progress; } return { @@ -114,7 +56,7 @@ export default function RequiredStepsCard({ progress, }; }); - }, [questionListItems, progressBySlug, sections, profile]); + }, [questionListItems, progressBySlug]); const { completed, total } = getRequiredStepStats(steps); const completion = total > 0 ? Math.round((completed / total) * 100) : 0; diff --git a/src/hooks/marriage/use-section-data.ts b/src/hooks/marriage/use-section-data.ts index 29d2b03..4a11981 100644 --- a/src/hooks/marriage/use-section-data.ts +++ b/src/hooks/marriage/use-section-data.ts @@ -135,94 +135,6 @@ export async function updateMarriageSectionData( payload: UpdateMarriageSectionDataPayload, ) { if (slug === "family_marital_history") { - const mergedItemEn = getQuestionListItemBySlug( - "family_marital_history", - "en", - ); - const mergedItemFa = getQuestionListItemBySlug( - "family_marital_history", - "fa", - ); - - const fbQuestionsEn = - mergedItemEn?.questions.filter( - (q) => q.originalSlug === "family_background", - ) || []; - const mhQuestionsEn = - mergedItemEn?.questions.filter( - (q) => q.originalSlug === "marital_history_children", - ) || []; - const fbQuestionsFa = - mergedItemFa?.questions.filter( - (q) => q.originalSlug === "family_background", - ) || []; - const mhQuestionsFa = - mergedItemFa?.questions.filter( - (q) => q.originalSlug === "marital_history_children", - ) || []; - - const fbKeys = new Set([ - ...fbQuestionsEn.map((q, idx) => - getQuestionFieldKey(q.title, q.originalIndex ?? idx, q.englishTitle), - ), - ...fbQuestionsFa.map((q, idx) => - getQuestionFieldKey(q.title, q.originalIndex ?? idx, q.englishTitle), - ), - ]); - const mhKeys = new Set([ - ...mhQuestionsEn.map((q, idx) => - getQuestionFieldKey(q.title, q.originalIndex ?? idx, q.englishTitle), - ), - ...mhQuestionsFa.map((q, idx) => - getQuestionFieldKey(q.title, q.originalIndex ?? idx, q.englishTitle), - ), - ]); - - const fbFields = payload.fields.filter((f) => fbKeys.has(f.key)); - const mhFields = payload.fields.filter((f) => mhKeys.has(f.key)); - - const fbRequiredQuestions = fbQuestionsEn.filter( - (q) => q.required && !q.logic?.dependsOn, - ); - const mhRequiredQuestions = mhQuestionsEn.filter( - (q) => q.required && !q.logic?.dependsOn, - ); - - const fbRequiredCount = fbRequiredQuestions.length; - const mhRequiredCount = mhRequiredQuestions.length; - - const fbCurrentStep = fbQuestionsEn.filter((q, idx) => { - if (!q.required || q.logic?.dependsOn) return false; - const keyEn = getQuestionFieldKey( - q.title, - q.originalIndex ?? idx, - q.englishTitle, - ); - const keyFa = getQuestionFieldKey( - fbQuestionsFa[idx]?.title || q.title, - fbQuestionsFa[idx]?.originalIndex ?? q.originalIndex ?? idx, - fbQuestionsFa[idx]?.englishTitle || q.englishTitle, - ); - const field = fbFields.find((f) => f.key === keyEn || f.key === keyFa); - return field && hasQuestionAnswerValue(field.value); - }).length; - - const mhCurrentStep = mhQuestionsEn.filter((q, idx) => { - if (!q.required || q.logic?.dependsOn) return false; - const keyEn = getQuestionFieldKey( - q.title, - q.originalIndex ?? idx, - q.englishTitle, - ); - const keyFa = getQuestionFieldKey( - mhQuestionsFa[idx]?.title || q.title, - mhQuestionsFa[idx]?.originalIndex ?? q.originalIndex ?? idx, - mhQuestionsFa[idx]?.englishTitle || q.englishTitle, - ); - const field = mhFields.find((f) => f.key === keyEn || f.key === keyFa); - return field && hasQuestionAnswerValue(field.value); - }).length; - const answersPayload = payload.fields.map((f) => ({ question_id: f.key, value: f.value, @@ -329,6 +241,9 @@ export function useUpdateMarriageSectionDataMutation( queryClient.invalidateQueries({ queryKey: marriageQueryKeys.sectionData(slug), }), + queryClient.invalidateQueries({ + queryKey: ["marriage", "form-schema"], + }), ]); await options?.onSuccess?.(data, variables, onMutateResult, context); }, diff --git a/src/lib/schema-adapter.ts b/src/lib/schema-adapter.ts new file mode 100644 index 0000000..c07a5ce --- /dev/null +++ b/src/lib/schema-adapter.ts @@ -0,0 +1,130 @@ +import type { + FormSchemaResponse, + FormSection, + FormQuestion, +} from "@/hooks/marriage/use-form-schema"; +import type { + QuestionListItem, + QuestionField, + QuestionCardIcon, +} from "@/data/question-data"; +import { defaultLocale, type Locale } from "@/translations/config"; + +const iconMap: Record = { + "user-circle": "profile", + school: "education", + "heart-handshake": "details", + "file-text": "contact", + "layout-grid": "checklist", +}; + +export function mapBackendQuestionToFrontend(bq: FormQuestion, index: number, originalSlug?: string): QuestionField { + return { + title: bq.title || "Untitled", + englishTitle: bq.title, + type: bq.type, + required: bq.required, + private: bq.ui_config?.private, + description: bq.description || "", + tooltip: bq.tooltip || "", + extras: { + placeHolder: bq.placeholder || "", + options: bq.options?.map((o) => o.label) || [], + range: bq.ui_config?.range || [0, 0], + noSearch: bq.ui_config?.noSearch, + }, + logic: bq.logic ? { dependsOn: bq.logic.dependsOn } : undefined, + showGuardianNotice: bq.show_guardian_notice, + originalSlug: originalSlug, + originalIndex: index, + }; +} + +export function mapBackendSectionToFrontend( + section: FormSection, + progress: number +): QuestionListItem { + const allQuestions: QuestionField[] = []; + let index = 0; + + section.cards.forEach((card) => { + card.questions.forEach((q) => { + const fq = mapBackendQuestionToFrontend(q, index, section.id); + fq.required = q.required; + (fq as any).backendId = q.id; + (fq as any).isVisible = q.is_visible; + (fq as any).backendOptions = q.options; + + allQuestions.push(fq); + index++; + }); + }); + + return { + slug: section.id, + title: section.title, + estimate: section.estimated_minutes ? `${section.estimated_minutes} min` : "5 min", + progress: progress, + icon: iconMap[section.icon] ?? "details", + required: section.is_required, + showInfoBadge: false, + summary: "", + checkpoints: allQuestions.map((q) => q.title), + tooltip: "", + questions: allQuestions, + }; +} + +export function convertSchemaToFrontendItems( + schema: FormSchemaResponse | undefined, + locale: Locale = defaultLocale +): QuestionListItem[] { + if (!schema) return []; + + const rawItems = schema.sections.map((sec) => { + const progInfo = schema.progress?.sections_progress?.[sec.id]; + const progress = progInfo ? progInfo.completion_percent : 0; + return mapBackendSectionToFrontend(sec, progress); + }); + + const fbIndex = rawItems.findIndex((item) => item.slug === "family_background"); + const mhIndex = rawItems.findIndex((item) => item.slug === "marital_history_children" || item.slug === "marital_history"); + + if (fbIndex !== -1 && mhIndex !== -1) { + const fbItem = rawItems[fbIndex]; + const mhItem = rawItems[mhIndex]; + + const fbProgInfo = schema.progress?.sections_progress?.[fbItem.slug]; + const mhProgInfo = schema.progress?.sections_progress?.[mhItem.slug]; + + const fbTotal = fbProgInfo?.total_steps ?? 0; + const mhTotal = mhProgInfo?.total_steps ?? 0; + const fbCurrent = fbProgInfo?.current_step ?? 0; + const mhCurrent = mhProgInfo?.current_step ?? 0; + + let combinedProgress = 0; + if (fbTotal + mhTotal > 0) { + combinedProgress = Math.round(((fbCurrent + mhCurrent) / (fbTotal + mhTotal)) * 100); + } + + const mergedItem: QuestionListItem = { + slug: "family_marital_history", + title: locale === "fa" ? "سوابق ازدواج و خانواده" : "Family & Marital History", + estimate: "5 min", + progress: Math.max(0, Math.min(100, combinedProgress)), + icon: "family_marital" as QuestionCardIcon, + required: fbItem.required || mhItem.required, + summary: "", + tooltip: "", + checkpoints: [...fbItem.checkpoints, ...mhItem.checkpoints], + questions: [...fbItem.questions, ...mhItem.questions], + }; + + const newItems = [...rawItems]; + newItems.splice(Math.max(fbIndex, mhIndex), 1); + newItems.splice(Math.min(fbIndex, mhIndex), 1, mergedItem); + return newItems; + } + + return rawItems; +}