From bcaa4dac4d012b6ff491fd3bb2d8e76c51325bd4 Mon Sep 17 00:00:00 2001 From: ghorbani Date: Thu, 6 Aug 2026 11:45:17 +0330 Subject: [PATCH] feat: implement candidate contact flow with status management, outcome sheets, and redirects --- .agents/AGENTS.md | 9 ++++++ src/app/candidate-contact/page.tsx | 38 ++++++++++++++-------- src/app/finding-match/page.tsx | 21 ++++++++++-- src/app/new-match/page.tsx | 16 +++++++++ src/app/new-match/profile/page.tsx | 20 +++++++++++- src/app/request-accepted/page.tsx | 52 +++++++++++++++++++++--------- src/app/request-sent/page.tsx | 51 ++++++++++++++++++++--------- 7 files changed, 160 insertions(+), 47 deletions(-) diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md index d816885..8b3db16 100644 --- a/.agents/AGENTS.md +++ b/.agents/AGENTS.md @@ -10,3 +10,12 @@ This version has breaking changes — APIs, conventions, and file structure may - Registration form structure data is kept separately in `src/data/questions/`. - CRITICAL AUTOMATION RULE: Whenever any new static text, data, field, component label, tooltip, help description, key, or text item is added or updated in the project, its translation key and values MUST automatically be added/updated in ALL 24 locale JSON files (`src/translations/locales/` & `src/data/questions/`). - QUESTION HELP & TOOLTIPS RULE: All question help texts, tooltips, and modal descriptions must be stored with key-value entries in the respective locale JSON files, ensuring complete synchronization across all supported languages. + +# Logic and UI Language Separation Rule (Language-Agnostic Contract) + +- **Goal**: Create a language-agnostic contract between frontend and backend for field mappings and logic. +- **Strict Logic Isolation**: Program logic (e.g. conditional visibility, backend validations, validations, and step progressions) MUST NEVER depend on localized UI strings/labels (e.g., Farsi or Arabic text). +- **System Keys Usage**: All business logic and API payloads must rely exclusively on fixed English identifiers called **System Keys** (e.g., `email`, `phone_number`, `marital_status`). +- **Frontend Role**: When submitting data, the frontend must send values associated only with these fixed System Keys. The localized view the user interacts with (Persian, Arabic, Turkish, etc.) is strictly for presentation. +- **Backend Role**: All DB operations, validation rules, condition checks, and calculations must be run exclusively on the System Keys. The backend must never receive, process, or validate display labels. +- **i18n Mapping**: Localization/localization translation is handled solely in the frontend presentation layer via translation keys/files (e.g., `en.json`, `fa.json`, `ar.json`) mapping back to the same System Keys. diff --git a/src/app/candidate-contact/page.tsx b/src/app/candidate-contact/page.tsx index abee2b3..c04c925 100644 --- a/src/app/candidate-contact/page.tsx +++ b/src/app/candidate-contact/page.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, 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"; @@ -48,6 +48,11 @@ export default function CandidateContactPage() { } }, [profile, router, locale]); + const isRedirecting = useMemo(() => { + if (!profile) return false; + return getSubmitPath(profile) !== "/candidate-contact"; + }, [profile]); + const caseId = profile?.active_case?.case_id; const caseStatus = profile?.active_case?.status; const isFemale = profile?.gender === "female"; @@ -69,7 +74,7 @@ export default function CandidateContactPage() { }); }; - if (isProfileLoading) { + if (isProfileLoading || isRedirecting) { return ( <> @@ -237,7 +242,7 @@ export default function CandidateContactPage() { )} -
+
{/* Advisor section */} {/* Profile Locked banner */} -
-
- lock -

- {t.requestSent.profileLocked} -

+
+
+
+ lock +

+ {t.requestSent.profileLocked} +

