Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions public/locales/de/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions public/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
25 changes: 25 additions & 0 deletions src/components/Dashboard/Opportunities/OpportunityCard.helpers.tsx
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 @@ -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, string> = {
[OpportunityStatusType.NEW]: "var(--color-red-500)",
[OpportunityStatusType.SEARCHING]: "var(--color-orange-500, var(--color-red-500))",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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 (
Expand Down
12 changes: 12 additions & 0 deletions src/hooks/useUpdateOpportunityDetails.ts
Original file line number Diff line number Diff line change
@@ -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<ApiOpportunityPatch, { message: string; data: ApiOpportunityGet }>({
apiPath: `${apiPathOpportunity}/${opportunityId}`,
method: "patch",
successMessage: "dashboard.opportunityProfile.opportunityDetails.saveSuccess",
queryKeyToInvalidate: ["opportunity", String(opportunityId)],
});
};