From b029b89ef536bd2ef6a17529919cc7eac61b37ca Mon Sep 17 00:00:00 2001 From: Nadav Nir Date: Thu, 21 May 2026 12:02:34 +0200 Subject: [PATCH 1/3] fix(#548): derive match status from live opportunities instead of stale statusMatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit volunteer.statusMatch is never updated by the BE when opportunity-volunteer status changes, so the header always showed the original DB value. Now the header queries the same opportunities cache used by VolunteerOpportunities and derives the badge from actual opportunity statuses — no extra request since React Query deduplicates the key. Co-Authored-By: Claude Sonnet 4.6 --- .../volunteer/VolunteerHeader.tsx | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/components/Dashboard/Profile/sections/ProfileHeader/volunteer/VolunteerHeader.tsx b/src/components/Dashboard/Profile/sections/ProfileHeader/volunteer/VolunteerHeader.tsx index 29898cb0..70c6ad9b 100644 --- a/src/components/Dashboard/Profile/sections/ProfileHeader/volunteer/VolunteerHeader.tsx +++ b/src/components/Dashboard/Profile/sections/ProfileHeader/volunteer/VolunteerHeader.tsx @@ -1,8 +1,15 @@ "use client"; -import { defaultAvatarVolunteerProfile, EMPTY_PLACEHOLDER_VALUE } from "@/config/constants"; +import { apiPathVolunteer, cacheTTL, defaultAvatarVolunteerProfile, EMPTY_PLACEHOLDER_VALUE } from "@/config/constants"; +import { useGetQuery } from "@/hooks/useGetQuery"; import { formatDateTime, getImageUrl } from "@/utils"; import { CheckCircleIcon } from "@phosphor-icons/react"; -import { ApiVolunteerGet, VolunteerStateEngagementType } from "need4deed-sdk"; +import { + ApiOpportunityVolunteerGet, + ApiVolunteerGet, + OpportunityVolunteerStatusType, + VolunteerStateEngagementType, + VolunteerStateMatchType, +} from "need4deed-sdk"; import Image from "next/image"; import { useTranslation } from "react-i18next"; import { @@ -18,6 +25,21 @@ import { ChangeEngagementStatusDialog } from "./ChangeEngagementStatusDialog"; import { createEngagementLabelMap, createMatchLabelMap } from "./constants"; import { useEngagementStatusDialog } from "./useEngagementStatusDialog"; +function deriveMatchStatus(opportunities: ApiOpportunityVolunteerGet[]): VolunteerStateMatchType { + if (!opportunities.length) return VolunteerStateMatchType.NO_MATCHES; + const statuses = opportunities.map((o) => o.status); + if (statuses.some((s) => s === OpportunityVolunteerStatusType.MATCHED || s === OpportunityVolunteerStatusType.ACTIVE)) { + return VolunteerStateMatchType.MATCHED; + } + if (statuses.some((s) => s === OpportunityVolunteerStatusType.PENDING)) { + return VolunteerStateMatchType.PENDING_MATCH; + } + if (statuses.some((s) => s === OpportunityVolunteerStatusType.PAST)) { + return VolunteerStateMatchType.PAST; + } + return VolunteerStateMatchType.NO_MATCHES; +} + type Props = { volunteer: ApiVolunteerGet; }; @@ -26,6 +48,15 @@ export const VolunteerHeader = ({ volunteer }: Props) => { const { t } = useTranslation(); const dialog = useEngagementStatusDialog(volunteer); + const { data: opportunitiesData } = useGetQuery({ + queryKey: ["volunteer-opportunities", String(volunteer.id)], + apiPath: `${apiPathVolunteer}/${volunteer.id}/opportunity-linked`, + staleTime: cacheTTL, + enabled: !!volunteer.id, + }); + + const matchStatus = deriveMatchStatus(opportunitiesData ?? []); + const engagementLabelMap = createEngagementLabelMap(t); const matchLabelMap = createMatchLabelMap(t); const volunteerTypeLabelMap = createVolunteerTypeLabelMap(t); @@ -68,8 +99,8 @@ export const VolunteerHeader = ({ volunteer }: Props) => { Date: Thu, 21 May 2026 12:04:04 +0200 Subject: [PATCH 2/3] fix: use statusMatch fallback while opportunities loading to avoid flash Co-Authored-By: Claude Sonnet 4.6 --- .../sections/ProfileHeader/volunteer/VolunteerHeader.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/Dashboard/Profile/sections/ProfileHeader/volunteer/VolunteerHeader.tsx b/src/components/Dashboard/Profile/sections/ProfileHeader/volunteer/VolunteerHeader.tsx index 70c6ad9b..84f9f3a0 100644 --- a/src/components/Dashboard/Profile/sections/ProfileHeader/volunteer/VolunteerHeader.tsx +++ b/src/components/Dashboard/Profile/sections/ProfileHeader/volunteer/VolunteerHeader.tsx @@ -55,7 +55,13 @@ export const VolunteerHeader = ({ volunteer }: Props) => { enabled: !!volunteer.id, }); - const matchStatus = deriveMatchStatus(opportunitiesData ?? []); + // While opportunities are loading, fall back to the stored value to avoid + // a flash of "No matches". Once loaded, derive from live opportunity statuses + // so the badge stays in sync without requiring the BE to update statusMatch. + const matchStatus = + opportunitiesData !== undefined + ? deriveMatchStatus(opportunitiesData) + : (volunteer.statusMatch ?? VolunteerStateMatchType.NO_MATCHES); const engagementLabelMap = createEngagementLabelMap(t); const matchLabelMap = createMatchLabelMap(t); From 193985b8cbb7f622962efea18d78a823a8caacaa Mon Sep 17 00:00:00 2001 From: Nadav Nir Date: Thu, 21 May 2026 12:05:53 +0200 Subject: [PATCH 3/3] simplify: always derive match status, remove statusMatch fallback Co-Authored-By: Claude Sonnet 4.6 --- .../sections/ProfileHeader/volunteer/VolunteerHeader.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/Dashboard/Profile/sections/ProfileHeader/volunteer/VolunteerHeader.tsx b/src/components/Dashboard/Profile/sections/ProfileHeader/volunteer/VolunteerHeader.tsx index 84f9f3a0..70c6ad9b 100644 --- a/src/components/Dashboard/Profile/sections/ProfileHeader/volunteer/VolunteerHeader.tsx +++ b/src/components/Dashboard/Profile/sections/ProfileHeader/volunteer/VolunteerHeader.tsx @@ -55,13 +55,7 @@ export const VolunteerHeader = ({ volunteer }: Props) => { enabled: !!volunteer.id, }); - // While opportunities are loading, fall back to the stored value to avoid - // a flash of "No matches". Once loaded, derive from live opportunity statuses - // so the badge stays in sync without requiring the BE to update statusMatch. - const matchStatus = - opportunitiesData !== undefined - ? deriveMatchStatus(opportunitiesData) - : (volunteer.statusMatch ?? VolunteerStateMatchType.NO_MATCHES); + const matchStatus = deriveMatchStatus(opportunitiesData ?? []); const engagementLabelMap = createEngagementLabelMap(t); const matchLabelMap = createMatchLabelMap(t);