diff --git a/public/assets/images/diamond-color.svg b/public/assets/images/diamond-color.svg new file mode 100644 index 0000000..58e69e0 --- /dev/null +++ b/public/assets/images/diamond-color.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/new-match/new-match-client.tsx b/src/app/new-match/new-match-client.tsx index 624c821..ae1c0d7 100644 --- a/src/app/new-match/new-match-client.tsx +++ b/src/app/new-match/new-match-client.tsx @@ -5,7 +5,17 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; import { useHabibWebReady } from "@/hooks/use-habib-web-ready"; -import { FaLock } from "react-icons/fa6"; +import { + FaCalendarDays, + FaLocationDot, + FaGraduationCap, + FaBookOpen, + FaBriefcase, + FaStar, + FaLock, +} from "react-icons/fa6"; +import { LuEye } from "react-icons/lu"; +import { IoHeartOutline } from "react-icons/io5"; import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card"; import MarriageAdvisorsOverlay, { useMarriageAdvisorsOverlay, @@ -16,6 +26,7 @@ import MatchProfileOverlay, { import PageHeader from "@/components/Componentes/page-header"; import { PageBackground } from "@/components/Componentes/page-background"; import InformationSheet from "@/components/Componentes/information-sheet"; +import SwipeButton from "@/components/Componentes/swipe-button"; import { LoadingSkeleton } from "@/components/Componentes/loading-skeleton"; import { LoadingThreeDot } from "@/components/Componentes/loading-three-dot"; import { DiscountWidget } from "@/components/Componentes/discount-widget"; @@ -56,6 +67,24 @@ const fieldCandidateMatchers = { "q1_full_name", ], age: ["age", "date_of_birth", "birth_date", "dob", "birth_year"], + currentCountry: [ + "country_of_current_residence", + "current_country", + "residence_country", + "country", + "current_residence_country", + ], + currentCity: [ + "city_state_of_current_residence", + "city_of_current_residence", + "current_city", + "residence_city", + "city", + "city_state", + "state_city", + "state", + "province", + ], residence: [ "current_residence", "residence", @@ -65,8 +94,7 @@ const fieldCandidateMatchers = { "residence_city", "city", "country", - "birth_city", - "birthplace", + "current_country", ], educationLevel: [ "highest_level_of_education", @@ -79,38 +107,21 @@ const fieldCandidateMatchers = { "study_field", "study", ], - job: [ + jobTitle: [ "job_title", "job_title_and_description", + "job_position", + "employment_status", "job", "occupation", "profession", "career", - "employment_status", - "work", ], hobbies: [ "your_hobbies_and_main_interests", "hobbies_and_interests", "hobbies", "interests", - "your_personality_traits", - "personality_traits", - ], - maritalStatus: [ - "current_marital_status", - "marital_status", - "maritalstatus", - "relationship_status", - ], - cityPreference: [ - "willingness_to_relocate", - "city_preference", - "citypreference", - "preferred_city", - "preferred_location", - "future_residence", - "residence_preference_after_marriage", ], } as const; @@ -225,29 +236,28 @@ function useMatchSummaryDisplay( const fields = matchSummary?.public_info ?? []; const usedIndexes = new Set(); - // 1. Name: Combine first_name and last_name if available, or find general name + // 1. Name: Show only first_name (do not show last_name) let displayName: string | null = null; const firstNameIdx = fields.findIndex( (f) => f.key === "personal_identity.first_name" || - f.key?.endsWith(".first_name"), + f.key?.endsWith(".first_name") || + f.key === "first_name", ); const lastNameIdx = fields.findIndex( (f) => f.key === "personal_identity.last_name" || - f.key?.endsWith(".last_name"), + f.key?.endsWith(".last_name") || + f.key === "last_name", ); + if (lastNameIdx !== -1) { + usedIndexes.add(lastNameIdx); + } + if (firstNameIdx !== -1 && fields[firstNameIdx].value) { usedIndexes.add(firstNameIdx); - const firstName = formatFieldValue(fields[firstNameIdx].value); - if (lastNameIdx !== -1 && fields[lastNameIdx].value) { - usedIndexes.add(lastNameIdx); - const lastName = formatFieldValue(fields[lastNameIdx].value); - displayName = `${firstName} ${lastName}`.trim(); - } else { - displayName = firstName; - } + displayName = formatFieldValue(fields[firstNameIdx].value); } else { const nameField = pickField( fields, @@ -256,7 +266,8 @@ function useMatchSummaryDisplay( t, ); if (nameField) { - displayName = nameField.value; + const rawName = String(nameField.value).trim(); + displayName = rawName.split(/\s+/)[0] || rawName; } } @@ -288,107 +299,193 @@ function useMatchSummaryDisplay( age = pickField(fields, fieldCandidateMatchers.age, usedIndexes, t); } - // 3. Country / City / Residence - const residence = pickField( + // 3. Country of Current Residence + let currentCountry = pickField( fields, - fieldCandidateMatchers.residence, + fieldCandidateMatchers.currentCountry, usedIndexes, t, ); - // 4. Highest level of education - const educationLevel = pickField( + // 4. City / State of Current Residence + let currentCity = pickField( fields, - fieldCandidateMatchers.educationLevel, + fieldCandidateMatchers.currentCity, usedIndexes, t, ); - // 5. Field of study - const fieldOfStudy = pickField( - fields, - fieldCandidateMatchers.fieldOfStudy, - usedIndexes, - t, - ); + // If either country or city was not matched as a standalone field, check composite residence + if (!currentCountry || !currentCity) { + const residenceIdx = fields.findIndex( + (f, idx) => + !usedIndexes.has(idx) && + matchesCandidate(f, fieldCandidateMatchers.residence), + ); + + if (residenceIdx !== -1) { + const residenceField = fields[residenceIdx]; + if ( + typeof residenceField.value === "object" && + residenceField.value !== null && + !Array.isArray(residenceField.value) + ) { + const valObj = residenceField.value as { + country?: string; + city?: string; + state?: string; + }; + if (valObj.country && !currentCountry) { + currentCountry = { + id: `${residenceField.key}.country`, + label: + (t && + (t["Current Country of Residence"] || + t["Country of Residence"] || + t["Country"])) || + "Country of Residence", + value: formatOptionValue(valObj.country, t) || valObj.country, + }; + } + const cityVal = [valObj.city, valObj.state].filter(Boolean).join(", "); + if (cityVal && !currentCity) { + currentCity = { + id: `${residenceField.key}.city`, + label: + (t && + (t["Current City / State of Residence"] || + t["City / State of Residence"] || + t["City"])) || + "City / State of Residence", + value: formatOptionValue(cityVal, t) || cityVal, + }; + } + usedIndexes.add(residenceIdx); + } else if (!currentCountry && !currentCity) { + const disp = toDisplayField(residenceField, t); + if (disp) { + usedIndexes.add(residenceIdx); + currentCountry = disp; + } + } + } + } - // 6. Job / Occupation - const job = pickField( + // 5. Highest level of education + const educationLevel = pickField( fields, - fieldCandidateMatchers.job, + fieldCandidateMatchers.educationLevel, usedIndexes, t, ); - // 7. Hobbies & Interests - const hobbies = pickField( + // 6. Field of study + const fieldOfStudy = pickField( fields, - fieldCandidateMatchers.hobbies, + fieldCandidateMatchers.fieldOfStudy, usedIndexes, t, ); - // 8. Marital Status - const maritalStatus = pickField( + // 7. Job Title + const jobTitle = pickField( fields, - fieldCandidateMatchers.maritalStatus, + fieldCandidateMatchers.jobTitle, usedIndexes, t, ); - // 9. City Preference / Relocation - const cityPreference = pickField( + // 8. Hobbies & Main Interests + const hobbies = pickField( fields, - fieldCandidateMatchers.cityPreference, + fieldCandidateMatchers.hobbies, usedIndexes, t, ); - const extraFields = fields - .filter((_, index) => !usedIndexes.has(index)) - .map((f) => toDisplayField(f, t)) - .filter((field): field is DisplayField => Boolean(field)) - .slice(0, 4); - - if (typeof window !== "undefined") { - console.log( - "🔍 [useMatchSummaryDisplay] computed 8 fields output:", - { - displayName, - age, - residence, - educationLevel, - fieldOfStudy, - job, - hobbies, - maritalStatus, - cityPreference, - extraFieldsCount: extraFields.length, - }, - ); - } - return { + name: displayName, age, - residence, + currentCountry, + currentCity, educationLevel, fieldOfStudy, - job, + jobTitle, hobbies, - maritalStatus, - cityPreference, - name: displayName, - extraFields, }; }, [matchSummary, t]); } -function FieldLine({ field }: { field: DisplayField }) { +function ProfileInfoItem({ + icon, + field, +}: { + icon: React.ReactNode; + field: DisplayField; +}) { return ( -

- {field.label}: - {field.value} -

