From 5ba7b37b8e727c3f7ea8e000b0cd8257a2007adf Mon Sep 17 00:00:00 2001 From: ivannissimrch Date: Tue, 11 Aug 2026 20:23:27 -0400 Subject: [PATCH 1/9] add send per post option to appreciation dialog --- public/locales/de/translations.json | 3 ++ public/locales/en/translations.json | 3 ++ .../sections/Appreciation/Appreciation.tsx | 36 +++++++-------- .../Appreciation/AppreciationDialog.tsx | 46 +++++++++++-------- .../Profile/sections/Appreciation/styles.ts | 10 +++- .../Profile/sections/Appreciation/types.ts | 4 ++ src/hooks/useAppreciationTracker.ts | 5 +- 7 files changed, 67 insertions(+), 40 deletions(-) create mode 100644 src/components/Dashboard/Profile/sections/Appreciation/types.ts diff --git a/public/locales/de/translations.json b/public/locales/de/translations.json index a55ef906..b1415143 100644 --- a/public/locales/de/translations.json +++ b/public/locales/de/translations.json @@ -1010,6 +1010,7 @@ "receivedOn": "Erhalten am", "statusReceived": "Erhalten", "statusDueTo": "Fällig am", + "statusMailedOn": "Geschickt am", "typeOptions": { "tshirt": "T-Shirt", "toteBag": "Stofftasche", @@ -1027,6 +1028,8 @@ "volunteerReceivedIt": "Der*die Ehrenamtliche hat es erhalten", "needToGiveIt": "Muss dem*der Ehrenamtlichen gegeben werden", "receivedOnRequired": "Erhalten am*", + "volunteerPostIt": "Wurde dem*der Ehrenamtlichen per Post geschickt", + "postOnRequired": "Geschickt am*", "today": "heute", "cancel": "Abbrechen", "save": "Speichern", diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 5bd0a631..c0bdbb5b 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -998,6 +998,7 @@ "receivedOn": "Received on", "statusReceived": "Received", "statusDueTo": "Due on", + "statusMailedOn": "Mailed on", "typeOptions": { "tshirt": "T-shirt", "toteBag": "Tote bag", @@ -1015,6 +1016,8 @@ "volunteerReceivedIt": "The volunteer received it", "needToGiveIt": "Need to give it to the volunteer", "receivedOnRequired": "Received on*", + "volunteerPostIt": "It was mailed to the volunteer", + "postOnRequired": "Mailed on*", "today": "today", "cancel": "Cancel", "save": "Save", diff --git a/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx b/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx index a9fc54fc..93ac85d6 100644 --- a/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx +++ b/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx @@ -1,12 +1,11 @@ import { EmptyPlaceholder } from "@/components/core/common/EmptyPlaceholder"; -import { EMPTY_PLACEHOLDER_VALUE } from "@/config/constants"; import { useAppreciationTracker } from "@/hooks/useAppreciationTracker"; import { PencilSimple, Trash } from "@phosphor-icons/react"; import { ApiVolunteerGet, - ApiAppreciationGet, ApiAppreciationPost, VolunteerStateAppreciationType, + ApiAppreciationPatch, } from "need4deed-sdk"; import { forwardRef, useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -26,6 +25,7 @@ import { } from "@/components/core/common/Table"; import { formatDate } from "../shared/utils/formatDate"; import { getAppreciationTypeLabel } from "./utils/translations"; +import { AppreciationWithStatus, DeliveryStatus } from "./types"; type Props = { volunteer: ApiVolunteerGet; @@ -38,8 +38,8 @@ export type AppreciationRef = { export const Appreciation = forwardRef(function Appreciation({ volunteer }, ref) { const { t } = useTranslation(); const [isDialogOpen, setIsDialogOpen] = useState(false); - const [editingEntry, setEditingEntry] = useState(undefined); - const [deleteConfirmEntry, setDeleteConfirmEntry] = useState(null); + const [editingEntry, setEditingEntry] = useState(undefined); + const [deleteConfirmEntry, setDeleteConfirmEntry] = useState(null); const { appreciations, createAppreciation, updateAppreciation, deleteAppreciation } = useAppreciationTracker( volunteer.id, @@ -54,12 +54,12 @@ export const Appreciation = forwardRef(function Apprecia handleAddNew, })); - const handleEdit = (entry: ApiAppreciationGet) => { + const handleEdit = (entry: AppreciationWithStatus) => { setEditingEntry(entry); setIsDialogOpen(true); }; - const handleDelete = (entry: ApiAppreciationGet) => { + const handleDelete = (entry: AppreciationWithStatus) => { setDeleteConfirmEntry(entry); }; @@ -77,37 +77,37 @@ export const Appreciation = forwardRef(function Apprecia title: VolunteerStateAppreciationType; dateDue: Date | null; dateDelivery: Date | null; + status: DeliveryStatus; }) => { if (data.id) { - const payload = { + const payload: ApiAppreciationPatch & { status: DeliveryStatus } = { title: data.title, dateDue: data.dateDue, dateDelivery: data.dateDelivery, + status: data.status, }; updateAppreciation({ id: data.id, data: payload }, { onSuccess: () => setIsDialogOpen(false) }); } else { - const payload: ApiAppreciationPost = { + const payload: ApiAppreciationPost & { status: DeliveryStatus } = { title: data.title, dateDue: data.dateDue || new Date(), dateDelivery: data.dateDelivery ?? undefined, + status: data.status, }; createAppreciation(payload, { onSuccess: () => setIsDialogOpen(false) }); } }; - const getStatus = (entry: ApiAppreciationGet): "received" | "pending" => - entry.dateDelivery ? "received" : "pending"; + const getStatus = (entry: AppreciationWithStatus) => entry.status; - const getStatusLabel = (entry: ApiAppreciationGet): string => { - if (entry.dateDelivery) { - return t("dashboard.appreciationSection.statusReceived"); - } - if (entry.dateDue) { - return `${t("dashboard.appreciationSection.statusDueTo")} ${formatDate(entry.dateDue)}`; - } - return EMPTY_PLACEHOLDER_VALUE; + const STATUS_LABEL: Record string> = { + received: () => t("dashboard.appreciationSection.statusReceived"), + pending: (entry) => `${t("dashboard.appreciationSection.statusDueTo")} ${formatDate(entry.dateDue ?? undefined)}`, + post: (entry) => `${t("dashboard.appreciationSection.statusMailedOn")} ${formatDate(entry.dateDue ?? undefined)}`, }; + const getStatusLabel = (entry: AppreciationWithStatus) => STATUS_LABEL[entry.status](entry); + return ( {appreciations.length === 0 ? ( diff --git a/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx b/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx index 64a1c87d..49242887 100644 --- a/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx +++ b/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx @@ -2,7 +2,7 @@ import { Button } from "@/components/core/button"; import { DatePickerWithLabel } from "@/components/core/common/DatePicker"; import { Modal } from "@/components/core/modal"; import { de, enUS, Locale } from "date-fns/locale"; -import { ApiAppreciationGet, VolunteerStateAppreciationType } from "need4deed-sdk"; +import { VolunteerStateAppreciationType } from "need4deed-sdk"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { SelectableOption } from "../shared/SelectableOption"; @@ -16,6 +16,7 @@ import { SubOptionContainer, SubQuestion, } from "./styles"; +import { AppreciationWithStatus, DeliveryStatus } from "./types"; type Props = { isOpen: boolean; @@ -25,12 +26,11 @@ type Props = { title: VolunteerStateAppreciationType; dateDue: Date | null; dateDelivery: Date | null; + status: DeliveryStatus; }) => void; - initialData?: ApiAppreciationGet; + initialData?: AppreciationWithStatus; }; -type DeliveryStatus = "received" | "pending"; - const APPRECIATION_TYPES = [ { value: VolunteerStateAppreciationType.TOTE_BAG, @@ -77,12 +77,7 @@ function DeliveryStatusOption({ }: DeliveryStatusOptionProps) { return ( <> - + {showDatePicker && ( + handleDeliveryStatusSelect("post")} + showDatePicker={deliveryStatus === "post"} + date={selectedDate} + onDateSelect={setSelectedDate} + locale={locale} + dateLabel={t("dashboard.appreciationSection.postOnRequired")} + todayText={todayText} + testId="post-date-field" + /> )} diff --git a/src/components/Dashboard/Profile/sections/Appreciation/styles.ts b/src/components/Dashboard/Profile/sections/Appreciation/styles.ts index 590ba13d..0a54b24e 100644 --- a/src/components/Dashboard/Profile/sections/Appreciation/styles.ts +++ b/src/components/Dashboard/Profile/sections/Appreciation/styles.ts @@ -6,8 +6,14 @@ export const AppreciationTableContainer = styled(TableContainer)` margin-top: var(--spacing-24); `; -export const StatusBadge = styled.div<{ $status: "received" | "pending" }>` - background: ${(props) => (props.$status === "received" ? "var(--color-green-100)" : "var(--color-red-50)")}; +export const StatusBadge = styled.div<{ $status: "received" | "pending" | "post" }>` + background: ${(props) => + props.$status === "received" + ? "var(--color-green-100)" + : props.$status === "post" + ? "var(--color-blue-100)" + : "var(--color-red-50)"}; + padding: var(--spacing-12); border-radius: var(--border-radius-xs); font-weight: var(--font-weight-semi-bold); diff --git a/src/components/Dashboard/Profile/sections/Appreciation/types.ts b/src/components/Dashboard/Profile/sections/Appreciation/types.ts new file mode 100644 index 00000000..65ca63c4 --- /dev/null +++ b/src/components/Dashboard/Profile/sections/Appreciation/types.ts @@ -0,0 +1,4 @@ +import { ApiAppreciationGet } from "need4deed-sdk"; + +export type DeliveryStatus = "received" | "pending" | "post"; +export type AppreciationWithStatus = ApiAppreciationGet & { status: DeliveryStatus }; diff --git a/src/hooks/useAppreciationTracker.ts b/src/hooks/useAppreciationTracker.ts index 3924ec4c..5a47db01 100644 --- a/src/hooks/useAppreciationTracker.ts +++ b/src/hooks/useAppreciationTracker.ts @@ -1,13 +1,14 @@ +import { AppreciationWithStatus } from "@/components/Dashboard/Profile/sections/Appreciation/types"; import { apiPathAppreciation, apiPathVolunteer } from "@/config/constants"; import { useGetQuery } from "@/hooks/useGetQuery"; import { useMutationQuery } from "@/hooks/useMutationQuery"; import axios from "axios"; -import { ApiAppreciationGet, ApiAppreciationPatch, ApiAppreciationPost } from "need4deed-sdk"; +import { ApiAppreciationPatch, ApiAppreciationPost } from "need4deed-sdk"; export const useAppreciationTracker = (volunteerId: number) => { const queryKey = ["volunteer", String(volunteerId), "appreciations"]; - const { data: appreciations = [], isLoading } = useGetQuery({ + const { data: appreciations = [], isLoading } = useGetQuery({ queryKey, apiPath: `${apiPathVolunteer}/${volunteerId}/appreciation`, }); From e3649e711dbb03c2287b8778a51bd3a3644b30b4 Mon Sep 17 00:00:00 2001 From: ivannissimrch Date: Wed, 26 Aug 2026 07:43:23 -0400 Subject: [PATCH 2/9] feat: consume the published status field from need4deed-sdk 0.0.147 --- package.json | 2 +- .../sections/Appreciation/Appreciation.tsx | 31 +++--- .../Appreciation/AppreciationDialog.tsx | 96 +++++++++---------- .../Profile/sections/Appreciation/styles.ts | 7 +- .../Profile/sections/Appreciation/types.ts | 4 - src/hooks/useAppreciationTracker.ts | 5 +- yarn.lock | 8 +- 7 files changed, 75 insertions(+), 78 deletions(-) delete mode 100644 src/components/Dashboard/Profile/sections/Appreciation/types.ts diff --git a/package.json b/package.json index c622f191..3eb7c962 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.145", + "need4deed-sdk": "0.0.147", "next": "15.3.8", "react": "^19.0.0", "react-day-picker": "^9.13.0", diff --git a/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx b/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx index 93ac85d6..e593001a 100644 --- a/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx +++ b/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx @@ -6,6 +6,8 @@ import { ApiAppreciationPost, VolunteerStateAppreciationType, ApiAppreciationPatch, + ApiAppreciationGet, + AppreciationStatusType, } from "need4deed-sdk"; import { forwardRef, useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -25,7 +27,6 @@ import { } from "@/components/core/common/Table"; import { formatDate } from "../shared/utils/formatDate"; import { getAppreciationTypeLabel } from "./utils/translations"; -import { AppreciationWithStatus, DeliveryStatus } from "./types"; type Props = { volunteer: ApiVolunteerGet; @@ -38,8 +39,8 @@ export type AppreciationRef = { export const Appreciation = forwardRef(function Appreciation({ volunteer }, ref) { const { t } = useTranslation(); const [isDialogOpen, setIsDialogOpen] = useState(false); - const [editingEntry, setEditingEntry] = useState(undefined); - const [deleteConfirmEntry, setDeleteConfirmEntry] = useState(null); + const [editingEntry, setEditingEntry] = useState(undefined); + const [deleteConfirmEntry, setDeleteConfirmEntry] = useState(null); const { appreciations, createAppreciation, updateAppreciation, deleteAppreciation } = useAppreciationTracker( volunteer.id, @@ -54,12 +55,12 @@ export const Appreciation = forwardRef(function Apprecia handleAddNew, })); - const handleEdit = (entry: AppreciationWithStatus) => { + const handleEdit = (entry: ApiAppreciationGet) => { setEditingEntry(entry); setIsDialogOpen(true); }; - const handleDelete = (entry: AppreciationWithStatus) => { + const handleDelete = (entry: ApiAppreciationGet) => { setDeleteConfirmEntry(entry); }; @@ -77,10 +78,10 @@ export const Appreciation = forwardRef(function Apprecia title: VolunteerStateAppreciationType; dateDue: Date | null; dateDelivery: Date | null; - status: DeliveryStatus; + status: AppreciationStatusType; }) => { if (data.id) { - const payload: ApiAppreciationPatch & { status: DeliveryStatus } = { + const payload: ApiAppreciationPatch = { title: data.title, dateDue: data.dateDue, dateDelivery: data.dateDelivery, @@ -88,7 +89,7 @@ export const Appreciation = forwardRef(function Apprecia }; updateAppreciation({ id: data.id, data: payload }, { onSuccess: () => setIsDialogOpen(false) }); } else { - const payload: ApiAppreciationPost & { status: DeliveryStatus } = { + const payload: ApiAppreciationPost = { title: data.title, dateDue: data.dateDue || new Date(), dateDelivery: data.dateDelivery ?? undefined, @@ -98,15 +99,17 @@ export const Appreciation = forwardRef(function Apprecia } }; - const getStatus = (entry: AppreciationWithStatus) => entry.status; + const getStatus = (entry: ApiAppreciationGet) => entry.status; - const STATUS_LABEL: Record string> = { - received: () => t("dashboard.appreciationSection.statusReceived"), - pending: (entry) => `${t("dashboard.appreciationSection.statusDueTo")} ${formatDate(entry.dateDue ?? undefined)}`, - post: (entry) => `${t("dashboard.appreciationSection.statusMailedOn")} ${formatDate(entry.dateDue ?? undefined)}`, + const STATUS_LABEL: Record string> = { + [AppreciationStatusType.RECEIVED]: () => t("dashboard.appreciationSection.statusReceived"), + [AppreciationStatusType.PENDING]: (entry) => + `${t("dashboard.appreciationSection.statusDueTo")} ${formatDate(entry.dateDue ?? undefined)}`, + [AppreciationStatusType.POST]: (entry) => + `${t("dashboard.appreciationSection.statusMailedOn")} ${formatDate(entry.dateDue ?? undefined)}`, }; - const getStatusLabel = (entry: AppreciationWithStatus) => STATUS_LABEL[entry.status](entry); + const getStatusLabel = (entry: ApiAppreciationGet) => STATUS_LABEL[entry.status](entry); return ( diff --git a/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx b/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx index 9e6d33f3..157d88ed 100644 --- a/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx +++ b/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx @@ -2,7 +2,7 @@ import { Button } from "@/components/core/button"; import { DatePickerWithLabel } from "@/components/core/common/DatePicker"; import { Modal } from "@/components/core/modal"; import { de, enUS, Locale } from "date-fns/locale"; -import { VolunteerStateAppreciationType } from "need4deed-sdk"; +import { VolunteerStateAppreciationType, ApiAppreciationGet, AppreciationStatusType } from "need4deed-sdk"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { SelectableOption } from "../shared/SelectableOption"; @@ -16,7 +16,6 @@ import { SubOptionContainer, SubQuestion, } from "./styles"; -import { AppreciationWithStatus, DeliveryStatus } from "./types"; type Props = { isOpen: boolean; @@ -26,9 +25,9 @@ type Props = { title: VolunteerStateAppreciationType; dateDue: Date | null; dateDelivery: Date | null; - status: DeliveryStatus; + status: AppreciationStatusType; }) => void; - initialData?: AppreciationWithStatus; + initialData?: ApiAppreciationGet; }; const APPRECIATION_TYPES = [ @@ -56,8 +55,30 @@ const APPRECIATION_TYPES = [ }, ]; +const DELIVERY_STATUSES = [ + { + status: AppreciationStatusType.RECEIVED, + labelKey: "dashboard.appreciationSection.volunteerReceivedIt", + dateLabelKey: "dashboard.appreciationSection.receivedOnRequired", + testId: "received-date-field", + }, + { + status: AppreciationStatusType.PENDING, + labelKey: "dashboard.appreciationSection.needToGiveIt", + dateLabelKey: "dashboard.appreciationSection.dueDateRequired", + testId: "due-date-field", + allowFuture: true, + }, + { + status: AppreciationStatusType.POST, + labelKey: "dashboard.appreciationSection.volunteerPostIt", + dateLabelKey: "dashboard.appreciationSection.postOnRequired", + testId: "post-date-field", + }, +]; + type DeliveryStatusOptionProps = { - status: DeliveryStatus; + status: AppreciationStatusType; isSelected: boolean; label: string; onSelect: () => void; @@ -110,7 +131,7 @@ export function AppreciationDialog({ isOpen, onClose, onSave, initialData }: Pro const locale = i18n.language === "de" ? de : enUS; const [selectedType, setSelectedType] = useState(undefined); - const [deliveryStatus, setDeliveryStatus] = useState(undefined); + const [deliveryStatus, setDeliveryStatus] = useState(undefined); const [selectedDate, setSelectedDate] = useState(undefined); useEffect(() => { @@ -141,7 +162,7 @@ export function AppreciationDialog({ isOpen, onClose, onSave, initialData }: Pro } }; - const handleDeliveryStatusSelect = (status: DeliveryStatus) => { + const handleDeliveryStatusSelect = (status: AppreciationStatusType) => { setDeliveryStatus(status); if (!selectedDate) { setSelectedDate(new Date()); @@ -155,8 +176,8 @@ export function AppreciationDialog({ isOpen, onClose, onSave, initialData }: Pro onSave({ id: initialData?.id, title: selectedType, - dateDue: deliveryStatus === "received" ? null : selectedDate, - dateDelivery: deliveryStatus === "received" ? selectedDate : null, + dateDue: deliveryStatus === AppreciationStatusType.RECEIVED ? null : selectedDate, + dateDelivery: deliveryStatus === AppreciationStatusType.RECEIVED ? selectedDate : null, status: deliveryStatus, }); }; @@ -191,46 +212,23 @@ export function AppreciationDialog({ isOpen, onClose, onSave, initialData }: Pro {t("dashboard.appreciationSection.didVolunteerReceive")} - handleDeliveryStatusSelect("received")} - showDatePicker={deliveryStatus === "received"} - date={selectedDate} - onDateSelect={setSelectedDate} - locale={locale} - dateLabel={t("dashboard.appreciationSection.receivedOnRequired")} - todayText={todayText} - testId="received-date-field" - /> - handleDeliveryStatusSelect("pending")} - showDatePicker={deliveryStatus === "pending"} - date={selectedDate} - onDateSelect={setSelectedDate} - locale={locale} - dateLabel={t("dashboard.appreciationSection.dueDateRequired")} - todayText={todayText} - allowFuture - testId="due-date-field" - /> - handleDeliveryStatusSelect("post")} - showDatePicker={deliveryStatus === "post"} - date={selectedDate} - onDateSelect={setSelectedDate} - locale={locale} - dateLabel={t("dashboard.appreciationSection.postOnRequired")} - todayText={todayText} - testId="post-date-field" - /> + {DELIVERY_STATUSES.map((option) => ( + handleDeliveryStatusSelect(option.status)} + showDatePicker={deliveryStatus === option.status} + date={selectedDate} + onDateSelect={setSelectedDate} + locale={locale} + dateLabel={t(option.dateLabelKey)} + todayText={todayText} + allowFuture={option.allowFuture} + testId={option.testId} + /> + ))} )} diff --git a/src/components/Dashboard/Profile/sections/Appreciation/styles.ts b/src/components/Dashboard/Profile/sections/Appreciation/styles.ts index 0a54b24e..0d3bd8a0 100644 --- a/src/components/Dashboard/Profile/sections/Appreciation/styles.ts +++ b/src/components/Dashboard/Profile/sections/Appreciation/styles.ts @@ -1,16 +1,17 @@ import styled from "styled-components"; import { TableContainer } from "@/components/core/common/Table"; import { FlexColumn } from "@/components/styled/FlexColumn"; +import { AppreciationStatusType } from "need4deed-sdk"; export const AppreciationTableContainer = styled(TableContainer)` margin-top: var(--spacing-24); `; -export const StatusBadge = styled.div<{ $status: "received" | "pending" | "post" }>` +export const StatusBadge = styled.div<{ $status: AppreciationStatusType }>` background: ${(props) => - props.$status === "received" + props.$status === AppreciationStatusType.RECEIVED ? "var(--color-green-100)" - : props.$status === "post" + : props.$status === AppreciationStatusType.POST ? "var(--color-blue-100)" : "var(--color-red-50)"}; diff --git a/src/components/Dashboard/Profile/sections/Appreciation/types.ts b/src/components/Dashboard/Profile/sections/Appreciation/types.ts deleted file mode 100644 index 65ca63c4..00000000 --- a/src/components/Dashboard/Profile/sections/Appreciation/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { ApiAppreciationGet } from "need4deed-sdk"; - -export type DeliveryStatus = "received" | "pending" | "post"; -export type AppreciationWithStatus = ApiAppreciationGet & { status: DeliveryStatus }; diff --git a/src/hooks/useAppreciationTracker.ts b/src/hooks/useAppreciationTracker.ts index 5a47db01..921d0ab7 100644 --- a/src/hooks/useAppreciationTracker.ts +++ b/src/hooks/useAppreciationTracker.ts @@ -1,14 +1,13 @@ -import { AppreciationWithStatus } from "@/components/Dashboard/Profile/sections/Appreciation/types"; import { apiPathAppreciation, apiPathVolunteer } from "@/config/constants"; import { useGetQuery } from "@/hooks/useGetQuery"; import { useMutationQuery } from "@/hooks/useMutationQuery"; import axios from "axios"; -import { ApiAppreciationPatch, ApiAppreciationPost } from "need4deed-sdk"; +import { ApiAppreciationPatch, ApiAppreciationPost, ApiAppreciationGet } from "need4deed-sdk"; export const useAppreciationTracker = (volunteerId: number) => { const queryKey = ["volunteer", String(volunteerId), "appreciations"]; - const { data: appreciations = [], isLoading } = useGetQuery({ + const { data: appreciations = [], isLoading } = useGetQuery({ queryKey, apiPath: `${apiPathVolunteer}/${volunteerId}/appreciation`, }); diff --git a/yarn.lock b/yarn.lock index 69cdad33..1bf4e78f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2412,10 +2412,10 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -need4deed-sdk@0.0.145: - version "0.0.145" - resolved "https://registry.yarnpkg.com/need4deed-sdk/-/need4deed-sdk-0.0.145.tgz#79c2e4ffefbe2bed5520dd356e644f7b572862fe" - integrity sha512-xLfKK9W9AvpwAoq95c/ifBTBGGU9ndALyGLpiUAp7DNLnWjyQjLghhVVFTwyKygKSxY04p6h/hvY0G7amsJSKg== +need4deed-sdk@0.0.147: + version "0.0.147" + resolved "https://registry.yarnpkg.com/need4deed-sdk/-/need4deed-sdk-0.0.147.tgz#b5d54178585dc519be975f7a0be4d29b6daf5713" + integrity sha512-SDZHVyVWrmMzESvjmEb87TV4299gLL3oyEZ4VgiiuLxwhqprdnzUDe5TaZT72FbYll/kQoV0fspMKjNq6+mKmw== next@15.3.8: version "15.3.8" From 738170545d0b059bbe2c1825005b1e8fd7f7f596 Mon Sep 17 00:00:00 2001 From: ivannissimrch Date: Thu, 27 Aug 2026 07:07:36 -0400 Subject: [PATCH 3/9] fix: clear a stale future date when switching to a status that disallows one --- .../Profile/sections/Appreciation/AppreciationDialog.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx b/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx index 157d88ed..ef337c1b 100644 --- a/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx +++ b/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx @@ -164,6 +164,15 @@ export function AppreciationDialog({ isOpen, onClose, onSave, initialData }: Pro const handleDeliveryStatusSelect = (status: AppreciationStatusType) => { setDeliveryStatus(status); + + const allowsFuture = DELIVERY_STATUSES.find((option) => option.status === status)?.allowFuture ?? false; + const isFuture = selectedDate !== undefined && selectedDate > new Date(); + + if (isFuture && !allowsFuture) { + setSelectedDate(undefined); + return; + } + if (!selectedDate) { setSelectedDate(new Date()); } From 84f18fa6ed1b98b8e90f7ae44645069134f7bd71 Mon Sep 17 00:00:00 2001 From: ivannissimrch Date: Thu, 27 Aug 2026 07:19:47 -0400 Subject: [PATCH 4/9] fix: fall back to a placeholder for an unrecognized appreciation status --- .../Dashboard/Profile/sections/Appreciation/Appreciation.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx b/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx index e593001a..0b227234 100644 --- a/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx +++ b/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx @@ -27,6 +27,7 @@ import { } from "@/components/core/common/Table"; import { formatDate } from "../shared/utils/formatDate"; import { getAppreciationTypeLabel } from "./utils/translations"; +import { EMPTY_PLACEHOLDER_VALUE } from "@/config/constants"; type Props = { volunteer: ApiVolunteerGet; @@ -109,7 +110,7 @@ export const Appreciation = forwardRef(function Apprecia `${t("dashboard.appreciationSection.statusMailedOn")} ${formatDate(entry.dateDue ?? undefined)}`, }; - const getStatusLabel = (entry: ApiAppreciationGet) => STATUS_LABEL[entry.status](entry); + const getStatusLabel = (entry: ApiAppreciationGet) => STATUS_LABEL[entry.status]?.(entry) ?? EMPTY_PLACEHOLDER_VALUE; return ( From b789a90246f82a67fe2456d4292fe20cf9cc6f19 Mon Sep 17 00:00:00 2001 From: ivannissimrch Date: Thu, 27 Aug 2026 09:05:01 -0400 Subject: [PATCH 5/9] fix: stop create stamping a due date over an intentional null --- .../Dashboard/Profile/sections/Appreciation/Appreciation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx b/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx index 0b227234..ec0dd727 100644 --- a/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx +++ b/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx @@ -92,7 +92,7 @@ export const Appreciation = forwardRef(function Apprecia } else { const payload: ApiAppreciationPost = { title: data.title, - dateDue: data.dateDue || new Date(), + dateDue: data.dateDue, dateDelivery: data.dateDelivery ?? undefined, status: data.status, }; From af9345c425039f9dcce1ea30b2713cb7dbdfcbce Mon Sep 17 00:00:00 2001 From: ivannissimrch Date: Thu, 27 Aug 2026 16:38:07 -0400 Subject: [PATCH 6/9] refactor: extract appreciation status color and label maps --- .../sections/Appreciation/Appreciation.tsx | 18 +++++------------- .../Profile/sections/Appreciation/constants.ts | 13 +++++++++++++ .../Profile/sections/Appreciation/styles.ts | 14 +++++++------- 3 files changed, 25 insertions(+), 20 deletions(-) create mode 100644 src/components/Dashboard/Profile/sections/Appreciation/constants.ts diff --git a/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx b/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx index ec0dd727..9245465d 100644 --- a/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx +++ b/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx @@ -9,7 +9,7 @@ import { ApiAppreciationGet, AppreciationStatusType, } from "need4deed-sdk"; -import { forwardRef, useImperativeHandle, useState } from "react"; +import { forwardRef, useImperativeHandle, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { AppreciationDialog } from "./AppreciationDialog"; import { ConfirmationDialog } from "../shared/ConfirmationDialog"; @@ -28,6 +28,7 @@ import { import { formatDate } from "../shared/utils/formatDate"; import { getAppreciationTypeLabel } from "./utils/translations"; import { EMPTY_PLACEHOLDER_VALUE } from "@/config/constants"; +import { createAppreciationStatusLabelMap } from "./constants"; type Props = { volunteer: ApiVolunteerGet; @@ -100,17 +101,8 @@ export const Appreciation = forwardRef(function Apprecia } }; - const getStatus = (entry: ApiAppreciationGet) => entry.status; - - const STATUS_LABEL: Record string> = { - [AppreciationStatusType.RECEIVED]: () => t("dashboard.appreciationSection.statusReceived"), - [AppreciationStatusType.PENDING]: (entry) => - `${t("dashboard.appreciationSection.statusDueTo")} ${formatDate(entry.dateDue ?? undefined)}`, - [AppreciationStatusType.POST]: (entry) => - `${t("dashboard.appreciationSection.statusMailedOn")} ${formatDate(entry.dateDue ?? undefined)}`, - }; - - const getStatusLabel = (entry: ApiAppreciationGet) => STATUS_LABEL[entry.status]?.(entry) ?? EMPTY_PLACEHOLDER_VALUE; + const statusLabels = useMemo(() => createAppreciationStatusLabelMap(t), [t]); + const getStatusLabel = (entry: ApiAppreciationGet) => statusLabels[entry.status]?.(entry) ?? EMPTY_PLACEHOLDER_VALUE; return ( @@ -135,7 +127,7 @@ export const Appreciation = forwardRef(function Apprecia > {getAppreciationTypeLabel(t, entry.title)} - {getStatusLabel(entry)} + {getStatusLabel(entry)} {entry.dateDelivery ? formatDate(entry.dateDelivery) : } diff --git a/src/components/Dashboard/Profile/sections/Appreciation/constants.ts b/src/components/Dashboard/Profile/sections/Appreciation/constants.ts new file mode 100644 index 00000000..cacd26f8 --- /dev/null +++ b/src/components/Dashboard/Profile/sections/Appreciation/constants.ts @@ -0,0 +1,13 @@ +import { TFunction } from "i18next"; +import { ApiAppreciationGet, AppreciationStatusType } from "need4deed-sdk"; +import { formatDate } from "../shared/utils/formatDate"; + +export const createAppreciationStatusLabelMap = ( + t: TFunction, +): Record string> => ({ + [AppreciationStatusType.RECEIVED]: () => t("dashboard.appreciationSection.statusReceived"), + [AppreciationStatusType.PENDING]: (entry) => + `${t("dashboard.appreciationSection.statusDueTo")} ${formatDate(entry.dateDue ?? undefined)}`, + [AppreciationStatusType.POST]: (entry) => + `${t("dashboard.appreciationSection.statusMailedOn")} ${formatDate(entry.dateDue ?? undefined)}`, +}); diff --git a/src/components/Dashboard/Profile/sections/Appreciation/styles.ts b/src/components/Dashboard/Profile/sections/Appreciation/styles.ts index 0d3bd8a0..0f379701 100644 --- a/src/components/Dashboard/Profile/sections/Appreciation/styles.ts +++ b/src/components/Dashboard/Profile/sections/Appreciation/styles.ts @@ -7,14 +7,14 @@ export const AppreciationTableContainer = styled(TableContainer)` margin-top: var(--spacing-24); `; -export const StatusBadge = styled.div<{ $status: AppreciationStatusType }>` - background: ${(props) => - props.$status === AppreciationStatusType.RECEIVED - ? "var(--color-green-100)" - : props.$status === AppreciationStatusType.POST - ? "var(--color-blue-100)" - : "var(--color-red-50)"}; +const statusColorMap: Record = { + [AppreciationStatusType.RECEIVED]: "var(--color-green-100)", + [AppreciationStatusType.POST]: "var(--color-blue-100)", + [AppreciationStatusType.PENDING]: "var(--color-red-50)", +}; +export const StatusBadge = styled.div<{ $status: AppreciationStatusType }>` + background: ${(props) => statusColorMap[props.$status]}; padding: var(--spacing-12); border-radius: var(--border-radius-xs); font-weight: var(--font-weight-semi-bold); From 612c3a94fc7a5b40b9b5cbe846a69438001d19b3 Mon Sep 17 00:00:00 2001 From: ivannissimrch Date: Sat, 29 Aug 2026 09:03:36 -0400 Subject: [PATCH 7/9] feat: show due, mailed and received dates as separate columns --- public/locales/de/translations.json | 2 ++ public/locales/en/translations.json | 2 ++ .../sections/Appreciation/Appreciation.tsx | 17 +++++++++++++++++ .../Appreciation/AppreciationDialog.tsx | 12 ++++++------ .../Profile/sections/Appreciation/constants.ts | 7 ++----- 5 files changed, 29 insertions(+), 11 deletions(-) diff --git a/public/locales/de/translations.json b/public/locales/de/translations.json index 0ca4be8b..b4317999 100644 --- a/public/locales/de/translations.json +++ b/public/locales/de/translations.json @@ -1067,6 +1067,8 @@ "status": "Status", "receivedOn": "Erhalten am", "statusReceived": "Erhalten", + "statusPending": "Ausstehend", + "statusPost": "Geschickt", "statusDueTo": "Fällig am", "statusMailedOn": "Geschickt am", "typeOptions": { diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 8956f3e7..b6540a88 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -1055,6 +1055,8 @@ "status": "Status", "receivedOn": "Received on", "statusReceived": "Received", + "statusPending": "Pending", + "statusPost": "Mailed", "statusDueTo": "Due on", "statusMailedOn": "Mailed on", "typeOptions": { diff --git a/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx b/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx index 9245465d..c3f89929 100644 --- a/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx +++ b/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx @@ -114,6 +114,8 @@ export const Appreciation = forwardRef(function Apprecia {t("dashboard.appreciationSection.typeOfAppreciation")} {t("dashboard.appreciationSection.status")} + {t("dashboard.appreciationSection.statusDueTo")} + {t("dashboard.appreciationSection.statusMailedOn")} {t("dashboard.appreciationSection.receivedOn")} @@ -129,6 +131,21 @@ export const Appreciation = forwardRef(function Apprecia {getStatusLabel(entry)} + + {entry.status === AppreciationStatusType.PENDING && entry.dateDue ? ( + formatDate(entry.dateDue) + ) : ( + + )} + + + {entry.status === AppreciationStatusType.POST && entry.dateDue ? ( + formatDate(entry.dateDue) + ) : ( + + )} + + {entry.dateDelivery ? formatDate(entry.dateDelivery) : } diff --git a/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx b/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx index ef337c1b..ccc7d15a 100644 --- a/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx +++ b/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx @@ -56,12 +56,6 @@ const APPRECIATION_TYPES = [ ]; const DELIVERY_STATUSES = [ - { - status: AppreciationStatusType.RECEIVED, - labelKey: "dashboard.appreciationSection.volunteerReceivedIt", - dateLabelKey: "dashboard.appreciationSection.receivedOnRequired", - testId: "received-date-field", - }, { status: AppreciationStatusType.PENDING, labelKey: "dashboard.appreciationSection.needToGiveIt", @@ -75,6 +69,12 @@ const DELIVERY_STATUSES = [ dateLabelKey: "dashboard.appreciationSection.postOnRequired", testId: "post-date-field", }, + { + status: AppreciationStatusType.RECEIVED, + labelKey: "dashboard.appreciationSection.volunteerReceivedIt", + dateLabelKey: "dashboard.appreciationSection.receivedOnRequired", + testId: "received-date-field", + }, ]; type DeliveryStatusOptionProps = { diff --git a/src/components/Dashboard/Profile/sections/Appreciation/constants.ts b/src/components/Dashboard/Profile/sections/Appreciation/constants.ts index cacd26f8..d97098af 100644 --- a/src/components/Dashboard/Profile/sections/Appreciation/constants.ts +++ b/src/components/Dashboard/Profile/sections/Appreciation/constants.ts @@ -1,13 +1,10 @@ import { TFunction } from "i18next"; import { ApiAppreciationGet, AppreciationStatusType } from "need4deed-sdk"; -import { formatDate } from "../shared/utils/formatDate"; export const createAppreciationStatusLabelMap = ( t: TFunction, ): Record string> => ({ [AppreciationStatusType.RECEIVED]: () => t("dashboard.appreciationSection.statusReceived"), - [AppreciationStatusType.PENDING]: (entry) => - `${t("dashboard.appreciationSection.statusDueTo")} ${formatDate(entry.dateDue ?? undefined)}`, - [AppreciationStatusType.POST]: (entry) => - `${t("dashboard.appreciationSection.statusMailedOn")} ${formatDate(entry.dateDue ?? undefined)}`, + [AppreciationStatusType.PENDING]: () => t("dashboard.appreciationSection.statusPending"), + [AppreciationStatusType.POST]: () => t("dashboard.appreciationSection.statusPost"), }); From c800352f8a5004fee16a43846eeb315e689e9d96 Mon Sep 17 00:00:00 2001 From: ivannissimrch Date: Sat, 29 Aug 2026 09:33:11 -0400 Subject: [PATCH 8/9] fix: fall back to a default badge colour and simplify the status label map --- .../Profile/sections/Appreciation/Appreciation.tsx | 2 +- .../Profile/sections/Appreciation/constants.ts | 12 +++++------- .../Profile/sections/Appreciation/styles.ts | 2 +- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx b/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx index c3f89929..db6f7f7e 100644 --- a/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx +++ b/src/components/Dashboard/Profile/sections/Appreciation/Appreciation.tsx @@ -102,7 +102,7 @@ export const Appreciation = forwardRef(function Apprecia }; const statusLabels = useMemo(() => createAppreciationStatusLabelMap(t), [t]); - const getStatusLabel = (entry: ApiAppreciationGet) => statusLabels[entry.status]?.(entry) ?? EMPTY_PLACEHOLDER_VALUE; + const getStatusLabel = (entry: ApiAppreciationGet) => statusLabels[entry.status] ?? EMPTY_PLACEHOLDER_VALUE; return ( diff --git a/src/components/Dashboard/Profile/sections/Appreciation/constants.ts b/src/components/Dashboard/Profile/sections/Appreciation/constants.ts index d97098af..5eca7314 100644 --- a/src/components/Dashboard/Profile/sections/Appreciation/constants.ts +++ b/src/components/Dashboard/Profile/sections/Appreciation/constants.ts @@ -1,10 +1,8 @@ import { TFunction } from "i18next"; -import { ApiAppreciationGet, AppreciationStatusType } from "need4deed-sdk"; +import { AppreciationStatusType } from "need4deed-sdk"; -export const createAppreciationStatusLabelMap = ( - t: TFunction, -): Record string> => ({ - [AppreciationStatusType.RECEIVED]: () => t("dashboard.appreciationSection.statusReceived"), - [AppreciationStatusType.PENDING]: () => t("dashboard.appreciationSection.statusPending"), - [AppreciationStatusType.POST]: () => t("dashboard.appreciationSection.statusPost"), +export const createAppreciationStatusLabelMap = (t: TFunction): Record => ({ + [AppreciationStatusType.RECEIVED]: t("dashboard.appreciationSection.statusReceived"), + [AppreciationStatusType.PENDING]: t("dashboard.appreciationSection.statusPending"), + [AppreciationStatusType.POST]: t("dashboard.appreciationSection.statusPost"), }); diff --git a/src/components/Dashboard/Profile/sections/Appreciation/styles.ts b/src/components/Dashboard/Profile/sections/Appreciation/styles.ts index 0f379701..16d8e629 100644 --- a/src/components/Dashboard/Profile/sections/Appreciation/styles.ts +++ b/src/components/Dashboard/Profile/sections/Appreciation/styles.ts @@ -14,7 +14,7 @@ const statusColorMap: Record = { }; export const StatusBadge = styled.div<{ $status: AppreciationStatusType }>` - background: ${(props) => statusColorMap[props.$status]}; + background: ${(props) => statusColorMap[props.$status] ?? "var(--color-grey-50)"}; padding: var(--spacing-12); border-radius: var(--border-radius-xs); font-weight: var(--font-weight-semi-bold); From cb6778314500605b4749ca81efd9b0d339e1e3de Mon Sep 17 00:00:00 2001 From: ivannissimrch Date: Sun, 30 Aug 2026 08:29:36 -0400 Subject: [PATCH 9/9] fix: apply the future-date rule when the dialog opens, not only on status change --- .../Appreciation/AppreciationDialog.tsx | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx b/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx index ccc7d15a..5f3d4282 100644 --- a/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx +++ b/src/components/Dashboard/Profile/sections/Appreciation/AppreciationDialog.tsx @@ -77,6 +77,12 @@ const DELIVERY_STATUSES = [ }, ]; +function withoutDisallowedFuture(date: Date | undefined, status: AppreciationStatusType | undefined): Date | undefined { + if (!date || !status) return date; + const allowsFuture = DELIVERY_STATUSES.find((option) => option.status === status)?.allowFuture ?? false; + return !allowsFuture && date > new Date() ? undefined : date; +} + type DeliveryStatusOptionProps = { status: AppreciationStatusType; isSelected: boolean; @@ -136,17 +142,17 @@ export function AppreciationDialog({ isOpen, onClose, onSave, initialData }: Pro useEffect(() => { if (!isOpen) return; - if (initialData) { setSelectedType(initialData.title); setDeliveryStatus(initialData.status); - setSelectedDate( - initialData.dateDelivery - ? new Date(initialData.dateDelivery) - : initialData.dateDue - ? new Date(initialData.dateDue) - : undefined, - ); + + const storedDate = initialData.dateDelivery + ? new Date(initialData.dateDelivery) + : initialData.dateDue + ? new Date(initialData.dateDue) + : undefined; + + setSelectedDate(withoutDisallowedFuture(storedDate, initialData.status)); } else { setSelectedType(undefined); setDeliveryStatus(undefined); @@ -164,11 +170,7 @@ export function AppreciationDialog({ isOpen, onClose, onSave, initialData }: Pro const handleDeliveryStatusSelect = (status: AppreciationStatusType) => { setDeliveryStatus(status); - - const allowsFuture = DELIVERY_STATUSES.find((option) => option.status === status)?.allowFuture ?? false; - const isFuture = selectedDate !== undefined && selectedDate > new Date(); - - if (isFuture && !allowsFuture) { + if (selectedDate && !withoutDisallowedFuture(selectedDate, status)) { setSelectedDate(undefined); return; }