// components/lists/CO2GalvoList.tsx "use client"; import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; type Owner = | string | number | { id?: string | number; username?: string | null; first_name?: string | null; last_name?: string | null; email?: string | null; } | null | undefined; type SettingsRow = { submission_id: string | number; setting_title?: string | null; uploader?: string | null; owner?: Owner; mat?: { name?: string | null } | null; mat_coat?: { name?: string | null } | null; source?: { model?: string | null } | null; lens?: { field_size?: string | number | null } | null; }; export type CO2GalvoListProps = { /** Build the href for a record (portal vs standalone) */ linkFor: (submission_id: string | number, opts?: { edit?: boolean }) => string; /** Optional controlled search text (else internal input) */ queryText?: string; onQueryChange?: (q: string) => void; }; async function readJson(res: Response) { const text = await res.text(); try { return text ? JSON.parse(text) : null; } catch { throw new Error(`Unexpected response (status ${res.status})`); } } export default function CO2GalvoList({ linkFor, queryText, onQueryChange }: CO2GalvoListProps) { const [settings, setSettings] = useState([]); const [ownerMap, setOwnerMap] = useState>({}); const [loading, setLoading] = useState(true); const [resolvingOwners, setResolvingOwners] = useState(false); const [meId, setMeId] = useState(null); const [localQuery, setLocalQuery] = useState(queryText ?? ""); // keep local and external search text in sync useEffect(() => { if (queryText !== undefined) setLocalQuery(queryText); }, [queryText]); // load current user id (for edit visibility) useEffect(() => { let dead = false; (async () => { try { const r = await fetch(`/api/dx/users/me?fields=id`, { cache: "no-store", credentials: "include", }); if (!r.ok) return; const j = await readJson(r); const id = j?.data?.id ?? j?.id ?? null; if (!dead) setMeId(id ? String(id) : null); } catch { /* ignore */ } })(); return () => { dead = true; }; }, []); // load list useEffect(() => { const fields = [ "submission_id", "setting_title", "uploader", "owner", "owner.id", "owner.username", "photo.id", "photo.title", "mat.name", "mat_coat.name", "source.model", "lens.field_size", ].join(","); const url = `/api/dx/items/settings_co2gal?fields=${encodeURIComponent(fields)}&limit=-1`; fetch(url, { cache: "no-store", credentials: "include" }) .then(async (res) => { if (!res.ok) { const j = await readJson(res).catch(() => null); const msg = (j as any)?.errors?.[0]?.message || `HTTP ${res.status}`; throw new Error(msg); } return readJson(res); }) .then((json: any) => setSettings(Array.isArray(json?.data) ? json.data : [])) .catch((e) => { console.error("CO2 Galvo list fetch failed:", e); setSettings([]); }) .finally(() => setLoading(false)); }, []); // resolve owner usernames if needed useEffect(() => { if (!settings.length) return; const ids = new Set(); for (const s of settings) { const o = s.owner; if (!o) continue; if (typeof o === "string" || typeof o === "number") { const k = String(o); if (!ownerMap[k]) ids.add(k); } else { const id = o.id != null ? String(o.id) : ""; const hasUsername = !!o.username; if (id && !hasUsername && !ownerMap[id]) ids.add(id); } } if (!ids.size) return; const all = Array.from(ids); const chunk = 100; setResolvingOwners(true); (async () => { const updates: Record = {}; for (let i = 0; i < all.length; i += chunk) { const slice = all.slice(i, i + chunk); const qs = new URLSearchParams(); qs.set("fields", "id,username"); qs.set("limit", String(slice.length)); qs.set("filter[id][_in]", slice.join(",")); try { const r = await fetch(`/api/dx/users?${qs.toString()}`, { credentials: "include", cache: "no-store", }); if (!r.ok) { const j = await readJson(r).catch(() => null); const msg = (j as any)?.errors?.[0]?.message || `HTTP ${r.status}`; console.warn("Owner lookup failed:", msg); if (r.status === 401 || r.status === 403) break; continue; } const j = await readJson(r); const rows: Array<{ id: string; username?: string | null }> = j?.data || []; for (const row of rows) { updates[String(row.id)] = row.username || String(row.id); } } catch (e) { console.warn("Owner lookup error:", e); break; } } if (Object.keys(updates).length) { setOwnerMap((prev) => ({ ...prev, ...updates })); } setResolvingOwners(false); })(); }, [settings, ownerMap]); const isMine = (o: Owner) => { if (!meId || !o) return false; if (typeof o === "string" || typeof o === "number") return String(o) === meId; if (o.id != null) return String(o.id) === meId; return false; }; const ownerLabel = (o: Owner) => { if (!o) return "—"; if (typeof o === "string" || typeof o === "number") { const id = String(o); return ownerMap[id] || id; } return ( o.username || [o.first_name, o.last_name].filter(Boolean).join(" ").trim() || o.email || (o.id != null ? String(o.id) : "—") ); }; const filtered = useMemo(() => { const nq = (localQuery || "").toLowerCase(); if (!nq) return settings; return settings.filter((entry) => { const fields = [ entry.setting_title, ownerLabel(entry.owner), entry.uploader, entry.mat?.name, entry.mat_coat?.name, entry.source?.model, entry.lens?.field_size as any, ].filter(Boolean); return fields.some((v: any) => String(v).toLowerCase().includes(nq)); }); }, [settings, localQuery, ownerMap]); return (
{ setLocalQuery(e.currentTarget.value); onQueryChange?.(e.currentTarget.value); }} placeholder="Search by title, owner, material, model…" className="w-full border rounded px-3 py-2" />
{loading ? (

Loading…

) : (
{filtered.map((s) => { const mine = isMine(s.owner); const ownerText = ownerLabel(s.owner) + (mine ? " (you)" : ""); return ( ); })}
Title Owner {resolvingOwners ? "…resolving" : ""} Material Coating Model Field Edit
{s.setting_title || "Untitled"} {ownerText} {s.mat?.name || "—"} {s.mat_coat?.name || "—"} {s.source?.model || "—"} {s.lens?.field_size || "—"} {mine ? ( Edit ) : ( )}
)}
); }