+
+
+ {icon} +
+ +
+ + {field.label} + + + + {field.value} + +
+
); } @@ -401,6 +498,7 @@ export default function NewMatchClient() { const { isProfileOpen, openProfile, closeProfile } = useMatchProfileOverlay(); const { data: profile, isError, isLoading } = useMarriageProfileQuery(); const [isPaymentSheetOpen, setIsPaymentSheetOpen] = useState(false); + const [isDeclineConfirmOpen, setIsDeclineConfirmOpen] = useState(false); const [paymentError, setPaymentError] = useState(null); const [isInsufficientCoins, setIsInsufficientCoins] = useState(false); @@ -468,6 +566,16 @@ export default function NewMatchClient() { } }; + const openDeclineConfirm = () => { + setIsPaymentSheetOpen(false); + setIsDeclineConfirmOpen(true); + }; + + const cancelDeclineConfirm = () => { + setIsDeclineConfirmOpen(false); + setIsPaymentSheetOpen(true); + }; + useEffect(() => { console.log("🔍 [NewMatchClient] Current React Query Profile State:", { isLoading, @@ -522,57 +630,59 @@ export default function NewMatchClient() {
-
+
-
+
{/* 1. Header Section Skeleton */}
- - - - + + +
{/* 2. Match Card Skeleton */} -
+
{/* Name line */} - + {/* Subtitle / Details lines */} -
+
{/* Button skeleton */} - +
{/* 3. Advisor Card Skeleton */}
-
- -
- - +
+ +
+ +
-
+
- - - - + + +
- +
@@ -582,16 +692,6 @@ export default function NewMatchClient() { ); } - const pairedPersonalFields = [ - matchDisplay.age, - matchDisplay.residence, - ].filter((field): field is DisplayField => Boolean(field)); - - const pairedEduFields = [ - matchDisplay.educationLevel, - matchDisplay.fieldOfStudy, - ].filter((field): field is DisplayField => Boolean(field)); - const isFemaleProfile = profile?.gender === "female"; const matchHeadingTitle = isFemaleProfile ? t["New Marriage Proposal"] @@ -617,33 +717,37 @@ export default function NewMatchClient() {
-
+
-
+
{/* 1. Header Section */}
-

+

{matchHeadingTitle}

-

+

{matchHeadingDescription}

@@ -651,98 +755,173 @@ export default function NewMatchClient() { {/* 2. Match Summary Card Section */}
{isLoading ? ( -
-
- {/* Name line */} - - - {/* Subtitle / Details lines */} -
- - -
+
+
+ +
+ +
- {/* Button skeleton */} - +
+ + +
+ +
+ {[1, 2, 3, 4, 5, 6, 7].map((i) => ( +
+ +
+ + +
+
+ ))} +
) : ( -
+
{isError ? ( -

+

Unable to load match summary.

) : matchSummary ? ( <> -

- {matchDisplay.name} -

- -
- {pairedPersonalFields.length ? ( -

- {pairedPersonalFields.map((field, index) => ( - - {index > 0 ? ( - | - ) : null} - - {field.label}:{" "} - - {field.value} - - ))} -

- ) : null} - - {pairedEduFields.length ? ( -

- {pairedEduFields.map((field, index) => ( - - {index > 0 ? ( - | - ) : null} - - {field.label}:{" "} - - {field.value} - - ))} -

- ) : null} - - {matchDisplay.job ? ( - - ) : null} - - {matchDisplay.hobbies ? ( - - ) : null} - - {matchDisplay.maritalStatus ? ( - - ) : null} - {matchDisplay.cityPreference ? ( - - ) : null} + {/* Top Curved Banner */} +
+ {/* Bottom Curve */} + + +
- + {/* Circular Overlapping Avatar */} +
+
+ candidate avatar +
+
+ + {/* Candidate Name & Heart Divider */} +
+

+ {matchDisplay.name} +

+ +
+ + + +
+
+ + {/* Info Items List */} +
+ {matchDisplay.age && ( + } + /> + )} + + {matchDisplay.currentCountry && ( + } + /> + )} + + {matchDisplay.currentCity && ( + } + /> + )} + + {matchDisplay.educationLevel && ( + } + /> + )} + + {matchDisplay.fieldOfStudy && ( + } + /> + )} + + {matchDisplay.jobTitle && ( + } + /> + )} + + {matchDisplay.hobbies && ( + } + /> + )} + + {/* Button */} + +
) : ( -

+

No match summary is available yet.

)} @@ -771,7 +950,7 @@ export default function NewMatchClient() { {profile?.can_edit_profile === false && (
{respondMutation.isPending ? ( @@ -944,6 +1123,46 @@ export default function NewMatchClient() { /> )} + {isDeclineConfirmOpen && ( + +

+ {t[ + "Are you sure you've fully reviewed the profile and want to reject this profile?" + ] || + (locale === "fa" + ? "آیا از رد این پیشنهاد مطمئن هستید؟ در صورت رد، این مورد دیگر در دسترس نخواهد بود." + : "Are you sure you want to decline this proposal? Once declined, this match will no longer be available.")} +

+
+ } + buttons={({ close }) => ( + { + close(); + cancelDeclineConfirm(); + }} + onSuccess={async () => { + await handleDecline(); + close(); + setIsDeclineConfirmOpen(false); + setIsPaymentSheetOpen(false); + }} + /> + )} + onClose={cancelDeclineConfirm} + /> + )} + diff --git a/src/app/questions-list/[slug]/question-detail-client.tsx b/src/app/questions-list/[slug]/question-detail-client.tsx index eb7c626..1ec396f 100644 --- a/src/app/questions-list/[slug]/question-detail-client.tsx +++ b/src/app/questions-list/[slug]/question-detail-client.tsx @@ -21,6 +21,7 @@ import { parseValue as parseBirthplaceValue } from "@/components/Componentes/que import QuestionSectionFlow from "@/components/Componentes/question-section-flow"; import StickyHeader from "@/components/Componentes/sticky-header"; import TestIntroPage from "@/components/Componentes/test-intro-page"; +import TestCompletedSheet from "@/components/Componentes/test-completed-sheet"; import TestQuestionsFlow, { type TestQuestion, } from "@/components/Componentes/test-questions-flow"; @@ -137,13 +138,34 @@ function QuestionFlowWrapper({ return undefined; }, [profile?.age, answers]); - const userContext = useMemo( - () => ({ - gender: profile?.gender, + const userContext = useMemo(() => { + let gender = profile?.gender; + if (!gender) { + const genderAns = + answers["personal_identity.gender"] || + answers["personal_info.gender"] || + answers["gender"] || + Object.entries(answers).find(([k]) => k.includes("gender"))?.[1]; + const gVal = + typeof genderAns === "object" && + genderAns !== null && + "value" in genderAns + ? genderAns.value + : genderAns; + if (typeof gVal === "string" && gVal) { + gender = + gVal.toLowerCase().includes("female") || + gVal.toLowerCase().includes("woman") || + gVal.toLowerCase().includes("زن") + ? "female" + : "male"; + } + } + return { + gender, age: computedAge, - }), - [profile?.gender, computedAge], - ); + }; + }, [profile?.gender, answers, computedAge]); const dynamicQuestions = useMemo(() => { @@ -286,10 +308,38 @@ export default function QuestionDetailClient({ const isGlasserSlug = itemSlug === "glasser_5_needs_test" || itemSlug === "glasser_test"; const isAssessment = isCattellSlug || isGlasserSlug; + const [isCompletedSheetOpen, setIsCompletedSheetOpen] = useState(false); + const { data: overview, isLoading: isOverviewLoading, isError: isOverviewError, refetch: refetchOverview } = useFormOverviewQuery( "profile", locale, ); + + const isAssessmentCompleted = useMemo(() => { + if (!isAssessment) return false; + if ( + overview?.progress?.sections_progress?.[itemSlug]?.completion_percent === + 100 + ) { + return true; + } + const currentOverviewItem = overview?.sections?.find( + (s) => s.id === itemSlug, + ); + if (currentOverviewItem?.progress?.completion_percent === 100) { + return true; + } + try { + const completionKey = getQuestionStorageKey(itemSlug, profileId); + if (completionKey && typeof window !== "undefined") { + const raw = window.localStorage.getItem(completionKey); + if (raw && JSON.parse(raw)?.completed === true) { + return true; + } + } + } catch {} + return false; + }, [isAssessment, overview, itemSlug, profileId]); const { data: sectionResponse, isLoading: isSectionLoading, isError: isSectionError, refetch: refetchSection } = useFormSectionQuery( "profile", @@ -694,7 +744,10 @@ export default function QuestionDetailClient({ questions={activeTestQuestions} closeLabel={closeLabel} informationLabel={informationLabel} - onClose={() => setIsTestStarted(false)} + onClose={() => { + setIsTestStarted(false); + handleExit(); + }} onFinish={handleTestFinish} draftStorageKey={getTestDraftStorageKey(item.slug, profileId)} /> @@ -771,8 +824,18 @@ export default function QuestionDetailClient({ "All provided information is held in strict confidence and will not be disclosed to third parties. This data is utilized exclusively to ensure optimal matchmaking accuracy. Consequently, it is imperative that this assessment be completed with the utmost diligence, precision, and integrity." ] } - startLabel={hasTestProgress ? t["Continue"] : t["Start"]} + startLabel={ + isAssessmentCompleted + ? (t as Record)["Completed"] || (locale === "fa" ? "تکمیل شده" : "Completed") + : hasTestProgress + ? t["Continue"] + : t["Start"] + } onStart={() => { + if (isAssessmentCompleted) { + setIsCompletedSheetOpen(true); + return; + } setIsTestStarted(true); }} > @@ -887,6 +950,15 @@ export default function QuestionDetailClient({
+ + { + setIsCompletedSheetOpen(false); + handleExit(); + }} + /> ); } diff --git a/src/app/questions-list/questions-list-client.tsx b/src/app/questions-list/questions-list-client.tsx index ffb38ea..d8e08c2 100644 --- a/src/app/questions-list/questions-list-client.tsx +++ b/src/app/questions-list/questions-list-client.tsx @@ -56,13 +56,28 @@ import { fetchGeoCountryCode } from "@/components/Componentes/question-phone"; import SectionsRequest from "./sections-request"; import SectionOverlayHost from "@/components/Componentes/section-overlay-host"; import QuestionDetailClient from "@/app/questions-list/[slug]/question-detail-client"; +import TestCompletedSheet from "@/components/Componentes/test-completed-sheet"; export default function QuestionsListClient() { + const [isTermsSheetOpen, setIsTermsSheetOpen] = useState(false); + const [completedTestSheet, setCompletedTestSheet] = useState<{ + isOpen: boolean; + title?: string; + }>({ isOpen: false, title: undefined }); + // Hardware back on the root questions list = close the Flutter service. // Unlike the old useCloseServiceOnBack, this does NOT push fake history // entries. Flutter calls __habibHandleHardwareBack() and we return false // (meaning "I didn't handle it — you should close"). useHardwareBackHandler(() => { + if (isTermsSheetOpen) { + setIsTermsSheetOpen(false); + return true; + } + if (completedTestSheet.isOpen) { + setCompletedTestSheet({ isOpen: false }); + return true; + } if (isOptionalInfoSheetOpen) { setIsOptionalInfoSheetOpen(false); return true; // Handled: closed the tips sheet @@ -235,6 +250,31 @@ export default function QuestionsListClient() { return progressBySlug; }, [overview, questionListItems, localAssessmentProgress]); + const isAssessmentSlug = useCallback( + (slug: string) => + slug === "personality_test" || + slug === "glasser_5_needs_test" || + slug === "personality" || + slug === "glasser" || + slug === "cattell", + [], + ); + + const handleCardSelect = useCallback( + (item: QuestionListItem) => { + const progress = sectionProgressBySlug.get(item.slug) ?? 0; + if (isAssessmentSlug(item.slug) && progress >= 100) { + setCompletedTestSheet({ + isOpen: true, + title: item.title, + }); + return; + } + handleOpenSection(item.slug); + }, + [handleOpenSection, isAssessmentSlug, sectionProgressBySlug], + ); + const requiredQuestionListItems = useMemo( () => questionListItems.filter((item) => Boolean(item.required)), [questionListItems], @@ -784,7 +824,11 @@ export default function QuestionsListClient() { className="text-left" /> ) : null} - + setIsTermsSheetOpen(false)} + /> {process.env.NODE_ENV === "development" ? : null} @@ -820,7 +864,9 @@ export default function QuestionsListClient() { setIsTermsSheetOpen(true)} className="shadow-[0_10px_26px_rgba(15,23,42,0.05)]" /> @@ -842,7 +888,7 @@ export default function QuestionsListClient() { onInfoClick={(section) => setSelectedSection(section)} onPrefetch={prefetchSection} onNearViewport={prefetchSection} - onSelect={(item) => handleOpenSection(item.slug)} + onSelect={handleCardSelect} /> ))}
@@ -899,6 +945,12 @@ export default function QuestionsListClient() { /> ) : null} + + setCompletedTestSheet({ isOpen: false })} + /> ); } diff --git a/src/app/questions-list/sections-request.tsx b/src/app/questions-list/sections-request.tsx index e559495..398f075 100644 --- a/src/app/questions-list/sections-request.tsx +++ b/src/app/questions-list/sections-request.tsx @@ -5,6 +5,7 @@ import { IoClose } from "react-icons/io5"; import Button from "@/components/Componentes/button"; import InformationSheet from "@/components/Componentes/information-sheet"; import type { FormOverviewSection } from "@/hooks/marriage/use-form-schema"; +import { useI18n } from "@/translations/provider"; const bookingTerms = [ "All provided information is held in strict confidence.", @@ -32,6 +33,7 @@ export default function SectionsRequest({ isOpen?: boolean; onClose?: () => void; }) { + const { dictionary: t } = useI18n(); const [hasSeenSheet, setHasSeenSheet] = useState(true); const [isAutoOpenDismissed, setIsAutoOpenDismissed] = useState(false); @@ -62,8 +64,7 @@ export default function SectionsRequest({ const isAutoOpen = Boolean(sections) && hasNoProgression && !hasSeenSheet && !isAutoOpenDismissed; - const isSheetOpen = - controlledIsOpen !== undefined ? controlledIsOpen : isAutoOpen; + const isSheetOpen = Boolean(controlledIsOpen) || isAutoOpen; const handleClose = () => { setIsAutoOpenDismissed(true); @@ -80,18 +81,21 @@ export default function SectionsRequest({ return null; } + const termsTitle = t["terms & conditions"] || "Terms & Conditions"; + const gotItLabel = t["Got it"] || "Got it"; + return ( ( - + - Terms & Conditions + {termsTitle} )} onClose={handleClose} - className="text-left" + className="text-start" /> ); } - diff --git a/src/app/request-accepted/request-accepted-client.tsx b/src/app/request-accepted/request-accepted-client.tsx index 45684de..ae175f1 100644 --- a/src/app/request-accepted/request-accepted-client.tsx +++ b/src/app/request-accepted/request-accepted-client.tsx @@ -266,7 +266,7 @@ export default function RequestAcceptedClient() { const secondaryActionText = isFemaleProfile ? t["Contact Received"] : caseStatus === "payment_done" || caseStatus === "contacted" - ? t["View contact number"] + ? t["Contact"] : t["Pay and get contact"]; const contactInfoPhoneItems = getContactInfoPhoneItems( contactInfoQuery.data?.contact_info, @@ -549,29 +549,34 @@ export default function RequestAcceptedClient() {
) : ( <> -
- {t["Request + {/* Illustration */} +
+ {/* soft glow */} +
+ +
+ {t["Request +
-

+

{titleText}

{caseStatus === "contacted" || isFemaleContactConfirmed || (isFemaleProfile && contactStatusMutation.isPending) ? ( -
+
{isFemaleProfile && contactStatusMutation.isPending ? ( ) : ( -

+

{isFemaleProfile ? t[ "Thank you for your feedback. To complete the process, please submit the final outcome of this introduction/contact so that the final status can be determined. If the final status is not yet determined, you can stay in this state until it is finalized." @@ -583,7 +588,7 @@ export default function RequestAcceptedClient() { )}

) : ( -

+

{noContactReportedSuccess ? t[ "Thank you for your feedback. Our support team will investigate the matter and notify you of the result. Please wait patiently during the review; our support will contact you." @@ -602,14 +607,14 @@ export default function RequestAcceptedClient() { {caseStatus === "contacted" || isFemaleContactConfirmed || (isFemaleProfile && contactStatusMutation.isPending) ? ( -

+
{isFemaleProfile && contactStatusMutation.isPending ? null : isFemaleProfile ? ( @@ -631,7 +636,7 @@ export default function RequestAcceptedClient() { type="button" onClick={() => setIsOutcomeSheetOpen(true)} disabled={outcomeMutation.isPending} - className="flex-1 h-[52px] rounded-[15px] bg-gradient-to-r from-[#FE6F82] to-[#E03950] text-white font-semibold group-14 flex items-center justify-center shadow-lg shadow-[#FE6F82]/30 transition-transform active:scale-[0.98] cursor-pointer hover:opacity-95 disabled:opacity-50" + className="flex-1 h-[50px] px-3 rounded-full bg-gradient-to-r from-[#FF6687] to-[#FF456C] text-white font-bold text-[14px] flex items-center justify-center shadow-sm transition-transform active:scale-[0.98] cursor-pointer hover:opacity-95 disabled:opacity-50" > {outcomeMutation.isPending ? ( @@ -643,7 +648,7 @@ export default function RequestAcceptedClient() { )}
) : ( -
+
{isFemaleProfile ? ( diff --git a/src/components/Componentes/advisor-actions-card.tsx b/src/components/Componentes/advisor-actions-card.tsx index f1722a6..1fde0ac 100644 --- a/src/components/Componentes/advisor-actions-card.tsx +++ b/src/components/Componentes/advisor-actions-card.tsx @@ -1,7 +1,9 @@ "use client"; +import Link from "next/link"; import { useMarriageAdvisorsQuery } from "@/hooks/marriage/use-marriage-advisors"; -import Button from "./button"; +import { localizePath } from "@/translations/config"; +import { useI18n } from "@/translations/provider"; import NetworkImage from "./network-image"; const FALLBACK_AVATAR = "/assets/images/Avatar Image.png"; @@ -33,13 +35,14 @@ export function AdvisorActionsCard({ onGetAdvisor, className, }: AdvisorActionsCardProps) { + const { locale } = useI18n(); const { data, isLoading } = useMarriageAdvisorsQuery(); const advisors = data?.results ?? []; - // Derive real avatars from API response (pick first 3 with an avatar). + // Derive real avatars from API response (pick up to 4 with an avatar). const realAvatars: AdvisorAvatar[] = advisors .filter((a) => a.avatar_url) - .slice(0, 3) + .slice(0, 4) .map((a) => ({ id: a.username, src: a.avatar_url ?? FALLBACK_AVATAR, @@ -55,54 +58,63 @@ export function AdvisorActionsCard({ : fallbackExtraCount; return ( -
-
-

+
+
+

{title}

-

+

{description}

-
-
+
+
{isLoading ? /* ── Shimmer circles while API loads ── */ Array.from({ length: 3 }).map((_, i) => ( )) : displayAvatars.map((avatar) => ( ))} {!isLoading && displayExtraCount > 0 && ( - + +{displayExtraCount} )}
- + {getAdvisorHref ? ( + + {getAdvisorLabel} + + ) : ( + + )}
diff --git a/src/components/Componentes/information-sheet.tsx b/src/components/Componentes/information-sheet.tsx index 53b72d8..cfcfafb 100644 --- a/src/components/Componentes/information-sheet.tsx +++ b/src/components/Componentes/information-sheet.tsx @@ -16,6 +16,12 @@ type InformationSheetPresetIcon = | "warning" | "coin" | "check" + | "diamond" + | "diamond-color" + | "diamond-color.svg" + | "diamond-color.png" + | "/assets/images/diamond-color.png" + | "/assets/images/diamond-color.svg" | "stash_play-solid.svg" | "warning.svg" | "coin.svg" @@ -42,6 +48,7 @@ export type InformationSheetProps = Omit< closeOnOutside?: boolean; onClose?: () => void; isLoading?: boolean; + showCloseButton?: boolean; }; const DEFAULT_ICON = { @@ -98,6 +105,42 @@ const ICON_PRESETS: Record< width: 36, height: 36, }, + diamond: { + src: "/assets/images/diamond-color.svg", + alt: "Subscription", + width: 48, + height: 48, + }, + "diamond-color": { + src: "/assets/images/diamond-color.svg", + alt: "Subscription", + width: 48, + height: 48, + }, + "diamond-color.svg": { + src: "/assets/images/diamond-color.svg", + alt: "Subscription", + width: 48, + height: 48, + }, + "diamond-color.png": { + src: "/assets/images/diamond-color.svg", + alt: "Subscription", + width: 48, + height: 48, + }, + "/assets/images/diamond-color.png": { + src: "/assets/images/diamond-color.svg", + alt: "Subscription", + width: 48, + height: 48, + }, + "/assets/images/diamond-color.svg": { + src: "/assets/images/diamond-color.svg", + alt: "Subscription", + width: 48, + height: 48, + }, }; function resolveIcon(icon: InformationSheetIcon | null | undefined) { @@ -110,6 +153,16 @@ function resolveIcon(icon: InformationSheetIcon | null | undefined) { } if (typeof icon === "string") { + // If diamond PNG was requested, automatically upgrade to high-res vector SVG + if (icon.includes("diamond-color")) { + return { + src: "/assets/images/diamond-color.svg", + alt: "Subscription", + width: 48, + height: 48, + }; + } + return ( ICON_PRESETS[icon as InformationSheetPresetIcon] ?? { src: icon, @@ -137,6 +190,7 @@ export function InformationSheet({ onClose, className, isLoading = false, + showCloseButton = true, ...props }: InformationSheetProps) { const { locale, dictionary: t } = useI18n(); @@ -241,17 +295,68 @@ export function InformationSheet({
+ {showCloseButton && ( + + )} +
{isLoading ? ( -
- +
+ {/* Icon Skeleton */} + + + {/* Title Skeleton */} + + + {/* Description Lines Skeleton */} +
+ + + +
+ + {/* Middle Box Skeleton (e.g. Plan / Info) */} +
+ + +
+ + {/* Subtext Skeleton */} +
+ + +
+ + {/* Action / Swipe Buttons Skeleton */} +
+ + +
) : ( <> diff --git a/src/components/Componentes/navigation-button.tsx b/src/components/Componentes/navigation-button.tsx index ae4e5d7..3942773 100644 --- a/src/components/Componentes/navigation-button.tsx +++ b/src/components/Componentes/navigation-button.tsx @@ -135,7 +135,7 @@ export function NavigationButton({ return hasActiveSubscription ? (

+ + setIsExitSheetOpen(false)} + onConfirmExit={handleConfirmExit} + /> ); } + diff --git a/src/lib/conditional-rules.test.ts b/src/lib/conditional-rules.test.ts index bf41d9a..b0d48cd 100644 --- a/src/lib/conditional-rules.test.ts +++ b/src/lib/conditional-rules.test.ts @@ -200,4 +200,643 @@ describe("Conditional Rules Evaluator", () => { expect(isQuestionVisible(q, matchingAnswers)).toBe(true); expect(isQuestionRequired(q, matchingAnswers)).toBe(true); }); + + it("should evaluate representative questions as optional for men and conditional for women based on age", () => { + const repNameQuestion: QuestionField = { + ...dummyQuestion, + id: "contact_residence.representative_s_full_name", + title: "Representative's Full Name", + required: false, + baseRequired: false, + requiredWhen: { + genders: ["female"], + maxAge: 26, + }, + }; + + const repPhoneQuestion: QuestionField = { + ...dummyQuestion, + id: "contact_residence.representative_s_contact_number", + title: "Representative's Contact Number", + type: "phone", + required: false, + baseRequired: false, + requiredWhen: { + genders: ["female"], + maxAge: 26, + }, + }; + + const repRelQuestion: QuestionField = { + ...dummyQuestion, + id: "contact_residence.relationship_to_representative", + title: "Relationship to Representative", + type: "dropdown", + required: false, + baseRequired: false, + requiredWhen: { + genders: ["female"], + maxAge: 26, + }, + }; + + const repQuestions = [repNameQuestion, repPhoneQuestion, repRelQuestion]; + + // For Male (regardless of age: 20, 26, 30): ALWAYS OPTIONAL + for (const age of [18, 20, 25, 26, 27, 35]) { + const maleContext = { gender: "male", age }; + for (const question of repQuestions) { + expect(isQuestionRequired(question, {}, maleContext)).toBe(false); + } + } + + // For Female <= 26: REQUIRED + for (const age of [18, 20, 25, 26]) { + const youngFemaleContext = { gender: "female", age }; + for (const question of repQuestions) { + expect(isQuestionRequired(question, {}, youngFemaleContext)).toBe(true); + } + } + + // For Female >= 27: OPTIONAL + for (const age of [27, 30, 35]) { + const olderFemaleContext = { gender: "female", age }; + for (const question of repQuestions) { + expect(isQuestionRequired(question, {}, olderFemaleContext)).toBe(false); + } + } + + // When gender is unknown: OPTIONAL + for (const question of repQuestions) { + expect(isQuestionRequired(question, {}, {})).toBe(false); + } + }); + + it("should correctly handle physical health 4-option visibility and medication requirement", () => { + // 1. Physical Health Description + const physicalHealthDescription: QuestionField = { + ...dummyQuestion, + id: "appearance_health.physical_health_description", + title: "Physical Health Description", + required: false, + baseRequired: false, + visibility: { + parent_question_id: "appearance_health.physical_health_status", + trigger_option_ids: [ + "appearance_health.physical_health_status.i_have_a_specific_or_chronic_illness", + "appearance_health.physical_health_status.i_have_a_disability_or_physical_limitation", + "appearance_health.physical_health_status.i_have_other_illness_or_physical_conditions", + ], + operator: "any_of", + }, + requiredWhen: { + parent_question_id: "appearance_health.physical_health_status", + trigger_option_ids: [ + "appearance_health.physical_health_status.i_have_a_specific_or_chronic_illness", + "appearance_health.physical_health_status.i_have_a_disability_or_physical_limitation", + "appearance_health.physical_health_status.i_have_other_illness_or_physical_conditions", + ], + operator: "any_of", + }, + }; + + // Option A: No illness -> hidden and not required + const optionAAnswers = { + "appearance_health.physical_health_status": { + value: "i_currently_have_no_specific_illness_or_physical_limitation", + option_id: + "appearance_health.physical_health_status.i_currently_have_no_specific_illness_or_physical_limitation", + }, + }; + expect(isQuestionVisible(physicalHealthDescription, optionAAnswers)).toBe(false); + expect(isQuestionRequired(physicalHealthDescription, optionAAnswers)).toBe(false); + + // Options B, C, D -> visible and required + const optionBAnswers = { + "appearance_health.physical_health_status": { + value: "i_have_a_specific_or_chronic_illness", + option_id: + "appearance_health.physical_health_status.i_have_a_specific_or_chronic_illness", + }, + }; + expect(isQuestionVisible(physicalHealthDescription, optionBAnswers)).toBe(true); + expect(isQuestionRequired(physicalHealthDescription, optionBAnswers)).toBe(true); + + const optionCAnswers = { + "appearance_health.physical_health_status": { + value: "i_have_a_disability_or_physical_limitation", + option_id: + "appearance_health.physical_health_status.i_have_a_disability_or_physical_limitation", + }, + }; + expect(isQuestionVisible(physicalHealthDescription, optionCAnswers)).toBe(true); + expect(isQuestionRequired(physicalHealthDescription, optionCAnswers)).toBe(true); + + const optionDAnswers = { + "appearance_health.physical_health_status": { + value: "i_have_other_illness_or_physical_conditions", + option_id: + "appearance_health.physical_health_status.i_have_other_illness_or_physical_conditions", + }, + }; + expect(isQuestionVisible(physicalHealthDescription, optionDAnswers)).toBe(true); + expect(isQuestionRequired(physicalHealthDescription, optionDAnswers)).toBe(true); + + // 2. Medication Name and Reason + const medicationDescription: QuestionField = { + ...dummyQuestion, + id: "appearance_health.medication_name_and_reason_for_use", + title: "Medication Name and Reason for Use", + required: false, + baseRequired: false, + visibility: { + parent_question_id: + "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis", + trigger_option_ids: [ + "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis.yes", + ], + operator: "any_of", + conditions: [ + { + parent_question_id: + "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues", + trigger_option_ids: [ + "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues.yes", + ], + operator: "any_of", + }, + ], + root_operator: "any_of", + }, + requiredWhen: { + parent_question_id: + "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis", + trigger_option_ids: [ + "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis.yes", + ], + operator: "any_of", + conditions: [ + { + parent_question_id: + "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues", + trigger_option_ids: [ + "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues.yes", + ], + operator: "any_of", + }, + ], + root_operator: "any_of", + }, + }; + + // Both No -> hidden + const medNoAnswers = { + "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis": { + value: "no", + option_id: + "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis.no", + }, + "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues": { + value: "no", + option_id: + "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues.no", + }, + }; + expect(isQuestionVisible(medicationDescription, medNoAnswers)).toBe(false); + expect(isQuestionRequired(medicationDescription, medNoAnswers)).toBe(false); + + // Mental health med Yes -> visible and required + const medMentalYesAnswers = { + "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis": { + value: "no", + option_id: + "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis.no", + }, + "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues": { + value: "yes", + option_id: + "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues.yes", + }, + }; + expect(isQuestionVisible(medicationDescription, medMentalYesAnswers)).toBe(true); + expect(isQuestionRequired(medicationDescription, medMentalYesAnswers)).toBe(true); + + // Ongoing med Yes -> visible and required + const medOngoingYesAnswers = { + "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis": { + value: "yes", + option_id: + "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis.yes", + }, + "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues": { + value: "no", + option_id: + "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues.no", + }, + }; + expect(isQuestionVisible(medicationDescription, medOngoingYesAnswers)).toBe(true); + expect(isQuestionRequired(medicationDescription, medOngoingYesAnswers)).toBe(true); + }); + + it("should hide job title, work location, and monthly income for student, unemployed, student & job seeking, and homemaker", () => { + const jobTitle: QuestionField = { + ...dummyQuestion, + id: "education_career.job_title", + title: "عنوان شغلی", + visibility: { + parent_question_id: "education_career.employment_status", + trigger_option_ids: [ + "education_career.employment_status.full_time_employed", + "education_career.employment_status.part_time_employed", + "education_career.employment_status.self_employed_freelancer", + "education_career.employment_status.entrepreneur_business_owner", + "education_career.employment_status.working_student", + "education_career.employment_status.retired", + ], + operator: "any_of", + }, + }; + + const workLocation: QuestionField = { + ...dummyQuestion, + id: "education_career.work_location", + title: "محل فعالیت", + visibility: { + parent_question_id: "education_career.employment_status", + trigger_option_ids: [ + "education_career.employment_status.full_time_employed", + "education_career.employment_status.part_time_employed", + "education_career.employment_status.self_employed_freelancer", + "education_career.employment_status.entrepreneur_business_owner", + "education_career.employment_status.working_student", + ], + operator: "any_of", + }, + }; + + const monthlyIncome: QuestionField = { + ...dummyQuestion, + id: "education_career.monthly_income", + title: "میزان درآمد ماهانه", + required: false, + visibility: { + parent_question_id: "education_career.employment_status", + trigger_option_ids: [ + "education_career.employment_status.full_time_employed", + "education_career.employment_status.part_time_employed", + "education_career.employment_status.self_employed_freelancer", + "education_career.employment_status.entrepreneur_business_owner", + "education_career.employment_status.working_student", + "education_career.employment_status.retired", + ], + operator: "any_of", + }, + requiredWhen: { + parent_question_id: "education_career.employment_status", + trigger_option_ids: [ + "education_career.employment_status.full_time_employed", + "education_career.employment_status.part_time_employed", + "education_career.employment_status.self_employed_freelancer", + "education_career.employment_status.entrepreneur_business_owner", + "education_career.employment_status.working_student", + "education_career.employment_status.retired", + ], + operator: "any_of", + }, + }; + + // Hidden cases: دانشجو, دانشجو و جویای کار, جویای کار / بیکار, خانه‌دار + const hiddenStatuses = [ + "student", + "student_and_job_seeking", + "job_seeking_unemployed", + "homemaker", + ]; + + for (const status of hiddenStatuses) { + const answers = { + "education_career.employment_status": { + value: status, + option_id: `education_career.employment_status.${status}`, + }, + }; + + expect(isQuestionVisible(jobTitle, answers)).toBe(false); + expect(isQuestionVisible(workLocation, answers)).toBe(false); + expect(isQuestionVisible(monthlyIncome, answers)).toBe(false); + expect(isQuestionRequired(monthlyIncome, answers)).toBe(false); + } + + // Visible case: Full time employed + const fullTimeAnswers = { + "education_career.employment_status": { + value: "full_time_employed", + option_id: "education_career.employment_status.full_time_employed", + }, + }; + expect(isQuestionVisible(jobTitle, fullTimeAnswers)).toBe(true); + expect(isQuestionVisible(workLocation, fullTimeAnswers)).toBe(true); + expect(isQuestionVisible(monthlyIncome, fullTimeAnswers)).toBe(true); + expect(isQuestionRequired(monthlyIncome, fullTimeAnswers)).toBe(true); + + // Visible case: Working student + const workingStudentAnswers = { + "education_career.employment_status": { + value: "working_student", + option_id: "education_career.employment_status.working_student", + }, + }; + expect(isQuestionVisible(jobTitle, workingStudentAnswers)).toBe(true); + expect(isQuestionVisible(workLocation, workingStudentAnswers)).toBe(true); + expect(isQuestionVisible(monthlyIncome, workingStudentAnswers)).toBe(true); + expect(isQuestionRequired(monthlyIncome, workingStudentAnswers)).toBe(true); + + // Retired case: Job title and monthly income visible, work location hidden + const retiredAnswers = { + "education_career.employment_status": { + value: "retired", + option_id: "education_career.employment_status.retired", + }, + }; + expect(isQuestionVisible(jobTitle, retiredAnswers)).toBe(true); + expect(isQuestionVisible(workLocation, retiredAnswers)).toBe(false); + expect(isQuestionVisible(monthlyIncome, retiredAnswers)).toBe(true); + expect(isQuestionRequired(monthlyIncome, retiredAnswers)).toBe(true); + }); + + it("should show parents marital status only when both parents are alive (Option A) and hide for options B, C, D", () => { + const parentsMaritalStatus: QuestionField = { + ...dummyQuestion, + id: "family_background.parents_marital_status", + title: "وضعیت تأهل والدین", + required: false, + visibility: { + parent_question_id: "family_background.parents_survival_status", + trigger_option_ids: [ + "family_background.parents_survival_status.both_parents_are_alive", + ], + operator: "any_of", + }, + requiredWhen: { + parent_question_id: "family_background.parents_survival_status", + trigger_option_ids: [ + "family_background.parents_survival_status.both_parents_are_alive", + ], + operator: "any_of", + }, + }; + + // Option A: Both parents are alive -> Visible & Required + const optionAAnswers = { + "family_background.parents_survival_status": { + value: "both_parents_are_alive", + option_id: "family_background.parents_survival_status.both_parents_are_alive", + }, + }; + expect(isQuestionVisible(parentsMaritalStatus, optionAAnswers)).toBe(true); + expect(isQuestionRequired(parentsMaritalStatus, optionAAnswers)).toBe(true); + + // Option B: Father has passed away -> Hidden & Not Required + const optionBAnswers = { + "family_background.parents_survival_status": { + value: "father_has_passed_away", + option_id: "family_background.parents_survival_status.father_has_passed_away", + }, + }; + expect(isQuestionVisible(parentsMaritalStatus, optionBAnswers)).toBe(false); + expect(isQuestionRequired(parentsMaritalStatus, optionBAnswers)).toBe(false); + + // Option C: Mother has passed away -> Hidden & Not Required + const optionCAnswers = { + "family_background.parents_survival_status": { + value: "mother_has_passed_away", + option_id: "family_background.parents_survival_status.mother_has_passed_away", + }, + }; + expect(isQuestionVisible(parentsMaritalStatus, optionCAnswers)).toBe(false); + expect(isQuestionRequired(parentsMaritalStatus, optionCAnswers)).toBe(false); + + // Option D: Both parents have passed away -> Hidden & Not Required + const optionDAnswers = { + "family_background.parents_survival_status": { + value: "both_parents_have_passed_away", + option_id: "family_background.parents_survival_status.both_parents_have_passed_away", + }, + }; + expect(isQuestionVisible(parentsMaritalStatus, optionDAnswers)).toBe(false); + expect(isQuestionRequired(parentsMaritalStatus, optionDAnswers)).toBe(false); + }); + + it("should show family responsibility follow-up questions (live with you, additional details) and make them required only when responsibility options are selected", () => { + const parentId = "family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member"; + const triggerOptionIds = [ + `${parentId}.i_am_responsible_for_caring_for_my_father`, + `${parentId}.i_am_responsible_for_caring_for_my_mother`, + `${parentId}.i_am_responsible_for_caring_for_both_parents`, + `${parentId}.i_am_responsible_for_caring_for_a_sibling_brother_sister`, + `${parentId}.i_am_the_legal_guardian_or_supervisor_of_a_family_member`, + `${parentId}.i_regularly_provide_financial_support_for_a_family_member_s_living_expenses`, + `${parentId}.i_have_other_circumstances_and_will_explain_in_the_description`, + `${parentId}.i_am_responsible_for_caring_for_my_parent_s_father_mother_or_both`, + ]; + + const liveWithYou: QuestionField = { + ...dummyQuestion, + id: "family_background.do_the_supported_individual_s_live_with_you", + title: "آیا فرد یا افراد تحت حمایت با شما زندگی می‌کنند؟", + required: false, + visibility: { + parent_question_id: parentId, + trigger_option_ids: triggerOptionIds, + operator: "any_of", + }, + requiredWhen: { + parent_question_id: parentId, + trigger_option_ids: triggerOptionIds, + operator: "any_of", + }, + }; + + const additionalDetails: QuestionField = { + ...dummyQuestion, + id: "family_background.additional_details_about_family_responsibility", + title: "توضیحات تکمیلی درباره مسئولیت خانوادگی", + required: false, + visibility: { + parent_question_id: parentId, + trigger_option_ids: triggerOptionIds, + operator: "any_of", + }, + requiredWhen: { + parent_question_id: parentId, + trigger_option_ids: triggerOptionIds, + operator: "any_of", + }, + }; + + // Case 1: No ongoing responsibility selected -> Hidden & Not Required + const noRespAnswers = { + [parentId]: { + value: ["خیر، مسئولیت مستمری ندارم."], + option_id: [`${parentId}.no_i_do_not_have_any_ongoing_responsibility`], + }, + }; + expect(isQuestionVisible(liveWithYou, noRespAnswers)).toBe(false); + expect(isQuestionRequired(liveWithYou, noRespAnswers)).toBe(false); + expect(isQuestionVisible(additionalDetails, noRespAnswers)).toBe(false); + expect(isQuestionRequired(additionalDetails, noRespAnswers)).toBe(false); + + // Case 2: Responsibility selected (e.g. Caring for father) -> Visible & Required + const fatherRespAnswers = { + [parentId]: { + value: ["مسئولیت مراقبت از پدر را بر عهده دارم."], + option_id: [`${parentId}.i_am_responsible_for_caring_for_my_father`], + }, + }; + expect(isQuestionVisible(liveWithYou, fatherRespAnswers)).toBe(true); + expect(isQuestionRequired(liveWithYou, fatherRespAnswers)).toBe(true); + expect(isQuestionVisible(additionalDetails, fatherRespAnswers)).toBe(true); + expect(isQuestionRequired(additionalDetails, fatherRespAnswers)).toBe(true); + + // Case 3: Multiple responsibilities selected -> Visible & Required + const multiRespAnswers = { + [parentId]: { + value: ["مسئولیت مراقبت از پدر", "حمایت مالی"], + option_id: [ + `${parentId}.i_am_responsible_for_caring_for_my_father`, + `${parentId}.i_regularly_provide_financial_support_for_a_family_member_s_living_expenses`, + ], + }, + }; + expect(isQuestionVisible(liveWithYou, multiRespAnswers)).toBe(true); + expect(isQuestionRequired(liveWithYou, multiRespAnswers)).toBe(true); + expect(isQuestionVisible(additionalDetails, multiRespAnswers)).toBe(true); + expect(isQuestionRequired(additionalDetails, multiRespAnswers)).toBe(true); + }); + + it("should handle Section 6 marital history and children follow-up rules properly", () => { + const mStatusId = "marital_history.current_marital_status"; + const previousTriggerIds = [ + `${mStatusId}.failed_engagement_annulled_marriage_without_living_together`, + `${mStatusId}.failed_marriage_contract_annulled_engagement_without_starting_joint_life`, + `${mStatusId}.divorced_after_living_together`, + `${mStatusId}.widowed`, + ]; + const divorcedOrAnnulledIds = [ + `${mStatusId}.failed_engagement_annulled_marriage_without_living_together`, + `${mStatusId}.failed_marriage_contract_annulled_engagement_without_starting_joint_life`, + `${mStatusId}.divorced_after_living_together`, + ]; + + const cStatusId = "marital_history.children_and_guardianship_status"; + const hasChildrenTriggerIds = [ + `${cStatusId}.have_children_living_with_me`, + `${cStatusId}.have_children_not_living_with_me`, + ]; + + const prevDuration: QuestionField = { + ...dummyQuestion, + id: "marital_history.previous_marriage_duration", + title: "مدت ازدواج یا عقد قبلی", + required: false, + visibility: { parent_question_id: mStatusId, trigger_option_ids: previousTriggerIds, operator: "any_of" }, + requiredWhen: { parent_question_id: mStatusId, trigger_option_ids: previousTriggerIds, operator: "any_of" }, + }; + + const reasonSeparation: QuestionField = { + ...dummyQuestion, + id: "marital_history.reason_for_separation", + title: "علت جدایی، در صورت وجود", + required: false, + visibility: { parent_question_id: mStatusId, trigger_option_ids: divorcedOrAnnulledIds, operator: "any_of" }, + }; + + const childrenStatus: QuestionField = { + ...dummyQuestion, + id: "marital_history.children_and_guardianship_status", + title: "وضعیت فرزند و تکفل", + required: false, + visibility: { parent_question_id: mStatusId, trigger_option_ids: previousTriggerIds, operator: "any_of" }, + requiredWhen: { parent_question_id: mStatusId, trigger_option_ids: previousTriggerIds, operator: "any_of" }, + }; + + const numChildren: QuestionField = { + ...dummyQuestion, + id: "marital_history.number_of_children", + title: "تعداد فرزندان", + required: false, + visibility: { parent_question_id: cStatusId, trigger_option_ids: hasChildrenTriggerIds, operator: "any_of" }, + requiredWhen: { parent_question_id: cStatusId, trigger_option_ids: hasChildrenTriggerIds, operator: "any_of" }, + }; + + const custodyStatus: QuestionField = { + ...dummyQuestion, + id: "marital_history.what_is_the_custody_status_of_your_child_ren", + title: "وضعیت حضانت فرزند یا فرزندان شما چگونه است؟", + required: false, + visibility: { parent_question_id: cStatusId, trigger_option_ids: hasChildrenTriggerIds, operator: "any_of" }, + requiredWhen: { parent_question_id: cStatusId, trigger_option_ids: hasChildrenTriggerIds, operator: "any_of" }, + }; + + // 1. Single: previous duration, reason for separation, and children status are all HIDDEN + const singleAns = { + [mStatusId]: { + value: "single_never_married", + option_id: `${mStatusId}.single_never_married`, + }, + }; + expect(isQuestionVisible(prevDuration, singleAns)).toBe(false); + expect(isQuestionVisible(reasonSeparation, singleAns)).toBe(false); + expect(isQuestionVisible(childrenStatus, singleAns)).toBe(false); + + // 2. Divorced: duration, reason, and children status are all VISIBLE + const divorcedAns = { + [mStatusId]: { + value: "divorced_after_living_together", + option_id: `${mStatusId}.divorced_after_living_together`, + }, + }; + expect(isQuestionVisible(prevDuration, divorcedAns)).toBe(true); + expect(isQuestionRequired(prevDuration, divorcedAns)).toBe(true); + expect(isQuestionVisible(reasonSeparation, divorcedAns)).toBe(true); + expect(isQuestionVisible(childrenStatus, divorcedAns)).toBe(true); + expect(isQuestionRequired(childrenStatus, divorcedAns)).toBe(true); + + // 3. Widowed: duration & children VISIBLE, reason for separation HIDDEN + const widowedAns = { + [mStatusId]: { + value: "widowed", + option_id: `${mStatusId}.widowed`, + }, + }; + expect(isQuestionVisible(prevDuration, widowedAns)).toBe(true); + expect(isQuestionRequired(prevDuration, widowedAns)).toBe(true); + expect(isQuestionVisible(reasonSeparation, widowedAns)).toBe(false); + expect(isQuestionVisible(childrenStatus, widowedAns)).toBe(true); + expect(isQuestionRequired(childrenStatus, widowedAns)).toBe(true); + + // 4. Children follow-ups: when children exist -> VISIBLE & REQUIRED + const hasChildAns = { + [cStatusId]: { + value: "have_children_living_with_me", + option_id: `${cStatusId}.have_children_living_with_me`, + }, + }; + expect(isQuestionVisible(numChildren, hasChildAns)).toBe(true); + expect(isQuestionRequired(numChildren, hasChildAns)).toBe(true); + expect(isQuestionVisible(custodyStatus, hasChildAns)).toBe(true); + expect(isQuestionRequired(custodyStatus, hasChildAns)).toBe(true); + + // 5. No children: child questions HIDDEN & NOT REQUIRED + const noChildAns = { + [cStatusId]: { + value: "no_children", + option_id: `${cStatusId}.no_children`, + }, + }; + expect(isQuestionVisible(numChildren, noChildAns)).toBe(false); + expect(isQuestionRequired(numChildren, noChildAns)).toBe(false); + expect(isQuestionVisible(custodyStatus, noChildAns)).toBe(false); + expect(isQuestionRequired(custodyStatus, noChildAns)).toBe(false); + }); }); diff --git a/src/lib/conditional-rules.ts b/src/lib/conditional-rules.ts index 7aeee6e..b9fd253 100644 --- a/src/lib/conditional-rules.ts +++ b/src/lib/conditional-rules.ts @@ -58,6 +58,16 @@ export function canonicalRule(rule: any): CanonicalRule | null { if (rule.audience && typeof rule.audience === "object") { result.audience = rule.audience; + } else if ( + rule.genders || + rule.minAge !== undefined || + rule.maxAge !== undefined + ) { + result.audience = { + genders: rule.genders, + minAge: rule.minAge, + maxAge: rule.maxAge, + }; } if (Array.isArray(rule.conditions)) { @@ -140,19 +150,24 @@ export function matchesAudience( } if (audience.genders && audience.genders.length > 0) { - if (context?.gender && !audience.genders.includes(context.gender)) { + if ( + !context?.gender || + !audience.genders + .map((g) => g.toLowerCase()) + .includes(context.gender.toLowerCase()) + ) { return false; } } - if (audience.minAge !== undefined && context?.age !== undefined && context.age !== null) { - if (context.age < audience.minAge) { + if (audience.minAge !== undefined) { + if (context?.age === undefined || context.age === null || context.age < audience.minAge) { return false; } } - if (audience.maxAge !== undefined && context?.age !== undefined && context.age !== null) { - if (context.age > audience.maxAge) { + if (audience.maxAge !== undefined) { + if (context?.age === undefined || context.age === null || context.age > audience.maxAge) { return false; } } @@ -250,7 +265,9 @@ export function ruleMatches( : mainMatches && conditionsResult; } - return mainMatches; + return parentId + ? mainMatches + : Boolean(!rule.audience || matchesAudience(rule.audience, context)); } export function isQuestionVisible( @@ -289,16 +306,16 @@ export function isQuestionRequired( return false; } - if (question.required || question.baseRequired) { - return true; - } - if (question.requiredWhen) { - if (question.requiredWhen.genders || question.requiredWhen.minAge || question.requiredWhen.maxAge) { + if ( + question.requiredWhen.genders || + question.requiredWhen.minAge !== undefined || + question.requiredWhen.maxAge !== undefined + ) { return matchesAudience(question.requiredWhen, context); } return ruleMatches(question.requiredWhen, answers, context); } - return false; + return Boolean(question.baseRequired ?? question.required); } diff --git a/src/lib/schema-adapter.ts b/src/lib/schema-adapter.ts index 6fbbe76..e4ad2a3 100644 --- a/src/lib/schema-adapter.ts +++ b/src/lib/schema-adapter.ts @@ -46,6 +46,49 @@ export const sectionSlugIconMap: Record = { glasser_test: "glasser", }; +export const sectionEstimatedMinutesMap: Record = { + personal_info: 2, + personal_identity: 2, + contact_residence_family_communication: 2, + contact_residence: 2, + appearance_health_activity: 2, + appearance_health: 2, + education_career_economic_status: 3, + education_career: 3, + family_background: 3, + marital_history_children: 2, + marital_history: 2, + beliefs_lifestyle_boundaries: 4, + beliefs_lifestyle: 4, + personality_test: 16, + cattell_test: 16, + glasser_5_needs_test: 5, + glasser_test: 5, + future_spouse_criteria: 6, + spouse_criteria: 6, + identity_verification: 2, + documents_verification: 2, +}; + +export function resolveSectionEstimatedMinutes( + slug: string, + backendEstimatedMinutes?: number | null, +): number { + if ( + (slug === "personality_test" || slug === "cattell_test") && + (!backendEstimatedMinutes || backendEstimatedMinutes === 8) + ) { + return 16; + } + if (typeof backendEstimatedMinutes === "number" && backendEstimatedMinutes > 0) { + return backendEstimatedMinutes; + } + if (slug && sectionEstimatedMinutesMap[slug]) { + return sectionEstimatedMinutesMap[slug]; + } + return 3; +} + export function resolveSectionIcon( slug: string, backendIcon?: string, @@ -231,12 +274,15 @@ export function mapBackendSectionToFrontend( }); }); + const estMinutes = resolveSectionEstimatedMinutes( + section.id, + section.estimated_minutes, + ); + return { slug: section.id, title: section.title, - estimate: section.estimated_minutes - ? `${section.estimated_minutes} min` - : "5 min", + estimate: `${estMinutes} min`, progress: progress, icon: resolveSectionIcon(section.id, section.icon), required: section.is_required, @@ -273,19 +319,23 @@ export function convertOverviewToFrontendItems( if (!overview) return []; return [...overview.sections] .sort((a, b) => a.order - b.order) - .map((section) => ({ - slug: section.id, - title: section.title, - estimate: section.estimated_minutes - ? `${section.estimated_minutes} min` - : "5 min", - progress: section.progress?.completion_percent ?? 0, - icon: resolveSectionIcon(section.id, section.icon), - required: section.is_required, - showInfoBadge: false, - summary: "", - checkpoints: [], - tooltip: "", - questions: [], - })); + .map((section) => { + const estMinutes = resolveSectionEstimatedMinutes( + section.id, + section.estimated_minutes, + ); + return { + slug: section.id, + title: section.title, + estimate: `${estMinutes} min`, + progress: section.progress?.completion_percent ?? 0, + icon: resolveSectionIcon(section.id, section.icon), + required: section.is_required, + showInfoBadge: false, + summary: "", + checkpoints: [], + tooltip: "", + questions: [], + }; + }); } diff --git a/src/translations/locales/ar.json b/src/translations/locales/ar.json index a4d1241..614fd9f 100644 --- a/src/translations/locales/ar.json +++ b/src/translations/locales/ar.json @@ -808,5 +808,10 @@ "نام دارو و دلیل مصرف را وارد نمایید...": "أدخل اسم الدواء وسبب الاستخدام...", "وضعیت سلامت جسمانی": "حالة الصحة الجسدية", "توضیحات وضعیت جسمانی": "وصف الصحة الجسدية", - "Name": "الاسم" -} \ No newline at end of file + "Name": "الاسم", + "upload_certificates": "تحميل الشهادات والوثائق", + "add_another_document": "إضافة وثيقة أخرى", + "max_files_reached": "تم تحميل الحد الأقصى (4 ملفات)", + "remove_document": "حذف الوثيقة", + "upload_failed": "فشل التحميل. يرجى المحاولة مرة أخرى." +} diff --git a/src/translations/locales/az.json b/src/translations/locales/az.json index be8ef8f..53cbbb8 100644 --- a/src/translations/locales/az.json +++ b/src/translations/locales/az.json @@ -808,5 +808,10 @@ "نام دارو و دلیل مصرف را وارد نمایید...": "Dərmanın adını və istifadə səbəbini daxil edin...", "وضعیت سلامت جسمانی": "Fiziki Sağlamlıq Vəziyyəti", "توضیحات وضعیت جسمانی": "Fiziki Sağlamlıq Təsviri", - "Name": "Ad" -} \ No newline at end of file + "Name": "Ad", + "upload_certificates": "Sənədləri yükləyin", + "add_another_document": "Başqa sənəd əlavə edin", + "max_files_reached": "Maksimum 4 fayl yükləndi", + "remove_document": "Sənədi sil", + "upload_failed": "Yükləmə uğursuz oldu. Yenidən cəhd edin." +} diff --git a/src/translations/locales/bn.json b/src/translations/locales/bn.json index cd3a011..d275f8e 100644 --- a/src/translations/locales/bn.json +++ b/src/translations/locales/bn.json @@ -808,5 +808,10 @@ "نام دارو و دلیل مصرف را وارد نمایید...": "ওষুধের নাম এবং কারণ লিখুন...", "وضعیت سلامت جسمانی": "শারীরিক স্বাস্থ্যের অবস্থা", "توضیحات وضعیت جسمانی": "শারীরিক স্বাস্থ্যের বিবরণ", - "Name": "নাম" -} \ No newline at end of file + "Name": "নাম", + "upload_certificates": "নথিপত্র আপলোড করুন", + "add_another_document": "অন্য নথি যোগ করুন", + "max_files_reached": "সর্বোচ্চ ৪টি ফাইল আপলোড করা হয়েছে", + "remove_document": "নথি মুছুন", + "upload_failed": "আপলোড ব্যর্থ হয়েছে। আবার চেষ্টা করুন।" +} diff --git a/src/translations/locales/da.json b/src/translations/locales/da.json index 6ca63b0..ac26ef6 100644 --- a/src/translations/locales/da.json +++ b/src/translations/locales/da.json @@ -808,5 +808,10 @@ "نام دارو و دلیل مصرف را وارد نمایید...": "Indtast medicinnavn og årsag...", "وضعیت سلامت جسمانی": "Fysisk helbredstilstand", "توضیحات وضعیت جسمانی": "Beskrivelse af fysisk helbred", - "Name": "Navn" -} \ No newline at end of file + "Name": "Navn", + "upload_certificates": "Upload certifikater", + "add_another_document": "Tilføj et andet dokument", + "max_files_reached": "Maksimalt 4 filer uploadet", + "remove_document": "Fjern dokument", + "upload_failed": "Upload mislykkedes. Prøv igen." +} diff --git a/src/translations/locales/de.json b/src/translations/locales/de.json index 4f19576..b3ab795 100644 --- a/src/translations/locales/de.json +++ b/src/translations/locales/de.json @@ -808,5 +808,10 @@ "نام دارو و دلیل مصرف را وارد نمایید...": "Geben Sie den Medikamentennamen und den Grund ein...", "وضعیت سلامت جسمانی": "Körperlicher Gesundheitszustand", "توضیحات وضعیت جسمانی": "Beschreibung des körperlichen Zustands", - "Name": "Name" -} \ No newline at end of file + "Name": "Name", + "upload_certificates": "Zertifikate hochladen", + "add_another_document": "Ein weiteres Dokument hinzufügen", + "max_files_reached": "Maximal 4 Dateien hochgeladen", + "remove_document": "Dokument entfernen", + "upload_failed": "Upload fehlgeschlagen. Bitte versuchen Sie es erneut." +} diff --git a/src/translations/locales/en.json b/src/translations/locales/en.json index a34416a..15abd91 100644 --- a/src/translations/locales/en.json +++ b/src/translations/locales/en.json @@ -248,8 +248,8 @@ "General Health:": "General Health:", "German": "German", "Germany": "Germany", - "Get Advisor": "Get Advisor", - "Get an advisor": "Get an advisor", + "Get Advisor": "Talk to Advisor", + "Get an advisor": "Need Marriage Guidance?", "Glasser 5 Needs Test": "Glasser 5 Needs Test", "Go back": "Go back", "Good": "Good", @@ -406,7 +406,7 @@ "Not a good personal fit": "Not a good personal fit", "Not committed": "Not committed", "Not important": "Not important", - "Not sure what to do next? Our psychology section is here to guide you at every step.": "Not sure what to do next? Our psychology section is here to guide you at every step.", + "Not sure what to do next? Our psychology section is here to guide you at every step.": "Unsure about your next step? Our expert counselors are here to help you make confident decisions.", "Nothing is shared without your consent.": "Nothing is shared without your consent.", "Number of Children": "Number of Children", "Number of Siblings": "Number of Siblings", @@ -690,10 +690,10 @@ "Very religious and committed": "Very religious and committed", "View Contact": "View Contact", "View Contact Details": "View Contact Details", - "View More Details": "View More Details", + "View More Details": "View Full Profile", "View Profile": "View Profile", "View contact number": "View contact number", - "View more details": "View more details", + "View more details": "View Full Profile", "View profile": "View profile", "Watch Video": "Watch Video", "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", @@ -741,6 +741,7 @@ "Your details are only used for the matching process.": "Your details are only used for the matching process.", "Your information is kept strictly confidential.": "Your information is kept strictly confidential.", "Your privacy and safety are our top priorities. We are committed to keeping your information secure and giving you full control throughout the process.": "Your privacy and safety are our top priorities. We are committed to keeping your information secure and giving you full control throughout the process.", + "Your request has been sent. Once the gentleman reviews your request, you will be notified.": "Your request has been sent. Once the gentleman reviews your request, you will be notified.", "Your request has been sent. Once the lady reviews your request, you will be notified.": "Your request has been sent. Once the lady reviews your request, you will be notified.", "Your request was rejected": "Your request was rejected", "Your request was rejected by the lady. You will be introduced to other candidates in the future.": "Your request was rejected by the lady. You will be introduced to other candidates in the future.", @@ -845,5 +846,10 @@ "Medication Name and Reason for Use": "Medication Name and Reason for Use", "Enter medication name and reason for use...": "Enter medication name and reason for use...", "Enter a valid phone number with country code.": "Enter a valid phone number with country code.", - "Name": "Name" -} \ No newline at end of file + "Name": "Name", + "upload_certificates": "Upload certificates", + "add_another_document": "Add another document", + "max_files_reached": "Maximum of 4 files uploaded", + "remove_document": "Remove document", + "upload_failed": "Upload failed. Please try again." +} diff --git a/src/translations/locales/es.json b/src/translations/locales/es.json index cdbd522..69c328d 100644 --- a/src/translations/locales/es.json +++ b/src/translations/locales/es.json @@ -808,5 +808,10 @@ "نام دارو و دلیل مصرف را وارد نمایید...": "Ingrese el nombre del medicamento y el motivo...", "وضعیت سلامت جسمانی": "Estado de salud física", "توضیحات وضعیت جسمانی": "Descripción de la salud física", - "Name": "Nombre" -} \ No newline at end of file + "Name": "Nombre", + "upload_certificates": "Subir certificados", + "add_another_document": "Agregar otro documento", + "max_files_reached": "Máximo de 4 archivos subidos", + "remove_document": "Eliminar documento", + "upload_failed": "Error al subir. Por favor, inténtelo de nuevo." +} diff --git a/src/translations/locales/fa.json b/src/translations/locales/fa.json index bb11a05..4748c18 100644 --- a/src/translations/locales/fa.json +++ b/src/translations/locales/fa.json @@ -248,8 +248,8 @@ "General Health:": "سلامت عمومی:", "German": "آلمانی", "Germany": "آلمان", - "Get Advisor": "دریافت مشاور", - "Get an advisor": "دریافت مشاور", + "Get Advisor": "گفتگو با مشاور", + "Get an advisor": "نیاز به راهنمایی دارید؟", "Glasser 5 Needs Test": "تست ۵ نیاز گلاسر", "Go back": "بازگشت", "Good": "خوب", @@ -406,7 +406,7 @@ "Not a good personal fit": "تناسب شخصی کافی نبود", "Not committed": "مقید نیستم", "Not important": "این معیار برایم اهمیت زیادی ندارد.", - "Not sure what to do next? Our psychology section is here to guide you at every step.": "نمی‌دانید قدم بعدی چیست؟ بخش روانشناسی ما در هر مرحله شما را راهنمایی می‌کند.", + "Not sure what to do next? Our psychology section is here to guide you at every step.": "در تصمیم‌گیری یا ادامه مسیر مردد هستید؟ مشاوران متخصص ما در تمام مراحل آشنایی همراه شما هستند.", "Nothing is shared without your consent.": "هیچ چیز بدون رضایت شما به اشتراک گذاشته نمی‌شود.", "Number of Children": "تعداد فرزندان", "Number of Siblings": "تعداد خواهر و برادر", @@ -690,10 +690,10 @@ "Very religious and committed": "بسیار مذهبی و مقید", "View Contact": "مشاهده تماس", "View Contact Details": "مشاهده شماره تماس", - "View More Details": "مشاهده جزئیات بیشتر", + "View More Details": "مشاهده مشخصات کامل و ادامه فرایند", "View Profile": "مشاهده پروفایل", "View contact number": "مشاهده شماره تماس", - "View more details": "مشاهده جزئیات بیشتر", + "View more details": "مشاهده مشخصات کامل و ادامه فرایند", "View profile": "مشاهده پروفایل", "Watch Video": "مشاهده ویدیو", "We are in the acquaintance/proposal process and nothing is finalized yet": "در مسیر آشنایی و خواستگاری هستیم و هنوز هیچی نهایی نشده", @@ -740,7 +740,7 @@ "Your Personality Traits": "ویژگی‌های شخصیتی خودتان", "Your details are only used for the matching process.": "اطلاعات شما فقط برای فرآیند تطبیق‌دهی استفاده می‌شود.", "Your information is kept strictly confidential.": "اطلاعات شما کاملاً محرمانه نگهداری می‌شود.", - "Your privacy and safety are our top priorities. We are committed to keeping your information secure and giving you full control throughout the process.": "حفظ حریم خصوصی و امنیت شما بالاترین اولویت ماست. ما متعهد به حفظ امنیت اطلاعات شما و اعطای کنترل کامل به شما در طول فرآیند هستیم.", + "Your request has been sent. Once the gentleman reviews your request, you will be notified.": "درخواست شما ارسال شد. پس از بررسی درخواست شما توسط آقا، به شما اطلاع‌رسانی خواهد شد.", "Your request has been sent. Once the lady reviews your request, you will be notified.": "درخواست شما ارسال شد. پس از بررسی درخواست شما توسط خانم، به شما اطلاع‌رسانی خواهد شد.", "Your request was rejected": "درخواست شما رد شد", "Your request was rejected by the lady. You will be introduced to other candidates in the future.": "درخواست شما توسط خانم رد شد. به شما مورد های دیگه ای در اینده معرفی خواهد شد.", @@ -856,5 +856,10 @@ "نام دارو و دلیل مصرف را وارد نمایید...": "نام دارو و دلیل مصرف را وارد نمایید...", "وضعیت سلامت جسمانی": "وضعیت سلامت جسمانی", "توضیحات وضعیت جسمانی": "توضیحات وضعیت جسمانی", - "Name": "نام" -} \ No newline at end of file + "Name": "نام", + "upload_certificates": "بارگذاری مدارک", + "add_another_document": "افزودن مدرک جدید", + "max_files_reached": "حداکثر ۴ فایل بارگذاری شده است", + "remove_document": "حذف مدرک", + "upload_failed": "بارگذاری با خطا مواجه شد. لطفاً دوباره تلاش کنید." +} diff --git a/src/translations/locales/fr.json b/src/translations/locales/fr.json index 282e887..e3f2496 100644 --- a/src/translations/locales/fr.json +++ b/src/translations/locales/fr.json @@ -808,5 +808,10 @@ "نام دارو و دلیل مصرف را وارد نمایید...": "Entrez le nom du médicament et le motif...", "وضعیت سلامت جسمانی": "État de santé physique", "توضیحات وضعیت جسمانی": "Description de la santé physique", - "Name": "Nom" -} \ No newline at end of file + "Name": "Nom", + "upload_certificates": "Télécharger les certificats", + "add_another_document": "Ajouter un autre document", + "max_files_reached": "Maximum de 4 fichiers téléchargés", + "remove_document": "Supprimer le document", + "upload_failed": "Échec du téléchargement. Veuillez réessayer." +} diff --git a/src/translations/locales/gu.json b/src/translations/locales/gu.json index fe99916..dce5747 100644 --- a/src/translations/locales/gu.json +++ b/src/translations/locales/gu.json @@ -808,5 +808,10 @@ "نام دارو و دلیل مصرف را وارد نمایید...": "દવાનું નામ અને કારણ દાખલ કરો...", "وضعیت سلامت جسمانی": "શારીરિક સ્વાસ્થ્ય સ્થિતિ", "توضیحات وضعیت جسمانی": "શારીરિક સ્વાસ્થ્ય વર્ણન", - "Name": "નામ" -} \ No newline at end of file + "Name": "નામ", + "upload_certificates": "પ્રમાણપત્રો અપલોડ કરો", + "add_another_document": "બીજો દસ્તાવેજ ઉમેરો", + "max_files_reached": "મહત્તમ 4 ફાઇલો અપલોડ કરવામાં આવી છે", + "remove_document": "દસ્તાવેજ દૂર કરો", + "upload_failed": "અપલોડ નિષ્ફળ ગયું. કૃપા કરીને ફરી પ્રયાસ કરો." +} diff --git a/src/translations/locales/ha.json b/src/translations/locales/ha.json index 986c768..e7c1c8b 100644 --- a/src/translations/locales/ha.json +++ b/src/translations/locales/ha.json @@ -808,5 +808,10 @@ "نام دارو و دلیل مصرف را وارد نمایید...": "Shigar da sunan magani da dalilin sha...", "وضعیت سلامت جسمانی": "Yanayin Lafiyar Jiki", "توضیحات وضعیت جسمانی": "Bayanin Lafiyar Jiki", - "Name": "Suna" -} \ No newline at end of file + "Name": "Suna", + "upload_certificates": "Loda takardun shaida", + "add_another_document": "Ƙara wata takarda", + "max_files_reached": "An loda matsakaicin fayiloli 4", + "remove_document": "Cire takarda", + "upload_failed": "Loda ya faskara. Da fatan za a sake gwadawa." +} diff --git a/src/translations/locales/he.json b/src/translations/locales/he.json index 0fbd3ef..50b80e2 100644 --- a/src/translations/locales/he.json +++ b/src/translations/locales/he.json @@ -355,5 +355,10 @@ "Job Title": "תואר התפקיד", "Employment Status": "מצב תעסוקתי", "Your Hobbies and Main Interests": "התחביבים ותחומי העניין העיקריים שלך", - "View more details": "הצג פרטים נוספים" -} \ No newline at end of file + "View more details": "הצג פרטים נוספים", + "upload_certificates": "העלאת תעודות ומסמכים", + "add_another_document": "הוסף מסמך נוסף", + "max_files_reached": "הועלו מקסימום 4 קבצים", + "remove_document": "הסר מסמך", + "upload_failed": "ההעלאה נכשלה. אנא נסה שוב." +} diff --git a/src/translations/locales/hi.json b/src/translations/locales/hi.json index edfc79b..ca2ea00 100644 --- a/src/translations/locales/hi.json +++ b/src/translations/locales/hi.json @@ -808,5 +808,10 @@ "نام دارو و دلیل مصرف را وارد نمایید...": "दवा का नाम और उपयोग का कारण दर्ज करें...", "وضعیت سلامت جسمانی": "शारीरिक स्वास्थ्य की स्थिति", "توضیحات وضعیت جسمانی": "शारीरिक स्वास्थ्य का विवरण", - "Name": "नाम" -} \ No newline at end of file + "Name": "नाम", + "upload_certificates": "प्रमाणपत्र अपलोड करें", + "add_another_document": "दूसरा दस्तावेज़ जोड़ें", + "max_files_reached": "अधिकतम 4 फ़ाइलें अपलोड की गईं", + "remove_document": "दस्तावेज़ हटाएं", + "upload_failed": "अपलोड विफल रहा। कृपया पुन: प्रयास करें।" +} diff --git a/src/translations/locales/id.json b/src/translations/locales/id.json index 15bb748..598c76b 100644 --- a/src/translations/locales/id.json +++ b/src/translations/locales/id.json @@ -355,5 +355,10 @@ "Job Title": "Jabatan / Pekerjaan", "Employment Status": "Status Pekerjaan", "Your Hobbies and Main Interests": "Hobi dan Minat Utama Anda", - "View more details": "Lihat detail selengkapnya" -} \ No newline at end of file + "View more details": "Lihat detail selengkapnya", + "upload_certificates": "Unggah sertifikat", + "add_another_document": "Tambah dokumen lain", + "max_files_reached": "Maksimum 4 file diunggah", + "remove_document": "Hapus dokumen", + "upload_failed": "Pengunggahan gagal. Silakan coba lagi." +} diff --git a/src/translations/locales/ks.json b/src/translations/locales/ks.json index ff18a96..cdf402a 100644 --- a/src/translations/locales/ks.json +++ b/src/translations/locales/ks.json @@ -355,5 +355,10 @@ "Job Title": "کٲم ہُنٛد ناو", "Employment Status": "مُلازمتٕچ حالت", "Your Hobbies and Main Interests": "تُہنٛدؠ شۄق تہٕ اَہَم دِلچسپی", - "View more details": "مزید تفصیل وُچھِو" -} \ No newline at end of file + "View more details": "مزید تفصیل وُچھِو", + "upload_certificates": "دستاویز اپ لوڈ کریو", + "add_another_document": "بیٛاکھ دستاویز جمع کریو", + "max_files_reached": "زیاد کھوتہ زیاد ۴ فائل اپ لوڈ کرنہ آمژٕ", + "remove_document": "دستاویز ہٹاوِیو", + "upload_failed": "اپ لوڈ ناکام۔ مہربانی کرتھ دوبارہ کوشش کریو۔" +} diff --git a/src/translations/locales/pt.json b/src/translations/locales/pt.json index 5ed018f..2530412 100644 --- a/src/translations/locales/pt.json +++ b/src/translations/locales/pt.json @@ -355,5 +355,10 @@ "Job Title": "Cargo / Título profissional", "Employment Status": "Situação profissional", "Your Hobbies and Main Interests": "Seus hobbies e principais interesses", - "View more details": "Ver mais detalhes" -} \ No newline at end of file + "View more details": "Ver mais detalhes", + "upload_certificates": "Enviar certificados", + "add_another_document": "Adicionar outro documento", + "max_files_reached": "Máximo de 4 arquivos enviados", + "remove_document": "Remover documento", + "upload_failed": "Falha no envio. Por favor, tente novamente." +} diff --git a/src/translations/locales/ru.json b/src/translations/locales/ru.json index 268671b..5241ead 100644 --- a/src/translations/locales/ru.json +++ b/src/translations/locales/ru.json @@ -812,5 +812,10 @@ "نام دارو و دلیل مصرف را وارد نمایید...": "Введите название препарата и причину приема...", "وضعیت سلامت جسمانی": "Физическое состояние здоровья", "توضیحات وضعیت جسمانی": "Описание физического состояния", - "Name": "Имя" -} \ No newline at end of file + "Name": "Имя", + "upload_certificates": "Загрузить сертификаты", + "add_another_document": "Добавить еще один документ", + "max_files_reached": "Загружено максимум 4 файла", + "remove_document": "Удалить документ", + "upload_failed": "Ошибка загрузки. Пожалуйста, повторите попытку." +} diff --git a/src/translations/locales/sw.json b/src/translations/locales/sw.json index 224c42e..5190fa7 100644 --- a/src/translations/locales/sw.json +++ b/src/translations/locales/sw.json @@ -355,5 +355,10 @@ "Job Title": "Wadhifa wa Kazi", "Employment Status": "Hali ya Ajira", "Your Hobbies and Main Interests": "Mambo unayopenda na Maslahi Kuu", - "View more details": "Angalia maelezo zaidi" -} \ No newline at end of file + "View more details": "Angalia maelezo zaidi", + "upload_certificates": "Pakia vyeti", + "add_another_document": "Ongeza hati nyingine", + "max_files_reached": "Upeo wa faili 4 zimepakiwa", + "remove_document": "Ondoa hati", + "upload_failed": "Upakiaji umeshindwa. Tafadhali jaribu tena." +} diff --git a/src/translations/locales/tg.json b/src/translations/locales/tg.json index b0dbf5e..2dfac2f 100644 --- a/src/translations/locales/tg.json +++ b/src/translations/locales/tg.json @@ -355,5 +355,10 @@ "Job Title": "Унвони вазифа", "Employment Status": "Вазъи шуғл", "Your Hobbies and Main Interests": "Машғулиятҳо ва манфиатҳои асосии шумо", - "View more details": "Дидани тафсилоти бештар" -} \ No newline at end of file + "View more details": "Дидани тафсилоти бештар", + "upload_certificates": "Боргузории ҳуҷҷатҳо", + "add_another_document": "Ҳуҷҷати дигар илова кунед", + "max_files_reached": "Ҳадди аксар 4 файл боргузорӣ шудааст", + "remove_document": "Ҳуҷҷатро нест кунед", + "upload_failed": "Боргузорӣ ноком шуд. Лутфан бори дигар кӯшиш кунед." +} diff --git a/src/translations/locales/tr.json b/src/translations/locales/tr.json index 0f5be69..402c6ca 100644 --- a/src/translations/locales/tr.json +++ b/src/translations/locales/tr.json @@ -355,5 +355,10 @@ "Job Title": "Meslek / Unvan", "Employment Status": "Çalışma Durumu", "Your Hobbies and Main Interests": "Hobileriniz ve Temel İlgi Alanlarınız", - "View more details": "Daha fazla ayrıntı gör" -} \ No newline at end of file + "View more details": "Daha fazla ayrıntı gör", + "upload_certificates": "Belgeleri yükle", + "add_another_document": "Başka bir belge ekle", + "max_files_reached": "Maksimum 4 dosya yüklendi", + "remove_document": "Belgeyi kaldır", + "upload_failed": "Yükleme başarısız oldu. Lütfen tekrar deneyin." +} diff --git a/src/translations/locales/ul.json b/src/translations/locales/ul.json index 3ba710b..76f347d 100644 --- a/src/translations/locales/ul.json +++ b/src/translations/locales/ul.json @@ -355,5 +355,10 @@ "Job Title": "Job Title", "Employment Status": "Employment Status", "Your Hobbies and Main Interests": "Your Hobbies and Main Interests", - "View more details": "View more details" -} \ No newline at end of file + "View more details": "View more details", + "upload_certificates": "دستاویزات اپ لوڈ کریں", + "add_another_document": "مزید دستاویز شامل کریں", + "max_files_reached": "زیادہ سے زیادہ 4 فائلیں اپ لوڈ کی گئیں", + "remove_document": "دستاویز ہٹائیں", + "upload_failed": "اپ لوڈ ناکام ہو گیا۔ براہ کرم دوبارہ کوشش کریں۔" +} diff --git a/src/translations/locales/ur.json b/src/translations/locales/ur.json index 7677d0e..eb53bcc 100644 --- a/src/translations/locales/ur.json +++ b/src/translations/locales/ur.json @@ -355,5 +355,10 @@ "Job Title": "عہدہ / ملازمت کا عنوان", "Employment Status": "ملازمت کی صورتحال", "Your Hobbies and Main Interests": "آپ کے مشاغل اور اہم دلچسپیاں", - "View more details": "مزید تفصیلات دیکھیں" -} \ No newline at end of file + "View more details": "مزید تفصیلات دیکھیں", + "upload_certificates": "دستاویزات اپ لوڈ کریں", + "add_another_document": "مزید دستاویز شامل کریں", + "max_files_reached": "زیادہ سے زیادہ 4 فائلیں اپ لوڈ کی گئیں", + "remove_document": "دستاویز ہٹائیں", + "upload_failed": "اپ لوڈ ناکام ہو گیا۔ براہ کرم دوبارہ کوشش کریں۔" +} diff --git a/src/translations/locales/uz.json b/src/translations/locales/uz.json index 9c69367..7bd6271 100644 --- a/src/translations/locales/uz.json +++ b/src/translations/locales/uz.json @@ -355,5 +355,10 @@ "Job Title": "Kasb / Lavozim", "Employment Status": "Bandlik holati", "Your Hobbies and Main Interests": "Qiziqishlaringiz va asosiy mashgʻulotlaringiz", - "View more details": "Batafsil maʼlumotni koʻrish" -} \ No newline at end of file + "View more details": "Batafsil maʼlumotni koʻrish", + "upload_certificates": "Hujjatlarni yuklash", + "add_another_document": "Boshqa hujjat qo'shish", + "max_files_reached": "Maksimal 4 ta fayl yuklandi", + "remove_document": "Hujjatni o'chirish", + "upload_failed": "Yuklab bo'lmadi. Qayta urinib ko'ring." +} diff --git a/src/translations/locales/zh.json b/src/translations/locales/zh.json index 9cd097a..667bd97 100644 --- a/src/translations/locales/zh.json +++ b/src/translations/locales/zh.json @@ -808,5 +808,10 @@ "نام دارو و دلیل مصرف را وارد نمایید...": "输入药物名称和使用原因...", "وضعیت سلامت جسمانی": "身体健康状况", "توضیحات وضعیت جسمانی": "身体健康说明", - "Name": "姓名" -} \ No newline at end of file + "Name": "姓名", + "upload_certificates": "上传证书和文件", + "add_another_document": "添加其他文件", + "max_files_reached": "最多已上传 4 个文件", + "remove_document": "删除文件", + "upload_failed": "上传失败,请重试。" +}