-
Notifications
You must be signed in to change notification settings - Fork 24
add 'send per post' option to the appreciation dialog #924
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5ba7b37
1d8cb78
7fd27c1
65175a0
5427d02
10cc0cf
13e7f59
e3649e7
0b17069
83416da
7381705
84f18fa
207c35f
b789a90
74ac5f7
af9345c
1f91696
612c3a9
c800352
cb67783
834e8d6
80a9939
bc347eb
e7d2d99
6969c7c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, ApiAppreciationGet, AppreciationStatusType } from "need4deed-sdk"; | ||
| import { useEffect, useState } from "react"; | ||
| import { useTranslation } from "react-i18next"; | ||
| import { SelectableOption } from "../shared/SelectableOption"; | ||
|
|
@@ -25,12 +25,11 @@ type Props = { | |
| title: VolunteerStateAppreciationType; | ||
| dateDue: Date | null; | ||
| dateDelivery: Date | null; | ||
| status: AppreciationStatusType; | ||
| }) => void; | ||
| initialData?: ApiAppreciationGet; | ||
| }; | ||
|
|
||
| type DeliveryStatus = "received" | "pending"; | ||
|
|
||
| const APPRECIATION_TYPES = [ | ||
| { | ||
| value: VolunteerStateAppreciationType.TOTE_BAG, | ||
|
|
@@ -56,8 +55,36 @@ const APPRECIATION_TYPES = [ | |
| }, | ||
| ]; | ||
|
|
||
| const DELIVERY_STATUSES = [ | ||
| { | ||
| 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", | ||
| }, | ||
| { | ||
| status: AppreciationStatusType.RECEIVED, | ||
| labelKey: "dashboard.appreciationSection.volunteerReceivedIt", | ||
| dateLabelKey: "dashboard.appreciationSection.receivedOnRequired", | ||
| testId: "received-date-field", | ||
| }, | ||
| ]; | ||
|
|
||
| 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: DeliveryStatus; | ||
| status: AppreciationStatusType; | ||
| isSelected: boolean; | ||
| label: string; | ||
| onSelect: () => void; | ||
|
|
@@ -110,21 +137,22 @@ export function AppreciationDialog({ isOpen, onClose, onSave, initialData }: Pro | |
| const locale = i18n.language === "de" ? de : enUS; | ||
|
|
||
| const [selectedType, setSelectedType] = useState<VolunteerStateAppreciationType | undefined>(undefined); | ||
| const [deliveryStatus, setDeliveryStatus] = useState<DeliveryStatus | undefined>(undefined); | ||
| const [deliveryStatus, setDeliveryStatus] = useState<AppreciationStatusType | undefined>(undefined); | ||
| const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined); | ||
|
|
||
| useEffect(() => { | ||
| if (!isOpen) return; | ||
|
|
||
| if (initialData) { | ||
| setSelectedType(initialData.title); | ||
| if (initialData.dateDelivery) { | ||
| setDeliveryStatus("received"); | ||
| setSelectedDate(new Date(initialData.dateDelivery)); | ||
| } else if (initialData.dateDue) { | ||
| setDeliveryStatus("pending"); | ||
| setSelectedDate(new Date(initialData.dateDue)); | ||
| } | ||
| setDeliveryStatus(initialData.status); | ||
|
|
||
| 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); | ||
|
|
@@ -140,8 +168,13 @@ export function AppreciationDialog({ isOpen, onClose, onSave, initialData }: Pro | |
| } | ||
| }; | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. correctness: Failure scenario: user selects PENDING and picks a future due date (allowed, |
||
| const handleDeliveryStatusSelect = (status: DeliveryStatus) => { | ||
| const handleDeliveryStatusSelect = (status: AppreciationStatusType) => { | ||
| setDeliveryStatus(status); | ||
| if (selectedDate && !withoutDisallowedFuture(selectedDate, status)) { | ||
| setSelectedDate(undefined); | ||
| return; | ||
| } | ||
|
|
||
| if (!selectedDate) { | ||
| setSelectedDate(new Date()); | ||
| } | ||
|
|
@@ -154,8 +187,9 @@ export function AppreciationDialog({ isOpen, onClose, onSave, initialData }: Pro | |
| onSave({ | ||
| id: initialData?.id, | ||
| title: selectedType, | ||
| dateDue: deliveryStatus === "pending" ? selectedDate : null, | ||
| dateDelivery: deliveryStatus === "received" ? selectedDate : null, | ||
| dateDue: deliveryStatus === AppreciationStatusType.RECEIVED ? null : selectedDate, | ||
| dateDelivery: deliveryStatus === AppreciationStatusType.RECEIVED ? selectedDate : null, | ||
| status: deliveryStatus, | ||
| }); | ||
| }; | ||
|
|
||
|
|
@@ -189,33 +223,23 @@ export function AppreciationDialog({ isOpen, onClose, onSave, initialData }: Pro | |
| <ExpandedSection data-testid="expanded-section"> | ||
| <SubQuestion>{t("dashboard.appreciationSection.didVolunteerReceive")}</SubQuestion> | ||
| <SubOptionContainer> | ||
| <DeliveryStatusOption | ||
| status="received" | ||
| isSelected={deliveryStatus === "received"} | ||
| label={t("dashboard.appreciationSection.volunteerReceivedIt")} | ||
| onSelect={() => handleDeliveryStatusSelect("received")} | ||
| showDatePicker={deliveryStatus === "received"} | ||
| date={selectedDate} | ||
| onDateSelect={setSelectedDate} | ||
| locale={locale} | ||
| dateLabel={t("dashboard.appreciationSection.receivedOnRequired")} | ||
| todayText={todayText} | ||
| testId="received-date-field" | ||
| /> | ||
| <DeliveryStatusOption | ||
| status="pending" | ||
| isSelected={deliveryStatus === "pending"} | ||
| label={t("dashboard.appreciationSection.needToGiveIt")} | ||
| onSelect={() => handleDeliveryStatusSelect("pending")} | ||
| showDatePicker={deliveryStatus === "pending"} | ||
| date={selectedDate} | ||
| onDateSelect={setSelectedDate} | ||
| locale={locale} | ||
| dateLabel={t("dashboard.appreciationSection.dueDateRequired")} | ||
| todayText={todayText} | ||
| allowFuture | ||
| testId="due-date-field" | ||
| /> | ||
| {DELIVERY_STATUSES.map((option) => ( | ||
| <DeliveryStatusOption | ||
| key={option.status} | ||
| status={option.status} | ||
| isSelected={deliveryStatus === option.status} | ||
| label={t(option.labelKey)} | ||
| onSelect={() => 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} | ||
| /> | ||
| ))} | ||
| </SubOptionContainer> | ||
| </ExpandedSection> | ||
| )} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { TFunction } from "i18next"; | ||
| import { AppreciationStatusType } from "need4deed-sdk"; | ||
|
|
||
| export const createAppreciationStatusLabelMap = (t: TFunction): Record<AppreciationStatusType, string> => ({ | ||
| [AppreciationStatusType.RECEIVED]: t("dashboard.appreciationSection.statusReceived"), | ||
| [AppreciationStatusType.PENDING]: t("dashboard.appreciationSection.statusPending"), | ||
| [AppreciationStatusType.POST]: t("dashboard.appreciationSection.statusPost"), | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,20 @@ | ||
| 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" }>` | ||
| background: ${(props) => (props.$status === "received" ? "var(--color-green-100)" : "var(--color-red-50)")}; | ||
| const statusColorMap: Record<AppreciationStatusType, string> = { | ||
| [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 }>` | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. statusColorMap has no default/fallback entry (CONFIRMED) If a row's |
||
| 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); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Future-date invariant not enforced on dialog open (PLAUSIBLE)
handleDeliveryStatusSelectclearsselectedDatewhen switching to a non-allowFuture status (RECEIVED/POST) with a future date already set, but thisuseEffectseeding state frominitialDataon dialog open does no such check. Editing an existing entry whose stored status is RECEIVED or POST but whose date is in the future (possible via legacy data or direct API writes) loads that future date unfiltered; saving without touching the date field resubmits the invalid future date.