diff --git a/scratch/add_translations.py b/scratch/add_translations.py new file mode 100644 index 0000000..7dc9a80 --- /dev/null +++ b/scratch/add_translations.py @@ -0,0 +1,45 @@ +import os +import json + +translations = { + "ar": {"title": "الخلفية العائلية، الحالة الاجتماعية والأطفال", "estimate": "20 دقيقة"}, + "az": {"title": "Ailə Keçmişi, Ailə Vəziyyəti və Uşaqlar", "estimate": "20 dəqiqə"}, + "bn": {"title": "পারিবারিক পটভূমি, বৈবাহিক অবস্থা এবং সন্তানাদি", "estimate": "20 মিনিট"}, + "zh": {"title": "家庭背景、婚姻状况和子女", "estimate": "20 分钟"}, + "da": {"title": "Familiebaggrund, civilstand og børn", "estimate": "20 minutter"}, + "de": {"title": "Familiärer Hintergrund, Familienstand und Kinder", "estimate": "20 Minuten"}, + "en": {"title": "Family Background, Marital Status, and Children", "estimate": "20 minutes"}, + "es": {"title": "Antecedentes familiares, estado civil e hijos", "estimate": "20 minutos"}, + "fa": {"title": "پیشینه خانوادگی، وضعیت تأهل و فرزندان", "estimate": "20 دقیقه"}, + "fr": {"title": "Antécédents familiaux, état civil et enfants", "estimate": "20 minutes"}, + "gu": {"title": "પારિવારિક પૃષ્ઠભૂમિ, વૈવાહિક સ્થિતિ અને બાળકો", "estimate": "20 મિનિટ"}, + "ha": {"title": "Tarihin Iyali, Yanayin Aure da Yara", "estimate": "Minti 20"}, + "he": {"title": "רקע משפחתי, מצב משפחתי וילדים", "estimate": "20 דקות"}, + "hi": {"title": "पारिवारिक पृष्ठभूमि, वैवाहिक स्थिति और बच्चे", "estimate": "20 मिनट"}, + "id": {"title": "Latar Belakang Keluarga, Status Pernikahan, dan Anak-anak", "estimate": "20 menit"}, + "ks": {"title": "خاندانی پس منظر، ازدواجی حیثیت تہٰ شرے", "estimate": "20 منٹ"}, + "pt": {"title": "Histórico familiar, estado civil e filhos", "estimate": "20 minutos"}, + "ru": {"title": "Семейное положение, история брака и дети", "estimate": "20 минут"}, + "sw": {"title": "Historia ya Familia, Hali ya Ndoa na Watoto", "estimate": "Dakika 20"}, + "tg": {"title": "Маълумоти оилавӣ, вазъи оилавӣ ва кӯдакон", "estimate": "20 дақиқа"}, + "tr": {"title": "Aile Geçmişi, Medeni Durum ve Çocuklar", "estimate": "20 dakika"}, + "ul": {"title": "Khandani Pas-manzar, Azdawaji Haisiyat aur Bacche", "estimate": "20 minutes"}, + "ur": {"title": "خاندانی پس منظر، ازدواجی حیثیت اور بچے", "estimate": "20 منٹ"}, + "uz": {"title": "Oila tarixi, oilaviy ahvol va bolalar", "estimate": "20 daqiqa"} +} + +locales_dir = "src/translations/locales" +for lang, data in translations.items(): + file_path = os.path.join(locales_dir, f"{lang}.json") + if not os.path.exists(file_path): + print(f"Skipping {file_path} - not found") + continue + with open(file_path, "r", encoding="utf-8") as f: + content = json.load(f) + if "questions" not in content: + content["questions"] = {} + content["questions"]["familyMaritalTitle"] = data["title"] + content["questions"]["familyMaritalEstimate"] = data["estimate"] + with open(file_path, "w", encoding="utf-8") as f: + json.dump(content, f, ensure_ascii=False, indent=2) + print(f"Updated {file_path}") diff --git a/src/app/globals.css b/src/app/globals.css index 7791cf0..0a20fc3 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -6,6 +6,69 @@ --safe-bottom: 0px; --safe-left: 0px; --safe-right: 0px; + + /* ─── Semantic Tokens (Light) ─── */ + --semantic-neutral-bg: #F1F5F9; + --semantic-neutral-text: #475569; + --semantic-neutral-border: #CBD5E1; + --semantic-neutral-icon: #64748B; + + --semantic-info-bg: #EFF6FF; + --semantic-info-text: #1D4ED8; + --semantic-info-border: #BFDBFE; + --semantic-info-icon: #2563EB; + + --semantic-success-bg: #ECFDF5; + --semantic-success-text: #047857; + --semantic-success-border: #A7F3D0; + --semantic-success-icon: #059669; + + --semantic-warning-bg: #FFFBEB; + --semantic-warning-text: #B45309; + --semantic-warning-border: #FDE68A; + --semantic-warning-icon: #D97706; + + --semantic-danger-bg: #FEF2F2; + --semantic-danger-text: #B91C1C; + --semantic-danger-border: #FECACA; + --semantic-danger-icon: #DC2626; + + --semantic-ai-bg: #F5F3FF; + --semantic-ai-text: #6D28D9; + --semantic-ai-border: #DDD6FE; + --semantic-ai-icon: #7C3AED; +} + +.dark { + --semantic-neutral-bg: #1E293B; + --semantic-neutral-text: #E2E8F0; + --semantic-neutral-border: #475569; + --semantic-neutral-icon: #CBD5E1; + + --semantic-info-bg: #172554; + --semantic-info-text: #BFDBFE; + --semantic-info-border: #1D4ED8; + --semantic-info-icon: #93C5FD; + + --semantic-success-bg: #052E2B; + --semantic-success-text: #A7F3D0; + --semantic-success-border: #047857; + --semantic-success-icon: #6EE7B7; + + --semantic-warning-bg: #451A03; + --semantic-warning-text: #FDE68A; + --semantic-warning-border: #B45309; + --semantic-warning-icon: #FCD34D; + + --semantic-danger-bg: #450A0A; + --semantic-danger-text: #FECACA; + --semantic-danger-border: #B91C1C; + --semantic-danger-icon: #FCA5A5; + + --semantic-ai-bg: #2E1065; + --semantic-ai-text: #DDD6FE; + --semantic-ai-border: #7C3AED; + --semantic-ai-icon: #C4B5FD; } @theme inline { @@ -16,6 +79,36 @@ --font-arabic: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; --font-ryling: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; --font-faminela: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; + + --color-semantic-neutral-bg: var(--semantic-neutral-bg); + --color-semantic-neutral-text: var(--semantic-neutral-text); + --color-semantic-neutral-border: var(--semantic-neutral-border); + --color-semantic-neutral-icon: var(--semantic-neutral-icon); + + --color-semantic-info-bg: var(--semantic-info-bg); + --color-semantic-info-text: var(--semantic-info-text); + --color-semantic-info-border: var(--semantic-info-border); + --color-semantic-info-icon: var(--semantic-info-icon); + + --color-semantic-success-bg: var(--semantic-success-bg); + --color-semantic-success-text: var(--semantic-success-text); + --color-semantic-success-border: var(--semantic-success-border); + --color-semantic-success-icon: var(--semantic-success-icon); + + --color-semantic-warning-bg: var(--semantic-warning-bg); + --color-semantic-warning-text: var(--semantic-warning-text); + --color-semantic-warning-border: var(--semantic-warning-border); + --color-semantic-warning-icon: var(--semantic-warning-icon); + + --color-semantic-danger-bg: var(--semantic-danger-bg); + --color-semantic-danger-text: var(--semantic-danger-text); + --color-semantic-danger-border: var(--semantic-danger-border); + --color-semantic-danger-icon: var(--semantic-danger-icon); + + --color-semantic-ai-bg: var(--semantic-ai-bg); + --color-semantic-ai-text: var(--semantic-ai-text); + --color-semantic-ai-border: var(--semantic-ai-border); + --color-semantic-ai-icon: var(--semantic-ai-icon); } /* Typography Configuration - Centralized Design System Scale */ diff --git a/src/app/questions-list/[slug]/question-detail-client.tsx b/src/app/questions-list/[slug]/question-detail-client.tsx index 0bfffbf..b28fd66 100644 --- a/src/app/questions-list/[slug]/question-detail-client.tsx +++ b/src/app/questions-list/[slug]/question-detail-client.tsx @@ -119,29 +119,31 @@ function getStoredAge() { } const storedAnswers = JSON.parse(rawValue) as StoredAnswers; - const ageField = storedAnswers.fields?.find( - (field) => - field.type === "number" || - field.label === "Age" || - field.label === "سن" || - (typeof (field as any).key === "string" && - ((field as any).key.endsWith("_age") || - (field as any).key.endsWith("_sn"))), - ); + const ageField = storedAnswers.fields?.find((field) => { + const f = field as { key?: string; type?: string; label?: string }; + return ( + f.type === "number" || + f.label === "Age" || + f.label === "سن" || + (typeof f.key === "string" && + (f.key.endsWith("_age") || f.key.endsWith("_sn"))) + ); + }); if (ageField) { return parseStoredAge(ageField.value); } - const dateOfBirthField = storedAnswers.fields?.find( - (field) => - field.type === "date" || - field.label === "Date of Birth" || - field.label === "تاریخ تولد" || - (typeof (field as any).key === "string" && - ((field as any).key.endsWith("_date_of_birth") || - (field as any).key.endsWith("_tarykh_twld"))), - ); + const dateOfBirthField = storedAnswers.fields?.find((field) => { + const f = field as { key?: string; type?: string; label?: string }; + return ( + f.type === "date" || + f.label === "Date of Birth" || + f.label === "تاریخ تولد" || + (typeof f.key === "string" && + (f.key.endsWith("_date_of_birth") || f.key.endsWith("_tarykh_twld"))) + ); + }); return parseStoredAge(dateOfBirthField?.value); } catch { @@ -168,7 +170,188 @@ function QuestionFlowWrapper({ const { getAnswerValue } = useQuestionAnswers(); const dynamicQuestions = useMemo(() => { - return visibleQuestions.filter((question) => { + // Find employment status question by title or by choices length (10) + const employmentQuestionIndex = visibleQuestions.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 = visibleQuestions[employmentQuestionIndex]; + const ans = getAnswerValue(employmentQuestion, employmentQuestionIndex); + if (ans) { + employmentSelectedOptionIndex = + employmentQuestion.extras.options?.indexOf(String(ans)) ?? -1; + } + } + + // Find Parents' Marital Status question index and check selected index + const maritalStatusQuestionIndex = visibleQuestions.findIndex((q) => { + return ( + q.title === "Parents' Marital Status" || q.title === "وضعیت تأهل والدین" + ); + }); + + 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) => { + return ( + q.title === "Current Marital Status" || q.title === "وضعیت تأهل فعلی" + ); + }); + + 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) => { + return ( + q.title === "Children and Guardianship Status" || + q.title === "وضعیت فرزند و تکفل" + ); + }); + + 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)]; + 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 = visibleQuestions.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; + } + } + + // Default dependsOn logic if (question.logic?.dependsOn) { const { title, values } = question.logic.dependsOn; const dependentQuestionIndex = visibleQuestions.findIndex( @@ -187,6 +370,33 @@ function QuestionFlowWrapper({ } return true; }); + + return 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 === "توضیح کوتاه درباره شرایط فرزند یا تکفل" + ) { + return { + ...question, + required: true, + }; + } + return question; + }); }, [visibleQuestions, getAnswerValue]); const requiredCount = useMemo( @@ -314,7 +524,7 @@ export default function QuestionDetailClient({ options: mappedOptions, }; }); - }, [cattellQuery.data]); + }, [cattellQuery.data, locale]); const glasserTestQuestions: TestQuestion[] = useMemo(() => { const questionsList = diff --git a/src/app/questions-list/page.tsx b/src/app/questions-list/page.tsx index b3dab6f..2288729 100644 --- a/src/app/questions-list/page.tsx +++ b/src/app/questions-list/page.tsx @@ -8,7 +8,6 @@ import { getLocalSectionProgress, getStoredAge, } from "@/components/questions/progress-helper"; -import { hasQuestionAnswerValue } from "@/components/questions/question-answer-storage"; import QuestionCard from "@/components/questions/question-card"; import RequiredStepsCard from "@/components/questions/required-steps-card"; import Button from "@/components/ui/button"; @@ -18,8 +17,6 @@ import { PageBackground } from "@/components/utils/page-background"; import { getQuestionListItems, isQuestionListItemVisibleForProfile, - isQuestionRequiredForProfile, - isQuestionVisibleForProfile, type QuestionListItem, } from "@/data/question-data"; import { toFrontendSlug } from "@/data/section-slug-map"; @@ -81,6 +78,27 @@ export default function QuestionsListPage() { 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))), + ); + } + questionListItems.forEach((item) => { const localProgress = getLocalSectionProgress(item, profile, age); if (localProgress !== null) { diff --git a/src/components/questions/progress-helper.ts b/src/components/questions/progress-helper.ts index 43c45a5..d8b5fd2 100644 --- a/src/components/questions/progress-helper.ts +++ b/src/components/questions/progress-helper.ts @@ -1,8 +1,11 @@ import { - isQuestionRequiredForProfile, isQuestionVisibleForProfile, type QuestionListItem, } from "@/data/question-data"; +import type { + MarriageFieldValue, + MarriageGender, +} from "@/hooks/marriage/types"; import { hasQuestionAnswerValue } from "./question-answer-storage"; export function getStoredAge(): number | null { @@ -14,7 +17,7 @@ export function getStoredAge(): number | null { if (!rawValue) return null; const storedAnswers = JSON.parse(rawValue); const ageField = storedAnswers.fields?.find( - (f: any) => + (f: Record) => f.type === "number" || f.label === "Age" || f.label === "سن" || @@ -26,15 +29,15 @@ export function getStoredAge(): number | null { if (Number.isFinite(num)) return num; } const dobField = storedAnswers.fields?.find( - (f: any) => + (f: Record) => f.type === "date" || f.label === "Date of Birth" || f.label === "تاریخ تولد" || (typeof f.key === "string" && (f.key.endsWith("_date_of_birth") || f.key.endsWith("_tarykh_twld"))), ); - if (dobField && dobField.value) { - const dob = new Date(dobField.value); + if (dobField?.value) { + const dob = new Date(String(dobField.value)); if (!Number.isNaN(dob.getTime())) { const today = new Date(); let age = today.getFullYear() - dob.getFullYear(); @@ -45,12 +48,15 @@ export function getStoredAge(): number | null { return age >= 0 ? age : null; } } - } catch (e) {} + } catch (_e) {} return null; } -function isFieldAnswered(q: any, field: any): boolean { - if (!field || !hasQuestionAnswerValue(field.value)) { +function isFieldAnswered( + q: Record, + field: Record | undefined, +): boolean { + if (!field || !hasQuestionAnswerValue(field.value as MarriageFieldValue)) { return false; } if (q.type === "birthplace") { @@ -61,27 +67,80 @@ function isFieldAnswered(q: any, field: any): boolean { 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: any, + profile: { gender?: MarriageGender | null } | null | undefined, age: number | null, ): number | null { try { if (typeof window === "undefined") return null; - const storageKey = `marriage:sections:${item.slug}:answers`; - const rawValue = window.localStorage.getItem(storageKey); - if (!rawValue) { - 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 {} } - const storedValue = JSON.parse(rawValue); - if (storedValue && storedValue.completed) { - return 100; + + 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 ( - !storedValue || - !Array.isArray(storedValue.fields) || - storedValue.fields.length === 0 - ) { + + if (!hasFoundStorage || fields.length === 0) { return null; } @@ -90,59 +149,80 @@ export function getLocalSectionProgress( gender: profile?.gender, }; - const visibleQuestions = item.questions - .filter((question) => - isQuestionVisibleForProfile(question, profileContext), - ) - .map((question) => ({ - ...question, - required: isQuestionRequiredForProfile(question, profileContext), - })); - - const requiredQuestions = visibleQuestions.filter((q) => q.required); - if (requiredQuestions.length === 0) { - if (visibleQuestions.length === 0) { - return 100; + 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; } - const answeredCount = visibleQuestions.filter((q, index) => { - const slugifyTitle = (title: string) => - title - .normalize("NFKD") - .replace(/[\u0300-\u036f]/g, "") - .toLowerCase() - .replace(/[^a-z0-9]+/g, "_") - .replace(/^_+|_+$/g, ""); - const fieldKey = `q${index + 1}_${slugifyTitle(q.title)}`; - const field = storedValue.fields.find( - (f: any) => f.key === fieldKey || f.label === q.title, + return isQuestionVisibleForProfile(question, profileContext); + }); + + const findAnswer = ( + questionTitle: string, + questionIndex: number, + originalIndex?: number, + ) => { + const indexToUse = + originalIndex !== undefined ? originalIndex : questionIndex; + const key = `q${indexToUse + 1}_${slugifyTitle(questionTitle)}`; + const field = fields.find( + (f) => f && (f.key === key || f.label === questionTitle), + ); + return field?.value; + }; + + const activeQuestions = profileVisible.filter((question, _idx) => { + if (question.logic?.dependsOn) { + const { title, values } = question.logic.dependsOn; + const depIndex = profileVisible.findIndex((q) => q.title === title); + const depQuestion = depIndex !== -1 ? profileVisible[depIndex] : null; + const depAnswer = depQuestion + ? findAnswer(depQuestion.title, depIndex, depQuestion.originalIndex) + : undefined; + + if (depAnswer === undefined || depAnswer === null) { + return false; + } + + const ansList = Array.isArray(depAnswer) + ? depAnswer.map(String) + : [String(depAnswer)]; + + const isMatch = values.some((val) => + ansList.some((a) => a === val || a.includes(val) || val.includes(a)), ); - return isFieldAnswered(q, field); - }).length; - return Math.round((answeredCount / visibleQuestions.length) * 100); + + if (!isMatch) { + return false; + } + } + return true; + }); + + if (activeQuestions.length === 0) { + return 100; } - const answeredRequiredCount = requiredQuestions.filter((q) => { - const originalIndex = item.questions.findIndex( - (origQ) => origQ.title === q.title, - ); - if (originalIndex === -1) return false; - - const slugifyTitle = (title: string) => - title - .normalize("NFKD") - .replace(/[\u0300-\u036f]/g, "") - .toLowerCase() - .replace(/[^a-z0-9]+/g, "_") - .replace(/^_+|_+$/g, ""); - const fieldKey = `q${originalIndex + 1}_${slugifyTitle(q.title)}`; - const field = storedValue.fields.find( - (f: any) => f.key === fieldKey || f.label === q.title, + const answeredCount = activeQuestions.filter((q, idx) => { + const indexToUse = q.originalIndex !== undefined ? q.originalIndex : idx; + const key = `q${indexToUse + 1}_${slugifyTitle(q.title)}`; + const field = fields.find( + (f) => f && (f.key === key || f.label === q.title), ); return isFieldAnswered(q, field); }).length; - return Math.round((answeredRequiredCount / requiredQuestions.length) * 100); - } catch (e) { + return Math.max( + 0, + Math.min(100, Math.round((answeredCount / activeQuestions.length) * 100)), + ); + } catch (_e) { return null; } } diff --git a/src/components/questions/question-answer-storage.tsx b/src/components/questions/question-answer-storage.tsx index d7306dc..a0d21b3 100644 --- a/src/components/questions/question-answer-storage.tsx +++ b/src/components/questions/question-answer-storage.tsx @@ -88,7 +88,11 @@ function slugifyQuestionTitle(title: string) { } function getQuestionFieldKey(question: QuestionField, questionIndex: number) { - return `q${questionIndex + 1}_${slugifyQuestionTitle(question.title)}`; + const index = + question.originalIndex !== undefined + ? question.originalIndex + : questionIndex; + return `q${index + 1}_${slugifyQuestionTitle(question.title)}`; } export function getQuestionAnswersStorageKey(slug: string) { diff --git a/src/components/questions/question-card.tsx b/src/components/questions/question-card.tsx index a83c95f..5c6c2df 100644 --- a/src/components/questions/question-card.tsx +++ b/src/components/questions/question-card.tsx @@ -28,6 +28,7 @@ const iconAssetMap: Partial> = { details: "/assets/images/Grfdasfoup.svg", checklist: "/assets/images/noun-test-4525471 1.svg", contact: "/assets/images/solar_user-id-bold.svg", + family_marital: "/assets/images/Grfdasfoup.svg", }; const iconMap: Record = { @@ -36,6 +37,7 @@ const iconMap: Record = { details: IoDocumentText, checklist: IoCheckbox, contact: IoPeople, + family_marital: IoPeople, }; export function QuestionCard({ @@ -63,7 +65,7 @@ export function QuestionCard({ data-question-slug={item.slug} className="rounded-[20px] border border-white/80 bg-white px-3 py-3 shadow-[0_12px_28px_rgba(15,23,42,0.05)] transition-transform duration-200 hover:-translate-y-0.5" > -
+
{iconAsset ? ( @@ -81,48 +83,37 @@ export function QuestionCard({
-
-

- {item.title} -

- {item.required ? ( - - ({t.common.required}) - - ) : null} -
+

+ {item.title} +

{t.common.estimateTime}: {item.estimate}

-
- {item.showInfoBadge ? ( - + {item.required ? ( + + {t.common.required} + + ) : item.showInfoBadge ? ( + ) : ( -