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> = { [STATE_IDS.SCHEDULED]: [ { activityId: ACTIVITY_IDS.MARK_TEST_DRIVE_DONE, label: "Mark Test Drive Done", icon: }, ], [STATE_IDS.SCHEDULING_CALL_FAILED]: [ { activityId: ACTIVITY_IDS.RETRY_SCHEDULING_CALL, label: "Retry Scheduling Call", icon: }, ], [STATE_IDS.FEEDBACK_CALL_FAILED]: [ { activityId: ACTIVITY_IDS.RETRY_FEEDBACK_CALL, label: "Retry Feedback Call", icon: }, ], }; // State name (as returned by record view) → state UID. Used to resolve row actions. const STATE_BY_NAME: Record = { "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 (
{Array.from({ length: cols }).map((_, i) => ( ))} {Array.from({ length: rows }).map((_, r) => ( {Array.from({ length: cols }).map((_, c) => ( ))} ))}
); } // --------------------------------------------------------------------------- // 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 { const out: Record = {}; 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(null); const [columnLabels, setColumnLabels] = useState>({}); const [fields, setFields] = useState([]); const [rows, setRows] = useState[]>([]); const [pagination, setPagination] = useState({ page: 1, limit: 10, total_count: 0, total_pages: 0 }); const [query, setQuery] = useState({ 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(null); const [formModal, setFormModal] = useState<{ activityId: string; instanceId: string; title: string } | null>(null); const searchRef = useRef(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) => { const id = String(row.instance_id ?? row._pk_value ?? ""); if (!id) return; onRowClick ? onRowClick(id) : navigate(`/detail/${id}`); }; if (loading) return ; if (error) { return (

{error}

); } 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 (
{/* ── Toolbar ───────────────────────────────────────────────────────── */}
{total_count} records
{query.search && (
Filtered: {query.search}
)} {toolbarAction && (
{toolbarAction}
)}
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 && ( )}
{/* ── Table ─────────────────────────────────────────────────────────── */}
{dataLoading && (
)} {fields.map((f, i) => { const isSorted = query.sort_by === f.field_key; return ( ); })} {rows.length === 0 ? ( ) : 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 ( 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 ( ); })} ); })}
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" } `} > {columnLabels[f.field_key] || f.output_label || f.field_key} {isSorted ? (query.sort_dir === "asc" ? : ) : }
No records found
{query.search ? "Try a different search term" : "No data available"}
{query.search && ( )}
{renderCell(row[f.field_key], f.data_type, f.field_key)} e.stopPropagation()}>
{actions && actions.length > 0 && ( setFormModal({ activityId, instanceId: String(row.instance_id), title: label }) } /> )}
{/* ── Pagination ────────────────────────────────────────────────────── */} {total_count > 0 && (
Show per page
{from}–{to} of {total_count}
{total_pages > 1 && (
{pages.map((p, idx) => p === "…" ? ( ) : ( ) )}
)}
)} {formModal && ( setFormModal(null)} onSuccess={() => { setFormModal(null); fetchData(); }} /> )}
); } // --------------------------------------------------------------------------- // 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(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 (
{open && (
{actions.map((a) => ( ))}
)}
); } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function buildPageRange(current: number, total: number): Array { 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 ; if (fieldKey === "current_state_name") return ; 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 ? {text} : ; } if (dataType === "number") { const n = Number(value); if (fieldKey.includes("amount") || fieldKey.includes("Amount") || fieldKey.includes("tax") || fieldKey.includes("price")) return {n.toLocaleString("en-IN", { style: "currency", currency: "INR", maximumFractionDigits: 0 })}; return {n.toLocaleString()}; } if (dataType === "datetime" || dataType === "date") { try { return {new Date(String(value)).toLocaleDateString("en-IN", { day: "2-digit", month: "short", year: "numeric" })}; } catch { return String(value); } } if (fieldKey === "instance_id") { return {String(value).slice(0, 8)}; } return String(value); } // --------------------------------------------------------------------------- // Status badge — Mahindra workflow states // --------------------------------------------------------------------------- const STATUS: Record = { "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 ( {value} ); }