+
diff --git a/src/app/finding-match/page.tsx b/src/app/finding-match/page.tsx index 8d60aa7..dadb079 100644 --- a/src/app/finding-match/page.tsx +++ b/src/app/finding-match/page.tsx @@ -3,7 +3,8 @@ import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; +import { DotsLoader } from "@/components/Componentes/button"; import { FaLock, FaPen } from "react-icons/fa6"; import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card"; import PageHeader from "@/components/Componentes/page-header"; @@ -22,7 +23,7 @@ const advisorAvatars = [ export default function FindingMatchPage() { const router = useRouter(); const { dictionary: t, locale } = useI18n(); - const { data: profile } = useMarriageProfileQuery({ + const { data: profile, isLoading } = useMarriageProfileQuery({ refetchInterval: 3000, }); @@ -36,6 +37,22 @@ export default function FindingMatchPage() { } }, [profile, locale, router]); + const isRedirecting = useMemo(() => { + if (!profile) return false; + return getSubmitPath(profile) !== "/finding-match"; + }, [profile]); + + if (isLoading || isRedirecting) { + return ( + <> + +
+ +
+ + ); + } + const copy = t.findingMatch; const matchImageSrc = "/assets/images/Group 1597880466.svg"; diff --git a/src/app/new-match/page.tsx b/src/app/new-match/page.tsx index 48764fe..da84e8b 100644 --- a/src/app/new-match/page.tsx +++ b/src/app/new-match/page.tsx @@ -271,6 +271,22 @@ export default function NewMatchPage() { } }, [profile, locale, router]); + const isRedirecting = useMemo(() => { + if (!profile) return false; + return getSubmitPath(profile) !== "/new-match"; + }, [profile]); + + if (isLoading || isRedirecting) { + return ( + <> + +
+ +
+ + ); + } + const matchSummary = profile?.match_summary ?? null; const matchDisplay = useMatchSummaryDisplay(matchSummary); const pairedFields = [matchDisplay.age, matchDisplay.city].filter( diff --git a/src/app/new-match/profile/page.tsx b/src/app/new-match/profile/page.tsx index 88f3917..5568e29 100644 --- a/src/app/new-match/profile/page.tsx +++ b/src/app/new-match/profile/page.tsx @@ -341,7 +341,25 @@ export default function NewMatchProfilePage() { ? "/assets/images/female_avatar.svg" : "/assets/images/Group 1597880481.png"; - if (isLoading || !profile) { + const isRedirecting = useMemo(() => { + if (!profile) return false; + const targetPath = getSubmitPath(profile); + const caseStatus = profile?.active_case?.status; + const isProfileMatched = profile?.status === "matched"; + const isViewingAllowed = + caseStatus === "payment_done" || + caseStatus === "contacted" || + caseStatus === "finalized" || + isProfileMatched; + + return ( + targetPath !== "/new-match" && + targetPath !== "/request-sent" && + !isViewingAllowed + ); + }, [profile]); + + if (isLoading || !profile || isRedirecting) { return ; } diff --git a/src/app/request-accepted/page.tsx b/src/app/request-accepted/page.tsx index 23c9266..53fddc8 100644 --- a/src/app/request-accepted/page.tsx +++ b/src/app/request-accepted/page.tsx @@ -3,7 +3,7 @@ import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card"; import { DotsLoader } from "@/components/Componentes/button"; import CallResultSheet from "@/components/Componentes/call-result-sheet"; @@ -177,7 +177,7 @@ export default function RequestAcceptedPage() { const [isSubscriptionSheetOpen, setIsSubscriptionSheetOpen] = useState(false); const [isOutcomeSheetOpen, setIsOutcomeSheetOpen] = useState(false); const profileHref = localizePath("/new-match/profile", locale); - const { data: profile } = useMarriageProfileQuery({ + const { data: profile, isLoading } = useMarriageProfileQuery({ refetchInterval: 3000, }); @@ -191,6 +191,11 @@ export default function RequestAcceptedPage() { } }, [profile, router, locale]); + const isRedirecting = useMemo(() => { + if (!profile) return false; + return getSubmitPath(profile) !== "/request-accepted"; + }, [profile]); + const isFemaleProfile = profile?.gender === "female"; const caseId = profile?.active_case?.case_id; const caseStatus = profile?.active_case?.status; @@ -208,6 +213,18 @@ export default function RequestAcceptedPage() { const contactInfoQuery = useMarriageContactInfoQuery(caseId, { enabled: false, }); + + if (isLoading || isRedirecting) { + return ( + <> + +
+ +
+ + ); + } + const titleText = isFemaleProfile ? t.requestAccepted.titleFemale : caseStatus === "payment_done" || caseStatus === "contacted" @@ -494,7 +511,7 @@ export default function RequestAcceptedPage() { )}
-
+
{/* Advisor section */} -
-
- lock - -

- {t.requestAccepted.profileLocked} -

+
+
+
+ lock + +

+ {t.requestAccepted.profileLocked} +

+
diff --git a/src/app/request-sent/page.tsx b/src/app/request-sent/page.tsx index bb68772..45eb242 100644 --- a/src/app/request-sent/page.tsx +++ b/src/app/request-sent/page.tsx @@ -3,7 +3,8 @@ import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; +import { DotsLoader } from "@/components/Componentes/button"; import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card"; import PageHeader from "@/components/Componentes/page-header"; import { PageBackground } from "@/components/Componentes/page-background"; @@ -21,7 +22,7 @@ const advisorAvatars = [ export default function RequestSentPage() { const router = useRouter(); const { dictionary: t, locale } = useI18n(); - const { data: profile } = useMarriageProfileQuery({ + const { data: profile, isLoading } = useMarriageProfileQuery({ refetchInterval: 3000, }); @@ -35,6 +36,22 @@ export default function RequestSentPage() { } }, [profile, locale, router]); + const isRedirecting = useMemo(() => { + if (!profile) return false; + return getSubmitPath(profile) !== "/request-sent"; + }, [profile]); + + if (isLoading || isRedirecting) { + return ( + <> + +
+ +
+ + ); + } + const copy = t.findingMatch; const requestSentCopy = (t as any).requestSent ?? { title: "درخواست ارسال شد", @@ -85,7 +102,7 @@ export default function RequestSentPage() {
-
+
-
-
- lock - -

- {requestSentCopy.profileLocked} -

+
+
+
+ lock +

+ {requestSentCopy.profileLocked} +

+