Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4ce31d3
✨ add name field to opportunity details section
Cy-fox Apr 21, 2026
c57a893
fix: revert representative to singular property name
Cy-fox Apr 21, 2026
1e9f3f0
fix: replace district free-text with dropdown in RAC section
Cy-fox Apr 22, 2026
e770a7e
fix: use strict undefined check for districtId before spreading
Cy-fox Apr 24, 2026
ac5007f
Fix: No ZIP/PLZ-code for Accompanying details #344
lourooo Apr 20, 2026
4780749
Fix: helpers.ts
lourooo Apr 21, 2026
83fc663
fix: preserve local time for accomp_datetime instead of converting to…
nadavosa Apr 24, 2026
5608b31
adds helper func to get activity titles from ids
DarrellRoberts Apr 23, 2026
3260033
removes console log
DarrellRoberts Apr 23, 2026
8b49fca
adds activity filter to opp-list
DarrellRoberts Apr 23, 2026
cbf8aac
adds String to ids & removes undefined from deserialFunc
DarrellRoberts Apr 23, 2026
b302660
🐛 preserve language purpose when saving volunteer profile
Cy-fox Apr 22, 2026
1f07b6d
fix: fetch and display connected opportunities on agent profile
Cy-fox Apr 22, 2026
0721aab
fix: add fallback for missing statusOpportunity in subtitle
Cy-fox Apr 24, 2026
49f1cdc
fix: rename language field to refugee language and enable multi-select
Cy-fox Apr 24, 2026
004ce6a
adds deserializeFilterFunc to Opp, Vol and Agents lists
DarrellRoberts Apr 24, 2026
e7e8184
🐛 split volunteer languages into main communication and translate fields
Cy-fox Apr 21, 2026
7f9d3fa
ci: retrigger lint check
Cy-fox Apr 21, 2026
d25b9ef
fix: restore languages translation key alongside new split keys
Cy-fox Apr 22, 2026
10621ae
Fix: Accompanying details: add new row #386
lourooo Apr 24, 2026
d359620
Fix: bugs
lourooo Apr 24, 2026
2d435ee
Update OpportunityCard.tsx
lourooo Apr 27, 2026
8c889b3
Fix opportunity details form blocked when no schedule is set
Cy-fox Apr 27, 2026
7cab478
fix: wire up opportunity details save mutation and fix UTC time display
nadavosa Apr 27, 2026
d286a9c
fix: correct languagesToTranslate field name and restore missing form…
nadavosa Apr 28, 2026
7612b7f
refactor: move UTC time conversion to shared utcHhmmToLocal utility i…
nadavosa Apr 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions public/locales/de/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -593,6 +594,8 @@
"title": "Freiwilligenprofil",
"edit": "Bearbeiten",
"languages": "Sprachen",
"mainCommunication": "Hauptkommunikation",
"languagesToTranslate": "Sprache zum Übersetzen",
"availability": "Verfügbarkeit",
"districts": "Bezirke",
"volunteerType": "Freiwilligen-Typ",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -1063,6 +1075,7 @@
},
"opportunityDetails": {
"title": "Details der Möglichkeit",
"opportunityName": "Name",
"description": "Beschreibung",
"mainCommunication": "Hauptkommunikation",
"residentsSpeak": "Bewohner sprechen",
Expand All @@ -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",
Expand Down
15 changes: 15 additions & 0 deletions public/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -1062,6 +1074,7 @@
},
"opportunityDetails": {
"title": "Opportunity details",
"opportunityName": "Name",
"description": "Description",
"mainCommunication": "Main communication",
"residentsSpeak": "Residents speak",
Expand All @@ -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",
Expand Down
12 changes: 8 additions & 4 deletions src/components/Dashboard/Agents/Agents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<FiltersContentContainer data-testid="opportunity-filters-content">
<AccordionFilter header={t("dashboard.opportunities.filters.type.header")} items={typeFilters} />
<AccordionFilter header={t("dashboard.opportunities.filters.status.header")} items={statusFilters} />
<AccordionFilter header={t("dashboard.volunteers.filters.district")} items={districtFilters} />
<AccordionFilter header={t("dashboard.volunteers.filters.languages")} items={languageFilters} />
<AccordionFilter header={t("dashboard.volunteers.filters.activities")} items={activityFilters} />
</FiltersContentContainer>
);
}
3 changes: 2 additions & 1 deletion src/components/Dashboard/Opportunities/Filters/constants.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -16,4 +16,5 @@ export const defaultOpportunityCardsFilter: OpportunityCardsFilter = {
[OpportunityType.EVENTS]: false,
[OpportunityType.REGULAR]: false,
},
[EntityTableName.ACTIVITY]: {},
};
20 changes: 13 additions & 7 deletions src/components/Dashboard/Opportunities/Filters/helpers.ts
Original file line number Diff line number Diff line change
@@ -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 = (
Expand Down Expand Up @@ -31,18 +31,24 @@ 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 = (
filter: OpportunityCardsFilter,
setFilter: SetFilter<OpportunityCardsFilter>,
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);
};
3 changes: 2 additions & 1 deletion src/components/Dashboard/Opportunities/Filters/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { QueryParamsKeys } from "need4deed-sdk";
import { EntityTableName, QueryParamsKeys } from "need4deed-sdk";
import { SelectionMap } from "../../common/CardsFilter/types";

export interface OpportunityCardsFilter {
Expand All @@ -7,6 +7,7 @@ export interface OpportunityCardsFilter {
[QueryParamsKeys.LANGUAGE]: SelectionMap;
status: SelectionMap;
type: SelectionMap;
[EntityTableName.ACTIVITY]: SelectionMap;
}

export type OpportunityCardFilterKeys = keyof OpportunityCardsFilter;
16 changes: 10 additions & 6 deletions src/components/Dashboard/Opportunities/Opportunities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 (
<DashboardLayout>
<OpportunitiesContainer data-testid="opportunities-container">
Expand Down
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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<OpportunityStatusType, string> = {
Expand Down
21 changes: 13 additions & 8 deletions src/components/Dashboard/Opportunities/OpportunityCard.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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,
Expand All @@ -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();

Expand All @@ -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
Expand Down Expand Up @@ -103,9 +106,11 @@ export function OpportunityCard({ opportunity, volunteerId }: Props) {
)}
</CardDetail>

<CardDetail header={t("dashboard.volunteers.activities")} iconName={IconName.ShootingStar}>
<Tags tags={activityTitles} />
</CardDetail>
{activityTitles.length > 0 && (
<CardDetail header={t("dashboard.volunteers.activities")} iconName={IconName.ShootingStar}>
<Tags tags={activityTitles} />
</CardDetail>
)}

<CardDetail header={t("dashboard.opportunities.dateOfAppointment")} iconName={IconName.CalendarDots}>
{scheduleText && <CardParagraph text={scheduleText} />}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -21,9 +22,10 @@ export function OpportunityCardList({
currentPage,
setCurrentPage,
volunteerId,
activitiesList,
}: Props) {
const items = opportunities.map((opp) => (
<OpportunityCard key={opp.id} opportunity={opp} volunteerId={volunteerId} />
<OpportunityCard key={opp.id} opportunity={opp} volunteerId={volunteerId} activitiesList={activitiesList} />
));

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export function OpportunityListController({

return (
<OpportunityCardList
activitiesList={apiFilterOptions?.activity}
opportunities={opportunities}
count={count}
columns={columns - (isFiltersOpen ? 1 : 0)}
Expand Down
Loading