From b0d6884b355809128c297f0b14fc65f3f7aedb24 Mon Sep 17 00:00:00 2001 From: Cy-fox Date: Mon, 18 May 2026 11:04:08 +0100 Subject: [PATCH 1/3] fix: render appointmentDistrict next to postcode and drop from PATCH payload appointmentDistrict is server-calculated from the postcode and should never be sent by the client. The display now reads the localized district title directly from the API response and renders it as ", " in a single row. The separate district field is removed from both the display and edit views, and the field is dropped from the PATCH payload type and form schema. Changes: - useUpdateOpportunityAccompanyingDetails: remove appointmentDistrict from payload type - accompanyingDetailsSchema: remove appointmentDistrict field - helpers: remove appointmentDistrict from getInitialFormValues - AccompanyingDetailsEdit: remove district Controller and its three props - AccompanyingDetails: compute postcodeDisplay from opportunity.accompanyingDetails using localized Option title with de fallback; drop appointmentDistrict from onSubmit - AccompanyingDetailsDisplay: merge postcode and district into one row using postcodeDisplay prop --- .../AccompanyingDetails.tsx | 36 +++++++++---------- .../AccompanyingDetailsDisplay.tsx | 15 ++------ .../AccompanyingDetailsEdit.tsx | 25 ------------- .../accompanyingDetailsSchema.ts | 1 - .../sections/AccompanyingDetails/helpers.ts | 5 +-- ...useUpdateOpportunityAccompanyingDetails.ts | 1 - 6 files changed, 22 insertions(+), 61 deletions(-) diff --git a/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetails.tsx b/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetails.tsx index 1dcd0705..95a54249 100644 --- a/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetails.tsx +++ b/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetails.tsx @@ -1,9 +1,9 @@ "use client"; -import { useApiDistricts, useApiLanguages } from "@/components/Dashboard/Profile/sections/VolunteerProfile/hooks"; +import { useApiLanguages } from "@/components/Dashboard/Profile/sections/VolunteerProfile/hooks"; import { useUpdateOpportunityAccompanyingDetails } from "@/hooks/useUpdateOpportunityAccompanyingDetails"; import { zodResolver } from "@hookform/resolvers/zod"; import { de, enUS } from "date-fns/locale"; -import { ApiOpportunityGet, LangPurpose } from "need4deed-sdk"; +import { ApiOpportunityAccompanyingDetails, ApiOpportunityGet, Lang, LangPurpose, Option } from "need4deed-sdk"; import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; @@ -31,7 +31,6 @@ export const AccompanyingDetails = forwardRef(functio useEditingChangeNotifier(isEditing, onEditingChange); const { data: apiLanguages } = useApiLanguages(); - const { data: apiDistricts } = useApiDistricts(); const showFullDetails = isAccompanyingType(opportunity.volunteerType); const minAppointmentDate = useMemo(() => getMinAppointmentDate(), []); @@ -54,14 +53,6 @@ export const AccompanyingDetails = forwardRef(functio }); const appointmentLanguageOptions = appointmentLanguageKeys.map((key) => appointmentLanguageKeyToLabel[key]); - const districtKeyToLabel: Record = {}; - const districtLabelToKey: Record = {}; - apiDistricts.forEach((district) => { - districtKeyToLabel[String(district.id)] = district.title; - districtLabelToKey[district.title] = String(district.id); - }); - const districtOptions = apiDistricts.map((district) => district.title); - const initialFormValues = getInitialFormValues(opportunity.accompanyingDetails); const methods = useForm({ @@ -94,7 +85,6 @@ export const AccompanyingDetails = forwardRef(functio accompanyingDetails: { appointmentAddress: values.appointmentAddress, appointmentPostcode: values.appointmentPostcode || undefined, - appointmentDistrict: values.appointmentDistrict || undefined, appointmentDate: values.appointmentDate ? values.appointmentDate.toISOString() : undefined, appointmentTime: values.appointmentTime || undefined, refugeeNumber: values.refugeeNumber, @@ -132,8 +122,17 @@ export const AccompanyingDetails = forwardRef(functio .filter((lang) => lang.purpose === LangPurpose.RECIPIENT) .map((lang) => lang.title) .join(", "); - const districtLabel = - districtKeyToLabel[formValues.appointmentDistrict || ""] || formValues.appointmentDistrict || ""; + + // appointmentDistrict is server-calculated from postcode — read from API response, never from form state + const rawDetails = opportunity.accompanyingDetails as ApiOpportunityAccompanyingDetails & { + appointmentPostcode?: string; + appointmentDistrict?: Option; + }; + const lang = i18n.language as Lang; + const districtTitle = + rawDetails?.appointmentDistrict?.title?.[lang] ?? rawDetails?.appointmentDistrict?.title?.de ?? ""; + const postcode = rawDetails?.appointmentPostcode || ""; + const postcodeDisplay = postcode && districtTitle ? `${postcode}, ${districtTitle}` : postcode; return ( @@ -147,16 +146,17 @@ export const AccompanyingDetails = forwardRef(functio appointmentLanguageOptions={appointmentLanguageOptions} appointmentLanguageKeyToLabel={appointmentLanguageKeyToLabel} appointmentLanguageLabelToKey={appointmentLanguageLabelToKey} - districtOptions={districtOptions} - districtKeyToLabel={districtKeyToLabel} - districtLabelToKey={districtLabelToKey} onCancel={handleCancel} onSubmit={handleSubmit(onSubmit)} isPending={isPending} minAppointmentDate={minAppointmentDate} /> ) : ( - + )} diff --git a/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetailsDisplay.tsx b/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetailsDisplay.tsx index f4b1671e..a7aa785d 100644 --- a/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetailsDisplay.tsx +++ b/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetailsDisplay.tsx @@ -9,10 +9,10 @@ import { DateFieldRow, Details } from "./styles"; type Props = { values: AccompanyingDetailsFormData; languageLabel: string; - districtLabel: string; + postcodeDisplay: string; }; -export const AccompanyingDetailsDisplay = ({ values, languageLabel, districtLabel }: Props) => { +export const AccompanyingDetailsDisplay = ({ values, languageLabel, postcodeDisplay }: Props) => { const { t } = useTranslation(); return ( @@ -29,19 +29,10 @@ export const AccompanyingDetailsDisplay = ({ values, languageLabel, districtLabe mode="display" type="text" label={t("dashboard.opportunityProfile.accompanyingDetails.appointmentPostcode")} - value={values.appointmentPostcode || ""} + value={postcodeDisplay} setValue={() => {}} /> - {}} - options={[]} - /> - {values.appointmentDate ? format(values.appointmentDate, "dd.MM.yyyy") : EMPTY_PLACEHOLDER_VALUE} diff --git a/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetailsEdit.tsx b/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetailsEdit.tsx index 51e3c8f3..c2c0299a 100644 --- a/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetailsEdit.tsx +++ b/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetailsEdit.tsx @@ -23,9 +23,6 @@ type Props = { appointmentLanguageOptions: string[]; appointmentLanguageKeyToLabel: Record; appointmentLanguageLabelToKey: Record; - districtOptions: string[]; - districtKeyToLabel: Record; - districtLabelToKey: Record; onCancel: () => void; onSubmit: () => void; isPending: boolean; @@ -40,9 +37,6 @@ export const AccompanyingDetailsEdit = ({ appointmentLanguageOptions, appointmentLanguageKeyToLabel, appointmentLanguageLabelToKey, - districtOptions, - districtKeyToLabel, - districtLabelToKey, onCancel, onSubmit, isPending, @@ -87,25 +81,6 @@ export const AccompanyingDetailsEdit = ({ )} /> - }) => ( - { - const label = Array.isArray(value) ? value[0] : value; - field.onChange(districtLabelToKey[label] || label); - }} - options={districtOptions} - errorMessage={errors.appointmentDistrict?.message} - /> - )} - /> - { // Converts a UTC HH:mm string from the API to the browser's local time for display only. // Do NOT use this for form state — it would cause the time to shift on every save. -export const formatTimeForDisplay = (time: string | undefined): string => - time ? utcHhmmToLocal(time) : ""; +export const formatTimeForDisplay = (time: string | undefined): string => (time ? utcHhmmToLocal(time) : ""); export const getInitialFormValues = ( details: ApiOpportunityAccompanyingDetails | undefined, @@ -46,8 +45,6 @@ export const getInitialFormValues = ( appointmentAddress: details?.appointmentAddress || "", appointmentPostcode: (details as ApiOpportunityAccompanyingDetails & { appointmentPostcode?: string })?.appointmentPostcode || "", - appointmentDistrict: - (details as ApiOpportunityAccompanyingDetails & { appointmentDistrict?: string })?.appointmentDistrict || "", appointmentDate: parseDate(details?.appointmentDate), appointmentTime: parseTime(details?.appointmentTime), refugeeNumber: details?.refugeeNumber || "", diff --git a/src/hooks/useUpdateOpportunityAccompanyingDetails.ts b/src/hooks/useUpdateOpportunityAccompanyingDetails.ts index 3f6f25c7..42fa6d60 100644 --- a/src/hooks/useUpdateOpportunityAccompanyingDetails.ts +++ b/src/hooks/useUpdateOpportunityAccompanyingDetails.ts @@ -6,7 +6,6 @@ export type OpportunityAccompanyingDetailsUpdateData = { accompanyingDetails: { appointmentAddress?: string; appointmentPostcode?: string; - appointmentDistrict?: string; appointmentDate?: string; appointmentTime?: string; refugeeNumber?: string; From 976c9d49128c0bbc0886dd5458d7d62d1bafb19a Mon Sep 17 00:00:00 2001 From: Cy-fox Date: Tue, 19 May 2026 16:53:43 +0100 Subject: [PATCH 2/3] fix: add loading placeholder to dashboard list pages Dashboard Volunteer, Opportunity, and Agent lists had no loading state, causing a layout shift and giving the impression of empty results while data was being fetched. Each list controller now shows a centred loading message with a min-height placeholder while isLoading is true. Changes: - Add shared DashboardListLoading component with min-height container - VolunteerListController: destructure isLoading, return placeholder while loading - OpportunityListController: same - AgentListController: same --- .../Dashboard/Agents/AgentListController.tsx | 5 ++++- .../OpportunityListController.tsx | 5 ++++- .../Volunteers/VolunteerListController.tsx | 7 +++++-- .../Dashboard/common/DashboardListLoading.tsx | 20 +++++++++++++++++++ 4 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 src/components/Dashboard/common/DashboardListLoading.tsx diff --git a/src/components/Dashboard/Agents/AgentListController.tsx b/src/components/Dashboard/Agents/AgentListController.tsx index 4f797a8a..cf3b55ff 100644 --- a/src/components/Dashboard/Agents/AgentListController.tsx +++ b/src/components/Dashboard/Agents/AgentListController.tsx @@ -1,6 +1,7 @@ import { ApiAgentGetList, ApiOptionLists, SortOrder } from "need4deed-sdk"; import { AgentCardList } from "./AgentCardList"; import { useEffect } from "react"; +import { DashboardListLoading } from "@/components/Dashboard/common/DashboardListLoading"; import { useGetQuery, usePageParam } from "@/hooks"; import { apiPathAgent, cacheTTL } from "@/config/constants"; import { serializeAgentFilters } from "./helpers"; @@ -29,7 +30,7 @@ export const AgentListController = ({ setNumOfAgents, sortOrder, isFiltersOpen, }), ); - const { data, count } = useGetQuery({ + const { data, count, isLoading } = useGetQuery({ queryKey: ["agents"], apiPath: `${apiPathAgent}/`, params: { @@ -48,6 +49,8 @@ export const AgentListController = ({ setNumOfAgents, sortOrder, isFiltersOpen, setNumOfAgents(count); }, [count, setNumOfAgents]); + if (isLoading) return ; + return ( ({ + const { data, count, isLoading } = useGetQuery({ queryKey: ["opportunities"], apiPath: `${apiPathOpportunity}/`, params: { @@ -85,6 +86,8 @@ export function OpportunityListController({ setNumOfOpps(count); }, [count, setNumOfOpps]); + if (isLoading) return ; + return ( ({ + const { data, count, isLoading } = useGetQuery({ queryKey: ["volunteers"], apiPath: apiPathVolunteer, params, @@ -55,6 +56,8 @@ export function VolunteerListController({ setNumOfVols(count); }, [count, setNumOfVols]); + if (isLoading) return ; + return ( + {t("dashboard.home.content.loading")} + + ); +} From b65db4fbebadd77c2d6f42362aba20542c3ed808 Mon Sep 17 00:00:00 2001 From: Nadav Nir Date: Wed, 20 May 2026 12:31:29 +0200 Subject: [PATCH 3/3] fix: remove unused LangPurpose import to pass lint Co-Authored-By: Claude Sonnet 4.6 --- .../sections/AccompanyingDetails/AccompanyingDetails.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetails.tsx b/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetails.tsx index df760590..2495f13d 100644 --- a/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetails.tsx +++ b/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetails.tsx @@ -3,7 +3,7 @@ import { useApiLanguages } from "@/components/Dashboard/Profile/sections/Volunte import { useUpdateOpportunityAccompanyingDetails } from "@/hooks/useUpdateOpportunityAccompanyingDetails"; import { zodResolver } from "@hookform/resolvers/zod"; import { de, enUS } from "date-fns/locale"; -import { ApiOpportunityAccompanyingDetails, ApiOpportunityGet, Lang, LangPurpose, Option, TranslatedIntoType } from "need4deed-sdk"; +import { ApiOpportunityAccompanyingDetails, ApiOpportunityGet, Lang, Option, TranslatedIntoType } from "need4deed-sdk"; import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next";