Files
inventory/inventory/src/components/dashboard/AnalyticsDashboard.jsx
T

454 lines
15 KiB
React

import React, { useState, useEffect } from "react";
import { apiFetch } from "@/utils/api";
import { Card, CardContent } from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
import { TrendingUp } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { CARD_STYLES } from "@/lib/dashboard/designTokens";
import {
DashboardSectionHeader,
DashboardStatCard,
DashboardStatCardSkeleton,
DashboardChartTooltip,
ChartSkeleton,
DashboardEmptyState,
MetricPill,
LegendChip,
PILL_TRIGGER_CLASS,
} from "@/components/dashboard/shared";
// Note: Using ChartSkeleton from @/components/dashboard/shared
const SkeletonStats = () => (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-2">
{[...Array(4)].map((_, i) => (
<DashboardStatCardSkeleton key={i} size="compact" hasIcon={false} hasSubtitle />
))}
</div>
);
// Note: Using shared DashboardStatCard from @/components/dashboard/shared
// Add color constants
const METRIC_COLORS = {
activeUsers: {
color: "#6b5fc7",
className: "text-purple-600 dark:text-purple-400",
},
newUsers: {
color: "#0f8f77",
className: "text-emerald-600 dark:text-emerald-400",
},
pageViews: {
color: "#a87a24",
className: "text-amber-600 dark:text-amber-400",
},
conversions: {
color: "#e06a4e",
className: "text-blue-600 dark:text-blue-400",
},
};
export const AnalyticsDashboard = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [timeRange, setTimeRange] = useState("30");
const [metrics, setMetrics] = useState({
activeUsers: true,
newUsers: true,
pageViews: true,
conversions: true,
});
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const response = await apiFetch(
`/api/dashboard-analytics/metrics?startDate=${timeRange}daysAgo`,
{
credentials: "include",
}
);
if (!response.ok) {
throw new Error("Failed to fetch metrics");
}
const result = await response.json();
if (!result?.data?.rows) {
return;
}
const processedData = result.data.rows.map((row) => ({
date: formatGADate(row.dimensionValues[0].value),
activeUsers: parseInt(row.metricValues[0].value),
newUsers: parseInt(row.metricValues[1].value),
avgSessionDuration: parseFloat(row.metricValues[2].value),
pageViews: parseInt(row.metricValues[3].value),
bounceRate: parseFloat(row.metricValues[4].value) * 100,
conversions: parseInt(row.metricValues[5].value),
}));
const sortedData = processedData.sort((a, b) => a.date - b.date);
setData(sortedData);
} catch (error) {
console.error("Failed to fetch analytics:", error);
} finally {
setLoading(false);
}
};
fetchData();
}, [timeRange]);
const formatGADate = (gaDate) => {
const year = gaDate.substring(0, 4);
const month = gaDate.substring(4, 6);
const day = gaDate.substring(6, 8);
return new Date(year, month - 1, day);
};
const formatXAxis = (date) => {
if (!date) return "";
return date.toLocaleDateString([], { month: "short", day: "numeric" });
};
const calculateSummaryStats = () => {
if (!data.length) return null;
const totals = data.reduce(
(acc, day) => ({
activeUsers: acc.activeUsers + day.activeUsers,
newUsers: acc.newUsers + day.newUsers,
pageViews: acc.pageViews + day.pageViews,
conversions: acc.conversions + day.conversions,
}),
{
activeUsers: 0,
newUsers: 0,
pageViews: 0,
conversions: 0,
}
);
const averages = {
activeUsers: totals.activeUsers / data.length,
newUsers: totals.newUsers / data.length,
pageViews: totals.pageViews / data.length,
conversions: totals.conversions / data.length,
};
return { totals, averages };
};
const summaryStats = calculateSummaryStats();
// Time selector for DashboardSectionHeader
const timeSelector = (
<Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger className={PILL_TRIGGER_CLASS}>
<SelectValue placeholder="Select range" />
</SelectTrigger>
<SelectContent>
<SelectItem value="7">Last 7 days</SelectItem>
<SelectItem value="14">Last 14 days</SelectItem>
<SelectItem value="30">Last 30 days</SelectItem>
<SelectItem value="90">Last 90 days</SelectItem>
</SelectContent>
</Select>
);
// Header actions: Details dialog
const headerActions = !loading ? (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="sm" className="h-8 rounded-full border-[#e7e5e1] text-xs font-semibold text-[#2b2925] shadow-none hover:bg-[#faf9f7]">
Details
</Button>
</DialogTrigger>
<DialogContent className={`max-w-[95vw] w-fit max-h-[85vh] overflow-hidden flex flex-col ${CARD_STYLES.base}`}>
<DialogHeader className="flex-none">
<DialogTitle className="text-foreground">Daily Details</DialogTitle>
<div className="flex items-center justify-center gap-2 pt-4">
<div className="flex flex-wrap gap-1">
{Object.entries(metrics).map(([key, value]) => (
<Button
key={key}
variant="ghost"
size="sm"
className={`h-7 rounded-full px-3 text-xs font-medium ${value ? "bg-[#f0eeea] text-[#2b2925] hover:bg-[#eae7e2]" : "text-[#7c7870] hover:bg-[#f5f3f0] hover:text-[#2b2925]"}`}
onClick={() =>
setMetrics((prev) => ({
...prev,
[key]: !prev[key],
}))
}
>
{key === "activeUsers" ? "Active Users" :
key === "newUsers" ? "New Users" :
key === "pageViews" ? "Page Views" :
"Conversions"}
</Button>
))}
</div>
</div>
</DialogHeader>
<div className="flex-1 overflow-y-auto mt-6">
<div className={`rounded-lg border ${CARD_STYLES.base} w-full`}>
<Table className="w-full">
<TableHeader>
<TableRow>
<TableHead className="text-center whitespace-nowrap px-6 w-[120px]">Date</TableHead>
{metrics.activeUsers && (
<TableHead className="text-center whitespace-nowrap px-6 min-w-[100px]">Active Users</TableHead>
)}
{metrics.newUsers && (
<TableHead className="text-center whitespace-nowrap px-6 min-w-[100px]">New Users</TableHead>
)}
{metrics.pageViews && (
<TableHead className="text-center whitespace-nowrap px-6 min-w-[140px]">Page Views</TableHead>
)}
{metrics.conversions && (
<TableHead className="text-center whitespace-nowrap px-6 min-w-[120px]">Conversions</TableHead>
)}
</TableRow>
</TableHeader>
<TableBody>
{data.map((day) => (
<TableRow key={day.date}>
<TableCell className="text-center whitespace-nowrap px-6">{formatXAxis(day.date)}</TableCell>
{metrics.activeUsers && (
<TableCell className="text-center whitespace-nowrap px-6">
{day.activeUsers.toLocaleString()}
</TableCell>
)}
{metrics.newUsers && (
<TableCell className="text-center whitespace-nowrap px-6">
{day.newUsers.toLocaleString()}
</TableCell>
)}
{metrics.pageViews && (
<TableCell className="text-center whitespace-nowrap px-6">
{day.pageViews.toLocaleString()}
</TableCell>
)}
{metrics.conversions && (
<TableCell className="text-center whitespace-nowrap px-6">
{day.conversions.toLocaleString()}
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
</DialogContent>
</Dialog>
) : <Skeleton className="h-9 w-20 bg-muted rounded-sm" />;
// Label formatter for chart tooltip
const analyticsLabelFormatter = (label) => {
const date = label instanceof Date ? label : new Date(label);
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
};
return (
<Card className={`w-full h-full ${CARD_STYLES.base}`}>
<DashboardSectionHeader
title="Analytics Overview"
size="large"
loading={loading}
timeSelector={timeSelector}
actions={headerActions}
/>
<CardContent className="p-4 pt-3 space-y-3">
{/* Stats cards */}
{loading ? (
<SkeletonStats />
) : summaryStats ? (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-2 dashboard-stagger">
<DashboardStatCard
title="Active Users"
value={summaryStats.totals.activeUsers.toLocaleString()}
subtitle={`Avg: ${Math.round(
summaryStats.averages.activeUsers
).toLocaleString()} per day`}
size="compact"
/>
<DashboardStatCard
title="New Users"
value={summaryStats.totals.newUsers.toLocaleString()}
subtitle={`Avg: ${Math.round(
summaryStats.averages.newUsers
).toLocaleString()} per day`}
size="compact"
/>
<DashboardStatCard
title="Page Views"
value={summaryStats.totals.pageViews.toLocaleString()}
subtitle={`Avg: ${Math.round(
summaryStats.averages.pageViews
).toLocaleString()} per day`}
size="compact"
/>
<DashboardStatCard
title="Conversions"
value={summaryStats.totals.conversions.toLocaleString()}
subtitle={`Avg: ${Math.round(
summaryStats.averages.conversions
).toLocaleString()} per day`}
size="compact"
/>
</div>
) : null}
{/* Metric toggle pills — the pill dot doubles as the series legend */}
<div className="flex flex-wrap items-center gap-0.5">
<MetricPill
active={metrics.activeUsers}
color={METRIC_COLORS.activeUsers.color}
onClick={() => setMetrics((prev) => ({ ...prev, activeUsers: !prev.activeUsers }))}
>
Active users
</MetricPill>
<MetricPill
active={metrics.newUsers}
color={METRIC_COLORS.newUsers.color}
onClick={() => setMetrics((prev) => ({ ...prev, newUsers: !prev.newUsers }))}
>
New users
</MetricPill>
<MetricPill
active={metrics.pageViews}
color={METRIC_COLORS.pageViews.color}
onClick={() => setMetrics((prev) => ({ ...prev, pageViews: !prev.pageViews }))}
>
Page views
</MetricPill>
<MetricPill
active={metrics.conversions}
color={METRIC_COLORS.conversions.color}
onClick={() => setMetrics((prev) => ({ ...prev, conversions: !prev.conversions }))}
>
Conversions
</MetricPill>
</div>
{loading ? (
<ChartSkeleton height="default" withCard={false} />
) : !data.length ? (
<DashboardEmptyState
icon={TrendingUp}
title="No analytics data available"
description="Try selecting a different time range"
/>
) : (
<div className="h-[340px] p-0 relative">
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={data}
margin={{ top: 8, right: 0, left: -8, bottom: 0 }}
>
<CartesianGrid
strokeDasharray="3 3"
className="stroke-muted"
/>
<XAxis
dataKey="date"
tickFormatter={formatXAxis}
className="text-xs text-muted-foreground"
tick={{ fill: "currentColor" }}
/>
<YAxis
yAxisId="left"
className="text-xs text-muted-foreground"
tick={{ fill: "currentColor" }}
/>
<YAxis
yAxisId="right"
orientation="right"
className="text-xs text-muted-foreground"
tick={{ fill: "currentColor" }}
/>
<Tooltip
content={
<DashboardChartTooltip
labelFormatter={analyticsLabelFormatter}
valueFormatter={(value) => value.toLocaleString()}
/>
}
/>
{metrics.activeUsers && (
<Line
yAxisId="left"
type="monotone"
dataKey="activeUsers"
name="Active Users"
stroke={METRIC_COLORS.activeUsers.color}
strokeWidth={2}
dot={false}
/>
)}
{metrics.newUsers && (
<Line
yAxisId="left"
type="monotone"
dataKey="newUsers"
name="New Users"
stroke={METRIC_COLORS.newUsers.color}
strokeWidth={2}
dot={false}
/>
)}
{metrics.pageViews && (
<Line
yAxisId="right"
type="monotone"
dataKey="pageViews"
name="Page Views"
stroke={METRIC_COLORS.pageViews.color}
strokeWidth={2}
dot={false}
/>
)}
{metrics.conversions && (
<Line
yAxisId="right"
type="monotone"
dataKey="conversions"
name="Conversions"
stroke={METRIC_COLORS.conversions.color}
strokeWidth={2}
dot={false}
/>
)}
</LineChart>
</ResponsiveContainer>
</div>
)}
</CardContent>
</Card>
);
};
export default AnalyticsDashboard;