diff --git a/app/api/admin/orders-by-timeslot/route.ts b/app/api/admin/orders-by-timeslot/route.ts index 3ad7eb1..c1a5ed2 100644 --- a/app/api/admin/orders-by-timeslot/route.ts +++ b/app/api/admin/orders-by-timeslot/route.ts @@ -133,7 +133,13 @@ export async function GET(request: NextRequest) { paymentMethod: string | null; bankTransferVerified: boolean | null; pricing: { total: number }; - files: { fileName: string; copies: number; colorMode?: string }[]; + files: { + fileName: string; + copies: number; + colorMode?: string; + stagingKey?: string; + permanentKey?: string; + }[]; customer: { name: string; email: string } | string; pickedUpAt: string | null; } @@ -150,6 +156,8 @@ export async function GET(request: NextRequest) { fileName: f.fileName as string, copies: f.copies as number, colorMode: (f.colorMode as string) || undefined, + stagingKey: (f.stagingKey as string) || undefined, + permanentKey: (f.permanentKey as string) || undefined, })), customer: typeof order.customer === "object" && order.customer diff --git a/components/admin/OrdersByTimeslotView.tsx b/components/admin/OrdersByTimeslotView.tsx index 2eb94f3..6d18572 100644 --- a/components/admin/OrdersByTimeslotView.tsx +++ b/components/admin/OrdersByTimeslotView.tsx @@ -1,7 +1,8 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; -import { OrderStatus } from "../../types/orderStatus"; +import { useCallback, useEffect, useRef, useState } from "react"; +import type { OrderStatusValue } from "../../types/orderStatus"; +import { OrderStatus, PAID_STATUSES } from "../../types/orderStatus"; import BackToDashboard from "./BackToDashboard"; interface TimeslotData { @@ -19,7 +20,13 @@ interface OrderData { paymentMethod: string | null; bankTransferVerified: boolean | null; pricing: { total: number }; - files: { fileName: string; copies: number; colorMode?: string }[]; + files: { + fileName: string; + copies: number; + colorMode?: string; + stagingKey?: string; + permanentKey?: string; + }[]; customer: { name: string; email: string } | string; pickedUpAt: string | null; } @@ -126,6 +133,116 @@ export default function OrdersByTimeslotView() { return { name: "—", email: "—" }; }; + /** Resolve the correct R2 key for a file based on order payment status */ + const getFileKey = useCallback( + ( + file: OrderData["files"][number], + orderStatus: string, + ): { key: string; staging: boolean } | null => { + const isPaid = PAID_STATUSES.includes(orderStatus as OrderStatusValue); + if (isPaid && file.permanentKey) { + return { key: file.permanentKey, staging: false }; + } + if (file.stagingKey) { + return { key: file.stagingKey, staging: true }; + } + return null; + }, + [], + ); + + /** Fetch a presigned URL and open it in a new tab */ + const handleDownloadFile = useCallback( + async (file: OrderData["files"][number], orderStatus: string) => { + const resolved = getFileKey(file, orderStatus); + if (!resolved) return; + + try { + const params = new URLSearchParams({ + key: resolved.key, + staging: resolved.staging.toString(), + }); + const res = await fetch(`/api/admin/file-url?${params}`); + if (!res.ok) throw new Error("Failed to get file URL"); + const { url } = await res.json(); + window.open(url, "_blank"); + } catch { + window.alert(`Failed to open file: ${file.fileName}`); + } + }, + [getFileKey], + ); + + /** Download all files for all orders in a timeslot group sequentially */ + const downloadingRef = useRef(false); + const handleDownloadAllFiles = useCallback( + async (orders: OrderData[]) => { + if (downloadingRef.current) return; + downloadingRef.current = true; + + try { + // Collect all files with their resolved keys + const filesToDownload: { + key: string; + staging: boolean; + fileName: string; + }[] = []; + for (const order of orders) { + for (const file of order.files || []) { + const resolved = getFileKey(file, order.status); + if (resolved) { + filesToDownload.push({ ...resolved, fileName: file.fileName }); + } + } + } + + if (filesToDownload.length === 0) { + window.alert("No downloadable files found."); + return; + } + + // Fetch all presigned URLs in parallel + const urlResults = await Promise.all( + filesToDownload.map(async ({ key, staging, fileName }) => { + try { + const params = new URLSearchParams({ + key, + staging: staging.toString(), + }); + const res = await fetch(`/api/admin/file-url?${params}`); + if (!res.ok) return null; + const { url } = await res.json(); + return { url, fileName }; + } catch { + return null; + } + }), + ); + + // Open each file in a new tab with a small delay to avoid popup blockers + const validUrls = urlResults.filter(Boolean) as { + url: string; + fileName: string; + }[]; + for (let i = 0; i < validUrls.length; i++) { + window.open(validUrls[i].url, "_blank"); + if (i < validUrls.length - 1) { + await new Promise((r) => setTimeout(r, 300)); + } + } + + if (validUrls.length < filesToDownload.length) { + window.alert( + `Opened ${validUrls.length} of ${filesToDownload.length} files. Some files could not be loaded.`, + ); + } + } finally { + downloadingRef.current = false; + } + }, + [getFileKey], + ); + return (