From b12eec218b2b18f3f9491f76199c2f1d0d69e1a9 Mon Sep 17 00:00:00 2001 From: tifpham021 Date: Sat, 28 Mar 2026 23:59:17 -0400 Subject: [PATCH 1/6] added partner signature functionality and ui --- .../migration.sql | 3 + prisma/schema.prisma | 2 + src/app/api/reset/route.ts | 16 +- src/app/api/signOffs/[signOffId]/route.ts | 13 ++ src/app/api/signOffs/route.ts | 11 + src/components/SignOffModal.tsx | 193 ++++++++++++------ src/components/SignOffsTable.tsx | 81 ++++++-- src/components/SignatureImageTooltip.tsx | 8 +- .../chips/ShipmentsLineItemChipGroup.tsx | 79 +++++-- .../dashboard/PartnerDistributionTable.tsx | 2 +- src/services/shippingStatusService.ts | 7 + src/services/signOffService.ts | 65 ++++-- src/types/api/distribution.types.ts | 2 + src/types/api/shippingStatus.types.ts | 2 + src/types/api/signOff.types.ts | 5 + 15 files changed, 356 insertions(+), 133 deletions(-) create mode 100644 prisma/migrations/20260328061136_add_partner_signature_to_signoff/migration.sql diff --git a/prisma/migrations/20260328061136_add_partner_signature_to_signoff/migration.sql b/prisma/migrations/20260328061136_add_partner_signature_to_signoff/migration.sql new file mode 100644 index 00000000..16e637dc --- /dev/null +++ b/prisma/migrations/20260328061136_add_partner_signature_to_signoff/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "SignOff" ADD COLUMN "partnerSignatureUrl" TEXT, +ADD COLUMN "partnerSignerName" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7593db7a..b9b770c9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -310,8 +310,10 @@ model SignOff { staffMemberName String partnerId Int partnerName String + partnerSignerName String? date DateTime signatureUrl String? + partnerSignatureUrl String? createdAt DateTime @default(now()) diff --git a/src/app/api/reset/route.ts b/src/app/api/reset/route.ts index f483aee8..6c82a4d3 100644 --- a/src/app/api/reset/route.ts +++ b/src/app/api/reset/route.ts @@ -17,19 +17,22 @@ async function resetDatabase() { const signOffs = await db.signOff.findMany({ select: { signatureUrl: true, + partnerSignatureUrl: true, }, }); // Delete signatures from Azure Storage for (const signOff of signOffs) { - if (signOff.signatureUrl) { + const signatureUrls = [ + signOff.signatureUrl, + signOff.partnerSignatureUrl, + ].filter(Boolean); + + for (const signatureUrl of signatureUrls) { try { - await FileService.deleteSignature(signOff.signatureUrl); + await FileService.deleteSignature(signatureUrl!); } catch (error) { - console.warn( - `Failed to delete signature: ${signOff.signatureUrl}`, - error - ); + console.warn(`Failed to delete signature: ${signatureUrl}`, error); } } } @@ -75,4 +78,3 @@ export async function POST() { return errorResponse(error); } } - diff --git a/src/app/api/signOffs/[signOffId]/route.ts b/src/app/api/signOffs/[signOffId]/route.ts index 821bb52f..545b1c7a 100644 --- a/src/app/api/signOffs/[signOffId]/route.ts +++ b/src/app/api/signOffs/[signOffId]/route.ts @@ -24,12 +24,15 @@ const patchSchema = z.object({ staffName: z.string().optional(), partnerId: z.number().int().positive().optional(), partnerName: z.string().optional(), + partnerSignerName: z.string().optional(), date: z .string() .transform((str) => new Date(str)) .optional(), signatureUrl: z.string().optional(), + partnerSignatureUrl: z.string().optional(), allocations: z.array(z.number().int().positive()).optional(), + staffUserId: z.number().int().positive().optional(), }); export async function GET( @@ -85,6 +88,7 @@ export async function PATCH( } const form = await request.formData(); + const userId = parseInt(session.user.id!); const formObj = { staffName: form.get("staffName") !== null @@ -98,15 +102,24 @@ export async function PATCH( form.get("partnerName") !== null ? String(form.get("partnerName")) : undefined, + partnerSignerName: + form.get("partnerSignerName") !== null + ? String(form.get("partnerSignerName")) + : undefined, date: form.get("date") !== null ? String(form.get("date")) : undefined, signatureUrl: form.get("signatureUrl") !== null ? String(form.get("signatureUrl")) : undefined, + partnerSignatureUrl: + form.get("partnerSignatureUrl") !== null + ? String(form.get("partnerSignatureUrl")) + : undefined, allocations: form.getAll("allocation").length > 0 ? form.getAll("allocation").map((val) => Number(val)) : undefined, + staffUserId: userId, }; const parsed = patchSchema.safeParse(formObj); diff --git a/src/app/api/signOffs/route.ts b/src/app/api/signOffs/route.ts index c047d1f0..9b198f4b 100644 --- a/src/app/api/signOffs/route.ts +++ b/src/app/api/signOffs/route.ts @@ -14,8 +14,10 @@ const createSignOffSchema = z.object({ partnerId: z.number().int().positive(), staffName: z.string(), partnerName: z.string(), + partnerSignerName: z.string(), date: z.string().transform((str) => new Date(str)), signatureUrl: z.string(), + partnerSignatureUrl: z.string(), allocations: z.array(z.number()), staffUserId: z.number().int().positive().optional(), }); @@ -85,6 +87,15 @@ export async function POST(req: NextRequest) { ? String(form.get("partnerName")) : undefined, date: form.get("date") != null ? String(form.get("date")) : undefined, + partnerSignerName: + form.get("partnerSignerName") != null + ? String(form.get("partnerSignerName")) + : undefined, + partnerSignatureUrl: + form.get("partnerSignatureUrl") != null + ? String(form.get("partnerSignatureUrl")) + : undefined, + signatureUrl: form.get("signatureUrl") != null ? String(form.get("signatureUrl")) diff --git a/src/components/SignOffModal.tsx b/src/components/SignOffModal.tsx index 4317c9df..f8fd4e94 100644 --- a/src/components/SignOffModal.tsx +++ b/src/components/SignOffModal.tsx @@ -28,7 +28,6 @@ export default function SignOffModal({ }: SignOffModalProps) { const { user } = useUser(); const { apiClient } = useApiClient(); - const signatureCanvasRef = useRef(null); const [isSubmitting, setIsSubmitting] = useState(false); const { data: userData } = useFetch<{ @@ -37,37 +36,56 @@ export default function SignOffModal({ const staffName = userData?.user?.name || userData?.user?.email || ""; const currentDate = new Date(); + const staffSignatureCanvasRef = useRef(null); + const partnerSignatureCanvasRef = useRef(null); + const [partnerSignerName, setPartnerSignerName] = useState(""); useEffect(() => { - if (isOpen && signatureCanvasRef.current) { - signatureCanvasRef.current.clear(); - } + if (!isOpen) return; + + staffSignatureCanvasRef.current?.clear(); + partnerSignatureCanvasRef.current?.clear(); + setPartnerSignerName(""); }, [isOpen]); const handleSubmit = async () => { - if (!signatureCanvasRef.current) { - toast.error("Please provide a signature"); + if (!partnerSignerName.trim()) { + toast.error("Please enter the partner name"); return; } - if (signatureCanvasRef.current.isEmpty()) { - toast.error("Please provide a signature"); + if (!staffSignatureCanvasRef.current || !partnerSignatureCanvasRef.current) { + toast.error("Please provide both signatures"); + return; + } + + if ( + staffSignatureCanvasRef.current.isEmpty() || + partnerSignatureCanvasRef.current.isEmpty() + ) { + toast.error("Please provide both signatures"); return; } setIsSubmitting(true); try { - const base64Data = signatureCanvasRef.current + const signatureUrl = staffSignatureCanvasRef.current + .getCanvas() + .toDataURL("image/png"); + const partnerSignatureUrl = partnerSignatureCanvasRef.current .getCanvas() .toDataURL("image/png"); const formData = new FormData(); formData.append("partnerId", partnerId.toString()); - formData.append("staffName", staffName); formData.append("partnerName", partnerName); + formData.append("partnerSignerName", partnerSignerName); + formData.append("staffName", staffName); formData.append("date", currentDate.toISOString()); - formData.append("signatureUrl", base64Data); + formData.append("signatureUrl", signatureUrl); + formData.append("partnerSignatureUrl", partnerSignatureUrl); + selectedAllocationIds.forEach((id) => { formData.append("allocation", id.toString()); }); @@ -87,17 +105,14 @@ export default function SignOffModal({ } }; - const handleClear = () => { - if (signatureCanvasRef.current) { - signatureCanvasRef.current.clear(); - } - }; + const clearStaffSignature = () => staffSignatureCanvasRef.current?.clear(); + const clearPartnerSignature = () => partnerSignatureCanvasRef.current?.clear(); if (!isOpen) return null; return ( -
-
+
+
-

- Sign Off Items -

- -
-
- - -
+
+

+ Sign Off Items +

+
+ +
+
+
+ + +
+ +
+ + +
-
- - +
+ +
+ +
+ +
-
- -
- +
+ + setPartnerSignerName(e.target.value)} + className="w-full rounded-[4px] border border-[rgba(34,7,11,0.05)] bg-[rgba(34,7,11,0.05)] p-3 text-[16px]" + /> +
+ +
+ +
- + +
+ +
+ +
+ +
-
+
-

+

Sign Off Items

@@ -136,7 +136,7 @@ export default function SignOffModal({ type="text" value={staffName} readOnly - className="w-full rounded-[4px] border border-[rgba(34,7,11,0.05)] bg-[rgba(34,7,11,0.05)] p-3 text-[16px] cursor-not-allowed" + className="w-full rounded-sm border border-gray-primary/10 bg-sunken p-3 text-base text-gray-primary cursor-not-allowed" />
@@ -148,7 +148,7 @@ export default function SignOffModal({ type="text" value={format(currentDate, "MMM d, yyyy 'at' h:mm a")} readOnly - className="w-full rounded-[4px] border border-[rgba(34,7,11,0.05)] bg-[rgba(34,7,11,0.05)] p-3 text-[16px] cursor-not-allowed" + className="w-full rounded-sm border border-gray-primary/10 bg-sunken p-3 text-base text-gray-primary cursor-not-allowed" />
@@ -156,7 +156,7 @@ export default function SignOffModal({ -
+
setPartnerSignerName(e.target.value)} - className="w-full rounded-[4px] border border-[rgba(34,7,11,0.05)] bg-[rgba(34,7,11,0.05)] p-3 text-[16px]" + className="w-full rounded-sm border border-gray-primary/10 bg-sunken p-3 text-base text-gray-primary" />
@@ -196,7 +196,7 @@ export default function SignOffModal({ type="text" value={format(currentDate, "MMM d, yyyy 'at' h:mm a")} readOnly - className="w-full rounded-[4px] border border-[rgba(34,7,11,0.05)] bg-[rgba(34,7,11,0.05)] p-3 text-[16px] cursor-not-allowed" + className="w-full rounded-sm border border-gray-primary/10 bg-sunken p-3 text-base text-gray-primary cursor-not-allowed" />
@@ -204,7 +204,7 @@ export default function SignOffModal({ -
+
Cancel diff --git a/src/services/signOffService.ts b/src/services/signOffService.ts index 0a212b39..370a5ca6 100644 --- a/src/services/signOffService.ts +++ b/src/services/signOffService.ts @@ -81,24 +81,44 @@ export class SignOffService { signOffId: number, data: Partial ) { + const { + allocations, + staffName, + staffUserId, + partnerId, + partnerName, + partnerSignerName, + date, + } = data; + const signatureUrl = await SignOffService.uploadSignatureIfNeeded( data.signatureUrl, - data.staffUserId + staffUserId ); const partnerSignatureUrl = await SignOffService.uploadSignatureIfNeeded( data.partnerSignatureUrl, - data.staffUserId + staffUserId ); return db.signOff.update({ where: { id: signOffId }, data: { - ...data, - signatureUrl, - partnerSignatureUrl, - allocations: data.allocations + ...(staffName !== undefined + ? { staffMemberName: staffName } + : {}), + ...(partnerId !== undefined ? { partnerId } : {}), + ...(partnerName !== undefined ? { partnerName } : {}), + ...(partnerSignerName !== undefined + ? { partnerSignerName } + : {}), + ...(date !== undefined ? { date } : {}), + ...(signatureUrl !== undefined ? { signatureUrl } : {}), + ...(partnerSignatureUrl !== undefined + ? { partnerSignatureUrl } + : {}), + allocations: allocations ? { - connect: data.allocations.map((id) => ({ id })), + connect: allocations.map((id) => ({ id })), } : undefined, }, From 48b74cec0de9cb26bf34104a239bf44fef467563 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 2 Apr 2026 00:21:01 -0400 Subject: [PATCH 4/6] display partner signature on partner end too --- .../dashboard/PartnerDistributionTable.tsx | 31 +++++++++++++++++++ src/services/allocationService.ts | 2 ++ src/types/api/allocation.types.ts | 2 ++ 3 files changed, 35 insertions(+) diff --git a/src/components/dashboard/PartnerDistributionTable.tsx b/src/components/dashboard/PartnerDistributionTable.tsx index 786c204b..5b4e490f 100644 --- a/src/components/dashboard/PartnerDistributionTable.tsx +++ b/src/components/dashboard/PartnerDistributionTable.tsx @@ -198,6 +198,37 @@ const PartnerDistributionTable = forwardRef< "-" ), }, + { + id: "partnerSignerName" as const, + header: "Received By", + cell: (row: PartnerAllocation) => + row.partnerSignerName ? ( + <> + + {row.partnerSignerName} + + {row.partnerSignatureUrl && ( + + )} + + ) : ( + "-" + ), + }, ]; return ( diff --git a/src/services/allocationService.ts b/src/services/allocationService.ts index 00830a3f..e715fb01 100644 --- a/src/services/allocationService.ts +++ b/src/services/allocationService.ts @@ -618,6 +618,8 @@ export default class AllocationService { signOffStaffMemberName: allocation.signOff?.staffMemberName, signOffId: allocation.signOff?.id, signOffSignatureUrl: allocation.signOff?.signatureUrl || undefined, + partnerSignerName: allocation.signOff?.partnerSignerName || undefined, + partnerSignatureUrl: allocation.signOff?.partnerSignatureUrl || undefined, }; }); diff --git a/src/types/api/allocation.types.ts b/src/types/api/allocation.types.ts index 1f6937ab..04ad9421 100644 --- a/src/types/api/allocation.types.ts +++ b/src/types/api/allocation.types.ts @@ -54,6 +54,8 @@ export interface PartnerAllocation { signOffStaffMemberName?: string; signOffId?: number; signOffSignatureUrl?: string; + partnerSignerName?: string; + partnerSignatureUrl?: string; } export interface PartnerAllocationsResponse { From 6e30b101e9d11b90defbe05a24b7d57b1db1243a Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 2 Apr 2026 00:24:57 -0400 Subject: [PATCH 5/6] updated tutorial to include recieved by column --- src/components/dashboard/PartnerDistributionsSection.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/dashboard/PartnerDistributionsSection.tsx b/src/components/dashboard/PartnerDistributionsSection.tsx index 11315e1d..a496bc48 100644 --- a/src/components/dashboard/PartnerDistributionsSection.tsx +++ b/src/components/dashboard/PartnerDistributionsSection.tsx @@ -76,7 +76,8 @@ const tutorialSteps: Step[] = [
  • Quantity
  • Sign off date
  • Signed off by
  • -
  • Donor offer
  • +
  • Recieved by
  • +
  • Donor
  • ), @@ -294,6 +295,7 @@ export default function PartnerDistributionsSection() { shipmentStatus: $Enums.ShipmentStatus.READY_FOR_DISTRIBUTION, signOffDate: new Date(), signOffStaffMemberName: "Tutorial Staff", + partnerSignerName: "Tutorial Partner", }; tableRef.current?.setItems((items) => [ From 0fcde990c835f0d6cf22f6733b88d3b36c92bdab Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 2 Apr 2026 00:27:54 -0400 Subject: [PATCH 6/6] undo notification handler lint --- src/components/NotificationHandler.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/components/NotificationHandler.tsx b/src/components/NotificationHandler.tsx index c47de083..e46361bd 100644 --- a/src/components/NotificationHandler.tsx +++ b/src/components/NotificationHandler.tsx @@ -66,13 +66,11 @@ export default function NotificationHandler({ const router = useRouter(); const [client, setClient] = useState(null); const [notifications, setNotifications] = useState([]); - const activeSupportTab = searchParams.get("activeTab"); const [chatUnreadCount, setChatUnreadCount] = useState(0); // Ensure that mirroring doesn't overwrite sessionStorage before loading const isHydrated = useRef(false); - const activeSupportTabRef = useRef(activeSupportTab); useEffect(() => { const realtime = getRealtimeClient(); @@ -131,10 +129,6 @@ export default function NotificationHandler({ [apiClient, notifications] ); - useEffect(() => { - activeSupportTabRef.current = activeSupportTab; - }, [activeSupportTab]); - useEffect(() => { if (!user) { sessionStorage.removeItem("chat_notifications"); @@ -299,7 +293,7 @@ export default function NotificationHandler({ const channelId = event.channel?.id ?? event.cid?.split(":")[1]; if (!channelId) return; - if (activeSupportTabRef.current === "Unresolved") return; + if (searchParams.get("activeTab") === "Unresolved") return; const text = event.message?.text?.trim(); const channelName = @@ -363,7 +357,7 @@ export default function NotificationHandler({ return () => { didInterrupt = true; - streamClient.off("message.new", handleTicketMessage); + streamClient.off("notification.message_new", handleTicketMessage); streamClient.off("notification.mark_read", handleMarkRead); streamClient .disconnectUser()