import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Container, Card, Button, Form, Modal, Spinner, } from "react-bootstrap"; import { useLocation, useNavigate, useParams } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import PageComponent from "../components/PageComponent/PageComponent"; import CustomIcon from "../components/CustomIcon/CustomIcon"; import SectionHeading from "../components/UI/SectionHeading/SectionHeading"; import metricReportService from "../services/MetricReport"; import authService from "../services/Auth"; import MetricReportDataTable from "../components/MetricReports/MetricReportDataTable"; import MetricReportValuePanel from "../components/MetricReports/MetricReportValuePanel"; import { useLinePathDraw } from "../components/MetricReports/metricChartDraw"; import { getTrendChartLayout, trendXLabelAnchor, } from "../components/MetricReports/metricTrendChartLayout"; import "../components/MetricReports/metricChartAnimations.css"; import { buildMetricExportFileBase, exportRowsCsv, } from "../components/MetricReports/metricReportExportUtil"; import LeaderboardExportModal from "../components/MetricReports/LeaderboardExportModal"; import { buildLeaderboardAdditionalFieldOptions, buildLeaderboardExportColumns, buildMetricLeaderboardExportColumns, getMetricLeaderboardExportFromApi, LEADERBOARD_EXPORT_FORM_DEFAULT, prepareLeaderboardExportRows, prepareLeaderboardExportRowsFromRaw, } from "../components/MetricReports/leaderboardExportUtil"; import { enrichMetricLeaderboardRow, enrichMetricLeaderboardTableExportRow, getLeaderboardTabularConfig, leaderboardConfigToExportColumns, mapLeaderboardRowsFromConfig, normalizeLeaderboardTabularConfig, } from "../components/MetricReports/leaderboardTabularUtil"; import { exportHtmlElementAsPng, exportTableAsPng, LEADERBOARD_PNG_MAX_ROWS, } from "../components/MetricReports/exportTableAsPng"; import "./MetricReports.css"; import { handleAlert } from "../util/util"; function isDoctorListTabName(tab) { if (!tab || typeof tab !== "string") return false; const t = tab.trim().toLowerCase().replace(/\s+/g, " "); if (t === "doctor list" || t === "doctorlist") return true; return t.includes("doctor") && t.includes("list"); } function isDetailOrDoctorListTab(tab) { if (!tab || typeof tab !== "string") return false; const t = tab.trim().toLowerCase().replace(/\s+/g, " "); if (t === "detail") return true; return isDoctorListTabName(tab); } /** Freeze first column only on wide tabular tables (Detail, Doctor List, Trend tabular). */ function tabularStickyLeadingCount(columns) { const n = Array.isArray(columns) ? columns.length : 0; return n > 3 ? 1 : 0; } /** Doctor List API: graph.values has contacts[], users { [contactId]: user }, metrics[] with stats { [contactId]: number } */ function isDoctorListValuesShape(values) { if (!values || typeof values !== "object") return false; if (!Array.isArray(values.contacts)) return false; const u = values.users; if (u == null || typeof u !== "object" || Array.isArray(u)) return false; return true; } function titleCaseChart(c) { if (!c) return ""; const s = String(c).toLowerCase(); if (s === "tabular") return "Tabular"; if (s === "line") return "Line"; if (s === "bar") return "Bar"; return s.charAt(0).toUpperCase() + s.slice(1); } function toNumber(v) { const n = typeof v === "number" ? v : Number(v); return Number.isFinite(n) ? n : 0; } function normalizeTrendSeries(graph) { // Supports both: // - simple: graph.values = [{x, y}, ...] // - grouped: graph.values = [{metric, stats: [{x, y}, ...]}, ...] const values = graph?.values; if (!values) return []; if (Array.isArray(values)) { const first = values[0]; if (first && typeof first === "object" && Array.isArray(first.stats)) { return values.map((s) => ({ label: s?.metric || s?.label || "Series", points: (s?.stats || []).map((p) => ({ x: p?.x, y: toNumber(p?.y) })), })); } return [ { label: graph?.label || graph?.name || "Trend", points: values.map((p) => ({ x: p?.x, y: toNumber(p?.y) })), }, ]; } return []; } function TrendLineSeriesPath({ d, color, delayMs }) { const pathRef = useRef(null); useLinePathDraw(pathRef, d); return ( ); } function TrendLineChart({ series }) { const safeSeries = Array.isArray(series) ? series : []; const pts = safeSeries?.[0]?.points || []; const { width, height, padL, padT, padB, innerW, innerH, yLabelX } = getTrendChartLayout(); const axisX = padL; const ys = safeSeries.flatMap((s) => (s?.points || []).map((p) => p.y)); const minY = ys.length ? Math.min(...ys) : 0; const maxY = ys.length ? Math.max(...ys) : 1; const rangeY = maxY - minY || 1; const toX = (i) => padL + (pts.length <= 1 ? innerW / 2 : (i / (pts.length - 1)) * innerW); const toY = (y) => padT + innerH - ((y - minY) / rangeY) * innerH; const COLORS = ["#00bcd4", "#4A5CD0", "#01C14B", "#FF8050", "#FECD00"]; const yTicks = 4; const yTickVals = Array.from({ length: yTicks + 1 }, (_, i) => { const t = i / yTicks; return minY + (maxY - minY) * t; }); const xLabelEvery = Math.max(1, Math.floor((pts.length - 1) / 4)); return (
{/* axes */} {/* y ticks */} {yTickVals.map((v, idx) => { const y = toY(v); return ( {Math.round(v)} ); })} {/* x ticks */} {pts.map((p, i) => { if (i !== 0 && i !== pts.length - 1 && i % xLabelEvery !== 0) return null; const x = toX(i); const labelAnchor = trendXLabelAnchor(i, pts.length); return ( {String(p?.x ?? "")} ); })} {safeSeries.map((s, sIdx) => { const sPts = s?.points || []; const d = sPts .map((p, i) => `${i === 0 ? "M" : "L"} ${toX(i)} ${toY(p.y)}`) .join(" "); const color = COLORS[sIdx % COLORS.length]; const seriesDelay = sIdx * 80; return ( {sPts.map((p, i) => ( ))} ); })} {safeSeries.length > 1 ? (
{safeSeries.map((s, idx) => (
{s.label}
))}
) : null}
); } function TrendBarChart({ series }) { const safeSeries = Array.isArray(series) ? series : []; const pts = safeSeries?.[0]?.points || []; const { width, height, padL, padT, padB, innerW, innerH, yLabelX } = getTrendChartLayout(); const axisX = padL; const ys = safeSeries.flatMap((s) => (s?.points || []).map((p) => p.y)); const maxY = ys.length ? Math.max(...ys) : 1; const scaleY = innerH / (maxY || 1); const seriesCount = Math.max(1, safeSeries.length); const groupW = pts.length ? innerW / pts.length : innerW; const barW = Math.max(4, (groupW - 6) / seriesCount); const COLORS = ["#00bcd4", "#4A5CD0", "#01C14B", "#FF8050", "#FECD00"]; const yTicks = 4; const yTickVals = Array.from({ length: yTicks + 1 }, (_, i) => (maxY * i) / yTicks); const xLabelEvery = Math.max(1, Math.floor((pts.length - 1) / 4)); return (
{/* axes */} {/* y ticks */} {yTickVals.map((v, idx) => { const y = padT + innerH - v * scaleY; return ( {Math.round(v)} ); })} {/* x ticks */} {pts.map((p, i) => { if (i !== 0 && i !== pts.length - 1 && i % xLabelEvery !== 0) return null; const x = axisX + i * groupW + groupW / 2; const labelAnchor = trendXLabelAnchor(i, pts.length); return ( {String(p?.x ?? "")} ); })} {pts.map((_, i) => ( {safeSeries.map((s, sIdx) => { const p = s?.points?.[i]; const h = toNumber(p?.y) * scaleY; const x = axisX + i * groupW + sIdx * barW; const y = padT + innerH - h; const color = COLORS[sIdx % COLORS.length]; const delayMs = i * 45 + sIdx * 25; return ( ); })} ))} {safeSeries.length > 1 ? (
{safeSeries.map((s, idx) => (
{s.label}
))}
) : null}
); } const MOCK_TREND_ROWS = [ { period: "Week 1", rx: 425, pob: 332, calls: 272, score: 2456 }, { period: "Week 2", rx: 418, pob: 325, calls: 268, score: 2398 }, { period: "Week 3", rx: 432, pob: 338, calls: 276, score: 2489 }, { period: "Week 4", rx: 420, pob: 326, calls: 272, score: 2421 }, ]; const MOCK_PEOPLE = [ { rank: 1, name: "Deepak Sharma", code: "EMP001", rx: 245, pob: 189, calls: 156, productivity: 94.5, score: 982, }, { rank: 2, name: "Dummy CHANDIGARH ZSM", code: "EMP002", rx: 238, pob: 182, calls: 148, productivity: 92.3, score: 956, }, { rank: 3, name: "DURGA NANENDLA", code: "EMP003", rx: 221, pob: 175, calls: 142, productivity: 89.7, score: 924, }, { rank: 4, name: "MOHAMMED M", code: "EMP004", rx: 215, pob: 168, calls: 138, productivity: 87.2, score: 901, }, { rank: 5, name: "T Ramana Babu", code: "EMP005", rx: 208, pob: 162, calls: 134, productivity: 85.6, score: 886, }, { rank: 6, name: "Rajesh Kumar", code: "EMP006", rx: 198, pob: 155, calls: 128, productivity: 82.4, score: 862, }, { rank: 7, name: "Priya Singh", code: "EMP007", rx: 192, pob: 148, calls: 124, productivity: 80.1, score: 845, }, { rank: 8, name: "Amit Patel", code: "EMP008", rx: 186, pob: 142, calls: 118, productivity: 78.5, score: 827, }, ]; function RankBadge({ rank }) { const cls = rank === 1 ? "metric-rank-badge metric-rank-badge--gold" : rank === 2 ? "metric-rank-badge metric-rank-badge--silver" : rank === 3 ? "metric-rank-badge metric-rank-badge--bronze" : "metric-rank-badge metric-rank-badge--muted"; return {rank}; } function MetricReports() { const navigate = useNavigate(); const location = useLocation(); const { metricViewId: metricViewIdParam } = useParams(); const metricViewId = metricViewIdParam || location?.state?.metricViewId; const scoreboardMetricViewId = location?.state?.scoreboardMetricViewId; const teamMetricViewId = location?.state?.teamMetricViewId; const [showMenu, setShowMenu] = useState(false); const [showExport, setShowExport] = useState(false); const valueChartExportRef = useRef(null); const trendChartExportRef = useRef(null); const initialTab = (() => { const tab = location?.state?.defaultTab; return typeof tab === "string" && tab.length ? tab : "Detail"; })(); const [activeTab, setActiveTab] = useState(initialTab); const [trendSubTab, setTrendSubTab] = useState( initialTab === "Trend" ? "line" : "line" ); const [selectedLevel, setSelectedLevel] = useState(1); const [levelTouched, setLevelTouched] = useState(false); const [selectedPeriod, setSelectedPeriod] = useState(""); const [availableCharts, setAvailableCharts] = useState([]); const [periodOptions, setPeriodOptions] = useState([]); const [post, setPost] = useState(() => ({ id: metricViewId, ...(initialTab === "Trend" ? { type: "trend", chart: "line" } : initialTab === "Value" ? { type: "value", chart: "value" } : initialTab === "Leaderboard" ? { type: "scoreboard", chart: "leaderboard" } : isDoctorListTabName(initialTab) ? { type: "contacts", chart: "contacts" } : { type: "details", chart: "tabular" }), })); const [exportForm, setExportForm] = useState({ ...LEADERBOARD_EXPORT_FORM_DEFAULT, }); const chartAnimationKey = useMemo(() => { const parts = [ metricViewId || "", activeTab || "", post?.type || "", post?.chart || "", post?.period || "", post?.level ?? "", // Trend sub-tab is used for chart choices inside Trend tab activeTab === "Trend" ? trendSubTab : "", ]; return parts.join("|"); }, [ metricViewId, activeTab, post?.type, post?.chart, post?.period, post?.level, trendSubTab, ]); useEffect(() => { if (!metricViewId) { navigate("/metric-reports", { replace: true }); return; } setPost((p) => ({ ...p, id: metricViewId })); }, [metricViewId, navigate]); useEffect(() => { if (!metricViewId) return; const tab = location?.state?.defaultTab; if (typeof tab !== "string" || !tab.length) return; setActiveTab(tab); if (tab === "Trend") { setTrendSubTab("line"); setPost((p) => ({ ...p, id: metricViewId, type: "trend", chart: "line", })); } else if (tab === "Value") { setPost((p) => ({ ...p, id: metricViewId, type: "value", chart: "value", })); } else if (tab === "Leaderboard") { setPost((p) => ({ ...p, id: metricViewId, type: "scoreboard", chart: "leaderboard", })); } else if (isDoctorListTabName(tab)) { setPost((p) => ({ ...p, id: metricViewId, type: "contacts", chart: "contacts", })); } else { setPost((p) => ({ ...p, id: metricViewId, type: "details", chart: "tabular", })); } }, [metricViewId, location?.state?.defaultTab]); const visibleTabs = useMemo(() => { const tabs = (availableCharts || []) .filter((t) => Array.isArray(t?.charts) && t.charts.length > 0) .map((t) => ({ name: t.name, type: t.type, charts: t.charts })); if (tabs.length > 0) return tabs; return [ { name: "Detail", type: "details", charts: ["tabular"] }, { name: "Leaderboard", type: "scoreboard", charts: ["leaderboard"] }, ]; }, [availableCharts]); const trendChartOptions = useMemo(() => { const trend = visibleTabs.find((t) => t.type === "trend"); const charts = Array.isArray(trend?.charts) ? trend.charts : []; return charts.map((c) => ({ value: String(c).toLowerCase(), label: titleCaseChart(c), })); }, [visibleTabs]); const showLevelRow = activeTab === "My Team" || activeTab === "Leaderboard"; const { data: viewRes, isFetching: isViewFetching, isError: isViewError, } = useQuery({ queryKey: ["metricReportView", post], enabled: !!post?.id && !!post?.type && !!post?.chart, queryFn: async () => { const res = await metricReportService.postReportView(post); return res?.data; }, }); useEffect(() => { const periods = viewRes?.data?.periods; const charts = viewRes?.data?.available_charts; const serverPost = viewRes?.data?.post; const graph = viewRes?.data?.graph; if (Array.isArray(periods)) setPeriodOptions(periods); if (Array.isArray(charts)) setAvailableCharts(charts); if (serverPost && typeof serverPost === "object") { setPost((p) => ({ ...p, ...serverPost, id: p?.id ?? serverPost?.id })); if (!selectedPeriod && serverPost?.period) { setSelectedPeriod(serverPost.period); } if (serverPost?.type === "trend" && serverPost?.chart) { setTrendSubTab(String(serverPost.chart).toLowerCase()); } } if (graph && typeof graph === "object") { const currentLevel = typeof graph.current_level === "number" ? graph.current_level : typeof graph.level === "number" ? graph.level : null; if (typeof currentLevel === "number") { setSelectedLevel(currentLevel); } } }, [viewRes, selectedPeriod]); const maxLevel = typeof viewRes?.data?.graph?.level === "number" ? viewRes.data.graph.level : 0; const levelButtons = useMemo(() => { if (!showLevelRow || !maxLevel || maxLevel < 1) return []; return Array.from({ length: maxLevel }, (_, i) => maxLevel - i); }, [showLevelRow, maxLevel]); useEffect(() => { if (!showLevelRow) return; if (!levelTouched) { setPost((p) => { if (!p || typeof p !== "object") return p; if (!("level" in p)) return p; const { level, ...rest } = p; return rest; }); } }, [showLevelRow, levelTouched]); const detailValues = viewRes?.data?.graph?.values; const detailUsers = Array.isArray(detailValues?.users) ? detailValues.users : []; const detailMetrics = detailValues?.metrics || []; const isDoctorListTabActive = isDoctorListTabName(activeTab) || post?.type === "contacts"; const doctorListValuesShape = isDoctorListValuesShape(detailValues); const leaderboardRows = Array.isArray(viewRes?.data?.graph?.values) ? viewRes.data.graph.values : []; const leaderboardMetrics = Array.isArray(viewRes?.data?.graph?.metrics) ? viewRes.data.graph.metrics : []; const leaderboardTabularColumns = useMemo( () => normalizeLeaderboardTabularConfig( getLeaderboardTabularConfig(viewRes?.data) ), [viewRes?.data] ); const usesLeaderboardTabularConfig = leaderboardTabularColumns.length > 0; const leaderboardExportParsed = useMemo( () => getMetricLeaderboardExportFromApi(viewRes?.data), [viewRes?.data] ); const usesLeaderboardExportConfig = leaderboardExportParsed.allColumns.length > 0; const leaderboardAdditionalFieldOptions = useMemo(() => { if (usesLeaderboardExportConfig) { return leaderboardExportParsed.additionalOptions; } if (usesLeaderboardTabularConfig) { return []; } return buildLeaderboardAdditionalFieldOptions(leaderboardMetrics); }, [ usesLeaderboardExportConfig, usesLeaderboardTabularConfig, leaderboardExportParsed.additionalOptions, leaderboardMetrics, ]); const defaultExportAdditionalFieldIds = useMemo( () => leaderboardAdditionalFieldOptions.map((f) => f.id), [leaderboardAdditionalFieldOptions] ); const showExportPrevRef = useRef(false); useEffect(() => { const justOpened = showExport && !showExportPrevRef.current; showExportPrevRef.current = showExport; if (!justOpened || !leaderboardAdditionalFieldOptions.length) return; setExportForm((prev) => ({ ...prev, additionalFieldIds: defaultExportAdditionalFieldIds, })); }, [ showExport, defaultExportAdditionalFieldIds, leaderboardAdditionalFieldOptions.length, ]); const metricTitle = useMemo(() => { const fromNav = location?.state?.reportTitle || location?.state?.pageTitle || ""; const fromApi = viewRes?.data?.graph?.name || ""; return String(fromNav || fromApi).trim(); }, [ location?.state?.reportTitle, location?.state?.pageTitle, viewRes?.data?.graph?.name, ]); const trendGraphSeries = useMemo(() => { if (activeTab !== "Trend") return []; const graph = viewRes?.data?.graph; return normalizeTrendSeries(graph); }, [activeTab, viewRes?.data?.graph]); const trendTabularRows = useMemo(() => { const series = trendGraphSeries; if (!Array.isArray(series) || series.length === 0) return []; const pts = series[0]?.points || []; return pts.map((p, idx) => { const row = { period: p?.x ?? "-" }; series.forEach((s, sIdx) => { row[`s_${sIdx}`] = s?.points?.[idx]?.y ?? 0; }); return row; }); }, [trendGraphSeries]); const trendTabularColumns = useMemo(() => { const series = trendGraphSeries; const cols = [ { accessorKey: "period", header: "Period", }, ]; series.forEach((s, sIdx) => { cols.push({ accessorKey: `s_${sIdx}`, header: s?.label || `Series ${sIdx + 1}`, cell: (info) => ( {info.getValue()} ), }); }); return cols; }, [trendGraphSeries]); const doctorListTableRows = useMemo(() => { if (!doctorListValuesShape || !detailValues) return []; const contacts = detailValues.contacts || []; const usersMap = detailValues.users || {}; const metricsArr = Array.isArray(detailValues.metrics) ? detailValues.metrics : []; return contacts.map((c) => { const contactId = c?.id; const u = contactId ? usersMap[contactId] : null; const row = { doctor: c?.name ?? "-", msl_code: c?.msl_code ?? "-", user_name: u?.name ?? "-", employee_code: u?.employee_code ?? "-", zone: u?.positions?.Zone ?? "-", region: u?.positions?.Region ?? "-", area: u?.positions?.Area ?? "-", territory: u?.positions?.Territory ?? "-", }; metricsArr.forEach((m) => { const mid = m?.metric_id ?? m?.metric; if (mid == null) return; const raw = m?.stats && contactId != null && m.stats[contactId] !== undefined ? m.stats[contactId] : ""; row[`metric_${mid}`] = raw === "" || raw == null ? "-" : raw; }); return row; }); }, [doctorListValuesShape, detailValues]); const graphMetricIdToDisplay = useMemo(() => { const gm = viewRes?.data?.graph?.metrics; if (!Array.isArray(gm)) return {}; const out = {}; gm.forEach((x) => { const id = x?.metric; if (id) out[id] = x?.display || String(id); }); return out; }, [viewRes?.data?.graph?.metrics]); const doctorListTableColumns = useMemo(() => { const base = [ { accessorKey: "doctor", header: "Doctor" }, { accessorKey: "msl_code", header: "MSL Code" }, { accessorKey: "user_name", header: "User" }, { accessorKey: "employee_code", header: "Employee Code" }, { accessorKey: "zone", header: "Zone" }, { accessorKey: "region", header: "Region" }, { accessorKey: "area", header: "Area" }, { accessorKey: "territory", header: "Territory" }, ]; const metricsArr = doctorListValuesShape && detailValues?.metrics ? detailValues.metrics : []; const dyn = (Array.isArray(metricsArr) ? metricsArr : []).map((m) => { const mid = m?.metric_id ?? m?.metric; const fromGraph = mid != null ? graphMetricIdToDisplay[mid] : null; const headerLabel = fromGraph || (typeof m?.metric === "string" && m.metric.trim() ? m.metric : null) || m?.display || mid; return { accessorKey: `metric_${mid}`, header: headerLabel, }; }); return [...base, ...dyn]; }, [ doctorListValuesShape, detailValues, graphMetricIdToDisplay, ]); const detailTableRows = useMemo(() => { return detailUsers.map((u) => { const row = { name: [u?.name, u?.designation ? ` (${u.designation})` : ""] .filter(Boolean) .join(""), employee_code: u?.employee_code ?? "-", zone: u?.positions?.Zone ?? "-", region: u?.positions?.Region ?? "-", area: u?.positions?.Area ?? "-", territory: u?.positions?.Territory ?? "-", role: u?.role ?? u?.roles ?? "-", }; detailMetrics.forEach((m) => { const mk = m?.metric ?? m?.metric_id; if (mk == null) return; row[`metric_${mk}`] = m?.stats && u?.id != null ? m.stats[u.id] ?? 0 : 0; }); return row; }); }, [detailUsers, detailMetrics]); const detailTableColumns = useMemo(() => { const base = [ { accessorKey: "name", header: "User" }, { accessorKey: "employee_code", header: "Employee Code" }, { accessorKey: "zone", header: "Zone" }, { accessorKey: "region", header: "Region" }, { accessorKey: "area", header: "Area" }, { accessorKey: "territory", header: "Territory" }, { accessorKey: "role", header: "Role" }, ]; const dyn = detailMetrics.map((m) => ({ accessorKey: `metric_${m?.metric ?? m?.metric_id}`, header: m?.display || m?.metric, })); return [...base, ...dyn]; }, [detailMetrics]); const authUser = useMemo(() => { try { const raw = authService.getAuthUser(); return raw ? JSON.parse(raw) : null; } catch { return null; } }, []); const isCurrentUserRow = useCallback( (row) => { if (!row || !authUser) return false; const authId = authUser.id ?? authUser.user_id; if (authId != null && row.user_id != null) { return String(row.user_id) === String(authId); } const authCode = authUser.employee_code; const rowCode = row.employee_code ?? row.user_employee_code; if ( authCode != null && String(authCode).trim() !== "" && rowCode != null && rowCode !== "-" ) { return String(rowCode) === String(authCode); } return false; }, [authUser] ); const leaderboardTableRows = useMemo(() => { if (usesLeaderboardTabularConfig) { return mapLeaderboardRowsFromConfig( leaderboardRows, leaderboardTabularColumns, { enrichRow: enrichMetricLeaderboardRow } ); } return Array.from(leaderboardRows) .filter((row) => row != null && typeof row === "object") .map((row) => { const u = row?.user || {}; const out = { rank: row?.rank ?? 0, user_id: u?.id ?? u?.user_id ?? row?.user_id ?? null, name: [u?.name, u?.designation ? ` (${u.designation})` : ""] .filter(Boolean) .join(""), employee_code: u?.employee_code ?? "-", zone: u?.positions?.Zone ?? "-", region: u?.positions?.Region ?? "-", area: u?.positions?.Area ?? "-", territory: u?.positions?.Territory ?? "-", role: u?.role ?? u?.roles ?? "-", units: typeof row?.score === "number" ? Math.round(row.score * 100) / 100 : row?.score ?? 0, }; leaderboardMetrics.forEach((m) => { const mk = m?.metric; if (mk == null) return; out[`m_${mk}`] = row?.stats && mk in row.stats ? row.stats[mk] : 0; }); return out; }) .sort((a, b) => Number(a.rank) - Number(b.rank)); }, [ usesLeaderboardTabularConfig, leaderboardRows, leaderboardTabularColumns, leaderboardMetrics, ]); const leaderboardTableColumns = useMemo(() => { if (usesLeaderboardTabularConfig) { return leaderboardTabularColumns.map((col, index) => { const column = { id: col.accessorKey, accessorKey: col.accessorKey, header: col.label, }; if (index < 2) { column.meta = { sticky: true }; } if (col.isRank) { column.cell = (info) => ( ); } return column; }); } return [ { id: "rank", accessorKey: "rank", header: "Rank", meta: { sticky: true }, cell: (info) => ( ), }, { accessorKey: "name", header: "User" }, { accessorKey: "employee_code", header: "Employee Code" }, { accessorKey: "zone", header: "Zone" }, { accessorKey: "region", header: "Region" }, { accessorKey: "area", header: "Area" }, { accessorKey: "territory", header: "Territory" }, { accessorKey: "role", header: "Role" }, ...leaderboardMetrics.map((m) => ({ accessorKey: `m_${m?.metric}`, header: m?.display || m?.metric, })), { accessorKey: "units", header: "Units" }, ]; }, [usesLeaderboardTabularConfig, leaderboardTabularColumns, leaderboardMetrics]); const leaderboardConfigExportColumns = useMemo( () => leaderboardConfigToExportColumns(leaderboardTabularColumns), [leaderboardTabularColumns] ); const metricExportFileBase = useMemo( () => buildMetricExportFileBase({ metricName: metricTitle, tabName: activeTab, trendSubTab: activeTab === "Trend" ? trendSubTab : undefined, useDoctorList: isDetailOrDoctorListTab(activeTab) && isDoctorListTabActive && !!doctorListValuesShape, }), [ metricTitle, activeTab, trendSubTab, isDoctorListTabActive, doctorListValuesShape, ] ); function openLeaderboardExportModal() { setExportForm({ ...LEADERBOARD_EXPORT_FORM_DEFAULT, additionalFieldIds: defaultExportAdditionalFieldIds, format: "csv", }); setShowExport(true); } async function handlePeriodHeaderDownload() { if (activeTab === "Leaderboard") { openLeaderboardExportModal(); return; } if (activeTab === "Trend") { if (trendSubTab === "tabular") { if (!trendTabularRows.length) { handleAlert("No data to export.", "info"); return; } try { await exportRowsCsv( trendTabularRows, trendTabularColumns, metricExportFileBase ); } catch { handleAlert("Export failed.", "info"); } return; } if (trendSubTab === "line" || trendSubTab === "bar") { const el = trendChartExportRef.current; if (!el) { handleAlert("Unable to capture chart.", "info"); return; } try { await exportHtmlElementAsPng(el, metricExportFileBase); } catch { handleAlert("Could not export PNG.", "info"); } return; } handleAlert("Choose Line or Bar to export as PNG.", "info"); return; } if (activeTab === "Value") { const graph = viewRes?.data?.graph; const slices = Array.isArray(graph?.values) ? graph.values : []; if (!slices.length) { handleAlert("No data to export.", "info"); return; } const el = valueChartExportRef.current; if (!el) { handleAlert("Unable to capture chart.", "info"); return; } try { await exportHtmlElementAsPng(el, metricExportFileBase); } catch { handleAlert("Could not export PNG.", "info"); } return; } if (isDetailOrDoctorListTab(activeTab)) { if (isDoctorListTabActive && doctorListValuesShape) { if (!doctorListTableRows.length) { handleAlert("No data to export.", "info"); return; } try { await exportRowsCsv( doctorListTableRows, doctorListTableColumns, metricExportFileBase ); } catch { handleAlert("Export failed.", "info"); } return; } if (!isDoctorListTabActive) { if (!detailTableRows.length) { handleAlert("No data to export.", "info"); return; } try { await exportRowsCsv( detailTableRows, detailTableColumns, metricExportFileBase ); } catch { handleAlert("Export failed.", "info"); } return; } handleAlert("No doctor list data to export.", "info"); return; } if (activeTab === "My Team") { if (!leaderboardTableRows.length) { handleAlert("No data to export.", "info"); return; } try { await exportRowsCsv( leaderboardTableRows, leaderboardTableColumns, metricExportFileBase ); } catch { handleAlert("Export failed.", "info"); } return; } handleAlert("Nothing to export for this tab.", "info"); } async function confirmLeaderboardExportDownload() { if (!leaderboardRows.length) { handleAlert("No leaderboard data to export.", "info"); return; } let cols; let exportRows; if (usesLeaderboardExportConfig) { const selectedAdditionalIds = (exportForm.additionalFieldIds || []).filter( (id) => leaderboardExportParsed.additionalOptions.some((o) => o.id === id) ); cols = buildMetricLeaderboardExportColumns( selectedAdditionalIds, leaderboardExportParsed ); exportRows = prepareLeaderboardExportRowsFromRaw( leaderboardRows, cols, { topRanks: exportForm.topRanks, pngMaxRows: LEADERBOARD_PNG_MAX_ROWS, format: exportForm.format, enrichRow: enrichMetricLeaderboardRow, } ); } else if (usesLeaderboardTabularConfig) { cols = leaderboardConfigExportColumns; exportRows = prepareLeaderboardExportRows( leaderboardTableRows, cols, { topRanks: exportForm.topRanks, pngMaxRows: LEADERBOARD_PNG_MAX_ROWS, format: exportForm.format, enrichRow: enrichMetricLeaderboardTableExportRow, } ); } else { const selectedAdditionalIds = (exportForm.additionalFieldIds || []).filter( (id) => leaderboardAdditionalFieldOptions.some((o) => o.id === id) ); cols = buildLeaderboardExportColumns( selectedAdditionalIds, leaderboardMetrics ); exportRows = prepareLeaderboardExportRows( leaderboardTableRows, cols, { topRanks: exportForm.topRanks, pngMaxRows: LEADERBOARD_PNG_MAX_ROWS, format: exportForm.format, enrichRow: enrichMetricLeaderboardTableExportRow, } ); } try { if (exportForm.format === "png") { await exportTableAsPng(exportRows, cols, metricExportFileBase, { useLeaderboardTableStyles: true, highlightRow: isCurrentUserRow, }); } else { await exportRowsCsv(exportRows, cols, metricExportFileBase); } setShowExport(false); } catch { handleAlert("Export failed.", "info"); } } return (
{showMenu ? ( <> {/* */}
) : null}
{metricTitle ? (
{metricTitle}
) : null}
{`${isViewFetching ? 'Fetching...' : viewRes?.data?.graph?.first_day + ' to ' + viewRes?.data?.graph?.last_day}`}
{ const period = e.target.value; setSelectedPeriod(period); setPost((p) => ({ ...p, period })); }} > {periodOptions.length === 0 ? ( ) : ( periodOptions.map((p, idx) => ( )) )}
{visibleTabs.map((tab) => ( ))}
{showLevelRow ? (
Level
{levelButtons.map((lvl) => ( ))}
) : null} {activeTab === "Value" && (
)} {activeTab === "Trend" && (
{trendChartOptions.map((sub) => ( ))}
{!metricViewId ? (
Missing metric id.
) : isViewError ? (
Failed to load trend.
) : isViewFetching ? (
Loading trend..
) : (() => { const graph = viewRes?.data?.graph; const series = normalizeTrendSeries(graph); if (trendSubTab === "line") { return (
); } if (trendSubTab === "bar") { return (
); } if (trendSubTab === "tabular") { return (
); } return (
Select a chart.
); })()}
)} {isDetailOrDoctorListTab(activeTab) && (
{!metricViewId ? (
Missing metric id.
) : isViewError ? (
Failed to load report data.
) : isViewFetching ? (
{" "} {isDoctorListTabActive ? "Loading doctor list…" : "Loading details…"}
) : isDoctorListTabActive && doctorListValuesShape ? ( ) : isDoctorListTabActive ? (
No doctor list data for this period.
) : ( )}
)} {(activeTab === "My Team" || activeTab === "Leaderboard") && (
{!metricViewId ? (
Missing metric id.
) : isViewError ? (
Failed to load leaderboard.
) : isViewFetching ? (
Loading leaderboard…
) : ( isCurrentUserRow(row) ? "metric-leaderboard-row--self" : "" } /> )}
)}
setShowExport(false)} showAdditionalFields={ usesLeaderboardExportConfig || !usesLeaderboardTabularConfig } alwaysIncludedHint={ usesLeaderboardExportConfig ? "Rank, User, Employee Code, and Units/Points are always included. Additional columns below are selected by default — uncheck any you do not need." : undefined } additionalFieldOptions={leaderboardAdditionalFieldOptions} exportForm={exportForm} setExportForm={setExportForm} onConfirm={confirmLeaderboardExportDownload} />
); } export default MetricReports;