160 lines
7.7 KiB
TypeScript
160 lines
7.7 KiB
TypeScript
"use client";
|
|
import { useEffect, useState } from "react";
|
|
import { X, Download, Power, RefreshCw, Copy, Check } from "lucide-react";
|
|
import { api, apiBase } from "@/lib/api";
|
|
|
|
type Tab = "overview" | "stats" | "history";
|
|
|
|
export default function QrDetailDrawer({ qrId, onClose, onChanged }: {
|
|
qrId: string; onClose: () => void; onChanged: () => void;
|
|
}) {
|
|
const [d, setD] = useState<any>(null);
|
|
const [tab, setTab] = useState<Tab>("overview");
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
function load() { api(`/qr/${qrId}/detail`).then(setD).catch(() => {}); }
|
|
useEffect(() => { load(); }, [qrId]);
|
|
|
|
async function disable() { await api(`/qr/${qrId}/disable`, { method: "PUT" }); load(); onChanged(); }
|
|
async function regenerate() { await api(`/qr/${qrId}/regenerate`, { method: "POST" }); load(); onChanged(); }
|
|
function copyUrl() {
|
|
navigator.clipboard?.writeText(d?.short_url ?? "");
|
|
setCopied(true); setTimeout(() => setCopied(false), 1500);
|
|
}
|
|
|
|
if (!d) return (
|
|
<div className="fixed inset-0 bg-black/40 z-50 grid place-items-center p-4" onClick={onClose}>
|
|
<div className="bg-white rounded-2xl w-full max-w-md p-5" onClick={(e) => e.stopPropagation()}>
|
|
<div className="skeleton h-40 rounded-xl" />
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const preview = d.image_url ? `${apiBase}${d.image_url}` : `${apiBase}/qr/${qrId}/download?format=png`;
|
|
const statusColor = d.status === "active" ? "bg-emerald-50 text-emerald-700" : "bg-rose-50 text-rose-600";
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/40 z-50 grid place-items-center p-4 overflow-y-auto" onClick={onClose}>
|
|
<div className="bg-white rounded-2xl w-full max-w-md my-4 flex flex-col max-h-[90vh] overflow-y-auto" onClick={(e) => e.stopPropagation()}>
|
|
<div className="flex items-center justify-between px-5 py-4 border-b border-slate-100 sticky top-0 bg-white rounded-t-2xl">
|
|
<h3 className="font-bold">QR Details</h3>
|
|
<button onClick={onClose}><X className="h-5 w-5 text-slate-400" /></button>
|
|
</div>
|
|
|
|
<div className="p-5 space-y-4">
|
|
<div className="flex gap-4">
|
|
<img src={preview} alt="QR" className="h-24 w-24 rounded-lg border border-slate-100 object-contain" />
|
|
<div className="text-sm space-y-1">
|
|
<span className={`text-xs font-semibold px-2 py-0.5 rounded-full ${statusColor} capitalize`}>{d.status}</span>
|
|
<div className="text-slate-400 text-xs mt-2">QR ID</div>
|
|
<div className="font-mono text-xs">{d.qr_id_label}</div>
|
|
<div className="text-slate-400 text-xs">Short URL</div>
|
|
<button onClick={copyUrl} className="text-brand-600 text-xs flex items-center gap-1">
|
|
{d.short_url?.replace(/^https?:\/\//, "")} {copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* tabs */}
|
|
<div className="flex gap-1 border-b border-slate-200 text-sm">
|
|
{([["overview", "Overview"], ["stats", "Scan Statistics"], ["history", "History"]] as const).map(([t, l]) => (
|
|
<button key={t} onClick={() => setTab(t)}
|
|
className={`px-3 py-2 border-b-2 -mb-px font-medium ${tab === t ? "border-brand-600 text-brand-700" : "border-transparent text-slate-500"}`}>
|
|
{l}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{tab === "overview" && (
|
|
<div className="space-y-2 text-sm">
|
|
<Row label="Product" value={d.overview.product} />
|
|
<Row label="SKU" value={d.overview.sku} />
|
|
<Row label="Batch No." value={d.overview.batch_number} />
|
|
<Row label="QR Type" value={d.qr_type} cap />
|
|
<Row label="Created By" value={d.overview.created_by} />
|
|
<Row label="Created On" value={d.overview.created_at ? new Date(d.overview.created_at).toLocaleString() : "—"} />
|
|
<div className="flex justify-between border-b border-slate-50 py-1.5">
|
|
<span className="text-slate-400">Redirect URL</span>
|
|
<a href={d.overview.redirect_url} target="_blank" rel="noreferrer"
|
|
className="text-brand-600 truncate max-w-[160px] text-right">{d.overview.redirect_url}</a>
|
|
</div>
|
|
<div className="flex justify-between border-b border-slate-50 py-1.5">
|
|
<span className="text-slate-400">Verification Enabled</span>
|
|
<span className={d.overview.verification_enabled ? "text-emerald-600 font-medium" : "text-rose-600 font-medium"}>
|
|
{d.overview.verification_enabled ? "✓ Yes" : "Disabled"}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between border-b border-slate-50 py-1.5">
|
|
<span className="text-slate-400">Expiry</span>
|
|
<span className="text-emerald-600 font-medium">{d.overview.expiry}</span>
|
|
</div>
|
|
<Row label="Remarks" value={d.overview.remarks} />
|
|
</div>
|
|
)}
|
|
|
|
{tab === "stats" && (
|
|
<div className="grid grid-cols-3 gap-3 text-center">
|
|
<Stat label="Total" value={d.stats.total} />
|
|
<Stat label="Today" value={d.stats.today} />
|
|
<Stat label="This Week" value={d.stats.week} />
|
|
</div>
|
|
)}
|
|
|
|
{tab === "history" && (
|
|
<div className="space-y-1 text-sm max-h-72 overflow-y-auto">
|
|
{d.history.map((h: any, i: number) => (
|
|
<div key={i} className="flex justify-between border-b border-slate-100 py-1.5">
|
|
<span className="text-slate-500">{new Date(h.at).toLocaleString()}</span>
|
|
<span>{h.city ?? "—"} · {h.device ?? "—"}</span>
|
|
</div>
|
|
))}
|
|
{d.history.length === 0 && <p className="text-slate-400">No scans yet.</p>}
|
|
</div>
|
|
)}
|
|
|
|
{/* downloads */}
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<a href={`${apiBase}/qr/${qrId}/download?format=png`}
|
|
className="flex items-center justify-center gap-1 bg-brand-600 text-white rounded-lg py-2 text-xs font-semibold">
|
|
<Download className="h-3.5 w-3.5" /> Download PNG
|
|
</a>
|
|
<a href={`${apiBase}/qr/${qrId}/download?format=svg`}
|
|
className="flex items-center justify-center gap-1 border border-emerald-300 text-emerald-700 bg-emerald-50 rounded-lg py-2 text-xs font-semibold">
|
|
<Download className="h-3.5 w-3.5" /> Download SVG
|
|
</a>
|
|
<a href={`${apiBase}/qr/${qrId}/download?format=pdf`}
|
|
className="flex items-center justify-center gap-1 border border-orange-300 text-orange-700 bg-orange-50 rounded-lg py-2 text-xs font-semibold">
|
|
<Download className="h-3.5 w-3.5" /> Download PDF
|
|
</a>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<button onClick={disable} className="flex items-center justify-center gap-1 border border-slate-200 rounded-lg py-2 text-sm font-medium">
|
|
<Power className="h-4 w-4" /> Disable QR
|
|
</button>
|
|
<button onClick={regenerate} className="flex items-center justify-center gap-1 bg-rose-50 text-rose-600 rounded-lg py-2 text-sm font-semibold">
|
|
<RefreshCw className="h-4 w-4" /> Regenerate QR
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Row({ label, value, cap }: any) {
|
|
return (
|
|
<div className="flex justify-between border-b border-slate-50 py-1.5">
|
|
<span className="text-slate-400">{label}</span>
|
|
<span className={`font-medium text-slate-700 ${cap ? "capitalize" : ""}`}>{value ?? "—"}</span>
|
|
</div>
|
|
);
|
|
}
|
|
function Stat({ label, value }: any) {
|
|
return (
|
|
<div className="card p-3">
|
|
<div className="text-xl font-extrabold">{value}</div>
|
|
<div className="text-xs text-slate-500">{label}</div>
|
|
</div>
|
|
);
|
|
}
|