From 50f0591b418c847da3a6b69ebd2a857e7c09c1b7 Mon Sep 17 00:00:00 2001 From: Bhanu Prakash Sai Potteri Date: Sat, 23 May 2026 11:08:10 +0530 Subject: [PATCH] Render rv_tile / rv_chart elements above record views --- src/api/viewService.ts | 20 ++++++++++++ src/components/BarChart.tsx | 52 ++++++++++++++++++++++++++++++ src/components/RecordViewTable.tsx | 6 ++-- src/components/ScreenRenderer.tsx | 38 ++++++++++++++++------ src/components/Tile.tsx | 21 ++++++++++++ 5 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 src/components/BarChart.tsx create mode 100644 src/components/Tile.tsx diff --git a/src/api/viewService.ts b/src/api/viewService.ts index c1c1e69..29e48ee 100644 --- a/src/api/viewService.ts +++ b/src/api/viewService.ts @@ -136,6 +136,24 @@ export interface RecordViewField { activity_id?: string; } +export interface TileValue { + tile_uid: string; + key: string; + value: unknown; +} + +export interface ChartDataRow { + dimension: unknown; + series?: string; + value: unknown; +} + +export interface ChartDataResponse { + chart_uid: string; + key: string; + rows: ChartDataRow[]; +} + export interface RecordViewResponse { config: { fields: RecordViewField[] }; data: Record[]; @@ -145,6 +163,8 @@ export interface RecordViewResponse { total_count: number; total_pages: number; }; + tile_values?: TileValue[]; + chart_data?: ChartDataResponse[]; } export function getRecordView( diff --git a/src/components/BarChart.tsx b/src/components/BarChart.tsx new file mode 100644 index 0000000..5439626 --- /dev/null +++ b/src/components/BarChart.tsx @@ -0,0 +1,52 @@ +interface Row { + dimension: unknown; + value: unknown; +} + +interface Props { + title?: string; + rows: Row[]; + loading?: boolean; +} + +export default function BarChart({ title, rows, loading }: Props) { + const data = (rows ?? []).map(r => ({ label: String(r.dimension ?? "—"), value: Number(r.value ?? 0) })); + const max = Math.max(1, ...data.map(d => d.value)); + + return ( +
+ {title && ( +
+ {title} +
+ )} + {loading ? ( +
+ {[0,1,2,3].map(i =>
)} +
+ ) : data.length === 0 ? ( +
No data
+ ) : ( +
+ {data.map((d, i) => { + const pct = (d.value / max) * 100; + return ( +
+ {d.label} +
+
+
+ + {d.value.toLocaleString("en-IN")} + +
+ ); + })} +
+ )} +
+ ); +} diff --git a/src/components/RecordViewTable.tsx b/src/components/RecordViewTable.tsx index 45938ea..3569c89 100644 --- a/src/components/RecordViewTable.tsx +++ b/src/components/RecordViewTable.tsx @@ -4,7 +4,7 @@ import { ChevronLeft, ChevronRight, Search, ArrowUp, ArrowDown, X, Inbox, ArrowRight, RefreshCw, CheckCircle2, } from "lucide-react"; -import { getRVScreen, getRecordView, type RecordViewField, type SearchQuery } from "../api/viewService"; +import { getRVScreen, getRecordView, type RecordViewField, type SearchQuery, type TileValue, type ChartDataResponse } from "../api/viewService"; import FormModal from "./FormModal"; // --------------------------------------------------------------------------- @@ -141,6 +141,7 @@ interface Props { viewId: number | string; onRowClick?: (instanceId: string) => void; toolbarAction?: React.ReactNode; + onAnalyticsLoaded?: (tiles: TileValue[], charts: ChartDataResponse[]) => void; } // Walks the rv-screen layout to find the rv_table element and returns a @@ -163,7 +164,7 @@ function extractColumnLabels(layout: any): Record { return out; } -export default function RecordViewTable({ viewId, onRowClick, toolbarAction }: Props) { +export default function RecordViewTable({ viewId, onRowClick, toolbarAction, onAnalyticsLoaded }: Props) { const navigate = useNavigate(); const [rvTemplateUid, setRvTemplateUid] = useState(null); const [columnLabels, setColumnLabels] = useState>({}); @@ -199,6 +200,7 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction }: P setError(null); try { const res = await getRecordView(rvTemplateUid, viewId, query); + onAnalyticsLoaded?.(res.tile_values ?? [], res.chart_data ?? []); setFields(res.config.fields.filter((f) => f.field_key !== "")); const dataRows = Array.isArray(res.data) ? res.data : []; setRows(dataRows); diff --git a/src/components/ScreenRenderer.tsx b/src/components/ScreenRenderer.tsx index 6ae0ba4..84d9d26 100644 --- a/src/components/ScreenRenderer.tsx +++ b/src/components/ScreenRenderer.tsx @@ -1,9 +1,11 @@ import { useState, ReactNode } from "react"; import { Plus } from "lucide-react"; -import type { LayoutElement } from "../api/viewService"; +import type { LayoutElement, TileValue, ChartDataResponse } from "../api/viewService"; import RecordViewTable from "./RecordViewTable"; import DetailViewPanel from "./DetailViewPanel"; import FormModal from "./FormModal"; +import Tile from "./Tile"; +import BarChart from "./BarChart"; interface Props { layout: LayoutElement[]; @@ -19,8 +21,9 @@ interface AnyNode { children?: AnyNode[]; } -// Flatten the screen layout depth-first. Container nodes (section/column/block) -// carry no config we care about — only their leaf "element" descendants do. +interface TileSpec { kind: "tile"; uid: string; title?: string; style?: Record } +interface ChartSpec { kind: "chart"; uid: string; title?: string; style?: Record } + function flatten(nodes: AnyNode[] | undefined): AnyNode[] { if (!nodes) return []; const out: AnyNode[] = []; @@ -33,16 +36,16 @@ function flatten(nodes: AnyNode[] | undefined): AnyNode[] { export default function ScreenRenderer({ layout, instanceId, onRefresh, onRowClick }: Props) { const [formModal, setFormModal] = useState<{ activityId: string; instanceId?: string; title?: string } | null>(null); + const [tileValues, setTileValues] = useState([]); + const [chartData, setChartData] = useState([]); - // Collect button elements; they get injected into the toolbar of the - // *next* record-view we find. Everything else renders inline in order. const buttons: ReactNode[] = []; const elements: { type: "rv" | "dv"; viewId: number | string; style?: Record }[] = []; + const analytics: (TileSpec | ChartSpec)[] = []; for (const node of flatten(layout as AnyNode[])) { const cfg = node.config!; if (cfg.type === "button" || cfg.job_template) { - // V1 uses activity_id_init / activity_id; V2 uses activity_uid_init / activity_uid. const aid = cfg.params?.activity_uid_init || cfg.params?.activity_id_init || @@ -61,19 +64,36 @@ export default function ScreenRenderer({ layout, instanceId, onRefresh, onRowCli ); } else if (cfg.type === "recordsview") { - // Prefer view_uuid — the v2 envelope renumbers numeric IDs across envs, - // so the UUID is the stable cross-env handle. const id = cfg.view_uuid || cfg.view_id; elements.push({ type: "rv", viewId: id, style: node.style }); } else if (cfg.type === "detailsview") { const id = cfg.view_uuid || cfg.view_id; elements.push({ type: "dv", viewId: id, style: node.style }); + } else if (cfg.type === "rv_tile" && cfg.tile_uid) { + analytics.push({ kind: "tile", uid: cfg.tile_uid, title: cfg.title, style: node.style }); + } else if (cfg.type === "rv_chart" && cfg.chart_uid) { + analytics.push({ kind: "chart", uid: cfg.chart_uid, title: cfg.title, style: node.style }); } } + const tilesLoading = analytics.length > 0 && tileValues.length === 0 && chartData.length === 0; + return ( <>
+ {analytics.length > 0 && ( +
+ {analytics.map((a, i) => { + if (a.kind === "tile") { + const tv = tileValues.find(t => t.tile_uid === a.uid); + return ; + } + const cd = chartData.find(c => c.chart_uid === a.uid); + return ; + })} +
+ )} + {elements.map((el, i) => { if (el.type === "rv") { return ( @@ -82,6 +102,7 @@ export default function ScreenRenderer({ layout, instanceId, onRefresh, onRowCli viewId={el.viewId} onRowClick={onRowClick} toolbarAction={buttons.length > 0 ?
{buttons}
: undefined} + onAnalyticsLoaded={(tiles, charts) => { setTileValues(tiles); setChartData(charts); }} />
); @@ -91,7 +112,6 @@ export default function ScreenRenderer({ layout, instanceId, onRefresh, onRowCli } return null; })} - {/* If there's no record view to host the buttons, show them standalone. */} {elements.length === 0 && buttons.length > 0 && (
{buttons}
)} diff --git a/src/components/Tile.tsx b/src/components/Tile.tsx new file mode 100644 index 0000000..aa5b018 --- /dev/null +++ b/src/components/Tile.tsx @@ -0,0 +1,21 @@ +interface Props { + title?: string; + value: number | string | null | undefined; + loading?: boolean; +} + +export default function Tile({ title, value, loading }: Props) { + const display = value == null ? "—" : typeof value === "number" ? value.toLocaleString("en-IN") : String(value); + return ( +
+ {title && ( +
+ {title} +
+ )} +
+ {loading ? : display} +
+
+ ); +}