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 (
{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 (
{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 ? (
<>
{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) => (
))
)}
{
void handlePeriodHeaderDownload();
}}
className="btn-theme btn-badges font-12 btn-download-report py-1 px-2 rounded-1"
aria-label="Download report"
>
{visibleTabs.map((tab) => (
{
setActiveTab(tab.name);
setLevelTouched(false);
setPost((p) => {
const nextId =
tab.type === "scoreboard"
? scoreboardMetricViewId ?? p.id
: tab.type === "leaderboard"
? teamMetricViewId ?? p.id
: p.id;
if (tab.type === "value") {
return { ...p, id: nextId, type: "value", chart: "value" };
}
if (tab.type === "trend") {
return { ...p, id: nextId, type: "trend", chart: tab?.charts?.[0] || "line" };
}
return {
...p,
id: nextId,
type: tab.type,
chart: tab?.charts?.[0] || p.chart,
};
});
}}
>
{tab.name}
))}
{showLevelRow ? (
Level
{levelButtons.map((lvl) => (
{
setLevelTouched(true);
setSelectedLevel(lvl);
setPost((p) => ({ ...p, level: lvl }));
}}
>
{lvl}
))}
) : null}
{activeTab === "Value" && (
)}
{activeTab === "Trend" && (
{trendChartOptions.map((sub) => (
{
setTrendSubTab(sub.value);
setPost((p) => ({
...p,
type: "trend",
chart: sub.value,
}));
}}
>
{sub.label}
))}
{!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;