diff --git a/public/locales/de/translations.json b/public/locales/de/translations.json index a3a58ba0..22136abf 100644 --- a/public/locales/de/translations.json +++ b/public/locales/de/translations.json @@ -348,6 +348,7 @@ "accompanyingDesc": "Nur Freiwillige anzeigen, die bereit sind, Bewohner zu einzelnen Terminen zu begleiten", "district": "Bezirk", "languages": "Sprachen", + "activities": "Aktivitäten", "engagement": { "header": "Engagement", "active": "Aktiv", @@ -593,6 +594,8 @@ "title": "Freiwilligenprofil", "edit": "Bearbeiten", "languages": "Sprachen", + "mainCommunication": "Hauptkommunikation", + "languagesToTranslate": "Sprache zum Übersetzen", "availability": "Verfügbarkeit", "districts": "Bezirke", "volunteerType": "Freiwilligen-Typ", @@ -1024,11 +1027,20 @@ "accompanyingDetailsTitle": "Begleitdetails", "accompanyingDetails": { "appointmentAddress": "Terminadresse", + "appointmentPostcode": "Terminpostleitzahl", "appointmentDate": "Termindatum", "appointmentTime": "Terminzeit", "refugeeNumber": "Geflüchtetennummer", "refugeeName": "Name des Geflüchteten", "languageToTranslate": "Zu übersetzende Sprache", + "refugeeLanguage": "Flüchtlingssprache", + "appointmentLanguage": "Terminsprache", + "appointmentLanguageOptions": { + "german": "Deutsch", + "english": "Englisch", + "englishGerman": "Englisch/Deutsch", + "noTranslation": "Keine Übersetzung erforderlich" + }, "cancel": "Abbrechen", "saveChanges": "Änderungen speichern", "saveSuccess": "Begleitdetails erfolgreich aktualisiert", @@ -1063,6 +1075,7 @@ }, "opportunityDetails": { "title": "Details der Möglichkeit", + "opportunityName": "Name", "description": "Beschreibung", "mainCommunication": "Hauptkommunikation", "residentsSpeak": "Bewohner sprechen", @@ -1075,7 +1088,9 @@ "descriptionHint": "Max. {{max}} Zeichen", "cancel": "Abbrechen", "saveChanges": "Änderungen speichern", + "saveSuccess": "Möglichkeit erfolgreich aktualisiert", "validation": { + "opportunityNameRequired": "Name ist erforderlich", "descriptionRequired": "Beschreibung ist erforderlich", "descriptionTooLong": "Beschreibung ist zu lang", "numberOfVolunteersRequired": "Anzahl der Freiwilligen muss mindestens 1 sein", diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 01a4133d..fed0d78f 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -348,6 +348,7 @@ "accompanyingDesc": "Only show volunteers that are open to bring residents to individual appointments", "district": "District", "languages": "Languages", + "activities": "Activities", "engagement": { "header": "Engagement", "active": "Active", @@ -592,6 +593,8 @@ "title": "Volunteer profile", "edit": "Edit", "languages": "Languages", + "mainCommunication": "Main communication", + "languagesToTranslate": "Language to translate", "availability": "Availability", "districts": "Districts", "volunteerType": "Volunteer type", @@ -1023,11 +1026,20 @@ "accompanyingDetailsTitle": "Accompanying details", "accompanyingDetails": { "appointmentAddress": "Appointment address", + "appointmentPostcode": "Appointment postcode", "appointmentDate": "Appointment date", "appointmentTime": "Appointment time", "refugeeNumber": "Refugee number", "refugeeName": "Refugee name", "languageToTranslate": "Language to translate", + "refugeeLanguage": "Refugee language", + "appointmentLanguage": "Appointment language", + "appointmentLanguageOptions": { + "german": "German", + "english": "English", + "englishGerman": "English/German", + "noTranslation": "No translation needed" + }, "cancel": "Cancel", "saveChanges": "Save changes", "saveSuccess": "Accompanying details updated successfully", @@ -1062,6 +1074,7 @@ }, "opportunityDetails": { "title": "Opportunity details", + "opportunityName": "Name", "description": "Description", "mainCommunication": "Main communication", "residentsSpeak": "Residents speak", @@ -1074,7 +1087,9 @@ "descriptionHint": "Max. {{max}} characters", "cancel": "Cancel", "saveChanges": "Save changes", + "saveSuccess": "Opportunity updated successfully", "validation": { + "opportunityNameRequired": "Name is required", "descriptionRequired": "Description is required", "descriptionTooLong": "Description is too long", "numberOfVolunteersRequired": "Number of volunteers must be at least 1", diff --git a/src/components/Dashboard/Agents/Agents.tsx b/src/components/Dashboard/Agents/Agents.tsx index 11161543..31f827ec 100644 --- a/src/components/Dashboard/Agents/Agents.tsx +++ b/src/components/Dashboard/Agents/Agents.tsx @@ -14,7 +14,7 @@ import { AgentCardsFilter } from "./Filters/types"; import { createSelectedAgentFiltersAsFlatArray } from "./Filters/helpers"; import { defaultAgentCardsFilter } from "./Filters/constants"; import { createFilterFromOption, getClearFilter } from "../common/CardsFilter/helpers"; -import { serializeAgentFilters } from "./helpers"; +import { deserializeAgentFilters, serializeAgentFilters } from "./helpers"; import Filters from "../common/CardsFilter/Filters"; import FiltersContent from "./Filters/FiltersContent"; @@ -55,10 +55,14 @@ export const Agents = () => { if (!apiFilterOptions) return; setCardsFilter((prev) => { - const district = createFilterFromOption(apiFilterOptions, EntityTableName.DISTRICT); - return { ...prev, district }; + const baseFilters = { + ...prev, + district: createFilterFromOption(apiFilterOptions, EntityTableName.DISTRICT), + }; + + return deserializeAgentFilters(baseFilters, searchParams); }); - }, [apiFilterOptions]); + }, [apiFilterOptions, searchParams]); const activeFilters = createSelectedAgentFiltersAsFlatArray(cardsFilter, setCardsFilter, t); return ( diff --git a/src/components/Dashboard/Opportunities/Filters/FiltersContent.tsx b/src/components/Dashboard/Opportunities/Filters/FiltersContent.tsx index a6483a90..798c3ae4 100644 --- a/src/components/Dashboard/Opportunities/Filters/FiltersContent.tsx +++ b/src/components/Dashboard/Opportunities/Filters/FiltersContent.tsx @@ -13,18 +13,15 @@ type Props = { export default function FiltersContent({ setFilter, filter }: Props) { const { t } = useTranslation(); - const { districtFilters, languageFilters, statusFilters, typeFilters } = createOpportunityFilterItems( - filter, - setFilter, - t, - ); - + const { districtFilters, languageFilters, statusFilters, typeFilters, activityFilters } = + createOpportunityFilterItems(filter, setFilter, t); return ( + ); } diff --git a/src/components/Dashboard/Opportunities/Filters/constants.ts b/src/components/Dashboard/Opportunities/Filters/constants.ts index ef49499a..4dae3c67 100644 --- a/src/components/Dashboard/Opportunities/Filters/constants.ts +++ b/src/components/Dashboard/Opportunities/Filters/constants.ts @@ -1,4 +1,4 @@ -import { OpportunityStatusType, OpportunityType, QueryParamsKeys } from "need4deed-sdk"; +import { EntityTableName, OpportunityStatusType, OpportunityType, QueryParamsKeys } from "need4deed-sdk"; import { OpportunityCardsFilter } from "./types"; export const defaultOpportunityCardsFilter: OpportunityCardsFilter = { @@ -16,4 +16,5 @@ export const defaultOpportunityCardsFilter: OpportunityCardsFilter = { [OpportunityType.EVENTS]: false, [OpportunityType.REGULAR]: false, }, + [EntityTableName.ACTIVITY]: {}, }; diff --git a/src/components/Dashboard/Opportunities/Filters/helpers.ts b/src/components/Dashboard/Opportunities/Filters/helpers.ts index 9009cd13..48536219 100644 --- a/src/components/Dashboard/Opportunities/Filters/helpers.ts +++ b/src/components/Dashboard/Opportunities/Filters/helpers.ts @@ -1,7 +1,7 @@ import { TFunction } from "i18next"; import { generateNestedFilterControlItems } from "../../common/CardsFilter/helpers"; import { SetFilter } from "../../common/CardsFilter/types"; -import { QueryParamsKeys } from "need4deed-sdk"; +import { EntityTableName, QueryParamsKeys } from "need4deed-sdk"; import { OpportunityCardsFilter } from "./types"; export const createOpportunityFilterItems = ( @@ -31,7 +31,14 @@ export const createOpportunityFilterItems = ( t(`dashboard.opportunities.filters.type.${key}`), ); - return { districtFilters, languageFilters, statusFilters, typeFilters }; + const activityFilters = generateNestedFilterControlItems( + filter[EntityTableName.ACTIVITY], + setFilter, + EntityTableName.ACTIVITY, + (key) => key, + ); + + return { districtFilters, languageFilters, statusFilters, typeFilters, activityFilters }; }; export const createSelectedOpportunityFiltersAsFlatArray = ( @@ -39,10 +46,9 @@ export const createSelectedOpportunityFiltersAsFlatArray = ( setFilter: SetFilter, t: TFunction, ) => { - const { districtFilters, languageFilters, statusFilters, typeFilters } = createOpportunityFilterItems( - filter, - setFilter, - t, + const { districtFilters, languageFilters, statusFilters, typeFilters, activityFilters } = + createOpportunityFilterItems(filter, setFilter, t); + return [...districtFilters, ...languageFilters, ...statusFilters, ...typeFilters, ...activityFilters].filter( + (f) => f.checked, ); - return [...districtFilters, ...languageFilters, ...statusFilters, ...typeFilters].filter((f) => f.checked); }; diff --git a/src/components/Dashboard/Opportunities/Filters/types.ts b/src/components/Dashboard/Opportunities/Filters/types.ts index f8229393..0e9b9bd6 100644 --- a/src/components/Dashboard/Opportunities/Filters/types.ts +++ b/src/components/Dashboard/Opportunities/Filters/types.ts @@ -1,4 +1,4 @@ -import { QueryParamsKeys } from "need4deed-sdk"; +import { EntityTableName, QueryParamsKeys } from "need4deed-sdk"; import { SelectionMap } from "../../common/CardsFilter/types"; export interface OpportunityCardsFilter { @@ -7,6 +7,7 @@ export interface OpportunityCardsFilter { [QueryParamsKeys.LANGUAGE]: SelectionMap; status: SelectionMap; type: SelectionMap; + [EntityTableName.ACTIVITY]: SelectionMap; } export type OpportunityCardFilterKeys = keyof OpportunityCardsFilter; diff --git a/src/components/Dashboard/Opportunities/Opportunities.tsx b/src/components/Dashboard/Opportunities/Opportunities.tsx index e593e3d2..c3ee78b0 100644 --- a/src/components/Dashboard/Opportunities/Opportunities.tsx +++ b/src/components/Dashboard/Opportunities/Opportunities.tsx @@ -14,7 +14,7 @@ import { defaultOpportunityCardsFilter } from "./Filters/constants"; import FiltersContent from "./Filters/FiltersContent"; import { OpportunityCardsFilter } from "./Filters/types"; import { createSelectedOpportunityFiltersAsFlatArray } from "./Filters/helpers"; -import { serializeOpportunityFilters } from "./helpers"; +import { deserializeOpportunityFilters, serializeOpportunityFilters } from "./helpers"; import { OpportunityListController } from "./OpportunityListController"; import { OpportunitiesContainer } from "./styles"; @@ -70,14 +70,18 @@ export function Opportunities() { if (!apiFilterOptions) return; setCardsFilter((prev) => { - const district = createFilterFromOption(apiFilterOptions, EntityTableName.DISTRICT); - const language = createFilterFromOption(apiFilterOptions, EntityTableName.LANGUAGE); - return { ...prev, district, language }; + const baseFilters = { + ...prev, + district: createFilterFromOption(apiFilterOptions, EntityTableName.DISTRICT), + language: createFilterFromOption(apiFilterOptions, EntityTableName.LANGUAGE), + activity: createFilterFromOption(apiFilterOptions, EntityTableName.ACTIVITY), + }; + + return deserializeOpportunityFilters(baseFilters, searchParams); }); - }, [apiFilterOptions]); + }, [apiFilterOptions, searchParams]); const activeFilters = createSelectedOpportunityFiltersAsFlatArray(cardsFilter, setCardsFilter, t); - return ( diff --git a/src/components/Dashboard/Opportunities/OpportunityCard.helpers.tsx b/src/components/Dashboard/Opportunities/OpportunityCard.helpers.tsx index 4434ca49..d4c63be4 100644 --- a/src/components/Dashboard/Opportunities/OpportunityCard.helpers.tsx +++ b/src/components/Dashboard/Opportunities/OpportunityCard.helpers.tsx @@ -1,4 +1,5 @@ import { ConfettiIcon, PersonSimpleWalkIcon, ShootingStarIcon, TranslateIcon } from "@phosphor-icons/react"; +import { format } from "date-fns"; import { ApiVolunteerOpportunityGetList, OpportunityStatusType, ProfileVolunteeringType } from "need4deed-sdk"; import { JSX } from "react"; @@ -14,7 +15,23 @@ export function formatAccompanyingDate(details?: { appointmentTime?: string; }): string | null { if (!details?.appointmentDate) return null; - return [details.appointmentDate, details.appointmentTime].filter(Boolean).join(" "); + + const date = new Date(details.appointmentDate); + const formattedDate = isNaN(date.getTime()) ? details.appointmentDate : format(date, "dd.MM.yyyy"); + + let formattedTime: string | null = null; + if (details.appointmentTime) { + const [h, m] = details.appointmentTime.split(":").map(Number); + if (!isNaN(h) && !isNaN(m)) { + const d = new Date(); + d.setUTCHours(h, m, 0, 0); + formattedTime = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; + } else { + formattedTime = details.appointmentTime; + } + } + + return [formattedDate, formattedTime].filter(Boolean).join(" "); } export const statusColorMap: Record = { diff --git a/src/components/Dashboard/Opportunities/OpportunityCard.tsx b/src/components/Dashboard/Opportunities/OpportunityCard.tsx index 8a42731c..01ee5277 100644 --- a/src/components/Dashboard/Opportunities/OpportunityCard.tsx +++ b/src/components/Dashboard/Opportunities/OpportunityCard.tsx @@ -1,4 +1,4 @@ -import { ApiVolunteerOpportunityGetList, LangPurpose, ProfileVolunteeringType } from "need4deed-sdk"; +import { ApiVolunteerOpportunityGetList, LangPurpose, OptionItem, ProfileVolunteeringType } from "need4deed-sdk"; import { useRouter } from "next/navigation"; import { useTranslation } from "react-i18next"; @@ -7,7 +7,7 @@ import { Paragraph } from "@/components/styled/text"; import CardDetail from "../Volunteers/CardDetail"; import { CardParagraph } from "../Volunteers/VolunteerCard"; import { IconName } from "../Volunteers/icon"; -import { getLanguagesByPurpose, getOptionTitles } from "./helpers"; +import { getActivityTitles, getLanguagesByPurpose, getOptionTitles } from "./helpers"; import { formatAccompanyingDate, formatAvailability, @@ -20,9 +20,10 @@ import { Card, LanguageRow, StatusDiv, StatusTagsDiv, TagDiv, TitleParagraph } f type Props = { opportunity: ApiVolunteerOpportunityGetList; volunteerId?: string; + activitiesList?: OptionItem[]; }; -export function OpportunityCard({ opportunity, volunteerId }: Props) { +export function OpportunityCard({ opportunity, volunteerId, activitiesList }: Props) { const { t, i18n } = useTranslation(); const router = useRouter(); @@ -36,12 +37,14 @@ export function OpportunityCard({ opportunity, volunteerId }: Props) { location, availability, accompanyingDetails, - } = opportunity; + } = opportunity as ApiVolunteerOpportunityGetList & { + accompanyingDetails?: { appointmentDate?: string; appointmentTime?: string }; + }; const mainCommunication = getLanguagesByPurpose(languages, LangPurpose.GENERAL); const recipientLanguage = getLanguagesByPurpose(languages, LangPurpose.RECIPIENT); - const activityTitles = getOptionTitles(activities); const locationTitles = getOptionTitles(location); + const activityTitles = getActivityTitles(activities, activitiesList); const isAccompanying = volunteerType === ProfileVolunteeringType.ACCOMPANYING; const scheduleText = isAccompanying @@ -103,9 +106,11 @@ export function OpportunityCard({ opportunity, volunteerId }: Props) { )} - - - + {activityTitles.length > 0 && ( + + + + )} {scheduleText && } diff --git a/src/components/Dashboard/Opportunities/OpportunityCardList.tsx b/src/components/Dashboard/Opportunities/OpportunityCardList.tsx index 2bda28ae..84997d58 100644 --- a/src/components/Dashboard/Opportunities/OpportunityCardList.tsx +++ b/src/components/Dashboard/Opportunities/OpportunityCardList.tsx @@ -1,9 +1,10 @@ -import { ApiVolunteerOpportunityGetList } from "need4deed-sdk"; +import { ApiVolunteerOpportunityGetList, OptionItem } from "need4deed-sdk"; import { PaginatedGrid } from "@/components/core/paginatedGrid"; import { OpportunityCard } from "./OpportunityCard"; import { OpportunityCardListContainer } from "./styles"; type Props = { + activitiesList?: OptionItem[]; opportunities: ApiVolunteerOpportunityGetList[]; count: number; columns: number; @@ -21,9 +22,10 @@ export function OpportunityCardList({ currentPage, setCurrentPage, volunteerId, + activitiesList, }: Props) { const items = opportunities.map((opp) => ( - + )); return ( diff --git a/src/components/Dashboard/Opportunities/OpportunityListController.tsx b/src/components/Dashboard/Opportunities/OpportunityListController.tsx index 80c8e1b1..4f52d085 100644 --- a/src/components/Dashboard/Opportunities/OpportunityListController.tsx +++ b/src/components/Dashboard/Opportunities/OpportunityListController.tsx @@ -58,6 +58,7 @@ export function OpportunityListController({ return ( { + if (value === true) { + const paramValue = + (options?.serializeToIDs && options.apiFilterOptions?.activity?.find((d) => d.title === key)?.id) || key; + params.append(EntityTableName.ACTIVITY, String(paramValue)); + } + }); + return asString ? params.toString() : params; } @@ -64,22 +81,26 @@ export function deserializeOpportunityFilters( const queryDistricts = searchParams.getAll(QueryParamsKeys.DISTRICT); queryDistricts.forEach((d) => { - if (newFilter.district[d] !== undefined) newFilter.district[d] = true; + newFilter.district[d] = true; }); - const queryLanguages = searchParams.getAll(QueryParamsKeys.LANGUAGE); queryLanguages.forEach((l) => { - if (newFilter.language[l] !== undefined) newFilter.language[l] = true; + newFilter.language[l] = true; }); const queryStatus = searchParams.getAll("status"); queryStatus.forEach((s) => { - if (newFilter.status[s] !== undefined) newFilter.status[s] = true; + newFilter.status[s] = true; }); const queryType = searchParams.getAll("type"); queryType.forEach((s) => { - if (newFilter.type[s] !== undefined) newFilter.type[s] = true; + newFilter.type[s] = true; + }); + + const queryActivities = searchParams.getAll(EntityTableName.ACTIVITY); + queryActivities.forEach((l) => { + newFilter.activity[l] = true; }); return newFilter; @@ -97,3 +118,9 @@ export function getOptionTitles(items: OptionById[] | undefined): string[] { if (!items || !Array.isArray(items)) return []; return items.map((item) => (typeof item.title === "string" ? item.title : "")).filter(Boolean); } + +export function getActivityTitles(activities: OptionById[], activityList: OptionItem[] | undefined): string[] { + if (!activities?.length || !activityList?.length) return []; + const activityMap = new Map(activityList.map((item) => [String(item.id), item.title])); + return activities.map((act) => activityMap.get(String(act.id))).filter((title): title is string => Boolean(title)); +} diff --git a/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetails.tsx b/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetails.tsx index a565978b..c605ad03 100644 --- a/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetails.tsx +++ b/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetails.tsx @@ -11,6 +11,7 @@ import { EditableSectionProps, EditableSectionRef } from "../shared/types"; import { useEditingChangeNotifier } from "../shared/useEditingChangeNotifier"; import { AccompanyingDetailsDisplay } from "./AccompanyingDetailsDisplay"; import { AccompanyingDetailsEdit } from "./AccompanyingDetailsEdit"; +import { AppointmentLanguages } from "@/config/constants"; import { getInitialFormValues, getMinAppointmentDate, isAccompanyingType } from "./helpers"; import { AccompanyingDetailsFormData, accompanyingDetailsSchema } from "./accompanyingDetailsSchema"; import { Container, NotAccompanyingMessage } from "./styles"; @@ -42,6 +43,16 @@ export const AccompanyingDetails = forwardRef(functio labelToKey[lang.title] = String(lang.id); }); + const appointmentLanguageKeys = Object.values(AppointmentLanguages); + const appointmentLanguageKeyToLabel: Record = {}; + const appointmentLanguageLabelToKey: Record = {}; + appointmentLanguageKeys.forEach((key) => { + const label = t(`dashboard.opportunityProfile.accompanyingDetails.appointmentLanguageOptions.${key}`); + appointmentLanguageKeyToLabel[key] = label; + appointmentLanguageLabelToKey[label] = key; + }); + const appointmentLanguageOptions = appointmentLanguageKeys.map((key) => appointmentLanguageKeyToLabel[key]); + const initialFormValues = getInitialFormValues(opportunity.accompanyingDetails); const methods = useForm({ @@ -77,7 +88,8 @@ export const AccompanyingDetails = forwardRef(functio appointmentTime: values.appointmentTime || undefined, refugeeNumber: values.refugeeNumber, refugeeName: values.refugeeName, - languagesToTranslate: values.languageToTranslate ? [values.languageToTranslate] : [], + languagesToTranslate: values.languagesToTranslate ?? [], + appointmentLanguage: values.appointmentLanguage || undefined, }, }, { @@ -104,7 +116,7 @@ export const AccompanyingDetails = forwardRef(functio ); } - const languageLabel = keyToLabel[formValues.languageToTranslate || ""] || formValues.languageToTranslate || ""; + const languageLabel = (formValues.languagesToTranslate ?? []).map((id) => keyToLabel[id] || id).join(", "); return ( @@ -115,6 +127,9 @@ export const AccompanyingDetails = forwardRef(functio languageOptions={languageOptions} keyToLabel={keyToLabel} labelToKey={labelToKey} + appointmentLanguageOptions={appointmentLanguageOptions} + appointmentLanguageKeyToLabel={appointmentLanguageKeyToLabel} + appointmentLanguageLabelToKey={appointmentLanguageLabelToKey} onCancel={handleCancel} onSubmit={handleSubmit(onSubmit)} isPending={isPending} diff --git a/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetailsDisplay.tsx b/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetailsDisplay.tsx index 64e9d9fd..8862c548 100644 --- a/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetailsDisplay.tsx +++ b/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetailsDisplay.tsx @@ -3,6 +3,7 @@ import { EMPTY_PLACEHOLDER_VALUE } from "@/config/constants"; import { format } from "date-fns"; import { useTranslation } from "react-i18next"; import { AccompanyingDetailsFormData } from "./accompanyingDetailsSchema"; +import { formatTimeForDisplay } from "./helpers"; import { DateFieldRow, Details } from "./styles"; type Props = { @@ -23,6 +24,14 @@ export const AccompanyingDetailsDisplay = ({ values, languageLabel }: Props) => setValue={() => {}} /> + {}} + /> + {values.appointmentDate ? format(values.appointmentDate, "dd.MM.yyyy") : EMPTY_PLACEHOLDER_VALUE} @@ -30,7 +39,7 @@ export const AccompanyingDetailsDisplay = ({ values, languageLabel }: Props) => - {values.appointmentTime || EMPTY_PLACEHOLDER_VALUE} + {formatTimeForDisplay(values.appointmentTime) || EMPTY_PLACEHOLDER_VALUE} {}} + /> + + {}} options={[]} /> diff --git a/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetailsEdit.tsx b/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetailsEdit.tsx index bcff065b..c2c0299a 100644 --- a/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetailsEdit.tsx +++ b/src/components/Dashboard/Profile/sections/AccompanyingDetails/AccompanyingDetailsEdit.tsx @@ -20,6 +20,9 @@ type Props = { languageOptions: string[]; keyToLabel: Record; labelToKey: Record; + appointmentLanguageOptions: string[]; + appointmentLanguageKeyToLabel: Record; + appointmentLanguageLabelToKey: Record; onCancel: () => void; onSubmit: () => void; isPending: boolean; @@ -31,6 +34,9 @@ export const AccompanyingDetailsEdit = ({ languageOptions, keyToLabel, labelToKey, + appointmentLanguageOptions, + appointmentLanguageKeyToLabel, + appointmentLanguageLabelToKey, onCancel, onSubmit, isPending, @@ -60,6 +66,21 @@ export const AccompanyingDetailsEdit = ({ )} /> + }) => ( + + )} + /> + {errors.appointmentTime && ( - {t( - `dashboard.opportunityProfile.accompanyingDetails.validation.${errors.appointmentTime.message}`, - )} + {t(`dashboard.opportunityProfile.accompanyingDetails.validation.${errors.appointmentTime.message}`)} )} @@ -139,20 +158,43 @@ export const AccompanyingDetailsEdit = ({ /> }) => ( + render={({ + field, + }: { + field: ControllerRenderProps; + }) => ( + keyToLabel[id] || id)} + setValue={(value) => { + const labels = Array.isArray(value) ? value : [value]; + field.onChange(labels.map((label) => labelToKey[label] || label)); + }} + options={languageOptions} + errorMessage={errors.languagesToTranslate?.message} + /> + )} + /> + + }) => ( { const label = Array.isArray(value) ? value[0] : value; - field.onChange(labelToKey[label] || label); + field.onChange(appointmentLanguageLabelToKey[label] || label); }} - options={languageOptions} - errorMessage={errors.languageToTranslate?.message} + options={appointmentLanguageOptions} + errorMessage={errors.appointmentLanguage?.message} /> )} /> diff --git a/src/components/Dashboard/Profile/sections/AccompanyingDetails/accompanyingDetailsSchema.ts b/src/components/Dashboard/Profile/sections/AccompanyingDetails/accompanyingDetailsSchema.ts index f36c634c..c03b6102 100644 --- a/src/components/Dashboard/Profile/sections/AccompanyingDetails/accompanyingDetailsSchema.ts +++ b/src/components/Dashboard/Profile/sections/AccompanyingDetails/accompanyingDetailsSchema.ts @@ -4,6 +4,7 @@ const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/; export const accompanyingDetailsSchema = z.object({ appointmentAddress: z.string().optional(), + appointmentPostcode: z.string().optional(), appointmentDate: z.date().nullable().optional(), appointmentTime: z .string() @@ -13,7 +14,8 @@ export const accompanyingDetailsSchema = z.object({ }), refugeeNumber: z.string().optional(), refugeeName: z.string().optional(), - languageToTranslate: z.string().optional(), + languagesToTranslate: z.array(z.string()).optional(), + appointmentLanguage: z.string().optional(), }); export type AccompanyingDetailsFormData = z.infer; diff --git a/src/components/Dashboard/Profile/sections/AccompanyingDetails/helpers.ts b/src/components/Dashboard/Profile/sections/AccompanyingDetails/helpers.ts index 26c76c85..3651b2b0 100644 --- a/src/components/Dashboard/Profile/sections/AccompanyingDetails/helpers.ts +++ b/src/components/Dashboard/Profile/sections/AccompanyingDetails/helpers.ts @@ -1,3 +1,4 @@ +import { utcHhmmToLocal } from "@/utils"; import { ApiOpportunityAccompanyingDetails, VolunteerStateTypeType } from "need4deed-sdk"; import { AccompanyingDetailsFormData } from "./accompanyingDetailsSchema"; @@ -34,13 +35,22 @@ export const parseTime = (time: Date | string | undefined): string => { return time.toTimeString().slice(0, 5); }; +// 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 getInitialFormValues = ( details: ApiOpportunityAccompanyingDetails | undefined, ): AccompanyingDetailsFormData => ({ appointmentAddress: details?.appointmentAddress || "", + appointmentPostcode: + (details as ApiOpportunityAccompanyingDetails & { appointmentPostcode?: string })?.appointmentPostcode || "", appointmentDate: parseDate(details?.appointmentDate), appointmentTime: parseTime(details?.appointmentTime), refugeeNumber: details?.refugeeNumber || "", refugeeName: details?.refugeeName || "", - languageToTranslate: details?.languageToTranslate?.toString() ?? "", + languagesToTranslate: details?.languageToTranslate !== undefined ? [details.languageToTranslate.toString()] : [], + appointmentLanguage: + (details as ApiOpportunityAccompanyingDetails & { appointmentLanguage?: string })?.appointmentLanguage || "", }); diff --git a/src/components/Dashboard/Profile/sections/AgentOpportunities/AgentOpportunities.tsx b/src/components/Dashboard/Profile/sections/AgentOpportunities/AgentOpportunities.tsx index dda42fd7..ff69ee40 100644 --- a/src/components/Dashboard/Profile/sections/AgentOpportunities/AgentOpportunities.tsx +++ b/src/components/Dashboard/Profile/sections/AgentOpportunities/AgentOpportunities.tsx @@ -1,32 +1,49 @@ -import { OpportunityVolunteerStatusType } from "need4deed-sdk"; +import { Heading4 } from "@/components/styled/text"; +import { apiPathAgent, cacheTTL } from "@/config/constants"; +import { useGetQuery } from "@/hooks/useGetQuery"; +import { ApiOpportunityGetList, Id } from "need4deed-sdk"; +import { useRouter } from "next/navigation"; import { useTranslation } from "react-i18next"; +import { Accordion } from "../shared/Accordion"; import { SectionEmptyState } from "../shared/styles"; -import { Tabs } from "../shared/Tabs"; -import { useTabTransitions } from "../shared/useTabTransitions"; import { AgentOpportunitiesContainer } from "./styles"; -const tabsKeys = ["lookingForVolunteers", "active", "past"] as const; +type Props = { agentId: Id }; -const agentTabStatusOrder: OpportunityVolunteerStatusType[] = [ - OpportunityVolunteerStatusType.PENDING, - OpportunityVolunteerStatusType.ACTIVE, - OpportunityVolunteerStatusType.PAST, -]; +export const AgentOpportunities = ({ agentId }: Props) => { + const { t, i18n } = useTranslation(); + const router = useRouter(); -export const AgentOpportunities = () => { - const { t } = useTranslation(); + const { data, isLoading } = useGetQuery({ + queryKey: ["agent-opportunities", String(agentId)], + apiPath: `${apiPathAgent}/${agentId}/opportunity-linked`, + staleTime: cacheTTL, + enabled: !!agentId, + addLang: false, + }); - const { selectedTabIndex, setSelectedTabIndex, tabCounts } = useTabTransitions([], agentTabStatusOrder); + const opportunities = data ?? []; - const tabs = tabsKeys.map((key, index) => ({ - label: t(`dashboard.agentProfile.opportunitiesSec.tabs.${key}`), - count: tabCounts[index], - })); + if (isLoading) return ; return ( - - {t("dashboard.volunteerProfile.opportunitiesSec.emptyState")} + {opportunities.length === 0 ? ( + {t("dashboard.volunteerProfile.opportunitiesSec.emptyState")} + ) : ( + opportunities.map((opp) => ( + + {opp.title} + + } + subtitle={opp.statusOpportunity ? t(`dashboard.opportunities.status.${opp.statusOpportunity}`) : "-"} + onGoToProfile={() => router.push(`/${i18n.language}/dashboard/opportunities/${opp.id}`)} + /> + )) + )} ); }; diff --git a/src/components/Dashboard/Profile/sections/OpportunityDetails/OpportunityDetailsDisplay.tsx b/src/components/Dashboard/Profile/sections/OpportunityDetails/OpportunityDetailsDisplay.tsx index ddc77ad9..6354a4b8 100644 --- a/src/components/Dashboard/Profile/sections/OpportunityDetails/OpportunityDetailsDisplay.tsx +++ b/src/components/Dashboard/Profile/sections/OpportunityDetails/OpportunityDetailsDisplay.tsx @@ -30,6 +30,14 @@ export function OpportunityDetailsDisplay({ opportunity }: Props) { return ( + {}} + /> + { + if (!language) return []; + const found = apiLanguages.find((a) => { + if (a.title === language || a.title.toLowerCase() === language.toLowerCase()) return true; + // languagesToFormValues stores translated names; reverse the lookup to match them + const key = `languageNames.${a.title.toLowerCase()}`; + const translated = t(key); + return translated !== key && translated === language; + }); + return found ? [{ id: found.id, title: found.title }] : []; + }); +} + +function toOptionItems(ids: string[], apiItems: ApiLanguageOption[]): OptionItem[] { + const map = new Map(apiItems.map((i) => [i.id, i.title])); + return ids.flatMap((id) => { + const numId = Number(id); + const title = map.get(numId); + return title ? [{ id: numId, title }] : []; + }); +} + type Props = { opportunity: ApiOpportunityGet; onCancel: () => void; @@ -37,6 +66,7 @@ export function OpportunityDetailsEdit({ opportunity, onCancel }: Props) { const isEventType = opp.volunteerType === VolunteerStateTypeType.EVENTS; + const { mutate: updateOpportunityDetails } = useUpdateOpportunityDetails(opp.id); const { data: apiLanguages = [] } = useApiLanguages(); const { data: apiActivities = [] } = useApiActivities(); const { data: apiSkills = [] } = useApiSkills(); @@ -78,9 +108,19 @@ export function OpportunityDetailsEdit({ opportunity, onCancel }: Props) { onCancel(); }; - const onSubmit = () => { - // Mutations will be added later - onCancel(); + const onSubmit = (values: OpportunityDetailsFormData) => { + updateOpportunityDetails( + { + description: values.description, + numberVolunteers: Number(values.numberOfVolunteers), + languagesMain: toLangOptionItems(values.mainCommunication, apiLanguages, t), + languagesResidents: toLangOptionItems(values.residentsSpeak, apiLanguages, t), + activities: toOptionItems(values.activities, apiActivities), + skills: toOptionItems(values.skills, apiSkills), + schedule: values.availability ? formToApiAvailability(values.availability) : undefined, + }, + { onSuccess: onCancel }, + ); }; return ( diff --git a/src/components/Dashboard/Profile/sections/OpportunityDetails/opportunityDetailsSchema.ts b/src/components/Dashboard/Profile/sections/OpportunityDetails/opportunityDetailsSchema.ts index 2c56fb2e..f79231ed 100644 --- a/src/components/Dashboard/Profile/sections/OpportunityDetails/opportunityDetailsSchema.ts +++ b/src/components/Dashboard/Profile/sections/OpportunityDetails/opportunityDetailsSchema.ts @@ -24,6 +24,7 @@ const languagesValidator = (t: (key: string) => string) => export const createOpportunityDetailsSchema = (t: (key: string) => string) => z.object({ + title: z.string().min(1, t(`${i18nPrefix}.opportunityNameRequired`)), description: z .string() .min(1, t(`${i18nPrefix}.descriptionRequired`)) @@ -33,17 +34,7 @@ export const createOpportunityDetailsSchema = (t: (key: string) => string) => }), mainCommunication: languagesValidator(t), residentsSpeak: languagesValidator(t), - availability: z - .custom( - (data) => { - if (data === null || data === undefined) return true; - if (!Array.isArray(data)) return false; - return data.some((day) => day.timeSlots.some((slot: { selected: boolean }) => slot.selected)); - }, - t(`${i18nPrefix}.availabilityRequired`), - ) - .nullable() - .optional(), + availability: z.custom().nullable().optional(), eventDate: z.date().nullable().optional(), eventTime: z.string().optional(), activities: z.array(z.string()).min(1, t(`${i18nPrefix}.activitiesRequired`)), diff --git a/src/components/Dashboard/Profile/sections/RefugeeAccommodationCentre/RefugeeAccommodationCentre.tsx b/src/components/Dashboard/Profile/sections/RefugeeAccommodationCentre/RefugeeAccommodationCentre.tsx index a9c5f901..11a82c5a 100644 --- a/src/components/Dashboard/Profile/sections/RefugeeAccommodationCentre/RefugeeAccommodationCentre.tsx +++ b/src/components/Dashboard/Profile/sections/RefugeeAccommodationCentre/RefugeeAccommodationCentre.tsx @@ -8,6 +8,8 @@ import { useTranslation } from "react-i18next"; import { FormContainer } from "../shared/styles"; import { EditableSectionProps, EditableSectionRef } from "../shared/types"; import { useEditingChangeNotifier } from "../shared/useEditingChangeNotifier"; +import { useApiDistricts } from "../VolunteerProfile/hooks"; +import { createMapping } from "../VolunteerProfile/mappingUtils"; import { RefugeeAccommodationCentreDisplay } from "./RefugeeAccommodationCentreDisplay"; import { RefugeeAccommodationCentreEdit } from "./RefugeeAccommodationCentreEdit"; import { @@ -29,6 +31,9 @@ export const RefugeeAccommodationCentre = forwardRef( useEditingChangeNotifier(isEditing, onEditingChange); + const { data: apiDistricts = [] } = useApiDistricts(); + const districtMapping = useMemo(() => createMapping(apiDistricts), [apiDistricts]); + const schema = createRefugeeAccommodationCentreSchema(t); const initialFormValues = useMemo((): RefugeeAccommodationCentreFormData => { @@ -61,11 +66,13 @@ export const RefugeeAccommodationCentre = forwardRef( }; const onSubmit = (values: RefugeeAccommodationCentreFormData) => { + const districtId = districtMapping.titleToId[values.district]; updateAgent( { agent: { name: values.name, address: values.address, + ...(districtId !== undefined && { district: districtId }), }, }, { onSuccess: () => setIsEditing(false) }, @@ -85,6 +92,7 @@ export const RefugeeAccommodationCentre = forwardRef( onCancel={handleCancel} onSubmit={handleSubmit(onSubmit)} isPending={isPending} + districts={apiDistricts.map((d) => d.title)} /> ) : ( diff --git a/src/components/Dashboard/Profile/sections/RefugeeAccommodationCentre/RefugeeAccommodationCentreEdit.tsx b/src/components/Dashboard/Profile/sections/RefugeeAccommodationCentre/RefugeeAccommodationCentreEdit.tsx index 00a79fc0..a556ad17 100644 --- a/src/components/Dashboard/Profile/sections/RefugeeAccommodationCentre/RefugeeAccommodationCentreEdit.tsx +++ b/src/components/Dashboard/Profile/sections/RefugeeAccommodationCentre/RefugeeAccommodationCentreEdit.tsx @@ -9,9 +9,10 @@ type Props = { onCancel: () => void; onSubmit: () => void; isPending: boolean; + districts: string[]; }; -export const RefugeeAccommodationCentreEdit = ({ onCancel, onSubmit, isPending }: Props) => { +export const RefugeeAccommodationCentreEdit = ({ onCancel, onSubmit, isPending, districts }: Props) => { const { t } = useTranslation(); const { control, @@ -57,10 +58,11 @@ export const RefugeeAccommodationCentreEdit = ({ onCancel, onSubmit, isPending } render={({ field }) => ( )} diff --git a/src/components/Dashboard/Profile/sections/VolunteerProfile/DisplayFields.tsx b/src/components/Dashboard/Profile/sections/VolunteerProfile/DisplayFields.tsx index 0a8f9781..72602cbe 100644 --- a/src/components/Dashboard/Profile/sections/VolunteerProfile/DisplayFields.tsx +++ b/src/components/Dashboard/Profile/sections/VolunteerProfile/DisplayFields.tsx @@ -40,7 +40,8 @@ const TagsWrapper = styled.div` `; type Props = { - languages: string; + mainCommunication: string; + languagesToTranslate: string; availability: string; districts: string; volunteerType: string; @@ -51,7 +52,8 @@ type Props = { }; export function DisplayFields({ - languages, + mainCommunication, + languagesToTranslate, availability, districts, volunteerType, @@ -63,8 +65,13 @@ export function DisplayFields({ return ( <> - {t("dashboard.volunteerProfile.profileSection.languages")} - {languages} + {t("dashboard.volunteerProfile.profileSection.mainCommunication")} + {mainCommunication} + + + + {t("dashboard.volunteerProfile.profileSection.languagesToTranslate")} + {languagesToTranslate} diff --git a/src/components/Dashboard/Profile/sections/VolunteerProfile/VolunteerProfile.tsx b/src/components/Dashboard/Profile/sections/VolunteerProfile/VolunteerProfile.tsx index db6d6769..c98fb2cf 100644 --- a/src/components/Dashboard/Profile/sections/VolunteerProfile/VolunteerProfile.tsx +++ b/src/components/Dashboard/Profile/sections/VolunteerProfile/VolunteerProfile.tsx @@ -2,7 +2,7 @@ import Button from "@/components/core/button/Button/Button"; import { useUpdateVolunteerProfile } from "@/hooks/useUpdateVolunteerProfile"; import { zodResolver } from "@hookform/resolvers/zod"; -import { ApiVolunteerGet, Lang, VolunteerStateTypeType } from "need4deed-sdk"; +import { ApiVolunteerGet, Lang, LangPurpose, VolunteerStateTypeType } from "need4deed-sdk"; import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; @@ -151,7 +151,18 @@ export const VolunteerProfile = forwardRef(function /> ) : ( , t: TFunction, + purpose?: LangPurpose, ): string { if (!langs || langs.length === 0) return EMPTY_PLACEHOLDER_VALUE; - return langs + const filtered = purpose ? langs.filter((lang) => lang.purpose === purpose) : langs; + if (filtered.length === 0) return EMPTY_PLACEHOLDER_VALUE; + return filtered .map((lang) => { const localizedTitle = languageIdToTitle[lang.id] || lang.title; const proficiencyKey = lang.proficiency?.toLowerCase(); diff --git a/src/components/Dashboard/Profile/sections/VolunteerProfile/transformers.ts b/src/components/Dashboard/Profile/sections/VolunteerProfile/transformers.ts index 99064b5f..acb35076 100644 --- a/src/components/Dashboard/Profile/sections/VolunteerProfile/transformers.ts +++ b/src/components/Dashboard/Profile/sections/VolunteerProfile/transformers.ts @@ -1,5 +1,5 @@ import { TFunction } from "i18next"; -import { ApiLanguage, ApiVolunteerGet, VolunteerStateTypeType } from "need4deed-sdk"; +import { ApiLanguage, ApiVolunteerGet, LangPurpose, VolunteerStateTypeType } from "need4deed-sdk"; import { apiToFormAvailability } from "./availabilityUtils"; import { LEVEL_TO_PROFICIENCY } from "./constants"; import { formatActivities, formatDistricts, formatLanguages, formatSkills, getVolunteerTypeLabel } from "./formatters"; @@ -52,6 +52,7 @@ export function transformLanguagesToApi(languages: VolunteerProfileFormData["lan id: parseInt(lang.language, 10), title: languageMapping.idToTitle[parseInt(lang.language, 10)] || "", proficiency: LEVEL_TO_PROFICIENCY[lang.level as unknown as number], + purpose: lang.purpose ?? LangPurpose.GENERAL, }) as ApiLanguage, ); } diff --git a/src/components/Dashboard/Profile/sections/VolunteerProfile/volunteerProfileSchema.ts b/src/components/Dashboard/Profile/sections/VolunteerProfile/volunteerProfileSchema.ts index b8c965ae..ea056bd2 100644 --- a/src/components/Dashboard/Profile/sections/VolunteerProfile/volunteerProfileSchema.ts +++ b/src/components/Dashboard/Profile/sections/VolunteerProfile/volunteerProfileSchema.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { Availability } from "@/components/forms/types"; import { LanguageLevel, LanguageObject } from "@/types"; +import { LangPurpose } from "need4deed-sdk"; export const createVolunteerProfileSchema = (t: (key: string) => string) => { return z.object({ @@ -10,6 +11,7 @@ export const createVolunteerProfileSchema = (t: (key: string) => string) => { id: z.number(), language: z.string(), level: z.union([z.enum(LanguageLevel), z.literal("")]), + purpose: z.nativeEnum(LangPurpose).optional(), }) satisfies z.ZodType, ) .min(1, t("dashboard.volunteerProfile.profileSection.validation.languageRequired")) diff --git a/src/components/Dashboard/Volunteers/Volunteers.tsx b/src/components/Dashboard/Volunteers/Volunteers.tsx index 7c3f078d..abee5b1d 100644 --- a/src/components/Dashboard/Volunteers/Volunteers.tsx +++ b/src/components/Dashboard/Volunteers/Volunteers.tsx @@ -1,8 +1,8 @@ "use client"; + import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import styled from "styled-components"; - import { DashboardLayout } from "@/components/Layout"; import { apiPathOption, questionMark } from "@/config/constants"; import { useGetOpportunity, useGetQuery } from "@/hooks"; @@ -14,7 +14,12 @@ import { getClearFilter } from "../common/CardsFilter/helpers"; import { defaultVolunteerCardsFilter } from "./Filters/constants"; import FiltersContent from "./Filters/FiltersContent"; import { CardsFilter } from "./Filters/types"; -import { createFilterFromOption, createSelectedFilterItemsAsFlatArray, serializeFilters } from "./helpers"; +import { + createFilterFromOption, + createSelectedFilterItemsAsFlatArray, + deserializeVolunteerFilters, + serializeFilters, +} from "./helpers"; import { VolunteerListController } from "./VolunteerListController"; export function Volunteers() { @@ -63,14 +68,16 @@ export function Volunteers() { useEffect(() => { if (!apiFilterOptions) return; - // Merge and set 'district' - 'languages' of query params and API option setCardsFilter((prev) => { - const district = createFilterFromOption(apiFilterOptions, EntityTableName.DISTRICT); - const language = createFilterFromOption(apiFilterOptions, EntityTableName.LANGUAGE); + const baseFilters = { + ...prev, + district: createFilterFromOption(apiFilterOptions, EntityTableName.DISTRICT), + language: createFilterFromOption(apiFilterOptions, EntityTableName.LANGUAGE), + }; - return { ...prev, district, language }; + return deserializeVolunteerFilters(baseFilters, searchParams); }); - }, [apiFilterOptions]); + }, [apiFilterOptions, searchParams]); const activeFilters = createSelectedFilterItemsAsFlatArray(cardsFilter, setCardsFilter, t); diff --git a/src/components/Dashboard/Volunteers/helpers.ts b/src/components/Dashboard/Volunteers/helpers.ts index 6a686eb8..2ab9f7f5 100644 --- a/src/components/Dashboard/Volunteers/helpers.ts +++ b/src/components/Dashboard/Volunteers/helpers.ts @@ -129,7 +129,7 @@ export function serializeFilters( return asString ? params.toString() : params; } -export function deserializeFilters(filter: CardsFilter, searchParams: ReadonlyURLSearchParams) { +export function deserializeVolunteerFilters(filter: CardsFilter, searchParams: ReadonlyURLSearchParams) { const newFilter: CardsFilter = structuredClone(filter); const search = searchParams.get(QueryParamsKeys.SEARCH); diff --git a/src/components/forms/utils.ts b/src/components/forms/utils.ts index 3ddefce8..7a08e1e3 100644 --- a/src/components/forms/utils.ts +++ b/src/components/forms/utils.ts @@ -156,7 +156,7 @@ export function parseFormStateDTOOpportunity(form: OpportunityData): Opportunity // Accompanying-specific fields const accomp_address = isAccompanying ? form.aaAddress || null : null; const accomp_postcode = isAccompanying ? form.aaPostcode || null : null; - const accomp_datetime = isAccompanying ? (form.dateTime ? new Date(form.dateTime).toISOString() : null) : null; + const accomp_datetime = isAccompanying ? (form.dateTime || null) : null; const accomp_name = isAccompanying ? form.refugeeName || null : null; const accomp_phone = isAccompanying ? form.refugeeNumber || null : null; const accomp_information = isAccompanying ? form.aaInformation || null : null; diff --git a/src/config/constants.ts b/src/config/constants.ts index 3718d519..a8c6e71c 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -74,3 +74,10 @@ export const MAX_DESCRIPTION_LENGTH = 500; export const PHONE_NUMBER_REGEX = /^\+[0-9\s]+$/; export const LOGGED_IN_COOKIE = "is_logged_in=true; path=/; max-age=6000; SameSite=Lax; Secure"; + +export enum AppointmentLanguages { + GERMAN = "german", + ENGLISH = "english", + ENGLISH_GERMAN = "englishGerman", + NO_TRANSLATION = "noTranslation", +} diff --git a/src/hooks/useAgentProfileSections.tsx b/src/hooks/useAgentProfileSections.tsx index c89d4a55..94ce73ab 100644 --- a/src/hooks/useAgentProfileSections.tsx +++ b/src/hooks/useAgentProfileSections.tsx @@ -61,7 +61,7 @@ export const useAgentProfileSections = (agent: ApiAgentProfileGet | undefined) = iconName: IconName.ShootingStar, title: t("dashboard.volunteerProfile.opportunities"), headerButtonName: t("dashboard.agentProfile.opportunitiesSec.postOpportunity"), - subComponent: , + subComponent: , }, { iconName: IconName.ChatsTeardrop, diff --git a/src/hooks/useUpdateOpportunityAccompanyingDetails.ts b/src/hooks/useUpdateOpportunityAccompanyingDetails.ts index 7f048ac4..1ce402a7 100644 --- a/src/hooks/useUpdateOpportunityAccompanyingDetails.ts +++ b/src/hooks/useUpdateOpportunityAccompanyingDetails.ts @@ -10,6 +10,7 @@ export type OpportunityAccompanyingDetailsUpdateData = { refugeeNumber?: string; refugeeName?: string; languagesToTranslate?: string[]; + appointmentLanguage?: string; }; }; diff --git a/src/hooks/useUpdateOpportunityDetails.ts b/src/hooks/useUpdateOpportunityDetails.ts new file mode 100644 index 00000000..57824e1f --- /dev/null +++ b/src/hooks/useUpdateOpportunityDetails.ts @@ -0,0 +1,12 @@ +import { apiPathOpportunity } from "@/config/constants"; +import { useMutationQuery } from "@/hooks"; +import { ApiOpportunityGet, ApiOpportunityPatch } from "need4deed-sdk"; + +export const useUpdateOpportunityDetails = (opportunityId: ApiOpportunityGet["id"]) => { + return useMutationQuery({ + apiPath: `${apiPathOpportunity}/${opportunityId}`, + method: "patch", + successMessage: "dashboard.opportunityProfile.opportunityDetails.saveSuccess", + queryKeyToInvalidate: ["opportunity", String(opportunityId)], + }); +}; diff --git a/src/hooks/useUpdateOpportunityTitle.ts b/src/hooks/useUpdateOpportunityTitle.ts new file mode 100644 index 00000000..127cd77b --- /dev/null +++ b/src/hooks/useUpdateOpportunityTitle.ts @@ -0,0 +1,16 @@ +import { apiPathOpportunity } from "@/config/constants"; +import { useMutationQuery } from "@/hooks"; +import { ApiOpportunityGet } from "need4deed-sdk"; + +export type OpportunityTitleUpdateData = { + title: string; +}; + +export const useUpdateOpportunityTitle = (opportunityId: ApiOpportunityGet["id"]) => { + return useMutationQuery({ + apiPath: `${apiPathOpportunity}/${opportunityId}`, + method: "patch", + successMessage: "dashboard.opportunityProfile.opportunityDetails.saveSuccess", + queryKeyToInvalidate: ["opportunity", String(opportunityId)], + }); +}; diff --git a/src/types.ts b/src/types.ts index 52e98db1..ed1588f7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -50,6 +50,7 @@ export type LanguageObject = { id: number; language: string; level: LanguageLevel | ""; + purpose?: import("need4deed-sdk").LangPurpose; }; export type FooterLink = [string, string]; diff --git a/src/utils/index.ts b/src/utils/index.ts index 82b7de1b..2c775a5e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,6 +3,14 @@ export * from "./helpers"; import { cloudfrontURL } from "@/config/constants"; import { DocumentStatusType } from "need4deed-sdk"; +export function utcHhmmToLocal(hhmm: string): string { + const [h, m] = hhmm.split(":").map(Number); + if (isNaN(h) || isNaN(m)) return hhmm; + const d = new Date(); + d.setUTCHours(h, m, 0, 0); + return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; +} + export function getDateLocalTooUTC(dateStr: string | undefined) { if (!dateStr) return undefined;