mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-26 00:31:12 -05:00
Add data quality metrics, growth trends visualization, entity comparison views, and automated health checks to the AdminDatabaseStats dashboard, including new TS types, hooks, UI components, and integrated tabbed layout.
205 lines
6.6 KiB
TypeScript
205 lines
6.6 KiB
TypeScript
import { useState } from 'react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useGrowthTrends } from '@/hooks/useGrowthTrends';
|
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
|
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart';
|
|
import type { GranularityType } from '@/types/database-analytics';
|
|
import { format } from 'date-fns';
|
|
|
|
const chartConfig = {
|
|
parks_added: {
|
|
label: "Parks",
|
|
color: "hsl(var(--chart-1))",
|
|
},
|
|
rides_added: {
|
|
label: "Rides",
|
|
color: "hsl(var(--chart-2))",
|
|
},
|
|
companies_added: {
|
|
label: "Companies",
|
|
color: "hsl(var(--chart-3))",
|
|
},
|
|
ride_models_added: {
|
|
label: "Models",
|
|
color: "hsl(var(--chart-4))",
|
|
},
|
|
photos_added: {
|
|
label: "Photos",
|
|
color: "hsl(var(--chart-5))",
|
|
},
|
|
} as const;
|
|
|
|
export function GrowthTrendsChart() {
|
|
const [timeRange, setTimeRange] = useState<number>(90);
|
|
const [granularity, setGranularity] = useState<GranularityType>('daily');
|
|
const [activeLines, setActiveLines] = useState({
|
|
parks_added: true,
|
|
rides_added: true,
|
|
companies_added: true,
|
|
ride_models_added: true,
|
|
photos_added: true,
|
|
});
|
|
|
|
const { data, isLoading } = useGrowthTrends(timeRange, granularity);
|
|
|
|
const toggleLine = (key: keyof typeof activeLines) => {
|
|
setActiveLines(prev => ({ ...prev, [key]: !prev[key] }));
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Growth Trends</CardTitle>
|
|
<CardDescription>Loading growth data...</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-80 bg-muted rounded animate-pulse" />
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
const formattedData = data?.map(point => ({
|
|
...point,
|
|
date: format(new Date(point.period), granularity === 'daily' ? 'MMM dd' : granularity === 'weekly' ? 'MMM dd' : 'MMM yyyy'),
|
|
})) || [];
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between flex-wrap gap-4">
|
|
<div>
|
|
<CardTitle>Growth Trends</CardTitle>
|
|
<CardDescription>Entity additions over time</CardDescription>
|
|
</div>
|
|
|
|
<div className="flex gap-2 flex-wrap">
|
|
{/* Time Range Controls */}
|
|
<div className="flex gap-1">
|
|
{[
|
|
{ label: '7D', days: 7 },
|
|
{ label: '30D', days: 30 },
|
|
{ label: '90D', days: 90 },
|
|
{ label: '1Y', days: 365 },
|
|
].map(({ label, days }) => (
|
|
<Button
|
|
key={label}
|
|
variant={timeRange === days ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => setTimeRange(days)}
|
|
>
|
|
{label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Granularity Controls */}
|
|
<div className="flex gap-1">
|
|
{(['daily', 'weekly', 'monthly'] as GranularityType[]).map((g) => (
|
|
<Button
|
|
key={g}
|
|
variant={granularity === g ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => setGranularity(g)}
|
|
className="capitalize"
|
|
>
|
|
{g}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardContent>
|
|
{/* Entity Type Toggles */}
|
|
<div className="flex gap-2 mb-4 flex-wrap">
|
|
{Object.entries(chartConfig).map(([key, config]) => (
|
|
<Button
|
|
key={key}
|
|
variant={activeLines[key as keyof typeof activeLines] ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => toggleLine(key as keyof typeof activeLines)}
|
|
>
|
|
{config.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Chart */}
|
|
<ChartContainer config={chartConfig} className="h-80">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<LineChart data={formattedData}>
|
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
|
<XAxis
|
|
dataKey="date"
|
|
className="text-xs"
|
|
tick={{ fill: 'hsl(var(--muted-foreground))' }}
|
|
/>
|
|
<YAxis
|
|
className="text-xs"
|
|
tick={{ fill: 'hsl(var(--muted-foreground))' }}
|
|
/>
|
|
<ChartTooltip content={<ChartTooltipContent />} />
|
|
<Legend />
|
|
|
|
{activeLines.parks_added && (
|
|
<Line
|
|
type="monotone"
|
|
dataKey="parks_added"
|
|
stroke={chartConfig.parks_added.color}
|
|
strokeWidth={2}
|
|
dot={false}
|
|
name={chartConfig.parks_added.label}
|
|
/>
|
|
)}
|
|
{activeLines.rides_added && (
|
|
<Line
|
|
type="monotone"
|
|
dataKey="rides_added"
|
|
stroke={chartConfig.rides_added.color}
|
|
strokeWidth={2}
|
|
dot={false}
|
|
name={chartConfig.rides_added.label}
|
|
/>
|
|
)}
|
|
{activeLines.companies_added && (
|
|
<Line
|
|
type="monotone"
|
|
dataKey="companies_added"
|
|
stroke={chartConfig.companies_added.color}
|
|
strokeWidth={2}
|
|
dot={false}
|
|
name={chartConfig.companies_added.label}
|
|
/>
|
|
)}
|
|
{activeLines.ride_models_added && (
|
|
<Line
|
|
type="monotone"
|
|
dataKey="ride_models_added"
|
|
stroke={chartConfig.ride_models_added.color}
|
|
strokeWidth={2}
|
|
dot={false}
|
|
name={chartConfig.ride_models_added.label}
|
|
/>
|
|
)}
|
|
{activeLines.photos_added && (
|
|
<Line
|
|
type="monotone"
|
|
dataKey="photos_added"
|
|
stroke={chartConfig.photos_added.color}
|
|
strokeWidth={2}
|
|
dot={false}
|
|
name={chartConfig.photos_added.label}
|
|
/>
|
|
)}
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</ChartContainer>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|