* 'main' of http://192.168.1.200:3000/AmeyBhagwat/Rapport-Mobileapp:
  Changes on Metric View and Campaign
This commit is contained in:
Amey Bhagwat 2026-05-08 16:57:46 +05:30
commit eecbb3c948
12 changed files with 1859 additions and 53 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -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() {
<Route index element={<MetricReports />} />
</Route>
<Route
path="/campaigns"
element={
<AuthGuard
auth={isLogin}
component={DashboardLayout}
page="campaigns"
/>
}
>
<Route index element={<CampaignDetails />} />
</Route>
<Route
path="/approve-reject-form-submission"
element={

View File

@ -0,0 +1,77 @@
import { Container, Card, Row, Col, Button } from "react-bootstrap";
import { useNavigate } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import dashboardService from "../../services/Dashboard";
function DashboardCampaignsContainer() {
const navigate = useNavigate();
const { data: campaignRes } = useQuery({
queryKey: ["dashboardCampaignList"],
queryFn: async () => {
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 (
<Container className="pt-1 pb-1 py-sm-3">
<div className="d-flex align-items-center justify-content-between">
<h4 className="section-title mb-0">Campaigns</h4>
</div>
<Card className="card-theme border-0 shadow-sm p-3 rounded-4 mt-3">
<h5 className="mb-3 fw-bold">
{campaign?.name || campaign?.title || "Campaign"}
</h5>
<Row className="g-2 mb-3">
<Col xs={4}>
<div className="border border-2 border-success rounded-3 p-2 text-center h-100 d-flex flex-column justify-content-center">
<div className="text-muted font-12 mb-1">Overall Rank</div>
<div className="fw-bold fs-4">
{campaign?.overall_rank ?? campaign?.overallRank ?? "-"}
</div>
</div>
</Col>
<Col xs={4}>
<div className="border border-2 border-success rounded-3 p-2 text-center h-100 d-flex flex-column justify-content-center">
<div className="text-muted font-12 mb-1">Rank in Team</div>
<div className="fw-bold fs-4">
{campaign?.rank_in_team ?? campaign?.rankInTeam ?? "-"}
</div>
</div>
</Col>
<Col xs={4}>
<div className="border border-2 border-success rounded-3 p-2 text-center h-100 d-flex flex-column justify-content-center">
<div className="text-muted font-12 mb-1">Team Rank</div>
<div className="fw-bold fs-4">
{campaign?.team_rank ?? campaign?.teamRank ?? "-"}
</div>
</div>
</Col>
</Row>
<Button
type="button"
className="w-100 py-3 rounded-4 border-0 fw-semibold"
style={{ backgroundColor: "#5B7FC4" }}
onClick={() => navigate("/campaigns", { state: { campaign } })}
>
View Details
</Button>
</Card>
</Container>
);
}
export default DashboardCampaignsContainer;

View File

@ -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);

View File

@ -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 (
<Container className="pt-1 pb-1 py-sm-3">

View File

@ -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 (
<Card

View File

@ -0,0 +1,165 @@
import React, { useEffect, useState } from "react";
import { Container, Card, Form } from "react-bootstrap";
import { useLocation, useNavigate } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import PageComponent from "../components/PageComponent/PageComponent";
import dashboardService from "../services/Dashboard";
const DEFAULT_PERIODS = [
{ label: "May Month", value: "month-0" },
{ label: "April Month", value: "month-1" },
{ label: "Last 3 Months", value: "month-3" },
{ label: "Overall", value: "all" },
{ label: "This week", value: "week-0" },
{ label: "Last week", value: "week-1" },
{ label: "Today", value: "day-0" },
{ label: "Yesterday", value: "day-1" },
{ label: "Last 7 Days", value: "day-7" },
{ label: "Last 30 Days", value: "day-30" },
];
function CampaignDetails() {
const location = useLocation();
const navigate = useNavigate();
const campaign = location?.state?.campaign;
const campaignId = campaign?.campaignId || campaign?.campaign_id || campaign?.id;
const [selectedPeriod, setSelectedPeriod] = useState("month-0");
const { data: detailsRes, isFetching, isError } = useQuery({
queryKey: ["campaignDetails", campaignId, selectedPeriod],
enabled: !!campaignId,
queryFn: async () => {
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 (
<PageComponent className="metric-report-page">
<div className="metric-report-shell">
<Container fluid className="metric-inner px-3 pt-3 pb-5">
<div className="pt-3">
<div className="d-flex align-items-center justify-content-end">
<Form.Select
size="sm"
className="metric-period-select"
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
>
{periodOptions.map((p, idx) => (
<option key={`${p?.value}-${idx}`} value={p?.value}>
{p?.label}
</option>
))}
</Form.Select>
</div>
</div>
<div className="p-3">
{!campaignId ? (
<div className="text-center text-muted small py-4">
Missing campaign id.
</div>
) : isError ? (
<div className="text-center text-danger small py-4">
Failed to load campaign details.
</div>
) : isFetching ? (
<div className="text-center text-muted small py-4">
Loading…
</div>
) : cards.length === 0 ? (
<div className="text-center text-muted small py-4">No data.</div>
) : (
<div className="d-flex flex-column gap-3">
{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 (
<Card key={idx} className="border-0 shadow-sm overflow-hidden">
<div className="bg-light py-2 px-3 border-bottom">
<div className="text-center fw-semibold">{name}</div>
</div>
<div className="d-flex">
<div className="flex-fill py-4 px-3 text-center border-end">
<div className={`fw-bold display-6 mb-1 ${valueColor}`}>
{value}
</div>
{units ? (
<div className="text-muted small">{units}</div>
) : null}
</div>
<div className="flex-fill py-4 px-3 text-center">
<div
className="fw-bold display-6 mb-1"
style={{ color: "#00BCD4" }}
>
{points}
</div>
<div className="text-muted small">Points</div>
</div>
</div>
</Card>
);
})}
</div>
)}
</div>
</Container>
</div>
</PageComponent>
);
}
export default CampaignDetails;

View File

@ -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() {
<PerformanceStatsContainer />
<DashboardCampaignsContainer />
{otherBanners?.length > 0 && (
<section
className={`overflow-hidden ${

293
src/pages/MetricReports.css Normal file
View File

@ -0,0 +1,293 @@
/* Scoped metric report screen (Figma-inspired, Bootstrap-friendly) */
.metric-report-page {
--metric-accent: #00bcd4;
--metric-accent-rgb: 0, 188, 212;
background: #f8f9fa;
min-height: calc(100vh - 140px);
}
.metric-report-page .x-small {
font-size: 0.7rem;
}
.metric-report-page .metric-report-shell {
max-width: 28rem;
margin: 0 auto;
position: relative;
background: #f8f9fa;
min-height: calc(100vh - 140px);
}
.metric-report-page .metric-inner {
max-width: 100%;
}
.metric-report-page .metric-period-header {
position: relative;
}
.metric-report-page .metric-menu-anchor-abs {
position: absolute;
top: 0;
right: 0;
z-index: 20;
}
.metric-report-page .metric-menu-trigger {
min-width: 2.375rem;
height: 31px;
line-height: 1;
}
.metric-report-page .metric-period-select {
width: auto;
max-width: 100%;
min-width: 9rem;
font-size: 0.75rem;
}
.metric-report-page .metric-content-card {
border-radius: 0.35rem;
overflow: hidden;
}
.metric-report-page .metric-subtab-row {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
}
.metric-report-page .metric-subtab-btn {
border: none;
background: transparent;
padding: 0.5rem 1.25rem;
font-size: 0.875rem;
white-space: nowrap;
color: #6c757d;
border-bottom: 2px solid transparent;
}
.metric-report-page .metric-subtab-btn.active {
color: var(--metric-accent);
font-weight: 600;
border-bottom-color: var(--metric-accent);
}
.metric-report-page .metric-tile {
border-radius: 0.5rem;
padding: 1rem;
height: 100%;
}
.metric-report-page .metric-tile-label {
font-size: 0.7rem;
color: #6c757d;
margin-bottom: 0.25rem;
}
.metric-report-page .metric-tile-value {
font-size: 1.35rem;
font-weight: 700;
color: #212529;
}
.metric-report-page .metric-tile-delta {
font-size: 0.7rem;
margin-top: 0.25rem;
}
.metric-report-page .metric-tile--blue {
background: #e8f4fe;
}
.metric-report-page .metric-tile--green {
background: #e6f7ed;
}
.metric-report-page .metric-tile--purple {
background: #f3e8ff;
}
.metric-report-page .metric-tile--orange {
background: #fff4e6;
}
.metric-report-page .metric-design-table thead th {
background-color: var(--metric-accent);
color: #fff;
font-size: 0.7rem;
font-weight: 600;
white-space: nowrap;
border-color: rgba(255, 255, 255, 0.2);
}
.metric-report-page .metric-design-table tbody td {
font-size: 0.8rem;
vertical-align: middle;
}
.metric-report-page .metric-design-table th,
.metric-report-page .metric-design-table td {
width: auto;
max-width: 30vw;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.metric-report-page .metric-sticky-col {
position: sticky;
left: 0;
z-index: 2;
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.04);
}
.metric-report-page .metric-rank-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 50%;
font-size: 0.8rem;
font-weight: 700;
color: #fff;
}
.metric-report-page .metric-rank-badge--gold {
background: linear-gradient(135deg, #facc15, #ca8a04);
box-shadow: 0 2px 6px rgba(202, 138, 4, 0.35);
}
.metric-report-page .metric-rank-badge--silver {
background: linear-gradient(135deg, #d1d5db, #6b7280);
box-shadow: 0 2px 6px rgba(107, 114, 128, 0.35);
}
.metric-report-page .metric-rank-badge--bronze {
background: linear-gradient(135deg, #fb923c, #c2410c);
box-shadow: 0 2px 6px rgba(194, 65, 12, 0.35);
}
.metric-report-page .metric-rank-badge--muted {
background: #f3f4f6;
color: #4b5563;
font-weight: 600;
}
.metric-report-page .metric-overlay-backdrop {
position: fixed;
inset: 0;
z-index: 1040;
border: 0;
padding: 0;
margin: 0;
background: transparent;
cursor: default;
}
.metric-report-page .metric-menu-anchor-abs .metric-flyout-menu {
position: absolute;
right: 0;
top: 100%;
margin-top: 0.25rem;
z-index: 1050;
width: 14rem;
background: #fff;
border-radius: 0.5rem;
overflow: hidden;
border: 1px solid #eee;
}
.metric-report-page .metric-flyout-item {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
padding: 0.75rem 1rem;
border: none;
border-bottom: 1px solid #f1f3f5;
background: #fff;
text-align: left;
font-size: 0.875rem;
}
.metric-report-page .metric-flyout-item:hover {
background: #f8f9fa;
}
.metric-report-page .metric-level-pill--on {
background-color: var(--metric-accent) !important;
color: #fff !important;
border: none !important;
}
.metric-report-page .metric-tab-row {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
border-bottom: 1px solid #dee2e6;
gap: 0.25rem;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.metric-report-page .metric-tab-row::-webkit-scrollbar {
display: none;
}
.metric-report-page .metric-tab-btn {
border: none;
background: transparent;
padding: 0.65rem 0.85rem;
font-size: 0.8rem;
white-space: nowrap;
color: #6c757d;
border-bottom: 2px solid transparent;
flex-shrink: 0;
}
.metric-report-page .metric-tab-btn.active {
color: var(--metric-accent);
font-weight: 600;
border-bottom-color: var(--metric-accent);
}
.metric-report-page .metric-subchart-row .btn {
font-size: 0.75rem;
border-radius: 999px;
}
.metric-report-page .metric-table-wrap {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.metric-report-page .metric-table thead th {
background-color: var(--metric-accent);
color: #fff;
font-size: 0.7rem;
font-weight: 600;
white-space: nowrap;
vertical-align: middle;
}
.metric-report-page .metric-table tbody td {
font-size: 0.75rem;
vertical-align: middle;
}
.metric-report-page .metric-value-card {
background: #e3f8fb;
border-radius: 0.5rem;
padding: 1rem;
}
.metric-report-page .metric-period-row {
font-size: 0.75rem;
}
.metric-report-page .metric-level-pill {
min-width: 2.5rem;
height: 2.5rem;
border-radius: 0.35rem;
font-weight: 600;
font-size: 0.85rem;
}

File diff suppressed because it is too large Load Diff

View File

@ -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() ...");

View File

@ -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<string, unknown>} 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;