From fa58c8d28247703eaacaf21a40987f8c0f746be1 Mon Sep 17 00:00:00 2001 From: Ravi Gulhane <> Date: Fri, 8 May 2026 16:27:12 +0530 Subject: [PATCH] Changes on Metric View and Campaign --- public/img/metric-report-header.png | Bin 0 -> 2234 bytes src/App.jsx | 14 + .../DashboardCampaignsContainer.jsx | 77 + .../DashboardHeader/DashboardHeader.jsx | 2 +- .../PerformanceStatsContainer.jsx | 50 +- src/components/StatsCard/StatsCard.jsx | 11 +- src/pages/CampaignDetails.jsx | 165 +++ src/pages/Dashboard.jsx | 3 + src/pages/MetricReports.css | 293 ++++ src/pages/MetricReports.jsx | 1259 ++++++++++++++++- src/services/Dashboard.js | 14 + src/services/MetricReport.js | 24 + 12 files changed, 1859 insertions(+), 53 deletions(-) create mode 100644 public/img/metric-report-header.png create mode 100644 src/components/DashboardCampaigns/DashboardCampaignsContainer.jsx create mode 100644 src/pages/CampaignDetails.jsx create mode 100644 src/pages/MetricReports.css create mode 100644 src/services/MetricReport.js diff --git a/public/img/metric-report-header.png b/public/img/metric-report-header.png new file mode 100644 index 0000000000000000000000000000000000000000..a02535cd8477f32494f1856f4d8ac7c85c8db09a GIT binary patch literal 2234 zcmYLLdpwhU8{gJ8+eFS3HI*pGCppd|bC|;j^*BZzISY%NjcoLaITWLkVo2mzk`$S# zoXRN?kB99g>Tzl%S($n7d++D-zWd|e{rlbD>$<+z_xj$4EN4d>G0Z*;1OgGWv$Z+{ zfj|?0a~~8EI3C09F$VtN(H3?tDBzcj3dn>&P*HYP<}L|7t23TCGOjX|e@EBZ5X@g8 z+yh2`qew-)Y(rF9d&)#*gy>1-T4nWUDahwwpf_#xPV(k<9q&}UbzG(5YyTC6+)7M-;?f%(4}7X_7cc%dfv)W$u%v9h3C$Zo2B9u`avH>^$i&z9?7h-X?JF zsOh*slWrA%z2d)Lclm_-`y{2DvFcXtWTt*@Oiqn{vfw@ardc@>ceA4R%202negoOG zV#%J9ygb-&f74v*lh^x1#jmB;lFc^g(~O~~`@RvElb6>!a}%7bJKYb>);VS7MZikD-$ z)2&-vlrjZRhx=S=uC1GHWT&eh>1qMCAh3Q^hi$qO@k<(f&&16f;WY!JB@Lcq5hg6 zaV)i(?}kV{+iA0)7OYEvK;aS!9(egtj) z@VNeg?%)0kLABY0aG!Y`EwBQzEXg=;J_JgUwp5o#aC-ucgmhaG2kxO-*!WoMCja}_serI z5+(`4)U=l%*>4fPBw(#6$lB4>mG$UDbZ zG70P1h?B-YIB;-25+S8{R}zC|c&)4$^Z*E0yAI0usO`f8obj}{WJ#v0XSES!3xylnQKUa4fZkc9PJG++2pVX zcZ*ryZP8#J#%XAgeF!&qE=mdPDUeEY1R?(9(VU@6r5`gu_c&Kmkib2&tmhA!_PQwb zLS*?xZpkX+Sh7-5KTq?(>Vx*Kl0~IOhjOdVrC@9zR0~m;z=Dfh#Z`=;-a?)j z1u8lPSI6Ze|7;~x`@kiRUd2DD8V8gqawW$ZtKTPr4XMD21`_rq+fCfomBcdwU`s2x|exS$h#KaBC6zK=nl_r5Rn!~g$yqkUA*YD+(VfQBQ z+-t4E0T5k)Zu5QKlAFrjD132Y=EAh}<+y&-QF%c6qYMg(-ikilP`yk=GMmG0z`r>{ zHLAdAGQ43!rfLxlre$>pLbxK0b7n1hsfo{2YUIGU0?OKTT28*PG?~-2P=|wr!YT7CxSFdhR*vIOr33Cg|jD>20R_#+Q}kuU(B0)(0N=PRyuh8=e-=ja58`3H)2Ec)yUYZ;T^24 z!zKo;w6)a1QhJ$>gI28t;N?rojBk%xK3h&qnl&7mrZ#b2o}QhJ%yX#|jfiee`~ly- zepc0YH!wp=moMV`sr&&l=S(KngzWmaESb1TFECuEcy%)G8W4=c;rc2i^DpkwzjP j>WR29CEePyz}*u|d?}v(H7o@97=hRk9IYBHeAE5|e6F!1 literal 0 HcmV?d00001 diff --git a/src/App.jsx b/src/App.jsx index bb03d21..3aca21f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -20,6 +20,7 @@ import Rewards from "./pages/Rewards"; import Appreciations from "./pages/Appreciations/Appreciations"; import Profile from "./pages/Profile"; import MetricReports from "./pages/MetricReports"; +import CampaignDetails from "./pages/CampaignDetails"; import QuickActionsContent from "./pages/QuickActionsContent"; import Magazines from "./pages/Magazines"; import Magazine from "./components/Magazines/Magazine"; @@ -490,6 +491,19 @@ function App() { } /> + + } + > + } /> + + { + const res = await dashboardService.getDashboardCampaignList(); + return res?.data; + }, + }); + + const campaigns = Array.isArray(campaignRes?.data) + ? campaignRes.data + : campaignRes?.data?.items || campaignRes?.data?.campaigns || []; + const hasCampaigns = Array.isArray(campaigns) && campaigns.length > 0; + const campaign = hasCampaigns ? campaigns[0] : null; + + if (!hasCampaigns) return null; + + return ( + +
+

