Browse Source

feat: implement question detail client logic with automated test flow and answer storage integration

front-test-2
ghorbani 2 weeks ago
parent
commit
e1310badef
  1. 275
      src/app/questions-list/[slug]/question-detail-client.tsx
  2. 62
      src/app/questions-list/page.tsx
  3. 474
      src/components/Componentes/progress-helper.ts
  4. 17
      src/components/Componentes/question-answer-storage.tsx
  5. 76
      src/components/Componentes/required-steps-card.tsx
  6. 91
      src/hooks/marriage/use-section-data.ts
  7. 130
      src/lib/schema-adapter.ts

275
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,

62
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<QuestionListItem | null>(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<string, number>();
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);
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))));
});
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))),
);
}
// 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 (
<>
<PageBackground disabled />

474
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<string, unknown>,
field: Record<string, unknown> | 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<string, unknown>[] = [];
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;
}
}

17
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(

76
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<typeof sections>[number];
const sectionMap = new Map<string, SectionType>();
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;

91
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);
},

130
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<string, QuestionCardIcon> = {
"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;
}
Loading…
Cancel
Save