From 2f17a0f36710d3a9aa089333f0f05ab6e0cfe9c0 Mon Sep 17 00:00:00 2001 From: ghorbani Date: Wed, 5 Aug 2026 19:39:55 +0330 Subject: [PATCH] feat: implement marriage case contact and outcome management flows with related UI and translations --- src/app/candidate-contact/page.tsx | 158 +++++++++---- .../[slug]/question-detail-client.tsx | 122 ++++------ src/app/request-accepted/page.tsx | 8 +- .../Componentes/outcome-selection-sheet.tsx | 217 ++++++++++++++++++ .../Componentes/question-answer-storage.tsx | 23 +- .../Componentes/question-renderer.tsx | 6 +- src/components/Componentes/question-text.tsx | 10 +- src/data/question-data.ts | 20 +- src/hooks/marriage/types.ts | 2 + src/hooks/marriage/use-contact-status.ts | 44 ++++ src/hooks/marriage/use-section-data.ts | 31 ++- src/lib/get-submit-path.ts | 4 +- src/translations/locales/en.json | 9 +- src/translations/locales/fa.json | 9 +- 14 files changed, 507 insertions(+), 156 deletions(-) create mode 100644 src/components/Componentes/outcome-selection-sheet.tsx diff --git a/src/app/candidate-contact/page.tsx b/src/app/candidate-contact/page.tsx index c477409..67f8274 100644 --- a/src/app/candidate-contact/page.tsx +++ b/src/app/candidate-contact/page.tsx @@ -6,11 +6,15 @@ import { useEffect, useState } from "react"; import { DotsLoader } from "@/components/Componentes/button"; import CallResultSheet from "@/components/Componentes/call-result-sheet"; import DismissReasonSheet from "@/components/Componentes/dismiss-reason-sheet"; +import OutcomeSelectionSheet from "@/components/Componentes/outcome-selection-sheet"; import PageHeader from "@/components/Componentes/page-header"; import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card"; import { PageBackground } from "@/components/Componentes/page-background"; import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; -import { useSubmitMarriageContactStatusMutation } from "@/hooks/marriage/use-contact-status"; +import { + useSubmitMarriageContactStatusMutation, + useSubmitMarriageOutcomeMutation, +} from "@/hooks/marriage/use-contact-status"; import { getSubmitPath } from "@/lib/get-submit-path"; import { localizePath } from "@/translations/config"; import { useI18n } from "@/translations/provider"; @@ -24,6 +28,7 @@ const advisorAvatars = [ export default function CandidateContactPage() { const { dictionary: t, locale } = useI18n(); const router = useRouter(); + const [isOutcomeSheetOpen, setIsOutcomeSheetOpen] = useState(false); const [isCallResultSheetOpen, setIsCallResultSheetOpen] = useState(false); const [isDismissReasonSheetOpen, setIsDismissReasonSheetOpen] = useState(false); @@ -43,15 +48,16 @@ export default function CandidateContactPage() { }, [profile, router, locale]); const caseId = profile?.active_case?.case_id; + const caseStatus = profile?.active_case?.status; const isFemale = profile?.gender === "female"; + const isFinalized = caseStatus === "finalized" || profile?.status === "matched"; const contactStatusMutation = useSubmitMarriageContactStatusMutation( caseId ?? "", - { - onSuccess: () => { - router.push(localizePath("/finding-match", locale)); - }, - }, + ); + + const outcomeMutation = useSubmitMarriageOutcomeMutation( + caseId ?? "", ); const handleNoContactReport = async () => { @@ -75,20 +81,43 @@ export default function CandidateContactPage() { // If female, render the beautiful, customized layout matching the design if (isFemale) { + if (isFinalized) { + return ( + <> + +
+
+
+ 🎉 +
+

+ {t.candidateContact.congratsTitle} +

+

+ {t.candidateContact.congratsMessage} +