Campaigns

+
+ + +
+ {campaign?.name || campaign?.title || "Campaign"} +
+ + + +
+
Overall Rank
+
+ {campaign?.overall_rank ?? campaign?.overallRank ?? "-"} +
+
+ + +
+
Rank in Team
+
+ {campaign?.rank_in_team ?? campaign?.rankInTeam ?? "-"} +
+
+ + +
+
Team Rank
+
+ {campaign?.team_rank ?? campaign?.teamRank ?? "-"} +
+
+ +
+ + +
+
+ ); +} + +export default DashboardCampaignsContainer; + diff --git a/src/components/DashboardHeader/DashboardHeader.jsx b/src/components/DashboardHeader/DashboardHeader.jsx index 9799ee5..06f4f55 100755 --- a/src/components/DashboardHeader/DashboardHeader.jsx +++ b/src/components/DashboardHeader/DashboardHeader.jsx @@ -71,7 +71,7 @@ function DashboardHeader(props) { } // const backLinkPath = backLinkPathArr.join("/"); // console.log("STATE =", location.state); - }, [location.pathname]); + }, [location.pathname, location.state?.pageTitle]); const [showLogoutConfirmationModal, setShowLogoutConfirmationModal] = useState(false); diff --git a/src/components/DashboardPerformanceStats/PerformanceStatsContainer.jsx b/src/components/DashboardPerformanceStats/PerformanceStatsContainer.jsx index fc4f329..a5b3e4d 100755 --- a/src/components/DashboardPerformanceStats/PerformanceStatsContainer.jsx +++ b/src/components/DashboardPerformanceStats/PerformanceStatsContainer.jsx @@ -8,14 +8,23 @@ import StatsCard from "../StatsCard/StatsCard"; //React Query import { useQuery } from "@tanstack/react-query"; -//Capacitor -import { Browser } from "@capacitor/browser"; - //Services import dashboardService from "../../services/Dashboard"; -//Constants -import { APP_PRIMARY_COLOR } from "../../util/constants"; +function parseMetricViewIdFromReportUrl(reportViewUrl) { + if (!reportViewUrl || typeof reportViewUrl !== "string") return null; + try { + const u = new URL( + reportViewUrl.startsWith("http") + ? reportViewUrl + : `https://placeholder.local${reportViewUrl.startsWith("/") ? "" : "/"}${reportViewUrl}` + ); + return u.searchParams.get("id"); + } catch { + const m = String(reportViewUrl).match(/[?&]id=([^&]+)/); + return m ? decodeURIComponent(m[1]) : null; + } +} function PerformanceStatsContainer() { const navigate = useNavigate(); @@ -57,22 +66,29 @@ function PerformanceStatsContainer() { // refetch({ isRefresh: isRefresh }); } - const handleUrl = async (url) => { - console.log("handleUrl is called...", url); + const handleStatsCardClick = (item) => { + const metricViewId = + item?.id ?? + item?.metric_view_id ?? + parseMetricViewIdFromReportUrl(item?.report_view_url); + if (!metricViewId) { + console.warn("Performance stat has no metric view id in report_view_url"); + return; + } + const title = item?.value_title || item?.title || "Metric report"; + const aggType = String(item?.agg_type || "").toLowerCase(); + const defaultTab = + aggType === "team" ? "Detail" : aggType === "individual" ? "Trend" : "Detail"; navigate("/metric-reports", { - state: { url: url }, + state: { + metricViewId, + reportTitle: title, + pageTitle: title, + defaultTab, + }, }); }; - const handleStatsCardClick = async (url) => { - // navigate("/metric-reports", { - // state: { url: url }, - // }); - - //Open url in In-App Browser - await Browser.open({ url: url, toolbarColor: APP_PRIMARY_COLOR }); - }; - if (isPending) { return ( diff --git a/src/components/StatsCard/StatsCard.jsx b/src/components/StatsCard/StatsCard.jsx index ff9229b..e8e17f4 100644 --- a/src/components/StatsCard/StatsCard.jsx +++ b/src/components/StatsCard/StatsCard.jsx @@ -1,23 +1,14 @@ import { Card, Row, Col } from "react-bootstrap"; -import authService from "../../services/Auth"; import { formatTextValue } from "../../util/util"; function StatsCard(props) { const title = props?.item?.title ? props?.item?.title : "Default title"; const sub_title = props?.item?.value_title; - const card_type = props?.item?.type && "simple"; const sub_title_value = formatTextValue(props?.item?.text); - // const sub_title_value = props?.item?.text || 0; - const view_url = props?.item?.report_view_url; let classes = props?.className ? props?.className : ""; function handleClick(data) { - console.log("HandleClick item =", data); - // alert(data.report_view_url); - let url = `${data.report_view_url}&authorization=${authService.getToken()}`; - // props.handleStatsCardClick(data.report_view_url); - console.log("url = ", url); - props.handleStatsCardClick(url); + props.handleStatsCardClick(data); } return ( { + const res = await dashboardService.getCampaignDetails(campaignId, selectedPeriod); + return res?.data; + }, + }); + + const periods = Array.isArray(detailsRes?.data?.periods) + ? detailsRes.data.periods + : Array.isArray(detailsRes?.periods) + ? detailsRes.periods + : []; + const periodOptions = periods.length ? periods : DEFAULT_PERIODS; + + useEffect(() => { + if (periodOptions.some((p) => p?.value === selectedPeriod)) return; + const first = periodOptions[0]?.value; + if (first) setSelectedPeriod(first); + }, [periodOptions, selectedPeriod]); + + const screenTitle = detailsRes?.data?.title || detailsRes?.title || "Campaign"; + useEffect(() => { + const currentTitle = location?.state?.pageTitle; + if (!screenTitle || currentTitle === screenTitle) return; + navigate(location.pathname, { + replace: true, + state: { ...(location.state || {}), pageTitle: screenTitle }, + }); + }, [navigate, location.pathname, location.state?.pageTitle, screenTitle]); + + const cards = Array.isArray(detailsRes?.data) + ? detailsRes.data + : Array.isArray(detailsRes?.data?.metrics) + ? detailsRes.data.metrics + : detailsRes?.data?.items || detailsRes?.data?.campaigns || []; + + return ( + +
+ +
+
+ setSelectedPeriod(e.target.value)} + > + {periodOptions.map((p, idx) => ( + + ))} + +
+
+ +
+ {!campaignId ? ( +
+ Missing campaign id. +
+ ) : isError ? ( +
+ Failed to load campaign details. +
+ ) : isFetching ? ( +
+ Loading… +
+ ) : cards.length === 0 ? ( +
No data.
+ ) : ( +
+ {cards.map((c, idx) => { + const name = c?.name || c?.title || `Campaign ${idx + 1}`; + const value = c?.value ?? c?.achieved ?? c?.achieved_value ?? 0; + const units = + c?.units ?? + c?.unit ?? + c?.display_unit ?? + c?.displayUnit ?? + c?.value_unit ?? + "Units"; + const weight = c?.weight ?? c?.metric_weight ?? 1; + const points = + c?.points ?? + c?.score ?? + (Number.isFinite(Number(value)) && Number.isFinite(Number(weight)) + ? Number(value) * Number(weight) + : 0); + const valueColor = + c?.valueColor === "text-success" || + c?.value_color === "green" + ? "text-success" + : "text-danger"; + + return ( + +
+
{name}
+
+
+
+
+ {value} +
+ {units ? ( +
{units}
+ ) : null} +
+
+
+ {points} +
+
Points
+
+
+
+ ); + })} +
+ )} +
+
+
+
+ ); +} + +export default CampaignDetails; + diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx index 0071cd1..c9f3dd6 100755 --- a/src/pages/Dashboard.jsx +++ b/src/pages/Dashboard.jsx @@ -26,6 +26,7 @@ import DashboardCompetitionContestsContainer from "../components/CompetitionCont import DashboardBanner from "../components/DashboardBanner/DashboardBanner"; import DashboardAdvertiseModal from "../components/DashboardAdvertiseModal/DashboardAdvertiseModal"; import OverlayNotificationModal from "../components/Notifications/OverlayNotificationModal"; +import DashboardCampaignsContainer from "../components/DashboardCampaigns/DashboardCampaignsContainer"; //API import notificationServices from "../services/NotificationServices"; @@ -401,6 +402,8 @@ function Dashboard() { + + {otherBanners?.length > 0 && (
acc + toNumber(s?.value), 0); + const cx = 110; + const cy = 110; + const r = 92; + const COLORS = [ + "#00bcd4", + "#4A5CD0", + "#01C14B", + "#FF8050", + "#FECD00", + "#9CA3AF", + "#A855F7", + "#F97316", + "#14B8A6", + ]; + + let startAngle = 0; + const paths = safeSlices + .filter((s) => toNumber(s?.value) > 0) + .map((s, idx) => { + const v = toNumber(s?.value); + const angle = total ? (v / total) * 360 : 0; + const endAngle = startAngle + angle; + const d = describeArc(cx, cy, r, startAngle, endAngle); + const color = COLORS[idx % COLORS.length]; + startAngle = endAngle; + return { d, color, label: s?.label, value: v }; + }); return ( - - + + + {paths.map((p, i) => ( + + ))} + + + {Math.round(total)} + + + Total + + + +
+
+ {paths.map((p, i) => ( +
+
+ +
+ {p.label} +
+
+
{p.value}
+
+ ))} +
+
+ + ); +} + +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 TrendLineChart({ series }) { + const safeSeries = Array.isArray(series) ? series : []; + const pts = safeSeries?.[0]?.points || []; + const width = 320; + const height = 180; + const pad = 18; + const innerW = width - pad * 2; + const innerH = height - pad * 2; + + 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) => + pad + (pts.length <= 1 ? innerW / 2 : (i / (pts.length - 1)) * innerW); + const toY = (y) => pad + 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 ( +
+ - {loadingIframe ? : null} - + + {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 = 320; + const height = 180; + const pad = 18; + const innerW = width - pad * 2; + const innerH = height - pad * 2; + + 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 = pad + 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 = pad + i * groupW + groupW / 2; + return ( + + + + {String(p?.x ?? "")} + + + ); + })} + {pts.map((_, i) => ( + + {safeSeries.map((s, sIdx) => { + const p = s?.points?.[i]; + const h = toNumber(p?.y) * scaleY; + const x = pad + i * groupW + sIdx * barW; + const y = pad + innerH - h; + const color = COLORS[sIdx % COLORS.length]; + 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 location = useLocation(); + const metricViewId = location?.state?.metricViewId; + const scoreboardMetricViewId = location?.state?.scoreboardMetricViewId; + const teamMetricViewId = location?.state?.teamMetricViewId; + const [showMenu, setShowMenu] = useState(false); + const [showExport, setShowExport] = useState(false); + 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" } + : { type: "details", chart: "tabular" }), + })); + const [exportForm, setExportForm] = useState({ + metric: "", + topRanks: "10", + interval: "monthly", + format: "pdf", + }); + + useEffect(() => { + if (!metricViewId) return; + setPost((p) => ({ ...p, id: metricViewId })); + }, [metricViewId]); + + 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 = detailValues?.users || []; + const detailMetrics = detailValues?.metrics || []; + + const leaderboardRows = Array.isArray(viewRes?.data?.graph?.values) + ? viewRes.data.graph.values + : []; + const leaderboardMetrics = Array.isArray(viewRes?.data?.graph?.metrics) + ? viewRes.data.graph.metrics + : []; + + return ( + +
+ +
+ + +
+ + + 01 Apr 2026 to 30 Apr 2026 + +
+ + + { + const period = e.target.value; + setSelectedPeriod(period); + setPost((p) => ({ ...p, period })); + }} + > + {periodOptions.length === 0 ? ( + + ) : ( + periodOptions.map((p, idx) => ( + + )) + )} + + +
+ +
+ + {showMenu ? ( + <> + + + +
+ + ) : null} +
+
+ +
+ {visibleTabs.map((tab) => ( + + ))} +
+ + {showLevelRow ? ( +
+ Level +
+ {levelButtons.map((lvl) => ( + + ))} +
+
+ ) : null} + + + + {activeTab === "Value" && ( +
+ {!metricViewId ? ( +
+ Missing metric id. +
+ ) : isViewError ? ( +
+ Failed to load value. +
+ ) : isViewFetching ? ( +
+ Loading… +
+ ) : (() => { + const graph = viewRes?.data?.graph; + const value = toNumber(graph?.value); + const slices = Array.isArray(graph?.values) ? graph.values : []; + return ( + <> +
+
+ {graph?.name || "Value"} +
+
+ Total: {value} +
+
+ + {slices.length > 0 ? ( + + ) : ( +
+ No data. +
+ )} + + ); + })()} +
+ )} + + {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") { + const first = series[0]; + const pts = first?.points || []; + return ( +
+ + + + + {series.map((s, idx) => ( + + ))} + + + + {pts.length === 0 ? ( + + + + ) : ( + pts.map((p, idx) => ( + + + {series.map((s, sIdx) => ( + + ))} + + )) + )} + +
Period{s?.label || "Value"}
+ No data. +
{p?.x ?? "-"} + {s?.points?.[idx]?.y ?? 0} +
+
+ ); + } + return ( +
+ Select a chart. +
+ ); + })()} +
+
+ )} + + {activeTab === "Detail" && ( +
+ {!metricViewId ? ( +
+ Missing metric id. +
+ ) : isViewError ? ( +
+ Failed to load details. +
+ ) : isViewFetching ? ( +
+ Loading details… +
+ ) : ( + + + + + + + + + + + {detailMetrics.map((m) => ( + + ))} + + + + {detailUsers.length === 0 ? ( + + + + ) : ( + detailUsers.map((u, index) => ( + + + + + + + + + {detailMetrics.map((m) => { + const v = + m?.stats && u?.id ? m.stats[u.id] : undefined; + return ( + + ); + })} + + )) + )} + +
UserEmployee CodeZoneRegionAreaTerritoryRole + {m?.display || m?.metric} +
+ No data. +
+ {u?.name ?? "-"} + {u?.designation ? ( + + {" "} + ({u.designation}) + + ) : null} + + {u?.employee_code ?? "-"} + + {u?.positions?.Zone ?? "-"} + + {u?.positions?.Region ?? "-"} + + {u?.positions?.Area ?? "-"} + + {u?.positions?.Territory ?? "-"} + + {u?.role ?? u?.roles ?? "-"} + + {v ?? 0} +
+ )} +
+ )} + + {(activeTab === "My Team" || activeTab === "Leaderboard") && ( +
+ {!metricViewId ? ( +
+ Missing metric id. +
+ ) : isViewError ? ( +
+ Failed to load leaderboard. +
+ ) : isViewFetching ? ( +
+ Loading leaderboard… +
+ ) : ( + + + + + + + + + + + + {leaderboardMetrics.map((m) => ( + + ))} + + + + + {leaderboardRows.length === 0 ? ( + + + + ) : ( + leaderboardRows.map((row, index) => { + const u = row?.user || {}; + return ( + + + + + + + + + + {leaderboardMetrics.map((m) => { + const key = m?.metric; + const v = + key && row?.stats ? row.stats[key] : undefined; + return ( + + ); + })} + + + ); + }) + )} + +
RankUserEmployee CodeZoneRegionAreaTerritoryRole + {m?.display || m?.metric} + Units
+ No data. +
+ + + {u?.name ?? "-"} + {u?.designation ? ( + + {" "} + ({u.designation}) + + ) : null} + + {u?.employee_code ?? "-"} + + {u?.positions?.Zone ?? "-"} + + {u?.positions?.Region ?? "-"} + + {u?.positions?.Area ?? "-"} + + {u?.positions?.Territory ?? "-"} + + {u?.role ?? u?.roles ?? "-"} + {v ?? 0} + {typeof row?.score === "number" + ? Math.round(row.score * 100) / 100 + : row?.score ?? 0} +
+ )} +
+ )} +
+
+ +
+ + + setShowExport(false)} centered> + + Export Leaderboard + + + + Select metric to generate leaderboard + + setExportForm((f) => ({ ...f, metric: e.target.value })) + } + > + + + + + + + + Select top "x" ranks + + setExportForm((f) => ({ ...f, topRanks: e.target.value })) + } + /> + + + Select interval + + setExportForm((f) => ({ ...f, interval: e.target.value })) + } + > + + + + + + + + Select export format + + setExportForm((f) => ({ ...f, format: e.target.value })) + } + > + + + + + + + +
); } diff --git a/src/services/Dashboard.js b/src/services/Dashboard.js index ae326f8..6d87e49 100644 --- a/src/services/Dashboard.js +++ b/src/services/Dashboard.js @@ -47,6 +47,20 @@ const dashboardService = { ); }, + //Dashboard Campaigns + getDashboardCampaignList: async () => { + console.log("Inside Service getDashboardCampaignList() ..."); + return await axios.get("metric/campaign_view/campaign_list"); + }, + + getCampaignDetails: async (id, period) => { + console.log("Inside Service getCampaignDetails() ...", id, period); + const qs = new URLSearchParams(); + if (id) qs.set("id", id); + if (period) qs.set("period", period); + return await axios.get(`metric/campaign_view/campaign_details?${qs.toString()}`); + }, + //Reloaded Dashboard User / Performance stats getReloadedDashboardUserStats: async () => { console.log("Inside Service getReloadedDashboardUserStats() ..."); diff --git a/src/services/MetricReport.js b/src/services/MetricReport.js new file mode 100644 index 0000000..08cc2ed --- /dev/null +++ b/src/services/MetricReport.js @@ -0,0 +1,24 @@ +import axios from "./Axios"; + +/** + * Metric report APIs (same paths Laravel calls via LambdaRequest). + * Auth: Axios interceptor attaches Authorization header. + */ +const metricReportService = { + /** + * @param {Record} payload — e.g. { id } then merged post from responses (type, chart, period, level, …) + */ + postReportView: async (payload) => { + return await axios.post("metric/metric_report/view", payload); + }, + + /** + * Metric report view helper for tab-specific loads. + * Example payload: { id, type: "details", chart: "tabular" } + */ + postMetricReportTab: async (payload) => { + return await axios.post("metric/metric_report/view", payload); + }, +}; + +export default metricReportService;