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/public/locales/de/translations.json b/public/locales/de/translations.json index 1a2ca512..0dd5f6d3 100644 --- a/public/locales/de/translations.json +++ b/public/locales/de/translations.json @@ -992,6 +992,20 @@ "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 / Validierung erforderlich", + "new_description": "Die Möglichkeit wurde noch nicht validiert.", + "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." + } + }, "contactDetailsTitle": "Kontaktdaten", "editButtonName": "Bearbeiten", "contactDetails": { diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index b21fb0b2..b79cb325 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -991,6 +991,20 @@ "unmatched": "Unmatched" }, "statusUpdateSuccess": "Opportunity status updated successfully", + "change_status": "Change status", + "statusModal": { + "title": "Change opportunity status", + "save": "Save", + "cancel": "Cancel", + "options": { + "new": "New / Needs validation", + "new_description": "The opportunity was not validated yet.", + "searching": "Validated", + "searching_description": "The opportunity was validated, it can be searched for.", + "inactive": "Inactive", + "inactive_description": "The opportunity is no longer active." + } + }, "contactDetailsTitle": "Contact details", "editButtonName": "Edit", "contactDetails": { 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 ea9dcdda..82af3325 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,106 @@ 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.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)"], + [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.INACTIVE, StopCircleIcon], + [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, + [OpportunityStatusType.ACTIVE]: OpportunityManualStatusType.SEARCHING, + [OpportunityStatusType.INACTIVE]: OpportunityManualStatusType.INACTIVE, + [OpportunityStatusType.PAST]: OpportunityManualStatusType.INACTIVE, +}; + +export const MANUAL_TO_SDK: Partial> = { + [OpportunityManualStatusType.NEW]: OpportunityStatusType.NEW, + [OpportunityManualStatusType.SEARCHING]: OpportunityStatusType.SEARCHING, + [OpportunityManualStatusType.INACTIVE]: OpportunityStatusType.INACTIVE, +}; + +export const STATUS_DESCRIPTION_KEYS: Record = { + [OpportunityManualStatusType.NEW]: "new_description", + [OpportunityManualStatusType.SEARCHING]: "searching_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.INACTIVE]: t("dashboard.opportunityProfile.statusModal.options.inactive"), +}); 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, + }); +}; 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"