+
+
+ + ); + } + return ( <> - {isCallResultSheetOpen ? ( - setIsCallResultSheetOpen(false)} - onOtherReasonsClick={() => setIsDismissReasonSheetOpen(true)} - onSubmit={async (value) => { - if (caseId) { - await contactStatusMutation.mutateAsync({ - action: "contacted", - custom_note: value, - }); + {isOutcomeSheetOpen ? ( + setIsOutcomeSheetOpen(false)} + onSubmit={async (status) => { + if (status === "success") { + if (caseId) { + await outcomeMutation.mutateAsync({ + status: "success", + }); + } + } else { + setIsDismissReasonSheetOpen(true); } }} /> @@ -99,8 +128,8 @@ export default function CandidateContactPage() { onClose={() => setIsDismissReasonSheetOpen(false)} onSubmit={async (value) => { if (caseId) { - await contactStatusMutation.mutateAsync({ - action: "contacted", + await outcomeMutation.mutateAsync({ + status: "failure", custom_note: value, }); } @@ -129,36 +158,73 @@ export default function CandidateContactPage() { {t.candidateContact.title} - {/* Action Buttons Row */} -
- + {caseStatus === "contacted" ? ( + <> + {/* Outcome instructions and button */} +
+

+ {t.candidateContact.outcomeGuide} +

+
- -
+ + + ) : ( + <> + {/* Action Buttons Row */} +
+ - {/* Red warning box */} -
-

- {t.candidateContact.contactWarning} -

-
+ +
+ + {/* Red warning box */} +
+

+ {t.candidateContact.contactWarning} +

+
+ + )}
diff --git a/src/app/questions-list/[slug]/question-detail-client.tsx b/src/app/questions-list/[slug]/question-detail-client.tsx index 57a255c..49ca667 100644 --- a/src/app/questions-list/[slug]/question-detail-client.tsx +++ b/src/app/questions-list/[slug]/question-detail-client.tsx @@ -172,9 +172,9 @@ function QuestionFlowWrapper({ const dynamicQuestions = useMemo(() => { // Find employment status question by title or by choices length (10) const employmentQuestionIndex = visibleQuestions.findIndex((q) => { + const enTitle = q.englishTitle || q.title; return ( - q.title === "Employment Status" || - q.title === "وضعیت اشتغال" || + enTitle === "Employment Status" || (q.type === "dropdown" && q.extras.options?.length === 10) ); }); @@ -191,10 +191,8 @@ function QuestionFlowWrapper({ // Find Parents' Survival Status question index and check selected index const survivalStatusQuestionIndex = visibleQuestions.findIndex((q) => { - return ( - q.title === "Parents' Survival Status" || - q.title === "وضعیت حیات والدین" - ); + const enTitle = q.englishTitle || q.title; + return enTitle === "Parents' Survival Status"; }); let survivalSelectedOptionIndex = -1; @@ -209,9 +207,8 @@ function QuestionFlowWrapper({ // Find Parents' Marital Status question index and check selected index const maritalStatusQuestionIndex = visibleQuestions.findIndex((q) => { - return ( - q.title === "Parents' Marital Status" || q.title === "وضعیت تأهل والدین" - ); + const enTitle = q.englishTitle || q.title; + return enTitle === "Parents' Marital Status"; }); let isCircumstancesSelected = false; @@ -227,9 +224,8 @@ function QuestionFlowWrapper({ // Find Current Marital Status question index and check selected index const currentMaritalQuestionIndex = visibleQuestions.findIndex((q) => { - return ( - q.title === "Current Marital Status" || q.title === "وضعیت تأهل فعلی" - ); + const enTitle = q.englishTitle || q.title; + return enTitle === "Current Marital Status"; }); let maritalSelectedIndex = -1; @@ -244,10 +240,8 @@ function QuestionFlowWrapper({ // Find Children and Guardianship Status question index const custodyQuestionIndex = visibleQuestions.findIndex((q) => { - return ( - q.title === "Children and Guardianship Status" || - q.title === "وضعیت فرزند و تکفل" - ); + const enTitle = q.englishTitle || q.title; + return enTitle === "Children and Guardianship Status"; }); let hasChildrenSelected = false; @@ -258,46 +252,41 @@ function QuestionFlowWrapper({ 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("فرزند دارم"), + const selectedIndices = ansList.map((val) => + custodyQuestion.extras.options?.indexOf(val), + ); + hasChildrenSelected = selectedIndices.some( + (idx) => idx === 1 || idx === 2, ); - hasAnyGuardianshipSelected = ansList.some( - (val) => - val.includes("Have children") || - val.includes("فرزند دارم") || - val.includes("under my guardianship") || - val.includes("تحت تکفل"), + hasAnyGuardianshipSelected = selectedIndices.some( + (idx) => idx === 2 || idx === 3, ); } } const filtered = visibleQuestions.filter((question, index) => { + const enTitle = question.englishTitle || question.title; // Check if this is one of the marital status/children questions if (currentMaritalQuestionIndex !== -1) { const isDuration = index === currentMaritalQuestionIndex + 1 || - question.title === "Previous Marriage Duration" || - question.title === "مدت ازدواج یا عقد قبلی"; + enTitle === "Previous Marriage Duration"; const isSeparation = index === currentMaritalQuestionIndex + 2 || - question.title === "Reason for Separation" || - question.title === "علت جدایی، در صورت وجود"; + enTitle === "Reason for Separation"; const isCustody = index === currentMaritalQuestionIndex + 3 || - question.title === "Children and Guardianship Status" || - question.title === "وضعیت فرزند و تکفل"; + enTitle === "Children and Guardianship Status"; const isChildrenCount = index === currentMaritalQuestionIndex + 4 || - question.title === "Number of Children" || - question.title === "تعداد فرزندان"; + enTitle === "Number of Children"; const isChildrenExplanation = index === currentMaritalQuestionIndex + 5 || - question.title === "Short Children/Guardianship Explanation" || - question.title === "توضیح کوتاه درباره شرایط فرزند یا تکفل"; + enTitle === "Short Children/Guardianship Explanation"; if (isDuration) { return [1, 2, 3].includes(maritalSelectedIndex); @@ -321,19 +310,13 @@ function QuestionFlowWrapper({ // 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 === "عنوان شغلی"; + index === employmentQuestionIndex + 1 || enTitle === "Job Title"; const isWorkLocation = - index === employmentQuestionIndex + 2 || - question.title === "Work Location" || - question.title === "محل فعالیت"; + index === employmentQuestionIndex + 2 || enTitle === "Work Location"; const isMonthlyIncome = - index === employmentQuestionIndex + 3 || - question.title === "Monthly Income" || - question.title === "میزان درآمد ماهانه"; + index === employmentQuestionIndex + 3 || enTitle === "Monthly Income"; if (isJobTitle || isWorkLocation || isMonthlyIncome) { // If no employment status is selected yet, hide them by default @@ -371,9 +354,7 @@ function QuestionFlowWrapper({ // Check Parents' Survival Status to decide if Parents' Marital Status is visible if (survivalStatusQuestionIndex !== -1) { - const isParentsMaritalStatus = - question.title === "Parents' Marital Status" || - question.title === "وضعیت تأهل والدین"; + const isParentsMaritalStatus = enTitle === "Parents' Marital Status"; if (isParentsMaritalStatus) { return survivalSelectedOptionIndex === 0; @@ -404,10 +385,8 @@ function QuestionFlowWrapper({ }); return filtered.map((question) => { - if ( - question.title === "Short Family Description" || - question.title === "توضیح کوتاه درباره خانواده" - ) { + const enTitle = question.englishTitle || question.title; + if (enTitle === "Short Family Description") { return { ...question, required: isCircumstancesSelected, @@ -415,30 +394,17 @@ function QuestionFlowWrapper({ } if ( - question.title === "Previous Marriage Duration" || - question.title === "مدت ازدواج یا عقد قبلی" || - question.title === "Number of Children" || - question.title === "تعداد فرزندان" || - question.title === "Short Children/Guardianship Explanation" || - question.title === "توضیح کوتاه درباره شرایط فرزند یا تکفل" || - question.title === "Additional details about family responsibility" || - question.title === "توضیحات تکمیلی درباره مسئولیت خانوادگی" || - question.title === "Do the supported individual(s) live with you?" || - question.title === "آیا فرد یا افراد تحت حمایت با شما زندگی میکنند؟" || - question.title === "What is the custody status of your child(ren)?" || - question.title === "وضعیت حضانت فرزند یا فرزندان شما چگونه است؟" || - question.title === + enTitle === "Previous Marriage Duration" || + enTitle === "Number of Children" || + enTitle === "Short Children/Guardianship Explanation" || + enTitle === "Additional details about family responsibility" || + enTitle === "Do the supported individual(s) live with you?" || + enTitle === "What is the custody status of your child(ren)?" || + enTitle === "Does the custody, visitation, or relocation schedule impact your residence or immigration?" || - question.title === - "آیا برنامه حضانت، ملاقات یا جابهجایی فرزند بر محل زندگی یا امکان مهاجرت شما تأثیر میگذارد؟" || - question.title === - "What is the payment or receipt status of child support?" || - question.title === - "وضعیت پرداخت یا دریافت نفقه و حمایت مالی فرزند چگونه است؟" || - question.title === - "Acceptance of necessary communication between future spouse and the other parent" || - question.title === "پذیرش ارتباط ضروری همسر آینده با والد دیگرِ فرزند" || - question.title === "پذیرش ارتباط ضروری همسر آینده با والد دیگر فرزند" + enTitle === "What is the payment or receipt status of child support?" || + enTitle === + "Acceptance of necessary communication between future spouse and the other parent" ) { return { ...question, @@ -470,7 +436,9 @@ function QuestionFlowWrapper({ if (originalIndex === -1) { // Spread-copied questions lose reference equality; fall back to title originalIndex = visibleQuestions.findIndex( - (q) => q.title === question.title, + (q) => + (q.englishTitle || q.title) === + (question.englishTitle || question.title), ); } const answer = getAnswerValue(question, originalIndex); @@ -478,9 +446,9 @@ function QuestionFlowWrapper({ let isAnswered = hasAnswer; if (hasAnswer) { - const isEmailQuestion = - question.title.toLowerCase().includes("email") || - question.title.includes("ایمیل"); + const isEmailQuestion = (question.englishTitle || question.title) + .toLowerCase() + .includes("email"); if (isEmailQuestion) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; isAnswered = emailRegex.test(String(answer).trim()); diff --git a/src/app/request-accepted/page.tsx b/src/app/request-accepted/page.tsx index 4ba05f7..b0da952 100644 --- a/src/app/request-accepted/page.tsx +++ b/src/app/request-accepted/page.tsx @@ -180,7 +180,7 @@ export default function RequestAcceptedPage() { }); const titleText = isFemaleProfile ? t.requestAccepted.titleFemale - : caseStatus === "payment_done" + : (caseStatus === "payment_done" || caseStatus === "contacted") ? t.requestAccepted.titleMalePaymentDone : t.requestAccepted.titleMalePaymentPending; const primaryActionText = isFemaleProfile @@ -188,7 +188,7 @@ export default function RequestAcceptedPage() { : t.requestAccepted.primaryMale; const secondaryActionText = isFemaleProfile ? t.requestAccepted.secondaryFemale - : caseStatus === "payment_done" + : (caseStatus === "payment_done" || caseStatus === "contacted") ? t.requestAccepted.secondaryMalePaymentDone : t.requestAccepted.secondaryMalePaymentPending; const contactInfoPhoneItems = getContactInfoPhoneItems( @@ -206,7 +206,7 @@ export default function RequestAcceptedPage() { return; } - if (caseStatus === "payment_done") { + if (caseStatus === "payment_done" || caseStatus === "contacted") { if (!caseId) { return; } @@ -337,7 +337,7 @@ export default function RequestAcceptedPage() {

- {t.requestAccepted.description} + {isFemaleProfile ? t.candidateContact.title : t.requestAccepted.description}

diff --git a/src/components/Componentes/outcome-selection-sheet.tsx b/src/components/Componentes/outcome-selection-sheet.tsx new file mode 100644 index 0000000..4420973 --- /dev/null +++ b/src/components/Componentes/outcome-selection-sheet.tsx @@ -0,0 +1,217 @@ +"use client"; + +import type { HTMLAttributes } from "react"; +import { useEffect, useId, useState } from "react"; +import { useI18n } from "@/translations/provider"; + +const EXIT_ANIMATION_MS = 220; + +export type OutcomeSelectionSheetProps = Omit< + HTMLAttributes, + "title" | "onSubmit" +> & { + closeOnOutside?: boolean; + onClose?: () => void; + onSubmit?: (status: "success" | "failure") => void; +}; + +export function OutcomeSelectionSheet({ + closeOnOutside = true, + onClose, + onSubmit, + className, + ...props +}: OutcomeSelectionSheetProps) { + const { dictionary: t } = useI18n(); + const groupId = useId(); + const [isVisible, setIsVisible] = useState(true); + const [isEntering, setIsEntering] = useState(true); + const [isClosing, setIsClosing] = useState(false); + const [selectedStatus, setSelectedStatus] = useState<"success" | "failure">( + "success", + ); + + const closeSheet = () => { + if (isClosing) { + return; + } + setIsClosing(true); + }; + + useEffect(() => { + const frameId = window.requestAnimationFrame(() => { + setIsEntering(false); + }); + + return () => { + window.cancelAnimationFrame(frameId); + }; + }, []); + + useEffect(() => { + if (!isVisible) { + return; + } + + const previousBodyOverflow = document.body.style.overflow; + const previousHtmlOverflow = document.documentElement.style.overflow; + + document.body.style.overflow = "hidden"; + document.documentElement.style.overflow = "hidden"; + + return () => { + document.body.style.overflow = previousBodyOverflow; + document.documentElement.style.overflow = previousHtmlOverflow; + }; + }, [isVisible]); + + useEffect(() => { + if (!isClosing) { + return; + } + + const timeoutId = window.setTimeout(() => { + setIsVisible(false); + onClose?.(); + }, EXIT_ANIMATION_MS); + + return () => { + window.clearTimeout(timeoutId); + }; + }, [isClosing, onClose]); + + if (!isVisible) { + return null; + } + + const options = [ + { value: "success" as const, label: t.candidateContact.marriageSuccess }, + { value: "failure" as const, label: t.candidateContact.marriageFailure }, + ]; + + return ( +
{ + if (closeOnOutside && event.target === event.currentTarget) { + closeSheet(); + } + }} + onKeyDown={(event) => { + if ( + closeOnOutside && + event.target === event.currentTarget && + (event.key === "Escape" || event.key === "Enter" || event.key === " ") + ) { + event.preventDefault(); + closeSheet(); + } + }} + > +
+
+

+ {t.candidateContact.outcomeTitle} +

+ +
+ + {t.candidateContact.outcomeTitle} + + +
+ {options.map((option) => { + const checked = selectedStatus === option.value; + + return ( + + ); + })} +
+
+ +
+ + + +
+
+
+
+ ); +} + +export default OutcomeSelectionSheet; diff --git a/src/components/Componentes/question-answer-storage.tsx b/src/components/Componentes/question-answer-storage.tsx index a5bc6a2..0916110 100644 --- a/src/components/Componentes/question-answer-storage.tsx +++ b/src/components/Componentes/question-answer-storage.tsx @@ -1,5 +1,6 @@ "use client"; +import { useQueryClient } from "@tanstack/react-query"; import { createContext, type ReactNode, @@ -13,19 +14,18 @@ import { import type { QuestionField } from "@/data/question-data"; import { toBackendSlug } from "@/data/section-slug-map"; import { pathParam } from "@/hooks/marriage/path-param"; +import { marriageQueryKeys } from "@/hooks/marriage/query-keys"; import type { MarriageField, MarriageFieldValue, MarriagePhoneFieldValue, UpdateMarriageSectionDataPayload, } from "@/hooks/marriage/types"; +import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { useMarriageSectionDataQuery, useUpdateMarriageSectionDataMutation, } from "@/hooks/marriage/use-section-data"; -import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; -import { useQueryClient } from "@tanstack/react-query"; -import { marriageQueryKeys } from "@/hooks/marriage/query-keys"; import { getApiRequestUrl } from "@/lib/http"; const STORAGE_VERSION = 1; @@ -96,7 +96,7 @@ function getQuestionFieldKey(question: QuestionField, questionIndex: number) { question.originalIndex !== undefined ? question.originalIndex : questionIndex; - return `q${index + 1}_${slugifyQuestionTitle(question.title)}`; + return `q${index + 1}_${slugifyQuestionTitle(question.englishTitle || question.title)}`; } export function getQuestionAnswersStorageKey(slug: string) { @@ -160,6 +160,7 @@ function createQuestionField( label: question.title, type: question.type, value, + private: question.private, }; } @@ -320,7 +321,8 @@ export function QuestionAnswersProvider({ const canEdit = profile?.can_edit_profile !== false; const backendSlug = useMemo(() => toBackendSlug(slug), [slug]); - const { data: serverSectionData, isLoading: isLoadingData } = useMarriageSectionDataQuery(backendSlug); + const { data: serverSectionData, isLoading: isLoadingData } = + useMarriageSectionDataQuery(backendSlug); useEffect(() => { questionsRef.current = questions; @@ -527,7 +529,7 @@ export function QuestionAnswersProvider({ window.removeEventListener("pagehide", flushWithKeepalive); flushWithKeepalive(); }; - }, [canEdit]); + }, [canEdit, queryClient.invalidateQueries]); const contextValue = useMemo( () => ({ @@ -538,7 +540,14 @@ export function QuestionAnswersProvider({ isLoading: isLoadingData, setAnswerValue, }), - [flushAnswers, getAnswerValue, hasPendingSync, isSaving, isLoadingData, setAnswerValue], + [ + flushAnswers, + getAnswerValue, + hasPendingSync, + isSaving, + isLoadingData, + setAnswerValue, + ], ); return ( diff --git a/src/components/Componentes/question-renderer.tsx b/src/components/Componentes/question-renderer.tsx index 8934dc5..9b37965 100644 --- a/src/components/Componentes/question-renderer.tsx +++ b/src/components/Componentes/question-renderer.tsx @@ -30,11 +30,9 @@ export function QuestionRenderer({ dobQuestion, dobQuestionIndex, }: QuestionRendererProps) { + const enTitle = (question.englishTitle || question.title).toLowerCase(); const compactTextHeight = - question.title.toLowerCase().includes("email") || - question.title.includes("ایمیل") || - question.title.toLowerCase().includes("duration") || - question.title.includes("مدت") + enTitle.includes("email") || enTitle.includes("duration") ? "h-[54px]" : undefined; diff --git a/src/components/Componentes/question-text.tsx b/src/components/Componentes/question-text.tsx index 63ed34f..68613b8 100644 --- a/src/components/Componentes/question-text.tsx +++ b/src/components/Componentes/question-text.tsx @@ -26,9 +26,9 @@ export default function QuestionText({ const isMuted = value === "-"; const stringValue = String(value ?? "").trim(); - const isEmailQuestion = - question.title.toLowerCase().includes("email") || - question.title.includes("ایمیل"); + const isEmailQuestion = (question.englishTitle || question.title) + .toLowerCase() + .includes("email"); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const isValidEmail = !isEmailQuestion || emailRegex.test(stringValue); @@ -40,8 +40,8 @@ export default function QuestionText({ : isValidEmail && stringValue.length > 0; const isMarjaQuestion = - question.title === "Marja' al-Taqlid (Religious Authority)" || - question.title === "مرجع تقلید"; + (question.englishTitle || question.title) === + "Marja' al-Taqlid (Religious Authority)"; if (isEmailQuestion) { return ( diff --git a/src/data/question-data.ts b/src/data/question-data.ts index a7f3db3..e6bbf57 100644 --- a/src/data/question-data.ts +++ b/src/data/question-data.ts @@ -52,6 +52,7 @@ export type QuestionField = { showGuardianNotice?: boolean; originalSlug?: string; originalIndex?: number; + englishTitle?: string; }; export type QuestionListItem = { @@ -118,7 +119,24 @@ export function getQuestionListItems(locale: Locale = defaultLocale) { questionsByLocale[defaultLocale] ?? questionsByLocale.en ?? []; - const items = rawItems.map(mapQuestionListItem); + + const enItems = questionsByLocale.en || []; + const mappedRawItems = rawItems.map((item) => { + const enItem = enItems.find((e) => e.slug === item.slug); + if (!enItem) return item; + return { + ...item, + questions: item.questions.map((q, idx) => { + const enQ = enItem.questions[idx]; + return { + ...q, + englishTitle: enQ ? enQ.title : q.title, + }; + }), + }; + }); + + const items = mappedRawItems.map(mapQuestionListItem); const fbIndex = items.findIndex((item) => item.slug === "family_background"); const mhIndex = items.findIndex( diff --git a/src/hooks/marriage/types.ts b/src/hooks/marriage/types.ts index 413aa91..5942d9f 100644 --- a/src/hooks/marriage/types.ts +++ b/src/hooks/marriage/types.ts @@ -23,6 +23,7 @@ export type MarriageCaseStatus = | "payment_pending" | "payment_done" | "finalized" + | "contacted" | "dismissed"; export type MarriagePhoneFieldValue = { @@ -43,6 +44,7 @@ export type MarriageField = { label: string; type: string; value: MarriageFieldValue; + private?: boolean; }; export type MarriageRecommendedPlan = { diff --git a/src/hooks/marriage/use-contact-status.ts b/src/hooks/marriage/use-contact-status.ts index 0872af6..855d75a 100644 --- a/src/hooks/marriage/use-contact-status.ts +++ b/src/hooks/marriage/use-contact-status.ts @@ -48,3 +48,47 @@ export function useSubmitMarriageContactStatusMutation( }, }); } + +export type SubmitMarriageOutcomePayload = { + status: "success" | "failure"; + reason_code?: string; + custom_note?: string; +}; + +export async function submitMarriageOutcome( + caseId: CaseId, + payload: SubmitMarriageOutcomePayload, +) { + const { data } = await http.post( + `/api/marriage/cases/${pathParam(caseId)}/outcome/`, + payload, + ); + + return data; +} + +export function useSubmitMarriageOutcomeMutation( + caseId: CaseId, + options?: MutationOptions< + MarriageCaseActionResponse, + SubmitMarriageOutcomePayload + >, +) { + const queryClient = useQueryClient(); + + return useMutation({ + ...options, + mutationFn: (payload) => submitMarriageOutcome(caseId, payload), + onSuccess: async (data, variables, onMutateResult, context) => { + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: marriageQueryKeys.profile(), + }), + queryClient.invalidateQueries({ + queryKey: marriageQueryKeys.contactInfo(caseId), + }), + ]); + await options?.onSuccess?.(data, variables, onMutateResult, context); + }, + }); +} diff --git a/src/hooks/marriage/use-section-data.ts b/src/hooks/marriage/use-section-data.ts index 081c937..412f98b 100644 --- a/src/hooks/marriage/use-section-data.ts +++ b/src/hooks/marriage/use-section-data.ts @@ -30,8 +30,13 @@ function slugifyQuestionTitle(title: string) { return slug || `field_${hashString(title)}`; } -function getQuestionFieldKey(title: string, index: number) { - return `q${index + 1}_${slugifyQuestionTitle(title)}`; +function getQuestionFieldKey( + title: string, + index: number, + englishTitle?: string, +) { + const finalTitle = englishTitle || title; + return `q${index + 1}_${slugifyQuestionTitle(finalTitle)}`; } function hasQuestionAnswerValue(value: unknown) { @@ -108,18 +113,18 @@ export async function updateMarriageSectionData( const fbKeys = new Set([ ...fbQuestionsEn.map((q, idx) => - getQuestionFieldKey(q.title, q.originalIndex ?? idx), + getQuestionFieldKey(q.title, q.originalIndex ?? idx, q.englishTitle), ), ...fbQuestionsFa.map((q, idx) => - getQuestionFieldKey(q.title, q.originalIndex ?? idx), + getQuestionFieldKey(q.title, q.originalIndex ?? idx, q.englishTitle), ), ]); const mhKeys = new Set([ ...mhQuestionsEn.map((q, idx) => - getQuestionFieldKey(q.title, q.originalIndex ?? idx), + getQuestionFieldKey(q.title, q.originalIndex ?? idx, q.englishTitle), ), ...mhQuestionsFa.map((q, idx) => - getQuestionFieldKey(q.title, q.originalIndex ?? idx), + getQuestionFieldKey(q.title, q.originalIndex ?? idx, q.englishTitle), ), ]); @@ -138,10 +143,15 @@ export async function updateMarriageSectionData( const fbCurrentStep = fbQuestionsEn.filter((q, idx) => { if (!q.required || q.logic?.dependsOn) return false; - const keyEn = getQuestionFieldKey(q.title, q.originalIndex ?? idx); + const keyEn = getQuestionFieldKey( + q.title, + q.originalIndex ?? idx, + q.englishTitle, + ); const keyFa = getQuestionFieldKey( fbQuestionsFa[idx]?.title || q.title, fbQuestionsFa[idx]?.originalIndex ?? q.originalIndex ?? idx, + fbQuestionsFa[idx]?.englishTitle || q.englishTitle, ); const field = fbFields.find((f) => f.key === keyEn || f.key === keyFa); return field && hasQuestionAnswerValue(field.value); @@ -149,10 +159,15 @@ export async function updateMarriageSectionData( const mhCurrentStep = mhQuestionsEn.filter((q, idx) => { if (!q.required || q.logic?.dependsOn) return false; - const keyEn = getQuestionFieldKey(q.title, q.originalIndex ?? idx); + const keyEn = getQuestionFieldKey( + q.title, + q.originalIndex ?? idx, + q.englishTitle, + ); const keyFa = getQuestionFieldKey( mhQuestionsFa[idx]?.title || q.title, mhQuestionsFa[idx]?.originalIndex ?? q.originalIndex ?? idx, + mhQuestionsFa[idx]?.englishTitle || q.englishTitle, ); const field = mhFields.find((f) => f.key === keyEn || f.key === keyFa); return field && hasQuestionAnswerValue(field.value); diff --git a/src/lib/get-submit-path.ts b/src/lib/get-submit-path.ts index 2bf261e..474bb42 100644 --- a/src/lib/get-submit-path.ts +++ b/src/lib/get-submit-path.ts @@ -18,7 +18,7 @@ export function getSubmitPath(profile: MarriageProfileResponse | undefined) { const myAction = activeCase.my_action; const isFemale = profile.gender === "female"; - if (caseStatus === "payment_done" || caseStatus === "finalized") { + if (caseStatus === "payment_done" || caseStatus === "finalized" || caseStatus === "contacted") { if (isFemale) { return "/candidate-contact"; } @@ -60,7 +60,7 @@ export function getSubmitPath(profile: MarriageProfileResponse | undefined) { } if (profile.status === "matched") { - return "/candidate-contact"; + return "/request-accepted"; } if (profile.status === "in_case") { diff --git a/src/translations/locales/en.json b/src/translations/locales/en.json index 3aef786..1af97ec 100644 --- a/src/translations/locales/en.json +++ b/src/translations/locales/en.json @@ -168,7 +168,14 @@ "contacted": "contacted", "noContactYet": "no contact yet ?", "afterTwoDays": "(after 2 days)", - "contactWarning": "If they don't contact you within 2 days, please inform us." + "contactWarning": "If they don't contact you within 2 days, please inform us.", + "submitOutcome": "Submit Final Outcome", + "outcomeGuide": "Please report the final outcome of the proposal and communication to the system.", + "outcomeTitle": "Declare Proposal Outcome", + "marriageSuccess": "Agreed to marry (Success)", + "marriageFailure": "Did not agree (No fit)", + "congratsTitle": "Congratulations! 🎉", + "congratsMessage": "Wishing you a lifetime of love, joy, and happiness. Your profile has been successfully closed." }, "sheets": { "informationSheet": "Information sheet", diff --git a/src/translations/locales/fa.json b/src/translations/locales/fa.json index 36d2c2f..4163b70 100644 --- a/src/translations/locales/fa.json +++ b/src/translations/locales/fa.json @@ -168,7 +168,14 @@ "contacted": "تماس گرفته شد", "noContactYet": "هنوز تماس نگرفته؟", "afterTwoDays": "(بعد از ۲ روز)", - "contactWarning": "اگر ظرف ۲ روز با شما تماس نگرفتند، لطفاً به ما اطلاع دهید." + "contactWarning": "اگر ظرف ۲ روز با شما تماس نگرفتند، لطفاً به ما اطلاع دهید.", + "submitOutcome": "اعلام نتیجه نهایی خواستگاری و ارتباط", + "outcomeGuide": "لطفاً نتیجه نهایی خواستگاری و ارتباط خود را به سیستم اعلام کنید تا وضعیت پرونده شما بروزرسانی شود.", + "outcomeTitle": "اعلام نتیجه نهایی خواستگاری", + "marriageSuccess": "توافق برای ازدواج (اعلام موفقیت)", + "marriageFailure": "عدم توافق (رد کیس)", + "congratsTitle": "پیوندتان مبارک! 🎉", + "congratsMessage": "با آرزوی شادمانی، خوشبختی و سلامتی برای ادامه زندگی مشترک و زیبای شما. پرونده کاربری شما با موفقیت بسته شد." }, "sheets": { "informationSheet": "پنل اطلاعات",