97 lines
4.7 KiB
TypeScript
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>;
|
|
}
|