From 8af516552a6abf1fa2352d07366ba3c566ce4b7c Mon Sep 17 00:00:00 2001 From: Rodrigo Louro Date: Thu, 16 Apr 2026 13:14:33 +0200 Subject: [PATCH 1/6] Fix:Opportunity statuses #332 Fix: Opportunity statuses #332 --- public/locales/de/translations.json | 20 +++ public/locales/en/translations.json | 20 +++ .../Dashboard/Profile/common/statusMaps.ts | 167 ++++++++++-------- .../ChangeOpportunityStatusDialog.tsx | 39 ++++ .../opportunity/OpportunityHeader.tsx | 34 ++-- .../ProfileHeader/opportunity/constants.ts | 43 +++++ .../opportunity/useOpportunityStatusDialog.ts | 27 +++ 7 files changed, 255 insertions(+), 95 deletions(-) create mode 100644 src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/ChangeOpportunityStatusDialog.tsx create mode 100644 src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/constants.ts create mode 100644 src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/useOpportunityStatusDialog.ts diff --git a/public/locales/de/translations.json b/public/locales/de/translations.json index 1a2ca512..f2e69b6c 100644 --- a/public/locales/de/translations.json +++ b/public/locales/de/translations.json @@ -992,6 +992,26 @@ "unmatched": "Nicht gematcht" }, "statusUpdateSuccess": "Status der Möglichkeit erfolgreich aktualisiert", + "change_status": "Status ändern", + "statusModal": { + "title": "Opportunitätsstatus ändern", + "save": "Speichern", + "cancel": "Abbrechen", + "options": { + "new": "Neu", + "new_description": "Diese Möglichkeit wurde gerade erstellt.", + "searching": "Suchend", + "searching_description": "Wir suchen einen Freiwilligen für diese Möglichkeit.", + "cancelledByUs": "Von uns abgesagt", + "cancelledByUs_description": "Diese Möglichkeit wurde von Need4Deed abgesagt.", + "cancelledByNGO": "Von der NGO abgesagt", + "cancelledByNGO_description": "Diese Möglichkeit wurde von der NGO abgesagt.", + "matchedNotNeeded": "Gematcht - nicht benötigt", + "matchedNotNeeded_description": "Ein Freiwilliger wurde gematcht, wird aber nicht mehr benötigt.", + "matchedVolunteerNoShow": "Gematcht - Freiwilliger nicht erschienen", + "matchedVolunteerNoShow_description": "Ein Freiwilliger wurde gematcht, ist aber nicht erschienen." + } + }, "contactDetailsTitle": "Kontaktdaten", "editButtonName": "Bearbeiten", "contactDetails": { diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index b21fb0b2..c38277d1 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -991,6 +991,26 @@ "unmatched": "Unmatched" }, "statusUpdateSuccess": "Opportunity status updated successfully", + "change_status": "Change status", + "statusModal": { + "title": "Change opportunity status", + "save": "Save", + "cancel": "Cancel", + "options": { + "new": "New", + "new_description": "This opportunity has just been created.", + "searching": "Searching", + "searching_description": "We are searching for a volunteer for this opportunity.", + "cancelledByUs": "Cancelled by us", + "cancelledByUs_description": "This opportunity was cancelled by Need4Deed.", + "cancelledByNGO": "Cancelled by NGO", + "cancelledByNGO_description": "This opportunity was cancelled by the NGO.", + "matchedNotNeeded": "Matched - not needed", + "matchedNotNeeded_description": "A volunteer was matched but is no longer needed.", + "matchedVolunteerNoShow": "Matched - volunteer no show", + "matchedVolunteerNoShow_description": "A volunteer was matched but did not show up." + } + }, "contactDetailsTitle": "Contact details", "editButtonName": "Edit", "contactDetails": { diff --git a/src/components/Dashboard/Profile/common/statusMaps.ts b/src/components/Dashboard/Profile/common/statusMaps.ts index ea9dcdda..63ea8d4d 100644 --- a/src/components/Dashboard/Profile/common/statusMaps.ts +++ b/src/components/Dashboard/Profile/common/statusMaps.ts @@ -25,6 +25,7 @@ import { VolunteerStateTypeType, } from "need4deed-sdk"; import type React from "react"; +import { OpportunityManualStatusType } from "../sections/ProfileHeader/opportunity/constants"; import { AgentEngagementStatus, AgentTrustLevel, AgentVolunteerSearch } from "../types"; export type StatusValue = @@ -35,80 +36,104 @@ export type StatusValue = | OpportunityMatchStatus | AgentEngagementStatus | AgentVolunteerSearch - | AgentTrustLevel; + | AgentTrustLevel + | OpportunityManualStatusType; + +type SdkStatusValue = Exclude; + +type IconComponent = React.ComponentType<{ size?: number; color?: string }>; // Several SDK enums share the same underlying string values (e.g. "new", "active", // "pending-match"). Object literals with computed duplicate keys are a TS error, so -// these maps are built with Object.fromEntries — arrays have no such restriction and +// the SDK entries are built with Object.fromEntries — arrays have no such restriction and // the last entry for a given key wins, matching the original intended behaviour. -export const statusColorMap: Record = Object.fromEntries([ - [VolunteerStateEngagementType.ACTIVE, "var(--color-green-100)"], - [VolunteerStateEngagementType.AVAILABLE, "var(--color-violet-100)"], - [VolunteerStateEngagementType.TEMP_UNAVAILABLE, "var( --color-red-50)"], - [VolunteerStateEngagementType.UNRESPONSIVE, "var(--color-grey-50)"], - [VolunteerStateEngagementType.INACTIVE, "var(--color-grey-50)"], - [VolunteerStateEngagementType.NEW, "var(--color-green-100)"], - [OpportunityMatchStatus.UNMATCHED, "var(--color-grey-50)"], - [OpportunityMatchStatus.PENDING_MATCH, "var(--color-violet-100)"], - [OpportunityMatchStatus.MATCHED, "var(--color-green-100)"], - [OpportunityMatchStatus.NEEDS_REMATCH, "var(--color-red-50)"], - [VolunteerStateMatchType.NO_MATCHES, "var(--color-grey-50)"], - [VolunteerStateMatchType.PENDING_MATCH, "var(--color-violet-100)"], - [VolunteerStateMatchType.MATCHED, "var(--color-green-100)"], - [VolunteerStateMatchType.NEEDS_REMATCH, "var(--color-red-50)"], - [VolunteerStateTypeType.ACCOMPANYING, "var(--color-blue-500)"], - [VolunteerStateTypeType.EVENTS, "var(--color-blue-500)"], - [VolunteerStateTypeType.REGULAR, "var(--color-blue-500)"], - [VolunteerStateTypeType.REGULAR_ACCOMPANYING, "var(--color-blue-500)"], - [OpportunityStatusType.SEARCHING, "var(--color-violet-100)"], - [OpportunityStatusType.NEW, "var(--color-violet-100)"], - [OpportunityStatusType.ACTIVE, "var(--color-green-50)"], - [OpportunityStatusType.PAST, "var(--color-grey-50)"], - [AgentVolunteerSearch.NOT_NEEDED, "var(--color-grey-50)"], - [AgentVolunteerSearch.VOLUNTEERS_FOUND, "var(--color-green-100)"], - [AgentVolunteerSearch.SEARCHING, "var(--color-red-50)"], - [AgentTrustLevel.UNKNOWN, "var(--color-grey-50)"], - [AgentTrustLevel.LOW, "var(--color-red-50)"], - [AgentTrustLevel.HIGH, "var(--color-green-100)"], - [AgentEngagementStatusType.NEW, "var(--color-violet-100)"], - [AgentEngagementStatusType.ACTIVE, "var(--color-green-100)"], - [AgentEngagementStatusType.INACTIVE, "var(--color-grey-50)"], - [AgentEngagementStatusType.UNRESPONSIVE, "var(--color-grey-50)"], -]) as Record; +// OpportunityManualStatusType values are unique ("opp-*") and use a typed Record +// to preserve exhaustiveness checking. -type IconComponent = React.ComponentType<{ size?: number; color?: string }>; +const manualStatusColorMap: Record = { + [OpportunityManualStatusType.NEW]: "var(--color-violet-100)", + [OpportunityManualStatusType.SEARCHING]: "var(--color-violet-100)", + [OpportunityManualStatusType.INACTIVE]: "var(--color-grey-50)", +}; + +export const statusColorMap: Record = { + ...(Object.fromEntries([ + [VolunteerStateEngagementType.ACTIVE, "var(--color-green-100)"], + [VolunteerStateEngagementType.AVAILABLE, "var(--color-violet-100)"], + [VolunteerStateEngagementType.TEMP_UNAVAILABLE, "var( --color-red-50)"], + [VolunteerStateEngagementType.UNRESPONSIVE, "var(--color-grey-50)"], + [VolunteerStateEngagementType.INACTIVE, "var(--color-grey-50)"], + [VolunteerStateEngagementType.NEW, "var(--color-green-100)"], + [OpportunityMatchStatus.UNMATCHED, "var(--color-grey-50)"], + [OpportunityMatchStatus.PENDING_MATCH, "var(--color-violet-100)"], + [OpportunityMatchStatus.MATCHED, "var(--color-green-100)"], + [OpportunityMatchStatus.NEEDS_REMATCH, "var(--color-red-50)"], + [VolunteerStateMatchType.NO_MATCHES, "var(--color-grey-50)"], + [VolunteerStateMatchType.PENDING_MATCH, "var(--color-violet-100)"], + [VolunteerStateMatchType.MATCHED, "var(--color-green-100)"], + [VolunteerStateMatchType.NEEDS_REMATCH, "var(--color-red-50)"], + [VolunteerStateTypeType.ACCOMPANYING, "var(--color-blue-500)"], + [VolunteerStateTypeType.EVENTS, "var(--color-blue-500)"], + [VolunteerStateTypeType.REGULAR, "var(--color-blue-500)"], + [VolunteerStateTypeType.REGULAR_ACCOMPANYING, "var(--color-blue-500)"], + [OpportunityStatusType.SEARCHING, "var(--color-violet-100)"], + [OpportunityStatusType.NEW, "var(--color-violet-100)"], + [OpportunityStatusType.ACTIVE, "var(--color-green-50)"], + [OpportunityStatusType.PAST, "var(--color-grey-50)"], + [AgentVolunteerSearch.NOT_NEEDED, "var(--color-grey-50)"], + [AgentVolunteerSearch.VOLUNTEERS_FOUND, "var(--color-green-100)"], + [AgentVolunteerSearch.SEARCHING, "var(--color-red-50)"], + [AgentTrustLevel.UNKNOWN, "var(--color-grey-50)"], + [AgentTrustLevel.LOW, "var(--color-red-50)"], + [AgentTrustLevel.HIGH, "var(--color-green-100)"], + [AgentEngagementStatusType.NEW, "var(--color-violet-100)"], + [AgentEngagementStatusType.ACTIVE, "var(--color-green-100)"], + [AgentEngagementStatusType.INACTIVE, "var(--color-grey-50)"], + [AgentEngagementStatusType.UNRESPONSIVE, "var(--color-grey-50)"], + ]) as Record), + ...manualStatusColorMap, +}; + +const manualStatusIconMap: Record = { + [OpportunityManualStatusType.NEW]: SparkleIcon, + [OpportunityManualStatusType.SEARCHING]: HourglassIcon, + [OpportunityManualStatusType.INACTIVE]: StopCircleIcon, +}; -export const statusIconMap: Record = Object.fromEntries([ - [VolunteerStateEngagementType.ACTIVE, ChartLineIcon], - [VolunteerStateEngagementType.AVAILABLE, CalendarBlankIcon], - [VolunteerStateEngagementType.TEMP_UNAVAILABLE, CalendarXIcon], - [VolunteerStateEngagementType.UNRESPONSIVE, PhoneXIcon], - [VolunteerStateEngagementType.INACTIVE, StopCircleIcon], - [VolunteerStateEngagementType.NEW, SparkleIcon], - [OpportunityMatchStatus.UNMATCHED, ProhibitInsetIcon], - [OpportunityMatchStatus.PENDING_MATCH, HourglassIcon], - [OpportunityMatchStatus.MATCHED, CheckCircleIcon], - [OpportunityMatchStatus.NEEDS_REMATCH, ArrowsClockwiseIcon], - [VolunteerStateMatchType.NO_MATCHES, ProhibitInsetIcon], - [VolunteerStateMatchType.PENDING_MATCH, HourglassIcon], - [VolunteerStateMatchType.MATCHED, CheckCircleIcon], - [VolunteerStateMatchType.NEEDS_REMATCH, ArrowsClockwiseIcon], - [VolunteerStateTypeType.ACCOMPANYING, UsersIcon], - [VolunteerStateTypeType.EVENTS, UsersIcon], - [VolunteerStateTypeType.REGULAR, UsersIcon], - [VolunteerStateTypeType.REGULAR_ACCOMPANYING, UsersIcon], - [OpportunityStatusType.NEW, SparkleIcon], - [OpportunityStatusType.ACTIVE, ChartLineIcon], - [OpportunityStatusType.SEARCHING, HourglassIcon], - [OpportunityStatusType.PAST, StopCircleIcon], - [AgentVolunteerSearch.NOT_NEEDED, HandPalmIcon], - [AgentVolunteerSearch.VOLUNTEERS_FOUND, CheckCircleIcon], - [AgentVolunteerSearch.SEARCHING, BinocularsIcon], - [AgentTrustLevel.UNKNOWN, QuestionIcon], - [AgentTrustLevel.LOW, SmileySadIcon], - [AgentTrustLevel.HIGH, SmileyIcon], - [AgentEngagementStatusType.NEW, SparkleIcon], - [AgentEngagementStatusType.ACTIVE, ChartLineIcon], - [AgentEngagementStatusType.INACTIVE, StopCircleIcon], - [AgentEngagementStatusType.UNRESPONSIVE, PhoneXIcon], -]) as unknown as Record; +export const statusIconMap: Record = { + ...(Object.fromEntries([ + [VolunteerStateEngagementType.ACTIVE, ChartLineIcon], + [VolunteerStateEngagementType.AVAILABLE, CalendarBlankIcon], + [VolunteerStateEngagementType.TEMP_UNAVAILABLE, CalendarXIcon], + [VolunteerStateEngagementType.UNRESPONSIVE, PhoneXIcon], + [VolunteerStateEngagementType.INACTIVE, StopCircleIcon], + [VolunteerStateEngagementType.NEW, SparkleIcon], + [OpportunityMatchStatus.UNMATCHED, ProhibitInsetIcon], + [OpportunityMatchStatus.PENDING_MATCH, HourglassIcon], + [OpportunityMatchStatus.MATCHED, CheckCircleIcon], + [OpportunityMatchStatus.NEEDS_REMATCH, ArrowsClockwiseIcon], + [VolunteerStateMatchType.NO_MATCHES, ProhibitInsetIcon], + [VolunteerStateMatchType.PENDING_MATCH, HourglassIcon], + [VolunteerStateMatchType.MATCHED, CheckCircleIcon], + [VolunteerStateMatchType.NEEDS_REMATCH, ArrowsClockwiseIcon], + [VolunteerStateTypeType.ACCOMPANYING, UsersIcon], + [VolunteerStateTypeType.EVENTS, UsersIcon], + [VolunteerStateTypeType.REGULAR, UsersIcon], + [VolunteerStateTypeType.REGULAR_ACCOMPANYING, UsersIcon], + [OpportunityStatusType.NEW, SparkleIcon], + [OpportunityStatusType.ACTIVE, ChartLineIcon], + [OpportunityStatusType.SEARCHING, HourglassIcon], + [OpportunityStatusType.PAST, StopCircleIcon], + [AgentVolunteerSearch.NOT_NEEDED, HandPalmIcon], + [AgentVolunteerSearch.VOLUNTEERS_FOUND, CheckCircleIcon], + [AgentVolunteerSearch.SEARCHING, BinocularsIcon], + [AgentTrustLevel.UNKNOWN, QuestionIcon], + [AgentTrustLevel.LOW, SmileySadIcon], + [AgentTrustLevel.HIGH, SmileyIcon], + [AgentEngagementStatusType.NEW, SparkleIcon], + [AgentEngagementStatusType.ACTIVE, ChartLineIcon], + [AgentEngagementStatusType.INACTIVE, StopCircleIcon], + [AgentEngagementStatusType.UNRESPONSIVE, PhoneXIcon], + ]) as unknown as Record), + ...manualStatusIconMap, +}; diff --git a/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/ChangeOpportunityStatusDialog.tsx b/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/ChangeOpportunityStatusDialog.tsx new file mode 100644 index 00000000..e1dafcd9 --- /dev/null +++ b/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/ChangeOpportunityStatusDialog.tsx @@ -0,0 +1,39 @@ +"use client"; +import { useTranslation } from "react-i18next"; +import { ChangeStatusDialog } from "../common"; +import { createOpportunityStatusLabelMap, OpportunityManualStatusType, STATUS_DESCRIPTION_KEYS } from "./constants"; +import { UseOpportunityStatusDialogReturn } from "./useOpportunityStatusDialog"; + +type Props = { + dialog: UseOpportunityStatusDialogReturn; +}; + +export const ChangeOpportunityStatusDialog = ({ + dialog: { isOpen, closeDialog, selected, setSelected, saveDialog, isSaveDisabled }, +}: Props) => { + const { t } = useTranslation(); + const statusLabelMap = createOpportunityStatusLabelMap(t); + + const options = Object.values(OpportunityManualStatusType).map((status) => ({ + value: status, + label: statusLabelMap[status], + description: t(`dashboard.opportunityProfile.statusModal.options.${STATUS_DESCRIPTION_KEYS[status]}`), + })); + + return ( + + ); +}; diff --git a/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/OpportunityHeader.tsx b/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/OpportunityHeader.tsx index 947ba9e6..3aa8eda8 100644 --- a/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/OpportunityHeader.tsx +++ b/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/OpportunityHeader.tsx @@ -1,37 +1,26 @@ "use client"; import { EMPTY_PLACEHOLDER_VALUE } from "@/config/constants"; -import { useUpdateOpportunityStatus } from "@/hooks/useUpdateOpportunityStatus"; import { formatDateTime } from "@/utils"; import { ShootingStarIcon } from "@phosphor-icons/react"; -import { ApiOpportunityGet, OpportunityStatusType } from "need4deed-sdk"; +import { ApiOpportunityGet } from "need4deed-sdk"; import { useTranslation } from "react-i18next"; import { createVolunteerTypeLabelMap, EditButton, HeaderCard, IconContainer, StatusRowField } from "../common"; +import { ChangeOpportunityStatusDialog } from "./ChangeOpportunityStatusDialog"; +import { createOpportunityStatusLabelMap } from "./constants"; +import { useOpportunityStatusDialog } from "./useOpportunityStatusDialog"; type Props = { opportunity: ApiOpportunityGet; }; -const createStatusLabelMap = (t: (key: string) => string): Record => ({ - [OpportunityStatusType.NEW]: t("dashboard.opportunityProfile.status.new"), - [OpportunityStatusType.ACTIVE]: t("dashboard.opportunityProfile.status.active"), - [OpportunityStatusType.PAST]: t("dashboard.opportunityProfile.status.past"), - [OpportunityStatusType.SEARCHING]: t("dashboard.opportunityProfile.status.searching"), -}); - export const OpportunityHeader = ({ opportunity }: Props) => { const { t } = useTranslation(); - const { mutate: updateStatus } = useUpdateOpportunityStatus(opportunity.id); - - const statusLabels = createStatusLabelMap(t); + const dialog = useOpportunityStatusDialog(opportunity); + const statusLabelMap = createOpportunityStatusLabelMap(t); const volunteerTypeLabelMap = createVolunteerTypeLabelMap(t); const postedDate = opportunity.createdAt ? formatDateTime(opportunity.createdAt) : EMPTY_PLACEHOLDER_VALUE; const subtitle = `${t("dashboard.opportunityProfile.postedOn")} ${postedDate}`; - const isButtonDisabled = opportunity.statusOpportunity !== OpportunityStatusType.NEW; - - const handleStatusChange = () => { - updateStatus({ statusOpportunity: OpportunityStatusType.SEARCHING }); - }; return ( { } title={opportunity.title} subtitle={subtitle} + after={} > - {t("dashboard.volunteerProfile.volunteerHeader.change_status")} - - } + status={dialog.selected} + label={statusLabelMap[dialog.selected]} + action={{t("dashboard.opportunityProfile.change_status")}} /> > = { + [OpportunityStatusType.NEW]: OpportunityManualStatusType.NEW, + [OpportunityStatusType.SEARCHING]: OpportunityManualStatusType.SEARCHING, +}; + +export const MANUAL_TO_SDK: Partial> = { + [OpportunityManualStatusType.NEW]: OpportunityStatusType.NEW, + [OpportunityManualStatusType.SEARCHING]: OpportunityStatusType.SEARCHING, +}; + +export const STATUS_DESCRIPTION_KEYS: Record = { + [OpportunityManualStatusType.NEW]: "new_description", + [OpportunityManualStatusType.SEARCHING]: "searching_description", + [OpportunityManualStatusType.CANCELLED_BY_US]: "cancelledByUs_description", + [OpportunityManualStatusType.CANCELLED_BY_NGO]: "cancelledByNGO_description", + [OpportunityManualStatusType.MATCHED_NOT_NEEDED]: "matchedNotNeeded_description", + [OpportunityManualStatusType.MATCHED_VOLUNTEER_NO_SHOW]: "matchedVolunteerNoShow_description", +}; + +export const createOpportunityStatusLabelMap = (t: TFunction): Record => ({ + [OpportunityManualStatusType.NEW]: t("dashboard.opportunityProfile.statusModal.options.new"), + [OpportunityManualStatusType.SEARCHING]: t("dashboard.opportunityProfile.statusModal.options.searching"), + [OpportunityManualStatusType.CANCELLED_BY_US]: t("dashboard.opportunityProfile.statusModal.options.cancelledByUs"), + [OpportunityManualStatusType.CANCELLED_BY_NGO]: t("dashboard.opportunityProfile.statusModal.options.cancelledByNGO"), + [OpportunityManualStatusType.MATCHED_NOT_NEEDED]: t( + "dashboard.opportunityProfile.statusModal.options.matchedNotNeeded", + ), + [OpportunityManualStatusType.MATCHED_VOLUNTEER_NO_SHOW]: t( + "dashboard.opportunityProfile.statusModal.options.matchedVolunteerNoShow", + ), +}); diff --git a/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/useOpportunityStatusDialog.ts b/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/useOpportunityStatusDialog.ts new file mode 100644 index 00000000..23233190 --- /dev/null +++ b/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/useOpportunityStatusDialog.ts @@ -0,0 +1,27 @@ +import { useUpdateOpportunityStatus } from "@/hooks/useUpdateOpportunityStatus"; +import { ApiOpportunityGet, OpportunityStatusType } from "need4deed-sdk"; +import { useStatusDialog, UseStatusDialogReturn } from "../common/useStatusDialog"; +import { MANUAL_TO_SDK, OpportunityManualStatusType, SDK_TO_MANUAL } from "./constants"; + +export type UseOpportunityStatusDialogReturn = UseStatusDialogReturn; + +const toManualStatus = (status: OpportunityStatusType): OpportunityManualStatusType => + SDK_TO_MANUAL[status] ?? OpportunityManualStatusType.NEW; + +export const useOpportunityStatusDialog = (opportunity: ApiOpportunityGet): UseOpportunityStatusDialogReturn => { + const { mutate: updateStatus } = useUpdateOpportunityStatus(opportunity.id); + + const onSave = (status: OpportunityManualStatusType, { onSuccess }: { onSuccess: () => void }) => { + const sdkStatus = MANUAL_TO_SDK[status]; + if (sdkStatus) { + updateStatus({ statusOpportunity: sdkStatus }, { onSuccess }); + } else { + onSuccess(); + } + }; + + return useStatusDialog({ + initial: toManualStatus(opportunity.statusOpportunity), + onSave, + }); +}; From 416a13cbe394d32404064417afce06ab068b244b Mon Sep 17 00:00:00 2001 From: Rodrigo Louro Date: Thu, 16 Apr 2026 15:32:08 +0200 Subject: [PATCH 2/6] fix: update opportunity status descriptions #332 fix: update opportunity status descriptions #332 --- public/locales/de/translations.json | 10 +++++----- public/locales/en/translations.json | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/public/locales/de/translations.json b/public/locales/de/translations.json index f2e69b6c..1a69ca1f 100644 --- a/public/locales/de/translations.json +++ b/public/locales/de/translations.json @@ -999,15 +999,15 @@ "cancel": "Abbrechen", "options": { "new": "Neu", - "new_description": "Diese Möglichkeit wurde gerade erstellt.", + "new_description": "Die Möglichkeit wurde noch nicht validiert.", "searching": "Suchend", - "searching_description": "Wir suchen einen Freiwilligen für diese Möglichkeit.", + "searching_description": "Die Möglichkeit wurde validiert, es kann nach ihr gesucht werden.", "cancelledByUs": "Von uns abgesagt", - "cancelledByUs_description": "Diese Möglichkeit wurde von Need4Deed abgesagt.", + "cancelledByUs_description": "Die Möglichkeit wurde abgesagt - kein Freiwilliger gefunden.", "cancelledByNGO": "Von der NGO abgesagt", - "cancelledByNGO_description": "Diese Möglichkeit wurde von der NGO abgesagt.", + "cancelledByNGO_description": "Die Möglichkeit wurde von der NGO abgesagt.", "matchedNotNeeded": "Gematcht - nicht benötigt", - "matchedNotNeeded_description": "Ein Freiwilliger wurde gematcht, wird aber nicht mehr benötigt.", + "matchedNotNeeded_description": "Ein Freiwilliger wurde gematcht, wird aber in letzter Minute nicht mehr benötigt.", "matchedVolunteerNoShow": "Gematcht - Freiwilliger nicht erschienen", "matchedVolunteerNoShow_description": "Ein Freiwilliger wurde gematcht, ist aber nicht erschienen." } diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index c38277d1..60c23d2f 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -998,15 +998,15 @@ "cancel": "Cancel", "options": { "new": "New", - "new_description": "This opportunity has just been created.", + "new_description": "The opportunity was not validated yet.", "searching": "Searching", - "searching_description": "We are searching for a volunteer for this opportunity.", + "searching_description": "The opportunity was validated, it can be searched for.", "cancelledByUs": "Cancelled by us", - "cancelledByUs_description": "This opportunity was cancelled by Need4Deed.", + "cancelledByUs_description": "The opportunity was cancelled - no volunteer found.", "cancelledByNGO": "Cancelled by NGO", - "cancelledByNGO_description": "This opportunity was cancelled by the NGO.", + "cancelledByNGO_description": "The opportunity was cancelled - by NGO.", "matchedNotNeeded": "Matched - not needed", - "matchedNotNeeded_description": "A volunteer was matched but is no longer needed.", + "matchedNotNeeded_description": "A volunteer was matched but at the last minute not needed.", "matchedVolunteerNoShow": "Matched - volunteer no show", "matchedVolunteerNoShow_description": "A volunteer was matched but did not show up." } From 70b53c6c51049c4f2e4108586561a2d224cf9a93 Mon Sep 17 00:00:00 2001 From: Rodrigo Louro Date: Fri, 24 Apr 2026 15:58:16 +0200 Subject: [PATCH 3/6] Fix: 3 Selections Fix: 3 Selections --- public/locales/de/translations.json | 12 +++-------- public/locales/en/translations.json | 12 +++-------- .../ProfileHeader/opportunity/constants.ts | 20 ++++--------------- 3 files changed, 10 insertions(+), 34 deletions(-) diff --git a/public/locales/de/translations.json b/public/locales/de/translations.json index 1a69ca1f..28afd3a4 100644 --- a/public/locales/de/translations.json +++ b/public/locales/de/translations.json @@ -998,18 +998,12 @@ "save": "Speichern", "cancel": "Abbrechen", "options": { - "new": "Neu", + "new": "Neu / Validierung erforderlich", "new_description": "Die Möglichkeit wurde noch nicht validiert.", "searching": "Suchend", "searching_description": "Die Möglichkeit wurde validiert, es kann nach ihr gesucht werden.", - "cancelledByUs": "Von uns abgesagt", - "cancelledByUs_description": "Die Möglichkeit wurde abgesagt - kein Freiwilliger gefunden.", - "cancelledByNGO": "Von der NGO abgesagt", - "cancelledByNGO_description": "Die Möglichkeit wurde von der NGO abgesagt.", - "matchedNotNeeded": "Gematcht - nicht benötigt", - "matchedNotNeeded_description": "Ein Freiwilliger wurde gematcht, wird aber in letzter Minute nicht mehr benötigt.", - "matchedVolunteerNoShow": "Gematcht - Freiwilliger nicht erschienen", - "matchedVolunteerNoShow_description": "Ein Freiwilliger wurde gematcht, ist aber nicht erschienen." + "inactive": "Inaktiv", + "inactive_description": "Die Möglichkeit ist nicht mehr aktiv." } }, "contactDetailsTitle": "Kontaktdaten", diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 60c23d2f..690e07b6 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -997,18 +997,12 @@ "save": "Save", "cancel": "Cancel", "options": { - "new": "New", + "new": "New / Needs validation", "new_description": "The opportunity was not validated yet.", "searching": "Searching", "searching_description": "The opportunity was validated, it can be searched for.", - "cancelledByUs": "Cancelled by us", - "cancelledByUs_description": "The opportunity was cancelled - no volunteer found.", - "cancelledByNGO": "Cancelled by NGO", - "cancelledByNGO_description": "The opportunity was cancelled - by NGO.", - "matchedNotNeeded": "Matched - not needed", - "matchedNotNeeded_description": "A volunteer was matched but at the last minute not needed.", - "matchedVolunteerNoShow": "Matched - volunteer no show", - "matchedVolunteerNoShow_description": "A volunteer was matched but did not show up." + "inactive": "Inactive", + "inactive_description": "The opportunity is no longer active." } }, "contactDetailsTitle": "Contact details", diff --git a/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/constants.ts b/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/constants.ts index 406a7ddc..e11f5c70 100644 --- a/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/constants.ts +++ b/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/constants.ts @@ -4,10 +4,7 @@ import { OpportunityStatusType } from "need4deed-sdk"; export enum OpportunityManualStatusType { NEW = "opp-new", SEARCHING = "opp-searching", - CANCELLED_BY_US = "opp-cancelled-by-us", - CANCELLED_BY_NGO = "opp-cancelled-by-ngo", - MATCHED_NOT_NEEDED = "opp-matched-not-needed", - MATCHED_VOLUNTEER_NO_SHOW = "opp-matched-volunteer-no-show", + INACTIVE = "opp-inactive", } export const SDK_TO_MANUAL: Partial> = { @@ -18,26 +15,17 @@ export const SDK_TO_MANUAL: Partial> = { [OpportunityManualStatusType.NEW]: OpportunityStatusType.NEW, [OpportunityManualStatusType.SEARCHING]: OpportunityStatusType.SEARCHING, + [OpportunityManualStatusType.INACTIVE]: OpportunityStatusType.PAST, }; export const STATUS_DESCRIPTION_KEYS: Record = { [OpportunityManualStatusType.NEW]: "new_description", [OpportunityManualStatusType.SEARCHING]: "searching_description", - [OpportunityManualStatusType.CANCELLED_BY_US]: "cancelledByUs_description", - [OpportunityManualStatusType.CANCELLED_BY_NGO]: "cancelledByNGO_description", - [OpportunityManualStatusType.MATCHED_NOT_NEEDED]: "matchedNotNeeded_description", - [OpportunityManualStatusType.MATCHED_VOLUNTEER_NO_SHOW]: "matchedVolunteerNoShow_description", + [OpportunityManualStatusType.INACTIVE]: "inactive_description", }; export const createOpportunityStatusLabelMap = (t: TFunction): Record => ({ [OpportunityManualStatusType.NEW]: t("dashboard.opportunityProfile.statusModal.options.new"), [OpportunityManualStatusType.SEARCHING]: t("dashboard.opportunityProfile.statusModal.options.searching"), - [OpportunityManualStatusType.CANCELLED_BY_US]: t("dashboard.opportunityProfile.statusModal.options.cancelledByUs"), - [OpportunityManualStatusType.CANCELLED_BY_NGO]: t("dashboard.opportunityProfile.statusModal.options.cancelledByNGO"), - [OpportunityManualStatusType.MATCHED_NOT_NEEDED]: t( - "dashboard.opportunityProfile.statusModal.options.matchedNotNeeded", - ), - [OpportunityManualStatusType.MATCHED_VOLUNTEER_NO_SHOW]: t( - "dashboard.opportunityProfile.statusModal.options.matchedVolunteerNoShow", - ), + [OpportunityManualStatusType.INACTIVE]: t("dashboard.opportunityProfile.statusModal.options.inactive"), }); From c9687f1ba8b52b5c8a1f9b10e96984875de38d3e Mon Sep 17 00:00:00 2001 From: Rodrigo Louro Date: Mon, 27 Apr 2026 10:39:31 +0200 Subject: [PATCH 4/6] Update : Opportunity statuses #332 Update : Opportunity statuses #332 --- public/locales/de/translations.json | 2 +- public/locales/en/translations.json | 2 +- .../Profile/sections/ProfileHeader/opportunity/constants.ts | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/public/locales/de/translations.json b/public/locales/de/translations.json index 28afd3a4..0dd5f6d3 100644 --- a/public/locales/de/translations.json +++ b/public/locales/de/translations.json @@ -1000,7 +1000,7 @@ "options": { "new": "Neu / Validierung erforderlich", "new_description": "Die Möglichkeit wurde noch nicht validiert.", - "searching": "Suchend", + "searching": "Validiert", "searching_description": "Die Möglichkeit wurde validiert, es kann nach ihr gesucht werden.", "inactive": "Inaktiv", "inactive_description": "Die Möglichkeit ist nicht mehr aktiv." diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 690e07b6..b79cb325 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -999,7 +999,7 @@ "options": { "new": "New / Needs validation", "new_description": "The opportunity was not validated yet.", - "searching": "Searching", + "searching": "Validated", "searching_description": "The opportunity was validated, it can be searched for.", "inactive": "Inactive", "inactive_description": "The opportunity is no longer active." diff --git a/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/constants.ts b/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/constants.ts index e11f5c70..5fcc1e5f 100644 --- a/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/constants.ts +++ b/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/constants.ts @@ -10,6 +10,8 @@ export enum OpportunityManualStatusType { export const SDK_TO_MANUAL: Partial> = { [OpportunityStatusType.NEW]: OpportunityManualStatusType.NEW, [OpportunityStatusType.SEARCHING]: OpportunityManualStatusType.SEARCHING, + [OpportunityStatusType.ACTIVE]: OpportunityManualStatusType.SEARCHING, + [OpportunityStatusType.PAST]: OpportunityManualStatusType.INACTIVE, }; export const MANUAL_TO_SDK: Partial> = { From fc420d07809d2d2179fc217b9761d8b4015eb095 Mon Sep 17 00:00:00 2001 From: Rodrigo Louro Date: Tue, 28 Apr 2026 11:21:52 +0200 Subject: [PATCH 5/6] Update: add INACTIVE opportunity status Update: add INACTIVE opportunity status --- .../Dashboard/Opportunities/OpportunityCard.helpers.tsx | 4 ++++ src/components/Dashboard/Profile/common/statusMaps.ts | 2 ++ .../Profile/sections/ProfileHeader/opportunity/constants.ts | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/Dashboard/Opportunities/OpportunityCard.helpers.tsx b/src/components/Dashboard/Opportunities/OpportunityCard.helpers.tsx index 4434ca49..6c0704a8 100644 --- a/src/components/Dashboard/Opportunities/OpportunityCard.helpers.tsx +++ b/src/components/Dashboard/Opportunities/OpportunityCard.helpers.tsx @@ -21,6 +21,7 @@ export const statusColorMap: Record = { [OpportunityStatusType.NEW]: "var(--color-red-500)", [OpportunityStatusType.SEARCHING]: "var(--color-orange-500, var(--color-red-500))", [OpportunityStatusType.ACTIVE]: "var(--color-green-700)", + [OpportunityStatusType.INACTIVE]: "var(--color-grey-700)", [OpportunityStatusType.PAST]: "var(--color-grey-700)", }; @@ -30,6 +31,9 @@ export const statusIconMap: Record = { ), [OpportunityStatusType.ACTIVE]: , + [OpportunityStatusType.INACTIVE]: ( + + ), [OpportunityStatusType.PAST]: , }; diff --git a/src/components/Dashboard/Profile/common/statusMaps.ts b/src/components/Dashboard/Profile/common/statusMaps.ts index 63ea8d4d..82af3325 100644 --- a/src/components/Dashboard/Profile/common/statusMaps.ts +++ b/src/components/Dashboard/Profile/common/statusMaps.ts @@ -79,6 +79,7 @@ export const statusColorMap: Record = { [OpportunityStatusType.SEARCHING, "var(--color-violet-100)"], [OpportunityStatusType.NEW, "var(--color-violet-100)"], [OpportunityStatusType.ACTIVE, "var(--color-green-50)"], + [OpportunityStatusType.INACTIVE, "var(--color-grey-50)"], [OpportunityStatusType.PAST, "var(--color-grey-50)"], [AgentVolunteerSearch.NOT_NEEDED, "var(--color-grey-50)"], [AgentVolunteerSearch.VOLUNTEERS_FOUND, "var(--color-green-100)"], @@ -123,6 +124,7 @@ export const statusIconMap: Record = { [OpportunityStatusType.NEW, SparkleIcon], [OpportunityStatusType.ACTIVE, ChartLineIcon], [OpportunityStatusType.SEARCHING, HourglassIcon], + [OpportunityStatusType.INACTIVE, StopCircleIcon], [OpportunityStatusType.PAST, StopCircleIcon], [AgentVolunteerSearch.NOT_NEEDED, HandPalmIcon], [AgentVolunteerSearch.VOLUNTEERS_FOUND, CheckCircleIcon], diff --git a/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/constants.ts b/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/constants.ts index 5fcc1e5f..5674b4e1 100644 --- a/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/constants.ts +++ b/src/components/Dashboard/Profile/sections/ProfileHeader/opportunity/constants.ts @@ -11,13 +11,14 @@ export const SDK_TO_MANUAL: Partial> = { [OpportunityManualStatusType.NEW]: OpportunityStatusType.NEW, [OpportunityManualStatusType.SEARCHING]: OpportunityStatusType.SEARCHING, - [OpportunityManualStatusType.INACTIVE]: OpportunityStatusType.PAST, + [OpportunityManualStatusType.INACTIVE]: OpportunityStatusType.INACTIVE, }; export const STATUS_DESCRIPTION_KEYS: Record = { From 6ef4f7553ba0ae41e1b662c0046168b7f26e4bca Mon Sep 17 00:00:00 2001 From: Rodrigo Louro Date: Tue, 28 Apr 2026 11:26:25 +0200 Subject: [PATCH 6/6] update need4deed-sdk to 0.0.78 update need4deed-sdk to 0.0.78 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 3ab76ba2..53797b48 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "date-fns": "^4.1.0", "email-validator": "^2.0.4", "i18next": "^25.3.2", - "need4deed-sdk": "^0.0.77", + "need4deed-sdk": "^0.0.78", "next": "15.3.8", "react": "^19.0.0", "react-day-picker": "^9.13.0", diff --git a/yarn.lock b/yarn.lock index e97c5d02..f5494698 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2407,10 +2407,10 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -need4deed-sdk@^0.0.77: - version "0.0.77" - resolved "https://registry.yarnpkg.com/need4deed-sdk/-/need4deed-sdk-0.0.77.tgz#2d48d96472ca525b8d9c01d9d927f4774b6f4585" - integrity sha512-8kgLl8kZqFzy1kGpLLtiZrvCJbfu67kDtjMBtupI6wmqh/0bNGzGMt1Gas1zRXHv0B7pRsyJJQg3Nl6hAPeG6Q== +need4deed-sdk@^0.0.78: + version "0.0.78" + resolved "https://registry.yarnpkg.com/need4deed-sdk/-/need4deed-sdk-0.0.78.tgz#cb4b307b39ab9c9a970e9466fe4cfe2a39675fad" + integrity sha512-gkJqi9q1vNUS1aHrbNK958Uu+WXqqJK+BJnvXEzOtSf2Fp8UBv7mzYGgBCgQ718IuW1Bj3VhJqtxoJqZZ2Xgiw== next@15.3.8: version "15.3.8"