121 lines
5.8 KiB
TypeScript
121 lines
5.8 KiB
TypeScript
"use client";
|
|
import { useEffect, useState } from "react";
|
|
import { Search, Plus, Lock } from "lucide-react";
|
|
import { api } from "@/lib/api";
|
|
|
|
export default function Support() {
|
|
const [orgs, setOrgs] = useState<any[]>([]);
|
|
const [q, setQ] = useState("");
|
|
const [selected, setSelected] = useState<string>("");
|
|
const [view, setView] = useState<any>(null);
|
|
const [notes, setNotes] = useState<any[]>([]);
|
|
const [note, setNote] = useState("");
|
|
|
|
useEffect(() => { api("/admin/organizations").then(setOrgs).catch(() => {}); }, []);
|
|
|
|
async function open(orgId: string) {
|
|
setSelected(orgId);
|
|
api(`/admin/organizations/${orgId}/support-view`).then(setView).catch(() => {});
|
|
api(`/admin/organizations/${orgId}/notes`).then(setNotes).catch(() => setNotes([]));
|
|
}
|
|
async function addNote() {
|
|
if (!note.trim()) return;
|
|
await api(`/admin/organizations/${selected}/notes`, { method: "POST", body: JSON.stringify({ note }) });
|
|
setNote("");
|
|
api(`/admin/organizations/${selected}/notes`).then(setNotes);
|
|
}
|
|
|
|
const filtered = orgs.filter((o) => o.name.toLowerCase().includes(q.toLowerCase()));
|
|
|
|
return (
|
|
<div className="space-y-5">
|
|
<div className="flex items-center gap-2">
|
|
<h2 className="text-xl font-bold">Support View</h2>
|
|
<span className="text-xs font-semibold bg-slate-100 text-slate-500 px-2 py-1 rounded-full flex items-center gap-1">
|
|
<Lock className="h-3 w-3" /> Read-only
|
|
</span>
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-[260px_1fr] gap-5">
|
|
{/* Org list */}
|
|
<div className="card p-4 h-fit">
|
|
<div className="relative mb-3">
|
|
<Search className="absolute left-3 top-2.5 h-4 w-4 text-slate-400" />
|
|
<input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search org…"
|
|
className="pl-9 pr-3 py-2 w-full rounded-lg border border-slate-200 text-sm" />
|
|
</div>
|
|
<div className="space-y-1 max-h-[60vh] overflow-y-auto">
|
|
{filtered.map((o) => (
|
|
<button key={o.org_id} onClick={() => open(o.org_id)}
|
|
className={`w-full text-left px-3 py-2 rounded-lg text-sm ${selected === o.org_id ? "bg-brand-50 text-brand-700" : "hover:bg-slate-50"}`}>
|
|
{o.name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Detail */}
|
|
<div className="space-y-4">
|
|
{!view ? (
|
|
<div className="card p-12 text-center text-slate-400">Select an organization to debug.</div>
|
|
) : (
|
|
<>
|
|
<div className="card p-5">
|
|
<h3 className="font-bold">{view.org.name}</h3>
|
|
<p className="text-sm text-slate-500 capitalize">{view.org.status} · {view.org.contact_email}</p>
|
|
</div>
|
|
|
|
<div className="card overflow-x-auto">
|
|
<h3 className="font-bold p-4 pb-2">Products ({view.products.length})</h3>
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-slate-50 text-slate-500 text-left">
|
|
<tr><th className="px-4 py-2">Name</th><th className="px-4 py-2">SKU</th><th className="px-4 py-2">Status</th><th className="px-4 py-2">QRs</th></tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-100">
|
|
{view.products.map((p: any, i: number) => (
|
|
<tr key={i}><td className="px-4 py-2">{p.name}</td><td className="px-4 py-2">{p.sku}</td><td className="px-4 py-2 capitalize">{p.status}</td><td className="px-4 py-2">{p.qr_count}</td></tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div className="card overflow-x-auto">
|
|
<h3 className="font-bold p-4 pb-2">Recent Scans</h3>
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-slate-50 text-slate-500 text-left">
|
|
<tr><th className="px-4 py-2">Time</th><th className="px-4 py-2">Code</th><th className="px-4 py-2">City</th><th className="px-4 py-2">Device</th></tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-100">
|
|
{view.recent_scans.map((s: any, i: number) => (
|
|
<tr key={i}><td className="px-4 py-2 text-slate-500">{new Date(s.at).toLocaleString()}</td><td className="px-4 py-2 font-mono">{s.code}</td><td className="px-4 py-2">{s.city ?? "—"}</td><td className="px-4 py-2">{s.device ?? "—"}</td></tr>
|
|
))}
|
|
{view.recent_scans.length === 0 && <tr><td colSpan={4} className="px-4 py-6 text-center text-slate-400">No scans.</td></tr>}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div className="card p-5">
|
|
<h3 className="font-bold mb-3">Internal Notes</h3>
|
|
<div className="flex gap-2 mb-3">
|
|
<input value={note} onChange={(e) => setNote(e.target.value)} placeholder="Add a debugging note…"
|
|
className="flex-1 px-3 py-2 rounded-lg border border-slate-200 text-sm" />
|
|
<button onClick={addNote} className="flex items-center gap-1 bg-brand-600 text-white px-3 py-2 rounded-lg text-sm font-semibold"><Plus className="h-4 w-4" /> Add</button>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{notes.map((n) => (
|
|
<div key={n.id} className="text-sm border-b border-slate-100 py-2">
|
|
<div>{n.note}</div>
|
|
<div className="text-xs text-slate-400">{n.author} · {new Date(n.at).toLocaleString()}</div>
|
|
</div>
|
|
))}
|
|
{notes.length === 0 && <p className="text-sm text-slate-400">No notes.</p>}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|