tech_mahindra/src/components/RecordViewTable.tsx
2026-05-22 23:19:34 +05:30

589 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState, useCallback, useRef } from "react";
import { useNavigate } from "react-router-dom";
import {
ChevronLeft, ChevronRight, Search, ArrowUp, ArrowDown,
X, Inbox, MoreHorizontal, ArrowRight, RefreshCw,
} from "lucide-react";
import { getRVScreen, getRecordView, type RecordViewField, type SearchQuery } from "../api/viewService";
import FormModal from "./FormModal";
import { STATE_IDS, ACTIVITY_IDS } from "../config";
import { CheckCircle2, RefreshCw as RefreshIcon } from "lucide-react";
// ---------------------------------------------------------------------------
// Row actions by state
// ---------------------------------------------------------------------------
const ROW_ACTIONS: Record<string, Array<{ activityId: string; label: string; icon: React.ReactNode }>> = {
[STATE_IDS.SCHEDULED]: [
{ activityId: ACTIVITY_IDS.MARK_TEST_DRIVE_DONE, label: "Mark Test Drive Done", icon: <CheckCircle2 size={13} /> },
],
[STATE_IDS.SCHEDULING_CALL_FAILED]: [
{ activityId: ACTIVITY_IDS.RETRY_SCHEDULING_CALL, label: "Retry Scheduling Call", icon: <RefreshIcon size={13} /> },
],
[STATE_IDS.FEEDBACK_CALL_FAILED]: [
{ activityId: ACTIVITY_IDS.RETRY_FEEDBACK_CALL, label: "Retry Feedback Call", icon: <RefreshIcon size={13} /> },
],
};
// State name (as returned by record view) → state UID. Used to resolve row actions.
const STATE_BY_NAME: Record<string, string> = {
"Awaiting Scheduler Call": STATE_IDS.AWAITING_SCHEDULER_CALL,
"Scheduled": STATE_IDS.SCHEDULED,
"Awaiting Feedback Call": STATE_IDS.AWAITING_FEEDBACK_CALL,
"Feedback Collected": STATE_IDS.FEEDBACK_COLLECTED,
"Scheduling Call Failed": STATE_IDS.SCHEDULING_CALL_FAILED,
"Feedback Call Failed": STATE_IDS.FEEDBACK_CALL_FAILED,
"End State": STATE_IDS.END_STATE,
};
// ---------------------------------------------------------------------------
// Skeleton
// ---------------------------------------------------------------------------
function TableSkeleton({ rows, cols }: { rows: number; cols: number }) {
return (
<div className="rounded-2xl border border-stone-200/80 dark:border-zinc-700/60 bg-white dark:bg-zinc-900 overflow-hidden shadow-sm">
<div className="px-5 py-3 border-b border-stone-100 dark:border-zinc-800 flex items-center gap-3">
<div className="h-5 w-16 bg-stone-100 dark:bg-zinc-800 rounded animate-pulse" />
<div className="ml-auto h-8 w-44 bg-stone-100 dark:bg-zinc-800 rounded-lg animate-pulse" />
</div>
<table className="w-full">
<thead>
<tr className="bg-stone-50/80 dark:bg-zinc-800/40 border-b border-stone-200/80 dark:border-zinc-700/80">
{Array.from({ length: cols }).map((_, i) => (
<th key={i} className={`py-2.5 ${i === 0 ? "pl-5" : "px-4"}`}>
<div className="h-3 bg-stone-200 dark:bg-zinc-700 rounded animate-pulse w-20" />
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-stone-100/80 dark:divide-zinc-800/60">
{Array.from({ length: rows }).map((_, r) => (
<tr key={r}>
{Array.from({ length: cols }).map((_, c) => (
<td key={c} className={`py-3.5 ${c === 0 ? "pl-5 pr-4" : "px-4"}`}>
<div className="h-3 bg-stone-100 dark:bg-zinc-800 rounded animate-pulse" style={{ width: `${60 + Math.random() * 40}%` }} />
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
// ---------------------------------------------------------------------------
// Main component
// ---------------------------------------------------------------------------
interface Props {
// Either the numeric source_screen_id or the source_uuid; the view-service
// accepts both at /app/:appId/rv-screens/:idOrUuid.
viewId: number | string;
onRowClick?: (instanceId: string) => void;
toolbarAction?: React.ReactNode;
}
// Walks the rv-screen layout to find the rv_table element and returns a
// key_name → display map for column header labels (the recordview response's
// output_label is empty for v2 envelope screens).
function extractColumnLabels(layout: any): Record<string, string> {
const out: Record<string, string> = {};
const walk = (nodes: any[]): void => {
for (const n of nodes ?? []) {
const cfg = n?.config;
if (cfg?.type === "rv_table" && Array.isArray(cfg.columns)) {
for (const c of cfg.columns) {
if (c?.key_name && c?.display) out[c.key_name] = c.display;
}
}
if (Array.isArray(n?.children)) walk(n.children);
}
};
walk(Array.isArray(layout) ? layout : []);
return out;
}
export default function RecordViewTable({ viewId, onRowClick, toolbarAction }: Props) {
const navigate = useNavigate();
const [rvTemplateUid, setRvTemplateUid] = useState<string | null>(null);
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({});
const [fields, setFields] = useState<RecordViewField[]>([]);
const [rows, setRows] = useState<Record<string, unknown>[]>([]);
const [pagination, setPagination] = useState({ page: 1, limit: 10, total_count: 0, total_pages: 0 });
const [query, setQuery] = useState<SearchQuery>({ page: 1, limit: 10, sort_dir: "desc" });
const [searchInput, setSearchInput] = useState("");
const [loading, setLoading] = useState(true);
const [dataLoading, setDataLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [formModal, setFormModal] = useState<{ activityId: string; instanceId: string; title: string } | null>(null);
const searchRef = useRef<HTMLInputElement>(null);
useEffect(() => {
setLoading(true);
setRvTemplateUid(null);
setColumnLabels({});
getRVScreen(viewId)
.then((rv) => {
setColumnLabels(extractColumnLabels(rv.layout));
setRvTemplateUid(rv.rv_template_uid);
})
.catch((err) => { setError(err.message); setLoading(false); });
}, [viewId]);
const fetchData = useCallback(async () => {
if (!rvTemplateUid) return;
if (!loading) setDataLoading(true);
setError(null);
try {
const res = await getRecordView(rvTemplateUid, viewId, query);
setFields(res.config.fields.filter((f) => f.field_key !== ""));
const dataRows = Array.isArray(res.data) ? res.data : [];
setRows(dataRows);
if (res.pagination) {
setPagination(res.pagination);
} else {
setPagination((p) => ({ ...p, total_count: dataRows.length, total_pages: Math.ceil(dataRows.length / p.limit) || 1 }));
}
} catch (err: any) { setError(err.message); }
finally { setLoading(false); setDataLoading(false); }
}, [rvTemplateUid, viewId, query]);
useEffect(() => { fetchData(); }, [fetchData]);
const handleSort = (key: string) => setQuery((q) => ({
...q, page: 1, sort_by: key,
sort_dir: q.sort_by === key && q.sort_dir === "asc" ? "desc" : "asc",
}));
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
setQuery((q) => ({ ...q, page: 1, search: searchInput }));
};
const clearSearch = () => {
setSearchInput("");
setQuery((q) => ({ ...q, page: 1, search: "" }));
};
const handlePage = (p: number) => setQuery((q) => ({ ...q, page: p }));
const handleRowClick = (row: Record<string, unknown>) => {
const id = String(row.instance_id ?? row._pk_value ?? "");
if (!id) return;
onRowClick ? onRowClick(id) : navigate(`/detail/${id}`);
};
if (loading) return <TableSkeleton rows={6} cols={fields.length || 6} />;
if (error) {
return (
<div className="rounded-2xl border border-red-100 dark:border-red-900/50 bg-red-50/50 dark:bg-red-950/20 p-10 text-center">
<p className="text-sm text-red-600 dark:text-red-400 mb-2">{error}</p>
<button onClick={fetchData} className="text-xs text-rose-600 dark:text-rose-400 hover:text-rose-800 font-semibold">Try again</button>
</div>
);
}
const primaryFieldKey = fields.find(
(f) => f.field_key !== "current_state_name" && f.field_key !== "instance_id" && f.data_type === "text"
)?.field_key;
const { page, total_count, total_pages, limit } = pagination;
const from = total_count > 0 ? (page - 1) * limit + 1 : 0;
const to = Math.min(page * limit, total_count);
const pages = buildPageRange(page, total_pages);
return (
<div className="rounded-2xl border border-stone-200/80 dark:border-zinc-700/60 bg-white dark:bg-zinc-900 overflow-hidden shadow-sm dark:shadow-black/20">
{/* ── Toolbar ───────────────────────────────────────────────────────── */}
<div className="px-5 py-3 flex items-center justify-between gap-4 border-b border-stone-100 dark:border-zinc-800">
<div className="flex items-center gap-3">
<div className="flex items-center gap-1.5">
<span className="text-[15px] font-bold text-stone-800 dark:text-zinc-100 tabular-nums leading-none">{total_count}</span>
<span className="text-[13px] text-stone-400 dark:text-zinc-500 leading-none">records</span>
</div>
{query.search && (
<div className="flex items-center gap-1.5 pl-3 border-l border-stone-200 dark:border-zinc-700">
<span className="text-[11px] text-stone-400 dark:text-zinc-500">Filtered:</span>
<span
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[11px] font-semibold border"
style={{ background: "rgba(200,16,46,0.08)", borderColor: "rgba(200,16,46,0.22)", color: "#C8102E" }}
>
{query.search}
<button onClick={clearSearch} className="ml-0.5 hover:opacity-60"><X size={9} /></button>
</span>
</div>
)}
{toolbarAction && (
<div className="pl-3 border-l border-stone-200 dark:border-zinc-700">
{toolbarAction}
</div>
)}
</div>
<div className="flex items-center gap-1.5">
<button
onClick={fetchData}
disabled={dataLoading}
title="Refresh"
className="w-8 h-8 rounded-lg flex items-center justify-center text-stone-400 dark:text-zinc-500 hover:text-stone-600 dark:hover:text-zinc-300 hover:bg-stone-100 dark:hover:bg-zinc-800 transition-colors disabled:opacity-40"
>
<RefreshCw size={14} className={dataLoading ? "animate-spin" : ""} />
</button>
<form onSubmit={handleSearch}>
<div className="relative">
<Search size={13} className="absolute left-2.5 top-1/2 -transtone-y-1/2 text-stone-400 dark:text-zinc-500 pointer-events-none" />
<input
ref={searchRef}
type="text"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
placeholder="Search…"
className="w-44 h-8 pl-8 pr-7 text-[13px] border border-stone-200 dark:border-zinc-700 rounded-lg bg-stone-50 dark:bg-zinc-800 text-stone-800 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-rose-500/20 focus:border-rose-400 dark:focus:border-rose-500 focus:bg-white dark:focus:bg-zinc-800 transition placeholder:text-stone-400 dark:placeholder:text-zinc-600"
/>
{searchInput && (
<button
type="button"
onClick={clearSearch}
className="absolute right-2 top-1/2 -transtone-y-1/2 text-stone-300 dark:text-zinc-600 hover:text-stone-500"
>
<X size={12} />
</button>
)}
</div>
</form>
</div>
</div>
{/* ── Table ─────────────────────────────────────────────────────────── */}
<div className="overflow-x-auto relative">
{dataLoading && (
<div className="absolute inset-0 bg-white/60 dark:bg-zinc-900/60 z-10 flex items-center justify-center backdrop-blur-[1px]">
<div className="w-5 h-5 border-2 border-rose-200 dark:border-rose-900 border-t-rose-600 rounded-full animate-spin" />
</div>
)}
<table className="w-full">
<thead>
<tr className="bg-stone-50/80 dark:bg-zinc-800/40 border-b border-stone-200/80 dark:border-zinc-700/80">
{fields.map((f, i) => {
const isSorted = query.sort_by === f.field_key;
return (
<th
key={f.field_key}
onClick={() => handleSort(f.field_key)}
className={`py-2.5 text-left text-[12px] font-semibold cursor-pointer select-none whitespace-nowrap transition-colors group/th
${i === 0 ? "pl-5 pr-4" : "px-4"}
${isSorted
? "text-rose-600 dark:text-rose-400"
: "text-stone-500 dark:text-zinc-400 hover:text-stone-700 dark:hover:text-zinc-200"
}
`}
>
<span className="inline-flex items-center gap-1">
{columnLabels[f.field_key] || f.output_label || f.field_key}
{isSorted
? (query.sort_dir === "asc"
? <ArrowUp size={11} className="text-rose-500" />
: <ArrowDown size={11} className="text-rose-500" />)
: <ArrowDown size={10} className="text-stone-300 dark:text-zinc-700 opacity-0 group-hover/th:opacity-100 transition-opacity" />
}
</span>
</th>
);
})}
<th className="py-2.5 pr-4 w-16" />
</tr>
</thead>
<tbody className="divide-y divide-stone-100/80 dark:divide-zinc-800/60">
{rows.length === 0 ? (
<tr>
<td colSpan={fields.length + 1} className="py-20 text-center">
<div className="flex flex-col items-center gap-3">
<div className="w-14 h-14 rounded-2xl flex items-center justify-center"
style={{ background: "linear-gradient(135deg,#fff5f5,#ffe4e6)" }}>
<Inbox size={22} className="text-rose-400" />
</div>
<div>
<div className="text-[14px] font-semibold text-stone-600 dark:text-zinc-300">No records found</div>
<div className="text-[12px] text-stone-400 dark:text-zinc-600 mt-1">
{query.search ? "Try a different search term" : "No data available"}
</div>
</div>
{query.search && (
<button onClick={clearSearch} className="text-[12px] font-semibold text-rose-600 dark:text-rose-400 hover:text-rose-800 transition-colors">
Clear search
</button>
)}
</div>
</td>
</tr>
) : rows.map((row, i) => {
const stateName = String(row.current_state_name ?? "");
const stateId = STATE_BY_NAME[stateName];
const actions = stateId ? ROW_ACTIONS[stateId] : undefined;
return (
<tr
key={String(row.instance_id ?? i)}
onClick={() => handleRowClick(row)}
className="group/row cursor-pointer hover:bg-stone-50/80 dark:hover:bg-zinc-800/40 transition-colors"
>
{fields.map((f, fi) => {
const isPrimary = f.field_key === primaryFieldKey;
const isFirst = fi === 0;
return (
<td
key={f.field_key}
className={`py-3.5 whitespace-nowrap text-[13px]
${isFirst
? "pl-5 pr-4 border-l-2 border-l-transparent group-hover/row:border-l-rose-500 transition-colors"
: "px-4"
}
${isPrimary
? "font-semibold text-stone-800 dark:text-zinc-100"
: "text-stone-500 dark:text-zinc-400"
}
`}
>
{renderCell(row[f.field_key], f.data_type, f.field_key)}
</td>
);
})}
<td className="pr-4 py-3 w-16 text-right" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-end gap-1 opacity-0 group-hover/row:opacity-100 transition-opacity">
{actions && actions.length > 0 && (
<RowMenu
actions={actions}
onAction={(activityId, label) =>
setFormModal({ activityId, instanceId: String(row.instance_id), title: label })
}
/>
)}
<button
onClick={() => handleRowClick(row)}
className="w-7 h-7 rounded-lg flex items-center justify-center text-stone-400 dark:text-zinc-500 hover:text-rose-600 dark:hover:text-rose-400 hover:bg-rose-100 dark:hover:bg-rose-950/50 transition-colors"
>
<ArrowRight size={14} />
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{/* ── Pagination ────────────────────────────────────────────────────── */}
{total_count > 0 && (
<div className="px-5 py-3 border-t border-stone-100 dark:border-zinc-800 flex items-center justify-between gap-4">
<div className="flex items-center gap-3 text-[12px] text-stone-400 dark:text-zinc-500">
<div className="flex items-center gap-1.5">
Show
<select
value={query.limit}
onChange={(e) => setQuery((q) => ({ ...q, page: 1, limit: Number(e.target.value) }))}
className="h-7 px-2 text-[12px] border border-stone-200 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-800 text-stone-600 dark:text-zinc-300 font-medium focus:outline-none cursor-pointer"
>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
</select>
per page
</div>
<span className="pl-3 border-l border-stone-200 dark:border-zinc-700 tabular-nums">
{from}{to} of {total_count}
</span>
</div>
{total_pages > 1 && (
<div className="flex items-center gap-0.5">
<button
disabled={page <= 1}
onClick={() => handlePage(page - 1)}
className="w-7 h-7 rounded-lg flex items-center justify-center text-stone-400 dark:text-zinc-500 hover:text-rose-600 dark:hover:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-950/30 disabled:opacity-30 disabled:pointer-events-none transition-colors"
>
<ChevronLeft size={15} />
</button>
{pages.map((p, idx) =>
p === "…" ? (
<span key={`e${idx}`} className="w-7 h-7 flex items-center justify-center text-[12px] text-stone-300 dark:text-zinc-700 select-none"></span>
) : (
<button
key={p}
onClick={() => handlePage(p as number)}
className={`w-7 h-7 rounded-lg text-[12px] font-medium transition-colors ${
p === page
? "bg-rose-600 text-white"
: "text-stone-500 dark:text-zinc-400 hover:bg-rose-50 dark:hover:bg-rose-950/30 hover:text-rose-600 dark:hover:text-rose-400"
}`}
>
{p}
</button>
)
)}
<button
disabled={page >= total_pages}
onClick={() => handlePage(page + 1)}
className="w-7 h-7 rounded-lg flex items-center justify-center text-stone-400 dark:text-zinc-500 hover:text-rose-600 dark:hover:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-950/30 disabled:opacity-30 disabled:pointer-events-none transition-colors"
>
<ChevronRight size={15} />
</button>
</div>
)}
</div>
)}
{formModal && (
<FormModal
activityId={formModal.activityId}
instanceId={formModal.instanceId}
title={formModal.title}
onClose={() => setFormModal(null)}
onSuccess={() => { setFormModal(null); fetchData(); }}
/>
)}
</div>
);
}
// ---------------------------------------------------------------------------
// Row menu
// ---------------------------------------------------------------------------
interface RowMenuProps {
actions: Array<{ activityId: string; label: string; icon: React.ReactNode }>;
onAction: (activityId: string, label: string) => void;
}
function RowMenu({ actions, onAction }: RowMenuProps) {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const close = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener("mousedown", close);
return () => document.removeEventListener("mousedown", close);
}, [open]);
return (
<div ref={ref} className="relative">
<button
onClick={() => setOpen(!open)}
className="w-7 h-7 rounded-lg flex items-center justify-center text-stone-400 dark:text-zinc-500 hover:text-stone-600 dark:hover:text-zinc-300 hover:bg-stone-100 dark:hover:bg-zinc-800 transition-colors"
>
<MoreHorizontal size={15} />
</button>
{open && (
<div className="absolute right-0 top-full mt-1 z-50 w-40 bg-white dark:bg-zinc-900 rounded-xl shadow-xl shadow-stone-900/10 dark:shadow-black/40 border border-stone-200 dark:border-zinc-700/70 py-1 overflow-hidden">
{actions.map((a) => (
<button
key={a.activityId}
onClick={() => { setOpen(false); onAction(a.activityId, a.label); }}
className="w-full text-left px-3.5 py-2 text-[13px] text-stone-600 dark:text-zinc-300 hover:bg-stone-50 dark:hover:bg-zinc-800 flex items-center gap-2.5 transition-colors"
>
<span className="text-stone-400 dark:text-zinc-500">{a.icon}</span>
{a.label}
</button>
))}
</div>
)}
</div>
);
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function buildPageRange(current: number, total: number): Array<number | ""> {
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
if (current <= 4) return [1, 2, 3, 4, 5, "…", total];
if (current >= total - 3) return [1, "…", total - 4, total - 3, total - 2, total - 1, total];
return [1, "…", current - 1, current, current + 1, "…", total];
}
function renderCell(value: unknown, dataType: string, fieldKey: string): React.ReactNode {
if (value == null || value === "") return <span className="text-stone-300 dark:text-zinc-700"></span>;
if (fieldKey === "current_state_name") return <StatusBadge value={String(value)} />;
if (dataType === "phone" && typeof value === "object") {
const p = value as { phone_with_dial_code?: string; phone?: string; dial_code?: string };
const text = p.phone_with_dial_code || (p.dial_code && p.phone ? `${p.dial_code}${p.phone}` : p.phone || "");
return text ? <span className="tabular-nums">{text}</span> : <span className="text-stone-300 dark:text-zinc-700"></span>;
}
if (dataType === "number") {
const n = Number(value);
if (fieldKey.includes("amount") || fieldKey.includes("Amount") || fieldKey.includes("tax") || fieldKey.includes("price"))
return <span className="tabular-nums font-medium text-stone-700 dark:text-zinc-200">{n.toLocaleString("en-IN", { style: "currency", currency: "INR", maximumFractionDigits: 0 })}</span>;
return <span className="tabular-nums">{n.toLocaleString()}</span>;
}
if (dataType === "datetime" || dataType === "date") {
try { return <span>{new Date(String(value)).toLocaleDateString("en-IN", { day: "2-digit", month: "short", year: "numeric" })}</span>; }
catch { return String(value); }
}
if (fieldKey === "instance_id") {
return <span className="font-mono text-[11px] text-stone-400 dark:text-zinc-500 bg-stone-100 dark:bg-zinc-800 px-1.5 py-0.5 rounded-md">{String(value).slice(0, 8)}</span>;
}
return String(value);
}
// ---------------------------------------------------------------------------
// Status badge — Mahindra workflow states
// ---------------------------------------------------------------------------
const STATUS: Record<string, { bg: string; text: string; dot: string; darkBg: string; darkText: string; darkDot: string }> = {
"Awaiting Scheduler Call": {
bg: "bg-rose-50 border-rose-200", text: "text-rose-700", dot: "bg-rose-500 animate-pulse",
darkBg: "dark:bg-rose-950/30 dark:border-rose-900", darkText: "dark:text-rose-300", darkDot: "dark:bg-rose-400",
},
"Scheduled": {
bg: "bg-amber-50 border-amber-200", text: "text-amber-700", dot: "bg-amber-500",
darkBg: "dark:bg-amber-950/30 dark:border-amber-900", darkText: "dark:text-amber-300", darkDot: "dark:bg-amber-400",
},
"Awaiting Feedback Call": {
bg: "bg-rose-50 border-rose-200", text: "text-rose-700", dot: "bg-rose-500 animate-pulse",
darkBg: "dark:bg-rose-950/30 dark:border-rose-900", darkText: "dark:text-rose-300", darkDot: "dark:bg-rose-400",
},
"Feedback Collected": {
bg: "bg-emerald-50 border-emerald-200", text: "text-emerald-700", dot: "bg-emerald-500",
darkBg: "dark:bg-emerald-950/30 dark:border-emerald-900", darkText: "dark:text-emerald-300", darkDot: "dark:bg-emerald-400",
},
"Scheduling Call Failed": {
bg: "bg-red-50 border-red-200", text: "text-red-700", dot: "bg-red-500",
darkBg: "dark:bg-red-950/30 dark:border-red-900", darkText: "dark:text-red-300", darkDot: "dark:bg-red-400",
},
"Feedback Call Failed": {
bg: "bg-red-50 border-red-200", text: "text-red-700", dot: "bg-red-500",
darkBg: "dark:bg-red-950/30 dark:border-red-900", darkText: "dark:text-red-300", darkDot: "dark:bg-red-400",
},
"End State": {
bg: "bg-stone-100 border-stone-200", text: "text-stone-600", dot: "bg-stone-400",
darkBg: "dark:bg-zinc-800 dark:border-zinc-700", darkText: "dark:text-zinc-400", darkDot: "dark:bg-zinc-500",
},
};
function StatusBadge({ value }: { value: string }) {
const s = STATUS[value] ?? { bg: "bg-stone-100 border-stone-200", text: "text-stone-600", dot: "bg-stone-400", darkBg: "dark:bg-zinc-800 dark:border-zinc-700", darkText: "dark:text-zinc-400", darkDot: "dark:bg-zinc-500" };
return (
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[11px] font-semibold border ${s.bg} ${s.text} ${s.darkBg} ${s.darkText}`}>
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${s.dot} ${s.darkDot}`} />
{value}
</span>
);
}