Files
buzz-sheet/components/commission/mobile-slip-handoff.tsx
T
gunshiz 2517982a27
CI / Verify (push) Successful in 2m4s
CI / Build immutable images and deploy (push) Successful in 3m8s
feat : also update
2026-10-01 19:47:02 +07:00

97 lines
4.7 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { verificationError } from "@/lib/commission/verification-error";
type LinkDetails = { qr: string; digest: string; expiresAt: number };
export function MobileSlipHandoff({ checkoutId }: { checkoutId: string }) {
const router = useRouter();
const [link, setLink] = useState<LinkDetails | null>(null);
const [open, setOpen] = useState(false);
const [busy, setBusy] = useState(false);
const [expired, setExpired] = useState(false);
const [error, setError] = useState("");
const [now, setNow] = useState(Date.now);
const secondsLeft = link ? Math.max(0, Math.ceil((link.expiresAt - now) / 1000)) : 0;
const timeLeft = `${String(Math.floor(secondsLeft / 60)).padStart(2, "0")}:${String(secondsLeft % 60).padStart(2, "0")}`;
async function createLink() {
setBusy(true); setError("");
try {
const response = await fetch(`/api/commission/checkouts/${checkoutId}/upload-link`, { method: "POST" });
const result = await response.json();
if (!response.ok) throw new Error(verificationError(result.error));
setLink(result as LinkDetails);
setNow(Date.now());
setExpired(false);
setOpen(true);
} catch (cause) { setError(cause instanceof Error ? cause.message : "สร้าง QR ไม่สำเร็จ"); }
finally { setBusy(false); }
}
useEffect(() => {
if (!link) return;
const timer = window.setInterval(() => setNow(Date.now()), 1000);
return () => window.clearInterval(timer);
}, [link]);
useEffect(() => {
if (!link) return;
let active = true;
const endpoint = `/api/commission/checkouts/${checkoutId}/upload-link?digest=${link.digest}`;
const check = async () => {
try {
const response = await fetch(endpoint, { cache: "no-store" });
if (!response.ok) return;
const result = await response.json();
if (!active) return;
if (result.state === "complete") router.push(`/commission/tickets/${result.ticketId}`);
else if (result.state === "expired") setExpired(true);
else setError(result.error ? verificationError(result.error) : "");
} catch { /* The next event or reconnect checks again. */ }
};
const source = new EventSource("/api/commission/events");
source.addEventListener("open", check);
source.addEventListener("changed", check);
const timer = window.setTimeout(() => void check(), Math.max(0, link.expiresAt - Date.now() + 500));
void check();
return () => {
active = false;
source.removeEventListener("open", check);
source.removeEventListener("changed", check);
source.close();
window.clearTimeout(timer);
};
}, [checkoutId, link, router]);
return <div className="flex w-full flex-col items-start gap-3">
<Button type="button" variant="link" className="h-auto px-0 text-foreground underline" disabled={busy}
onClick={() => { if (link && !expired && secondsLeft > 0) setOpen(true); else void createLink(); }}>
{busy ? "กำลังสร้าง QR..." : "อัปโหลดจากมือถือ"}
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>อัปโหลดสลิปจากมือถือ</DialogTitle>
<DialogDescription>สแกน QR ด้วยมือถือ แล้วเลือกรูปสลิปจากแอปธนาคาร</DialogDescription>
</DialogHeader>
{link && <div className="flex flex-col items-center gap-3">
{!expired && <>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={link.qr} alt="QR สำหรับเปิดหน้าอัปโหลดสลิปบนมือถือ" width={240} height={240} />
<p className="text-center text-sm text-muted-foreground">เวลาที่เหลือ {timeLeft}</p>
</>}
{expired && <p className="text-center text-sm text-muted-foreground">ลิงก์หมดอายุแล้ว ปิดหน้าต่างนี้แล้วกดอัปโหลดจากมือถืออีกครั้ง</p>}
</div>}
{error && <Alert variant="destructive"><AlertDescription>{error}</AlertDescription></Alert>}
</DialogContent>
</Dialog>
{error && !open && <Alert variant="destructive"><AlertDescription>{error}</AlertDescription></Alert>}
</div>;
}