From 74b1a7a3b88b87a997e2372d6949000b38787ba7 Mon Sep 17 00:00:00 2001 From: Rodrigo Louro Date: Mon, 20 Apr 2026 14:50:14 +0200 Subject: [PATCH 1/3] Fix: Volunteer card: Add tick box in EFZ documents #272 Fix: Volunteer card: Add tick box in EFZ documents #272 --- public/locales/de/translations.json | 2 + public/locales/en/translations.json | 2 + .../DocumentTableRow.tsx | 41 +++++++++++++------ .../VolunteerProfileDocument.tsx | 26 +++++++++++- .../VolunteerProfileDocument/styles.ts | 16 ++++++++ .../VolunteerProfileDocument/utils.ts | 8 +++- 6 files changed, 80 insertions(+), 15 deletions(-) diff --git a/public/locales/de/translations.json b/public/locales/de/translations.json index 1a2ca512..6fdf7f1a 100644 --- a/public/locales/de/translations.json +++ b/public/locales/de/translations.json @@ -770,6 +770,8 @@ "typeOfDocument": "Dokumenttyp", "status": "Status", "uploadedOn": "Hochgeladen am", + "received": "Erhalten", + "receivedOn": "Erhalten am", "actions": "Aktionen", "uploaded": "Hochgeladen", "missing": "Fehlend", diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index b21fb0b2..8e154fd7 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -625,6 +625,8 @@ "typeOfDocument": "Type of document", "status": "Status", "uploadedOn": "Uploaded on", + "received": "Received", + "receivedOn": "Received on", "actions": "Actions", "uploaded": "Uploaded", "missing": "Missing", diff --git a/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/DocumentTableRow.tsx b/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/DocumentTableRow.tsx index 0c50b2a8..fc307295 100644 --- a/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/DocumentTableRow.tsx +++ b/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/DocumentTableRow.tsx @@ -2,7 +2,7 @@ import { EmptyPlaceholder } from "@/components/core/common/EmptyPlaceholder"; import { DownloadSimple, Eye, Trash, UploadSimple } from "@phosphor-icons/react"; import { useTranslation } from "react-i18next"; import { ActionButtonWithTooltip } from "./ActionButtonWithTooltip"; -import { ActionCell, Cell, StatusBadge, TableRow } from "./styles"; +import { ActionCell, Cell, ReceivedCell, ReceivedCheckbox, StatusBadge, TableRow } from "./styles"; import { DocumentRow } from "./utils"; type Props = { @@ -12,6 +12,7 @@ type Props = { onPreview: () => void; onDownload: () => void; onDelete: () => void; + onToggleReceived: () => void; }; export function DocumentTableRow({ @@ -21,28 +22,42 @@ export function DocumentTableRow({ onPreview, onDownload, onDelete, + onToggleReceived, }: Props) { const { t } = useTranslation(); - const { nameKey, isUploaded, document } = documentRow; + const { nameKey, isUploaded, isReceived, receivedAt, document } = documentRow; return ( {t(`dashboard.documentSection.documentNames.${nameKey}`)} + + + - - {isUploaded - ? t("dashboard.documentSection.uploaded") - : t("dashboard.documentSection.missing")} + + {isUploaded || isReceived ? t("dashboard.documentSection.uploaded") : t("dashboard.documentSection.missing")} - {document?.createdAt - ? new Date(document.createdAt).toLocaleDateString("de-DE", { - day: "2-digit", - month: "2-digit", - year: "numeric", - }) - : } + {document?.createdAt ? ( + new Date(document.createdAt).toLocaleDateString("de-DE", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }) + ) : isReceived && receivedAt ? ( + receivedAt.toLocaleDateString("de-DE", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }) + ) : ( + + )} (null); + const [receivedState, setReceivedState] = useState>( + {}, + ); const { data: fetchedDocuments, isLoading, isError } = useVolunteerDocuments(volunteer.id); - const documentRows = useMemo(() => (fetchedDocuments ? enrichDocuments(fetchedDocuments) : []), [fetchedDocuments]); + const documentRows = useMemo( + () => (fetchedDocuments ? enrichDocuments(fetchedDocuments, receivedState) : []), + [fetchedDocuments, receivedState], + ); + + const handleToggleReceived = (type: string) => { + setReceivedState((prev) => { + const current = prev[type] ?? { isReceived: false, receivedAt: null }; + const isReceived = !current.isReceived; + return { + ...prev, + [type]: { + isReceived, + receivedAt: isReceived ? new Date() : null, + }, + }; + }); + }; const uploadMutation = useUploadDocument(volunteer.id, () => closeDialog("upload")); const deleteMutation = useDeleteDocument(volunteer.id, () => { @@ -103,6 +123,9 @@ export function VolunteerProfileDocument({ volunteer }: Props) { {t("dashboard.documentSection.typeOfDocument")} + + {t("dashboard.documentSection.received")} + {t("dashboard.documentSection.status")} {t("dashboard.documentSection.uploadedOn")} @@ -122,6 +145,7 @@ export function VolunteerProfileDocument({ volunteer }: Props) { onPreview={() => handlePreview(row)} onDownload={() => handleDownload(row)} onDelete={() => openDialog("delete", row)} + onToggleReceived={() => handleToggleReceived(row.type)} /> ))}
diff --git a/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/styles.ts b/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/styles.ts index dc325b6c..6e43c9ad 100644 --- a/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/styles.ts +++ b/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/styles.ts @@ -155,3 +155,19 @@ export const ActionCell = styled(Cell).attrs<{ $width?: string; $align?: string }))` padding: var(--document-section-action-cell-padding); `; + +export const ReceivedCell = styled(Cell)` + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + width: 120px; + flex: none; +`; + +export const ReceivedCheckbox = styled.input.attrs({ type: "checkbox" })` + width: 18px; + height: 18px; + cursor: pointer; + accent-color: var(--color-aubergine); +`; diff --git a/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/utils.ts b/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/utils.ts index d80af5db..2487b72b 100644 --- a/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/utils.ts +++ b/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/utils.ts @@ -10,6 +10,8 @@ export type DocumentRow = { nameKey: string; isUploaded: boolean; document?: ApiDocumentGet; + isReceived: boolean; + receivedAt: Date | null; }; const DOCUMENT_NAME_KEYS: Record = { @@ -41,17 +43,21 @@ export const extractDocumentUrl = (url: string): string | null => { }; export const enrichDocuments = ( - fetchedDocuments: ApiDocumentGet[] + fetchedDocuments: ApiDocumentGet[], + receivedState: Record, ): DocumentRow[] => { const allTypes = Object.keys(DOCUMENT_NAME_KEYS) as DocumentType[]; return allTypes.map((type) => { const document = fetchedDocuments.find((doc) => doc.type === type); + const received = receivedState[type] ?? { isReceived: false, receivedAt: null }; return { type, nameKey: DOCUMENT_NAME_KEYS[type], isUploaded: !!document, document, + isReceived: received.isReceived, + receivedAt: received.receivedAt, }; }); }; From e979b3d6b875fac1075aa2ad8f5ca13626a50d5e Mon Sep 17 00:00:00 2001 From: Rodrigo Louro Date: Tue, 21 Apr 2026 15:24:38 +0200 Subject: [PATCH 2/3] Fix: no API call is made Fix: no API call is made --- .../VolunteerProfileDocument.tsx | 8 ++++--- .../useDocumentOperations.ts | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/VolunteerProfileDocument.tsx b/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/VolunteerProfileDocument.tsx index 87a3749e..409344e1 100644 --- a/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/VolunteerProfileDocument.tsx +++ b/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/VolunteerProfileDocument.tsx @@ -1,6 +1,6 @@ "use client"; import { useVolunteerDocuments } from "@/hooks/useVolunteerDocuments"; -import { ApiVolunteerGet } from "need4deed-sdk"; +import { ApiVolunteerGet, DocumentType } from "need4deed-sdk"; import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "react-toastify"; @@ -11,7 +11,7 @@ import { DocumentTableRow } from "./DocumentTableRow"; import { ACTION_COLUMN_WIDTH, DocumentTableContainer, HeaderCell, Table, TableHeader } from "./styles"; import { UploadDocumentDialog } from "./UploadDocumentDialog"; import { useDialogState } from "./useDialogState"; -import { useDeleteDocument, useUploadDocument } from "./useDocumentOperations"; +import { useDeleteDocument, useMarkDocumentReceived, useUploadDocument } from "./useDocumentOperations"; import { DocumentRow, enrichDocuments, extractDocumentUrl } from "./utils"; type Props = { @@ -43,10 +43,11 @@ export function VolunteerProfileDocument({ volunteer }: Props) { [fetchedDocuments, receivedState], ); - const handleToggleReceived = (type: string) => { + const handleToggleReceived = (type: DocumentType) => { setReceivedState((prev) => { const current = prev[type] ?? { isReceived: false, receivedAt: null }; const isReceived = !current.isReceived; + receivedMutation.mutate({ volunteerId: volunteer.id, documentType: type, received: isReceived }); return { ...prev, [type]: { @@ -62,6 +63,7 @@ export function VolunteerProfileDocument({ volunteer }: Props) { closeDialog("delete"); closeDialog("preview"); }); + const receivedMutation = useMarkDocumentReceived(volunteer.id); const handleConfirmDelete = () => { if (deleteDialogDocument) { diff --git a/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/useDocumentOperations.ts b/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/useDocumentOperations.ts index 2aed2b75..53f94ba5 100644 --- a/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/useDocumentOperations.ts +++ b/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/useDocumentOperations.ts @@ -48,6 +48,20 @@ const deleteDocumentApi = async (volunteerId: number, documentType: DocumentType await axios.delete(`${apiPathVolunteer}/${volunteerId}/doc/${documentType}`); }; +type MarkReceivedPayload = { + volunteerId: number; + documentType: DocumentType; + received: boolean; +}; + +const markDocumentReceivedApi = async ( + volunteerId: number, + documentType: DocumentType, + received: boolean, +): Promise => { + await axios.patch(`${apiPathVolunteer}/${volunteerId}/doc/${documentType}/received`, { received }); +}; + export const useUploadDocument = (volunteerId: number, onSuccess?: () => void) => { const { t } = useTranslation(); @@ -74,3 +88,10 @@ export const useDeleteDocument = (volunteerId: number, onSuccess?: () => void) = onSuccessCallback: onSuccess, }); }; + +export const useMarkDocumentReceived = (volunteerId: number) => { + return useMutationQuery({ + mutationFn: ({ documentType, received }) => markDocumentReceivedApi(volunteerId, documentType, received), + queryKeyToInvalidate: ["volunteerDocuments", volunteerId.toString()], + }); +}; From 6e417d60855defe59964d575c778de76634b1df0 Mon Sep 17 00:00:00 2001 From: Rodrigo Louro Date: Tue, 28 Apr 2026 10:50:52 +0200 Subject: [PATCH 3/3] Update: Fix: wire received checkbox to API mutation and read state from API response Update: Fix: wire received checkbox to API mutation and read state from API response --- .../VolunteerProfileDocument.tsx | 25 +++---------------- .../useDocumentOperations.ts | 2 +- .../VolunteerProfileDocument/utils.ts | 23 +++++++++-------- 3 files changed, 18 insertions(+), 32 deletions(-) diff --git a/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/VolunteerProfileDocument.tsx b/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/VolunteerProfileDocument.tsx index 409344e1..6f957eba 100644 --- a/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/VolunteerProfileDocument.tsx +++ b/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/VolunteerProfileDocument.tsx @@ -32,30 +32,13 @@ export function VolunteerProfileDocument({ volunteer }: Props) { } = useDialogState(); const [documentUrl, setDocumentUrl] = useState(null); - const [receivedState, setReceivedState] = useState>( - {}, - ); const { data: fetchedDocuments, isLoading, isError } = useVolunteerDocuments(volunteer.id); - const documentRows = useMemo( - () => (fetchedDocuments ? enrichDocuments(fetchedDocuments, receivedState) : []), - [fetchedDocuments, receivedState], - ); + const documentRows = useMemo(() => (fetchedDocuments ? enrichDocuments(fetchedDocuments) : []), [fetchedDocuments]); - const handleToggleReceived = (type: DocumentType) => { - setReceivedState((prev) => { - const current = prev[type] ?? { isReceived: false, receivedAt: null }; - const isReceived = !current.isReceived; - receivedMutation.mutate({ volunteerId: volunteer.id, documentType: type, received: isReceived }); - return { - ...prev, - [type]: { - isReceived, - receivedAt: isReceived ? new Date() : null, - }, - }; - }); + const handleToggleReceived = (type: DocumentType, currentIsReceived: boolean) => { + receivedMutation.mutate({ volunteerId: volunteer.id, documentType: type, received: !currentIsReceived }); }; const uploadMutation = useUploadDocument(volunteer.id, () => closeDialog("upload")); @@ -147,7 +130,7 @@ export function VolunteerProfileDocument({ volunteer }: Props) { onPreview={() => handlePreview(row)} onDownload={() => handleDownload(row)} onDelete={() => openDialog("delete", row)} - onToggleReceived={() => handleToggleReceived(row.type)} + onToggleReceived={() => handleToggleReceived(row.type, row.isReceived)} /> ))} diff --git a/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/useDocumentOperations.ts b/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/useDocumentOperations.ts index 53f94ba5..2b0a3cf8 100644 --- a/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/useDocumentOperations.ts +++ b/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/useDocumentOperations.ts @@ -59,7 +59,7 @@ const markDocumentReceivedApi = async ( documentType: DocumentType, received: boolean, ): Promise => { - await axios.patch(`${apiPathVolunteer}/${volunteerId}/doc/${documentType}/received`, { received }); + await axios.patch(`${apiPathVolunteer}/${volunteerId}/doc/${documentType}`, { received }); }; export const useUploadDocument = (volunteerId: number, onSuccess?: () => void) => { diff --git a/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/utils.ts b/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/utils.ts index 2487b72b..5d54c963 100644 --- a/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/utils.ts +++ b/src/components/Dashboard/Profile/sections/VolunteerProfileDocument/utils.ts @@ -1,6 +1,12 @@ import { ApiDocumentGet, DocumentType } from "need4deed-sdk"; -export type EnrichedDocument = ApiDocumentGet & { +// TODO: remove cast once SDK is updated to include received and receivedOn (added by BE PR #417) +export type ApiDocumentGetWithReceived = ApiDocumentGet & { + received?: boolean; + receivedOn?: string; +}; + +export type EnrichedDocument = ApiDocumentGetWithReceived & { nameKey: string; isUploaded: boolean; }; @@ -9,7 +15,7 @@ export type DocumentRow = { type: DocumentType; nameKey: string; isUploaded: boolean; - document?: ApiDocumentGet; + document?: ApiDocumentGetWithReceived; isReceived: boolean; receivedAt: Date | null; }; @@ -42,22 +48,19 @@ export const extractDocumentUrl = (url: string): string | null => { } }; -export const enrichDocuments = ( - fetchedDocuments: ApiDocumentGet[], - receivedState: Record, -): DocumentRow[] => { +export const enrichDocuments = (fetchedDocuments: ApiDocumentGet[]): DocumentRow[] => { const allTypes = Object.keys(DOCUMENT_NAME_KEYS) as DocumentType[]; return allTypes.map((type) => { - const document = fetchedDocuments.find((doc) => doc.type === type); - const received = receivedState[type] ?? { isReceived: false, receivedAt: null }; + // TODO: remove cast once SDK is updated to include received and receivedOn (added by BE PR #417) + const document = fetchedDocuments.find((doc) => doc.type === type) as ApiDocumentGetWithReceived | undefined; return { type, nameKey: DOCUMENT_NAME_KEYS[type], isUploaded: !!document, document, - isReceived: received.isReceived, - receivedAt: received.receivedAt, + isReceived: document?.received ?? false, + receivedAt: document?.receivedOn ? new Date(document.receivedOn) : null, }; }); };