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;