diff --git a/public/locales/de/translations.json b/public/locales/de/translations.json index dd331d21..23983af6 100644 --- a/public/locales/de/translations.json +++ b/public/locales/de/translations.json @@ -1073,6 +1073,7 @@ "descriptionHint": "Max. {{max}} Zeichen", "cancel": "Abbrechen", "saveChanges": "Änderungen speichern", + "saveSuccess": "Opportunity-Details erfolgreich aktualisiert", "validation": { "descriptionRequired": "Beschreibung ist erforderlich", "descriptionTooLong": "Beschreibung ist zu lang", diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index c4d5abdf..cd64e390 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -1072,6 +1072,7 @@ "descriptionHint": "Max. {{max}} characters", "cancel": "Cancel", "saveChanges": "Save changes", + "saveSuccess": "Opportunity details updated successfully", "validation": { "descriptionRequired": "Description is required", "descriptionTooLong": "Description is too long", diff --git a/src/components/Dashboard/Opportunities/OpportunityCard.helpers.tsx b/src/components/Dashboard/Opportunities/OpportunityCard.helpers.tsx index 9423549d..be77bab8 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"; @@ -9,6 +10,30 @@ export function formatAvailability(availability: ApiVolunteerOpportunityGetList[ return parts.join(", "); } +export function formatAccompanyingDate(details?: { + appointmentDate?: string; + appointmentTime?: string; +}): string | null { + if (!details?.appointmentDate) return null; + + 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 = d.toTimeString().slice(0, 5); + } else { + formattedTime = details.appointmentTime; + } + } + + return [formattedDate, formattedTime].filter(Boolean).join(" "); +} + export const statusColorMap: Record = { [OpportunityStatusType.NEW]: "var(--color-red-500)", [OpportunityStatusType.SEARCHING]: "var(--color-orange-500, var(--color-red-500))", diff --git a/src/components/Dashboard/Profile/sections/AccompanyingDetails/helpers.ts b/src/components/Dashboard/Profile/sections/AccompanyingDetails/helpers.ts index 26c76c85..b8689e5b 100644 --- a/src/components/Dashboard/Profile/sections/AccompanyingDetails/helpers.ts +++ b/src/components/Dashboard/Profile/sections/AccompanyingDetails/helpers.ts @@ -30,7 +30,15 @@ export const parseDate = (date: Date | string | undefined): Date | null => { export const parseTime = (time: Date | string | undefined): string => { if (!time) return ""; - if (typeof time === "string") return time; + if (typeof time === "string") { + const [h, m] = time.split(":").map(Number); + if (!isNaN(h) && !isNaN(m)) { + const d = new Date(); + d.setUTCHours(h, m, 0, 0); + return d.toTimeString().slice(0, 5); + } + return time; + } return time.toTimeString().slice(0, 5); }; diff --git a/src/components/Dashboard/Profile/sections/OpportunityDetails/OpportunityDetailsEdit.tsx b/src/components/Dashboard/Profile/sections/OpportunityDetails/OpportunityDetailsEdit.tsx index cd566d5c..f4042bf3 100644 --- a/src/components/Dashboard/Profile/sections/OpportunityDetails/OpportunityDetailsEdit.tsx +++ b/src/components/Dashboard/Profile/sections/OpportunityDetails/OpportunityDetailsEdit.tsx @@ -4,17 +4,22 @@ import { DatePickerWithLabel } from "@/components/core/common/DatePicker"; import { EditableField } from "@/components/EditableField/EditableField"; import { AvailabilityGrid } from "@/components/forms/AvailabilityGrid/AvailabilityGrid"; import { LanguageFields } from "@/components/forms/LanguageFields"; -import { apiToFormAvailability } from "@/components/Dashboard/Profile/sections/VolunteerProfile/availabilityUtils"; import { + apiToFormAvailability, + formToApiAvailability, +} from "@/components/Dashboard/Profile/sections/VolunteerProfile/availabilityUtils"; +import { + ApiLanguageOption, useApiActivities, useApiLanguages, useApiSkills, } from "@/components/Dashboard/Profile/sections/VolunteerProfile/hooks"; import { createMapping } from "@/components/Dashboard/Profile/sections/VolunteerProfile/mappingUtils"; +import { useUpdateOpportunityDetails } from "@/hooks/useUpdateOpportunityDetails"; import { zodResolver } from "@hookform/resolvers/zod"; import { MAX_DESCRIPTION_LENGTH } from "@/config/constants"; import { de, enUS } from "date-fns/locale"; -import { ApiOpportunityGet, Lang, LangPurpose, VolunteerStateTypeType } from "need4deed-sdk"; +import { ApiOpportunityGet, Lang, LangPurpose, OptionItem, VolunteerStateTypeType } from "need4deed-sdk"; import { Controller, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { FormButtonRow, FormDetails } from "../shared/styles"; @@ -23,6 +28,23 @@ import { createOpportunityDetailsSchema, OpportunityDetailsFormData } from "./op import { DateFieldRow, DatePickerContainer, ErrorText, FieldGroup, TimeInput, TimeInputWrapper } from "./styles"; import { OpportunityWithDetails } from "./types"; +function toLangOptionItems(formLangs: { language: string }[], apiLanguages: ApiLanguageOption[]): OptionItem[] { + return formLangs.flatMap(({ language }) => { + if (!language) return []; + const found = apiLanguages.find((a) => a.title === language || a.title.toLowerCase() === language.toLowerCase()); + 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 +59,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 +101,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), + languagesResidents: toLangOptionItems(values.residentsSpeak, apiLanguages), + activities: toOptionItems(values.activities, apiActivities), + skills: toOptionItems(values.skills, apiSkills), + schedule: values.availability ? formToApiAvailability(values.availability) : undefined, + }, + { onSuccess: onCancel }, + ); }; return ( 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)], + }); +};