Standardize various design elements
This commit is contained in:
@@ -168,18 +168,18 @@ const AgentPerformanceTable = ({ agents, onSort }) => {
|
|||||||
const SkeletonMetricCard = () => (
|
const SkeletonMetricCard = () => (
|
||||||
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<CardHeader className="flex flex-col items-start p-4">
|
<CardHeader className="flex flex-col items-start p-4">
|
||||||
<Skeleton className="h-4 w-24 mb-2" />
|
<Skeleton className="h-4 w-24 mb-2 bg-muted" />
|
||||||
<Skeleton className="h-8 w-32 mb-2" />
|
<Skeleton className="h-8 w-32 mb-2 bg-muted" />
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<Skeleton className="h-4 w-20" />
|
<Skeleton className="h-4 w-20 bg-muted" />
|
||||||
<Skeleton className="h-4 w-20" />
|
<Skeleton className="h-4 w-20 bg-muted" />
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
const SkeletonChart = ({ type = "line" }) => (
|
const SkeletonChart = ({ type = "line" }) => (
|
||||||
<div className="h-[300px] w-full bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
|
<div className="h-[300px] w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm rounded-lg p-4">
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
{type === "bar" ? (
|
{type === "bar" ? (
|
||||||
@@ -187,7 +187,7 @@ const SkeletonChart = ({ type = "line" }) => (
|
|||||||
{[...Array(24)].map((_, i) => (
|
{[...Array(24)].map((_, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="w-full bg-gray-200 dark:bg-gray-700 rounded-t animate-pulse"
|
className="w-full bg-muted rounded-t animate-pulse"
|
||||||
style={{ height: `${15 + Math.random() * 70}%` }}
|
style={{ height: `${15 + Math.random() * 70}%` }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -197,12 +197,12 @@ const SkeletonChart = ({ type = "line" }) => (
|
|||||||
{[...Array(5)].map((_, i) => (
|
{[...Array(5)].map((_, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="absolute w-full h-px bg-gray-200 dark:bg-gray-700"
|
className="absolute w-full h-px bg-muted"
|
||||||
style={{ top: `${20 + i * 20}%` }}
|
style={{ top: `${20 + i * 20}%` }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-gray-300 dark:bg-gray-600 animate-pulse"
|
className="absolute inset-0 bg-muted animate-pulse"
|
||||||
style={{
|
style={{
|
||||||
opacity: 0.2,
|
opacity: 0.2,
|
||||||
clipPath: "polygon(0 50%, 100% 20%, 100% 100%, 0 100%)",
|
clipPath: "polygon(0 50%, 100% 20%, 100% 100%, 0 100%)",
|
||||||
@@ -219,21 +219,21 @@ const SkeletonTable = ({ rows = 5 }) => (
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="hover:bg-transparent">
|
<TableRow className="hover:bg-transparent">
|
||||||
<TableHead><Skeleton className="h-4 w-24" /></TableHead>
|
<TableHead><Skeleton className="h-4 w-24 bg-muted" /></TableHead>
|
||||||
<TableHead><Skeleton className="h-4 w-24" /></TableHead>
|
<TableHead><Skeleton className="h-4 w-24 bg-muted" /></TableHead>
|
||||||
<TableHead><Skeleton className="h-4 w-24" /></TableHead>
|
<TableHead><Skeleton className="h-4 w-24 bg-muted" /></TableHead>
|
||||||
<TableHead><Skeleton className="h-4 w-24" /></TableHead>
|
<TableHead><Skeleton className="h-4 w-24 bg-muted" /></TableHead>
|
||||||
<TableHead><Skeleton className="h-4 w-24" /></TableHead>
|
<TableHead><Skeleton className="h-4 w-24 bg-muted" /></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{[...Array(rows)].map((_, i) => (
|
{[...Array(rows)].map((_, i) => (
|
||||||
<TableRow key={i} className="hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
<TableRow key={i} className="hover:bg-muted/50 transition-colors">
|
||||||
<TableCell><Skeleton className="h-4 w-32" /></TableCell>
|
<TableCell><Skeleton className="h-4 w-32 bg-muted" /></TableCell>
|
||||||
<TableCell><Skeleton className="h-4 w-16" /></TableCell>
|
<TableCell><Skeleton className="h-4 w-16 bg-muted" /></TableCell>
|
||||||
<TableCell><Skeleton className="h-4 w-16" /></TableCell>
|
<TableCell><Skeleton className="h-4 w-16 bg-muted" /></TableCell>
|
||||||
<TableCell><Skeleton className="h-4 w-16" /></TableCell>
|
<TableCell><Skeleton className="h-4 w-16 bg-muted" /></TableCell>
|
||||||
<TableCell><Skeleton className="h-4 w-24" /></TableCell>
|
<TableCell><Skeleton className="h-4 w-24 bg-muted" /></TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@@ -356,9 +356,13 @@ const AircallDashboard = () => {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Alert variant="destructive" className="m-4">
|
<Card className="h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<AlertDescription>Error loading call data: {error}</AlertDescription>
|
<CardContent className="p-4">
|
||||||
</Alert>
|
<div className="p-4 m-6 text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/10 rounded-lg border border-red-200 dark:border-red-900/20">
|
||||||
|
Error loading call data: {error}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,7 +376,7 @@ const AircallDashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Select value={timeRange} onValueChange={setTimeRange}>
|
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||||
<SelectTrigger className="w-[130px] h-9">
|
<SelectTrigger className="w-[130px] h-9 bg-white dark:bg-gray-800">
|
||||||
<SelectValue placeholder="Select range" />
|
<SelectValue placeholder="Select range" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -400,10 +404,10 @@ const AircallDashboard = () => {
|
|||||||
<CardTitle className="text-sm font-medium text-gray-600 dark:text-gray-300">Total Calls</CardTitle>
|
<CardTitle className="text-sm font-medium text-gray-600 dark:text-gray-300">Total Calls</CardTitle>
|
||||||
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-2">{metrics.total}</div>
|
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-2">{metrics.total}</div>
|
||||||
<div className="flex gap-4 mt-2">
|
<div className="flex gap-4 mt-2">
|
||||||
<div className="text-sm">
|
<div className="text-sm text-muted-foreground">
|
||||||
<span className="text-blue-500">↑ {metrics.by_direction.inbound}</span> inbound
|
<span className="text-blue-500">↑ {metrics.by_direction.inbound}</span> inbound
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm">
|
<div className="text-sm text-muted-foreground">
|
||||||
<span className="text-emerald-500">↓ {metrics.by_direction.outbound}</span> outbound
|
<span className="text-emerald-500">↓ {metrics.by_direction.outbound}</span> outbound
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -416,10 +420,10 @@ const AircallDashboard = () => {
|
|||||||
{`${((metrics.by_status.answered / metrics.total) * 100).toFixed(1)}%`}
|
{`${((metrics.by_status.answered / metrics.total) * 100).toFixed(1)}%`}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-6">
|
<div className="flex gap-6">
|
||||||
<div className="text-sm">
|
<div className="text-sm text-muted-foreground">
|
||||||
<span className="text-emerald-500">{metrics.by_status.answered}</span> answered
|
<span className="text-emerald-500">{metrics.by_status.answered}</span> answered
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm">
|
<div className="text-sm text-muted-foreground">
|
||||||
<span className="text-rose-500">{metrics.by_status.missed}</span> missed
|
<span className="text-rose-500">{metrics.by_status.missed}</span> missed
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -453,11 +457,11 @@ const AircallDashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom" className="w-[300px]">
|
<TooltipContent side="bottom" className="w-[300px] bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="font-medium">Duration Distribution</p>
|
<p className="font-medium text-gray-900 dark:text-gray-100">Duration Distribution</p>
|
||||||
{metrics?.duration_distribution?.map((d, i) => (
|
{metrics?.duration_distribution?.map((d, i) => (
|
||||||
<div key={i} className="flex justify-between text-sm">
|
<div key={i} className="flex justify-between text-sm text-muted-foreground">
|
||||||
<span>{d.range}</span>
|
<span>{d.range}</span>
|
||||||
<span>{d.count} calls</span>
|
<span>{d.count} calls</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -487,15 +491,15 @@ const AircallDashboard = () => {
|
|||||||
) : (
|
) : (
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart data={chartData.daily} margin={{ top: 0, right: 5, left: -35, bottom: 0 }}>
|
<BarChart data={chartData.daily} margin={{ top: 0, right: 5, left: -35, bottom: 0 }}>
|
||||||
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-gray-700" />
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="date"
|
dataKey="date"
|
||||||
tick={{ fontSize: 12 }}
|
tick={{ fontSize: 12 }}
|
||||||
className="text-gray-600 dark:text-gray-300"
|
className="text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
tick={{ fontSize: 12 }}
|
tick={{ fontSize: 12 }}
|
||||||
className="text-gray-600 dark:text-gray-300"
|
className="text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<RechartsTooltip content={<CustomTooltip />} />
|
<RechartsTooltip content={<CustomTooltip />} />
|
||||||
<Legend />
|
<Legend />
|
||||||
@@ -518,16 +522,16 @@ const AircallDashboard = () => {
|
|||||||
) : (
|
) : (
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart data={chartData.hourly} margin={{ top: 0, right: 5, left: -35, bottom: 0 }}>
|
<BarChart data={chartData.hourly} margin={{ top: 0, right: 5, left: -35, bottom: 0 }}>
|
||||||
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-gray-700" />
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="hour"
|
dataKey="hour"
|
||||||
tick={{ fontSize: 12 }}
|
tick={{ fontSize: 12 }}
|
||||||
interval={2}
|
interval={2}
|
||||||
className="text-gray-600 dark:text-gray-300"
|
className="text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
tick={{ fontSize: 12 }}
|
tick={{ fontSize: 12 }}
|
||||||
className="text-gray-600 dark:text-gray-300"
|
className="text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<RechartsTooltip content={<CustomTooltip />} />
|
<RechartsTooltip content={<CustomTooltip />} />
|
||||||
<Bar dataKey="calls" fill={COLORS.hourly} name="Calls" />
|
<Bar dataKey="calls" fill={COLORS.hourly} name="Calls" />
|
||||||
@@ -549,10 +553,12 @@ const AircallDashboard = () => {
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<SkeletonTable rows={5} />
|
<SkeletonTable rows={5} />
|
||||||
) : (
|
) : (
|
||||||
<AgentPerformanceTable
|
<div className="overflow-y-auto max-h-[400px] scrollbar-thin scrollbar-thumb-gray-200 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent hover:scrollbar-thumb-gray-300 dark:hover:scrollbar-thumb-gray-600">
|
||||||
agents={sortedAgents}
|
<AgentPerformanceTable
|
||||||
onSort={(key, direction) => setAgentSort({ key, direction })}
|
agents={sortedAgents}
|
||||||
/>
|
onSort={(key, direction) => setAgentSort({ key, direction })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -566,26 +572,28 @@ const AircallDashboard = () => {
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<SkeletonTable rows={5} />
|
<SkeletonTable rows={5} />
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<div className="overflow-y-auto max-h-[400px] scrollbar-thin scrollbar-thumb-gray-200 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent hover:scrollbar-thumb-gray-300 dark:hover:scrollbar-thumb-gray-600">
|
||||||
<TableHeader>
|
<Table>
|
||||||
<TableRow className="hover:bg-transparent">
|
<TableHeader>
|
||||||
<TableHead>Reason</TableHead>
|
<TableRow className="hover:bg-transparent">
|
||||||
<TableHead className="text-right">Count</TableHead>
|
<TableHead className="font-medium text-gray-900 dark:text-gray-100">Reason</TableHead>
|
||||||
</TableRow>
|
<TableHead className="text-right font-medium text-gray-900 dark:text-gray-100">Count</TableHead>
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{chartData.missedReasons.map((reason, index) => (
|
|
||||||
<TableRow key={index} className="hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
|
||||||
<TableCell className="font-medium text-gray-900 dark:text-gray-100">
|
|
||||||
{reason.reason}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right text-rose-600 dark:text-rose-400">
|
|
||||||
{reason.count}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
</TableHeader>
|
||||||
</TableBody>
|
<TableBody>
|
||||||
</Table>
|
{chartData.missedReasons.map((reason, index) => (
|
||||||
|
<TableRow key={index} className="hover:bg-muted/50 transition-colors">
|
||||||
|
<TableCell className="font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{reason.reason}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right text-rose-600 dark:text-rose-400">
|
||||||
|
{reason.count}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import {
|
|||||||
import { Loader2, TrendingUp, AlertCircle } from "lucide-react";
|
import { Loader2, TrendingUp, AlertCircle } from "lucide-react";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
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";
|
||||||
|
|
||||||
// Add helper function for currency formatting
|
// Add helper function for currency formatting
|
||||||
const formatCurrency = (value, useFractionDigits = true) => {
|
const formatCurrency = (value, useFractionDigits = true) => {
|
||||||
@@ -38,40 +40,42 @@ const formatCurrency = (value, useFractionDigits = true) => {
|
|||||||
|
|
||||||
// Add skeleton components
|
// Add skeleton components
|
||||||
const SkeletonChart = () => (
|
const SkeletonChart = () => (
|
||||||
<div className="h-[400px] w-full bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
|
<div className="h-[400px] w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm rounded-lg p-6">
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
<div className="h-full w-full relative">
|
{/* Grid lines */}
|
||||||
{[...Array(5)].map((_, i) => (
|
{[...Array(5)].map((_, i) => (
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="absolute w-full h-px bg-gray-200 dark:bg-gray-700"
|
|
||||||
style={{ top: `${20 + i * 20}%` }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-gray-300 dark:bg-gray-600 animate-pulse"
|
key={i}
|
||||||
style={{
|
className="absolute w-full h-px bg-muted"
|
||||||
opacity: 0.2,
|
style={{ top: `${(i + 1) * 20}%` }}
|
||||||
clipPath: "polygon(0 50%, 100% 20%, 100% 100%, 0 100%)",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 flex justify-between">
|
))}
|
||||||
{[...Array(8)].map((_, i) => (
|
{/* Y-axis labels */}
|
||||||
<div
|
<div className="absolute left-0 top-0 bottom-0 w-8 flex flex-col justify-between py-4">
|
||||||
key={i}
|
{[...Array(5)].map((_, i) => (
|
||||||
className="w-px h-full bg-gray-200 dark:bg-gray-700"
|
<Skeleton key={i} className="h-3 w-6 bg-muted rounded-sm" />
|
||||||
style={{ opacity: 0.5 }}
|
))}
|
||||||
/>
|
</div>
|
||||||
))}
|
{/* X-axis labels */}
|
||||||
|
<div className="absolute left-8 right-4 bottom-0 flex justify-between">
|
||||||
|
{[...Array(6)].map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-3 w-8 bg-muted rounded-sm" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* Chart line */}
|
||||||
|
<div className="absolute inset-x-8 bottom-6 top-4">
|
||||||
|
<div className="h-full w-full relative">
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-muted rounded-sm"
|
||||||
|
style={{
|
||||||
|
opacity: 0.5,
|
||||||
|
clipPath: "polygon(0 50%, 100% 20%, 100% 100%, 0 100%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between pt-4">
|
|
||||||
{[...Array(6)].map((_, i) => (
|
|
||||||
<Skeleton key={i} className="h-4 w-12 dark:bg-gray-700" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -79,13 +83,13 @@ const SkeletonChart = () => (
|
|||||||
const SkeletonStats = () => (
|
const SkeletonStats = () => (
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 py-4">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 py-4">
|
||||||
{[...Array(4)].map((_, i) => (
|
{[...Array(4)].map((_, i) => (
|
||||||
<Card key={i}>
|
<Card key={i} className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-2">
|
||||||
<Skeleton className="h-4 w-24 dark:bg-gray-700" />
|
<Skeleton className="h-4 w-24 bg-muted rounded-sm" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-4 pt-0">
|
<CardContent className="p-4 pt-0">
|
||||||
<Skeleton className="h-8 w-32 mb-2 dark:bg-gray-700" />
|
<Skeleton className="h-8 w-32 bg-muted rounded-sm mb-2" />
|
||||||
<Skeleton className="h-4 w-24 dark:bg-gray-700" />
|
<Skeleton className="h-4 w-24 bg-muted rounded-sm" />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
@@ -95,7 +99,7 @@ const SkeletonStats = () => (
|
|||||||
const SkeletonButtons = () => (
|
const SkeletonButtons = () => (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{[...Array(4)].map((_, i) => (
|
{[...Array(4)].map((_, i) => (
|
||||||
<Skeleton key={i} className="h-8 w-20 dark:bg-gray-700" />
|
<Skeleton key={i} className="h-8 w-20 bg-muted rounded-sm" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -109,12 +113,12 @@ const StatCard = ({
|
|||||||
trendValue,
|
trendValue,
|
||||||
colorClass = "text-gray-900 dark:text-gray-100",
|
colorClass = "text-gray-900 dark:text-gray-100",
|
||||||
}) => (
|
}) => (
|
||||||
<Card>
|
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-2">
|
||||||
<span className="text-sm text-muted-foreground">{title}</span>
|
<span className="text-sm text-muted-foreground font-medium">{title}</span>
|
||||||
{trend && (
|
{trend && (
|
||||||
<span
|
<span
|
||||||
className={`text-sm flex items-center gap-1 ${
|
className={`text-sm flex items-center gap-1 font-medium ${
|
||||||
trend === "up"
|
trend === "up"
|
||||||
? "text-emerald-600 dark:text-emerald-400"
|
? "text-emerald-600 dark:text-emerald-400"
|
||||||
: "text-rose-600 dark:text-rose-400"
|
: "text-rose-600 dark:text-rose-400"
|
||||||
@@ -125,9 +129,9 @@ const StatCard = ({
|
|||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-4 pt-0">
|
<CardContent className="p-4 pt-0">
|
||||||
<div className={`text-2xl font-bold mb-1 ${colorClass}`}>{value}</div>
|
<div className={`text-2xl font-bold mb-1.5 ${colorClass}`}>{value}</div>
|
||||||
{description && (
|
{description && (
|
||||||
<div className="text-sm text-muted-foreground">{description}</div>
|
<div className="text-sm font-medium text-muted-foreground">{description}</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -252,19 +256,19 @@ export const AnalyticsDashboard = () => {
|
|||||||
const CustomTooltip = ({ active, payload, label }) => {
|
const CustomTooltip = ({ active, payload, label }) => {
|
||||||
if (active && payload && payload.length) {
|
if (active && payload && payload.length) {
|
||||||
return (
|
return (
|
||||||
<Card className="p-3 shadow-lg bg-white dark:bg-gray-800 border-none">
|
<Card className="p-3 shadow-lg bg-white dark:bg-gray-900/60 backdrop-blur-sm border border-border">
|
||||||
<CardContent className="p-0 space-y-2">
|
<CardContent className="p-0 space-y-2">
|
||||||
<p className="font-medium text-sm border-b pb-1 mb-2">
|
<p className="font-medium text-sm border-b border-border pb-1.5 mb-2 text-foreground">
|
||||||
{label instanceof Date ? label.toLocaleDateString() : label}
|
{label instanceof Date ? label.toLocaleDateString() : label}
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1.5">
|
||||||
{payload.map((entry, index) => (
|
{payload.map((entry, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex justify-between items-center text-sm"
|
className="flex justify-between items-center text-sm"
|
||||||
>
|
>
|
||||||
<span style={{ color: entry.color }}>{entry.name}:</span>
|
<span className="font-medium" style={{ color: entry.color }}>{entry.name}:</span>
|
||||||
<span className="font-medium ml-4">
|
<span className="font-medium ml-4 text-foreground">
|
||||||
{entry.value.toLocaleString()}
|
{entry.value.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -282,24 +286,114 @@ export const AnalyticsDashboard = () => {
|
|||||||
<CardHeader className="p-6 pb-4">
|
<CardHeader className="p-6 pb-4">
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
<div>
|
||||||
Analytics Overview
|
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
</CardTitle>
|
Analytics Overview
|
||||||
{loading ? (
|
</CardTitle>
|
||||||
<Skeleton className="h-9 w-[130px] dark:bg-gray-700" />
|
</div>
|
||||||
) : (
|
<div className="flex items-center gap-2">
|
||||||
<Select value={timeRange} onValueChange={setTimeRange}>
|
{loading ? (
|
||||||
<SelectTrigger className="w-[130px] h-9">
|
<Skeleton className="h-9 w-[130px] bg-muted rounded-sm" />
|
||||||
<SelectValue placeholder="Select range" />
|
) : (
|
||||||
</SelectTrigger>
|
<>
|
||||||
<SelectContent>
|
<Dialog>
|
||||||
<SelectItem value="7">Last 7 days</SelectItem>
|
<DialogTrigger asChild>
|
||||||
<SelectItem value="14">Last 14 days</SelectItem>
|
<Button variant="outline" className="h-9">
|
||||||
<SelectItem value="30">Last 30 days</SelectItem>
|
Details
|
||||||
<SelectItem value="90">Last 90 days</SelectItem>
|
</Button>
|
||||||
</SelectContent>
|
</DialogTrigger>
|
||||||
</Select>
|
<DialogContent className="min-w-[600px] max-w-[90vw] w-fit max-h-[85vh] overflow-hidden flex flex-col bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
)}
|
<DialogHeader className="flex-none">
|
||||||
|
<DialogTitle className="text-gray-900 dark:text-gray-100">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={value ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
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 bg-white dark:bg-gray-900/60 backdrop-blur-sm 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>
|
||||||
|
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||||
|
<SelectTrigger className="w-[130px] h-9">
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -341,65 +435,65 @@ export const AnalyticsDashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-0 sm:gap-4 pt-2">
|
<div className="flex items-center flex-col sm:flex-row gap-0 sm:gap-4 pt-2">
|
||||||
{loading ? (
|
<div className="flex flex-wrap gap-1">
|
||||||
<SkeletonButtons />
|
<Button
|
||||||
) : (
|
variant={metrics.activeUsers ? "default" : "outline"}
|
||||||
<div className="flex flex-wrap gap-1">
|
size="sm"
|
||||||
<Button
|
className="font-medium"
|
||||||
variant={metrics.activeUsers ? "default" : "outline"}
|
onClick={() =>
|
||||||
size="sm"
|
setMetrics((prev) => ({
|
||||||
onClick={() =>
|
...prev,
|
||||||
setMetrics((prev) => ({
|
activeUsers: !prev.activeUsers,
|
||||||
...prev,
|
}))
|
||||||
activeUsers: !prev.activeUsers,
|
}
|
||||||
}))
|
>
|
||||||
}
|
<span className="hidden sm:inline">Active Users</span>
|
||||||
>
|
<span className="sm:hidden">Active</span>
|
||||||
<span className="hidden sm:inline">Active Users</span>
|
</Button>
|
||||||
<span className="sm:hidden">Active</span>
|
<Button
|
||||||
</Button>
|
variant={metrics.newUsers ? "default" : "outline"}
|
||||||
<Button
|
size="sm"
|
||||||
variant={metrics.newUsers ? "default" : "outline"}
|
className="font-medium"
|
||||||
size="sm"
|
onClick={() =>
|
||||||
onClick={() =>
|
setMetrics((prev) => ({
|
||||||
setMetrics((prev) => ({
|
...prev,
|
||||||
...prev,
|
newUsers: !prev.newUsers,
|
||||||
newUsers: !prev.newUsers,
|
}))
|
||||||
}))
|
}
|
||||||
}
|
>
|
||||||
>
|
<span className="hidden sm:inline">New Users</span>
|
||||||
<span className="hidden sm:inline">New Users</span>
|
<span className="sm:hidden">New</span>
|
||||||
<span className="sm:hidden">New</span>
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
variant={metrics.pageViews ? "default" : "outline"}
|
||||||
variant={metrics.pageViews ? "default" : "outline"}
|
size="sm"
|
||||||
size="sm"
|
className="font-medium"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setMetrics((prev) => ({
|
setMetrics((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
pageViews: !prev.pageViews,
|
pageViews: !prev.pageViews,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="hidden sm:inline">Page Views</span>
|
<span className="hidden sm:inline">Page Views</span>
|
||||||
<span className="sm:hidden">Page Views</span>
|
<span className="sm:hidden">Views</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={metrics.conversions ? "default" : "outline"}
|
variant={metrics.conversions ? "default" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() =>
|
className="font-medium"
|
||||||
setMetrics((prev) => ({
|
onClick={() =>
|
||||||
...prev,
|
setMetrics((prev) => ({
|
||||||
conversions: !prev.conversions,
|
...prev,
|
||||||
}))
|
conversions: !prev.conversions,
|
||||||
}
|
}))
|
||||||
>
|
}
|
||||||
<span className="hidden sm:inline">Conversions</span>
|
>
|
||||||
<span className="sm:hidden">Conversions</span>
|
<span className="hidden sm:inline">Conversions</span>
|
||||||
</Button>
|
<span className="sm:hidden">Conv.</span>
|
||||||
</div>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -410,17 +504,19 @@ export const AnalyticsDashboard = () => {
|
|||||||
) : !data.length ? (
|
) : !data.length ? (
|
||||||
<div className="flex items-center justify-center h-[400px] text-muted-foreground">
|
<div className="flex items-center justify-center h-[400px] text-muted-foreground">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<TrendingUp className="h-12 w-12 mx-auto mb-4" />
|
<TrendingUp className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||||
<div className="font-medium mb-2">No analytics data available</div>
|
<div className="font-medium mb-2 text-gray-900 dark:text-gray-100">No analytics data available</div>
|
||||||
<div className="text-sm">Try selecting a different time range</div>
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Try selecting a different time range
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-[400px] mt-4 bg-card rounded-lg p-0">
|
<div className="h-[400px] mt-4 bg-white dark:bg-gray-900/60 backdrop-blur-sm rounded-lg p-0 relative">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<LineChart
|
<LineChart
|
||||||
data={data}
|
data={data}
|
||||||
margin={{ top: 5, right: 0, left: -5, bottom: 5 }}
|
margin={{ top: 5, right: -30, left: -5, bottom: 5 }}
|
||||||
>
|
>
|
||||||
<CartesianGrid
|
<CartesianGrid
|
||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
@@ -429,18 +525,18 @@ export const AnalyticsDashboard = () => {
|
|||||||
<XAxis
|
<XAxis
|
||||||
dataKey="date"
|
dataKey="date"
|
||||||
tickFormatter={formatXAxis}
|
tickFormatter={formatXAxis}
|
||||||
className="text-xs"
|
className="text-xs text-muted-foreground"
|
||||||
tick={{ fill: "currentColor" }}
|
tick={{ fill: "currentColor" }}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
yAxisId="left"
|
yAxisId="left"
|
||||||
className="text-xs"
|
className="text-xs text-muted-foreground"
|
||||||
tick={{ fill: "currentColor" }}
|
tick={{ fill: "currentColor" }}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
yAxisId="right"
|
yAxisId="right"
|
||||||
orientation="right"
|
orientation="right"
|
||||||
className="text-xs"
|
className="text-xs text-muted-foreground"
|
||||||
tick={{ fill: "currentColor" }}
|
tick={{ fill: "currentColor" }}
|
||||||
/>
|
/>
|
||||||
<Tooltip content={<CustomTooltip />} />
|
<Tooltip content={<CustomTooltip />} />
|
||||||
|
|||||||
@@ -148,24 +148,22 @@ const LoadingState = () => (
|
|||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<Skeleton className="h-10 w-10 rounded-full bg-muted" />
|
<Skeleton className="h-10 w-10 rounded-full bg-muted" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0 space-y-2">
|
||||||
<div className="space-y-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<Skeleton className="h-4 w-24 bg-muted rounded-sm" />
|
||||||
<Skeleton className="h-4 w-24 bg-muted" />
|
</div>
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<Skeleton className="h-4 w-48 bg-muted rounded-sm" />
|
||||||
<Skeleton className="h-4 w-48 bg-muted" />
|
</div>
|
||||||
</div>
|
<div className="flex gap-1.5 items-center flex-wrap">
|
||||||
<div className="flex gap-1.5 items-center flex-wrap">
|
<Skeleton className="h-5 w-16 bg-muted rounded-md" />
|
||||||
<Skeleton className="h-5 w-16 rounded-full bg-muted" />
|
<Skeleton className="h-5 w-20 bg-muted rounded-md" />
|
||||||
<Skeleton className="h-5 w-20 rounded-full bg-muted" />
|
<Skeleton className="h-5 w-14 bg-muted rounded-md" />
|
||||||
<Skeleton className="h-5 w-14 rounded-full bg-muted" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<Skeleton className="h-4 w-16 bg-muted" />
|
<Skeleton className="h-4 w-16 bg-muted rounded-sm" />
|
||||||
<Skeleton className="h-4 w-4 bg-muted" />
|
<Skeleton className="h-4 w-4 bg-muted rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -174,14 +172,14 @@ const LoadingState = () => (
|
|||||||
|
|
||||||
// Empty State Component
|
// Empty State Component
|
||||||
const EmptyState = () => (
|
const EmptyState = () => (
|
||||||
<div className="h-full flex flex-col items-center justify-center py-16 px-4">
|
<div className="h-full flex flex-col items-center justify-center py-16 px-4 text-center">
|
||||||
<div className="bg-gray-100 dark:bg-gray-800 rounded-full p-3 mb-4">
|
<div className="bg-gray-100 dark:bg-gray-800 rounded-full p-3 mb-4">
|
||||||
<Activity className="h-8 w-8 text-gray-400 dark:text-gray-500" />
|
<Activity className="h-8 w-8 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||||
No activity yet today
|
No activity yet today
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 text-center max-w-sm">
|
<p className="text-sm text-muted-foreground max-w-sm">
|
||||||
Recent activity will appear here as it happens
|
Recent activity will appear here as it happens
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1402,18 +1400,18 @@ const EventFeed = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="flex flex-col h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm w-full">
|
<Card className="flex flex-col h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm w-full">
|
||||||
<CardHeader className="p-6">
|
<CardHeader className="p-6 pb-2">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">{title}</CardTitle>
|
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">{title}</CardTitle>
|
||||||
{lastUpdate && (
|
{lastUpdate && (
|
||||||
<CardDescription className="text-xs">
|
<CardDescription className="text-sm text-muted-foreground">
|
||||||
Last updated {format(lastUpdate, "h:mm a")}
|
Last updated {format(lastUpdate, "h:mm a")}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!error && (
|
{!error && (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-2">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -1421,7 +1419,7 @@ const EventFeed = ({
|
|||||||
variant={activeEventTypes[METRIC_IDS.PLACED_ORDER] ? "default" : "outline"}
|
variant={activeEventTypes[METRIC_IDS.PLACED_ORDER] ? "default" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleEventTypeClick(METRIC_IDS.PLACED_ORDER)}
|
onClick={() => handleEventTypeClick(METRIC_IDS.PLACED_ORDER)}
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0 rounded-md"
|
||||||
>
|
>
|
||||||
<Package className="h-4 w-4" />
|
<Package className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1432,77 +1430,77 @@ const EventFeed = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant={activeEventTypes[METRIC_IDS.SHIPPED_ORDER] ? "default" : "outline"}
|
variant={activeEventTypes[METRIC_IDS.SHIPPED_ORDER] ? "default" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleEventTypeClick(METRIC_IDS.SHIPPED_ORDER)}
|
onClick={() => handleEventTypeClick(METRIC_IDS.SHIPPED_ORDER)}
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0 rounded-md"
|
||||||
>
|
>
|
||||||
<Truck className="h-4 w-4" />
|
<Truck className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<EventTypeTooltipContent />
|
<EventTypeTooltipContent />
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant={activeEventTypes[METRIC_IDS.ACCOUNT_CREATED] ? "default" : "outline"}
|
variant={activeEventTypes[METRIC_IDS.ACCOUNT_CREATED] ? "default" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleEventTypeClick(METRIC_IDS.ACCOUNT_CREATED)}
|
onClick={() => handleEventTypeClick(METRIC_IDS.ACCOUNT_CREATED)}
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0 rounded-md"
|
||||||
>
|
>
|
||||||
<UserPlus className="h-4 w-4" />
|
<UserPlus className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<EventTypeTooltipContent />
|
<EventTypeTooltipContent />
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant={activeEventTypes[METRIC_IDS.CANCELED_ORDER] ? "default" : "outline"}
|
variant={activeEventTypes[METRIC_IDS.CANCELED_ORDER] ? "default" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleEventTypeClick(METRIC_IDS.CANCELED_ORDER)}
|
onClick={() => handleEventTypeClick(METRIC_IDS.CANCELED_ORDER)}
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0 rounded-md"
|
||||||
>
|
>
|
||||||
<XCircle className="h-4 w-4" />
|
<XCircle className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<EventTypeTooltipContent />
|
<EventTypeTooltipContent />
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant={activeEventTypes[METRIC_IDS.PAYMENT_REFUNDED] ? "default" : "outline"}
|
variant={activeEventTypes[METRIC_IDS.PAYMENT_REFUNDED] ? "default" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleEventTypeClick(METRIC_IDS.PAYMENT_REFUNDED)}
|
onClick={() => handleEventTypeClick(METRIC_IDS.PAYMENT_REFUNDED)}
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0 rounded-md"
|
||||||
>
|
>
|
||||||
<DollarSign className="h-4 w-4" />
|
<DollarSign className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<EventTypeTooltipContent />
|
<EventTypeTooltipContent />
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -1511,7 +1509,7 @@ const EventFeed = ({
|
|||||||
variant={activeEventTypes[METRIC_IDS.NEW_BLOG_POST] ? "default" : "outline"}
|
variant={activeEventTypes[METRIC_IDS.NEW_BLOG_POST] ? "default" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleEventTypeClick(METRIC_IDS.NEW_BLOG_POST)}
|
onClick={() => handleEventTypeClick(METRIC_IDS.NEW_BLOG_POST)}
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0 rounded-md"
|
||||||
>
|
>
|
||||||
<FileText className="h-4 w-4" />
|
<FileText className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1525,7 +1523,7 @@ const EventFeed = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Order Property Filters - only show if not in error state */}
|
{/* Order Property Filters - update styling */}
|
||||||
{!error && (
|
{!error && (
|
||||||
<div className="flex flex-wrap gap-2 justify-center mt-4 pt-1">
|
<div className="flex flex-wrap gap-2 justify-center mt-4 pt-1">
|
||||||
{counts.orderProperties.hasPreorder > 0 && (
|
{counts.orderProperties.hasPreorder > 0 && (
|
||||||
@@ -1536,7 +1534,7 @@ const EventFeed = ({
|
|||||||
orderFilters.hasPreorder
|
orderFilters.hasPreorder
|
||||||
? 'bg-purple-800 text-purple-200 hover:bg-purple-700'
|
? 'bg-purple-800 text-purple-200 hover:bg-purple-700'
|
||||||
: 'bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-300 hover:bg-purple-100 dark:hover:bg-purple-900/20'
|
: 'bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-300 hover:bg-purple-100 dark:hover:bg-purple-900/20'
|
||||||
} cursor-pointer`}
|
} cursor-pointer rounded-md`}
|
||||||
>
|
>
|
||||||
Pre-order ({counts.orderProperties.hasPreorder})
|
Pre-order ({counts.orderProperties.hasPreorder})
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -1549,7 +1547,7 @@ const EventFeed = ({
|
|||||||
orderFilters.localPickup
|
orderFilters.localPickup
|
||||||
? 'bg-green-800 text-green-200 hover:bg-green-700'
|
? 'bg-green-800 text-green-200 hover:bg-green-700'
|
||||||
: 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300 hover:bg-green-100 dark:hover:bg-green-900/20'
|
: 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300 hover:bg-green-100 dark:hover:bg-green-900/20'
|
||||||
} cursor-pointer`}
|
} cursor-pointer rounded-md`}
|
||||||
>
|
>
|
||||||
Local ({counts.orderProperties.localPickup})
|
Local ({counts.orderProperties.localPickup})
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -1562,7 +1560,7 @@ const EventFeed = ({
|
|||||||
orderFilters.isOnHold
|
orderFilters.isOnHold
|
||||||
? 'bg-blue-800 text-blue-200 hover:bg-blue-700'
|
? 'bg-blue-800 text-blue-200 hover:bg-blue-700'
|
||||||
: 'bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/20'
|
: 'bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/20'
|
||||||
} cursor-pointer`}
|
} cursor-pointer rounded-md`}
|
||||||
>
|
>
|
||||||
On Hold ({counts.orderProperties.isOnHold})
|
On Hold ({counts.orderProperties.isOnHold})
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -1575,7 +1573,7 @@ const EventFeed = ({
|
|||||||
orderFilters.onHoldReleased
|
orderFilters.onHoldReleased
|
||||||
? 'bg-green-800 text-green-200 hover:bg-green-700'
|
? 'bg-green-800 text-green-200 hover:bg-green-700'
|
||||||
: 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300 hover:bg-green-100 dark:hover:bg-green-900/20'
|
: 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300 hover:bg-green-100 dark:hover:bg-green-900/20'
|
||||||
} cursor-pointer`}
|
} cursor-pointer rounded-md`}
|
||||||
>
|
>
|
||||||
Hold Released ({counts.orderProperties.onHoldReleased})
|
Hold Released ({counts.orderProperties.onHoldReleased})
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -1588,7 +1586,7 @@ const EventFeed = ({
|
|||||||
orderFilters.hasDigiItem
|
orderFilters.hasDigiItem
|
||||||
? 'bg-indigo-800 text-indigo-200 hover:bg-indigo-700'
|
? 'bg-indigo-800 text-indigo-200 hover:bg-indigo-700'
|
||||||
: 'bg-indigo-100 dark:bg-indigo-900/20 text-indigo-800 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-900/20'
|
: 'bg-indigo-100 dark:bg-indigo-900/20 text-indigo-800 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-900/20'
|
||||||
} cursor-pointer`}
|
} cursor-pointer rounded-md`}
|
||||||
>
|
>
|
||||||
Digital ({counts.orderProperties.hasDigiItem})
|
Digital ({counts.orderProperties.hasDigiItem})
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -1601,7 +1599,7 @@ const EventFeed = ({
|
|||||||
orderFilters.hasNotions
|
orderFilters.hasNotions
|
||||||
? 'bg-yellow-800 text-yellow-200 hover:bg-yellow-700'
|
? 'bg-yellow-800 text-yellow-200 hover:bg-yellow-700'
|
||||||
: 'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300 hover:bg-yellow-100 dark:hover:bg-yellow-900/20'
|
: 'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300 hover:bg-yellow-100 dark:hover:bg-yellow-900/20'
|
||||||
} cursor-pointer`}
|
} cursor-pointer rounded-md`}
|
||||||
>
|
>
|
||||||
Notions ({counts.orderProperties.hasNotions})
|
Notions ({counts.orderProperties.hasNotions})
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -1614,7 +1612,7 @@ const EventFeed = ({
|
|||||||
orderFilters.hasGiftCard
|
orderFilters.hasGiftCard
|
||||||
? 'bg-pink-800 text-pink-200 hover:bg-pink-700'
|
? 'bg-pink-800 text-pink-200 hover:bg-pink-700'
|
||||||
: 'bg-pink-100 dark:bg-pink-900/20 text-pink-800 dark:text-pink-300 hover:bg-pink-100 dark:hover:bg-pink-900/20'
|
: 'bg-pink-100 dark:bg-pink-900/20 text-pink-800 dark:text-pink-300 hover:bg-pink-100 dark:hover:bg-pink-900/20'
|
||||||
} cursor-pointer`}
|
} cursor-pointer rounded-md`}
|
||||||
>
|
>
|
||||||
eGift Card ({counts.orderProperties.hasGiftCard})
|
eGift Card ({counts.orderProperties.hasGiftCard})
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -1627,7 +1625,7 @@ const EventFeed = ({
|
|||||||
orderFilters.stillOwes
|
orderFilters.stillOwes
|
||||||
? 'bg-red-800 text-red-200 hover:bg-red-700'
|
? 'bg-red-800 text-red-200 hover:bg-red-700'
|
||||||
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300 hover:bg-red-100 dark:hover:bg-red-900/20'
|
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300 hover:bg-red-100 dark:hover:bg-red-900/20'
|
||||||
} cursor-pointer`}
|
} cursor-pointer rounded-md`}
|
||||||
>
|
>
|
||||||
Owes ({counts.orderProperties.stillOwes})
|
Owes ({counts.orderProperties.stillOwes})
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -1641,7 +1639,7 @@ const EventFeed = ({
|
|||||||
{loading && !events.length ? (
|
{loading && !events.length ? (
|
||||||
<LoadingState />
|
<LoadingState />
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<Alert variant="destructive" className="mt-1" >
|
<Alert variant="destructive" className="mt-1 mx-6">
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
<AlertTitle>Error</AlertTitle>
|
<AlertTitle>Error</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
@@ -1649,17 +1647,7 @@ const EventFeed = ({
|
|||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
) : !filteredEvents || filteredEvents.length === 0 ? (
|
) : !filteredEvents || filteredEvents.length === 0 ? (
|
||||||
<div className="h-full flex flex-col items-center justify-center py-16 px-4">
|
<EmptyState />
|
||||||
<div className="bg-gray-100 dark:bg-gray-800 rounded-full p-3 mb-4">
|
|
||||||
<Activity className="h-8 w-8 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
|
||||||
No activity yet today
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-muted-foreground text-center max-w-sm">
|
|
||||||
Recent activity will appear here as it happens
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-gray-100 dark:divide-gray-800">
|
<div className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||||
{filteredEvents.map((event) => (
|
{filteredEvents.map((event) => (
|
||||||
|
|||||||
@@ -168,13 +168,13 @@ const SkeletonMetricCard = () => (
|
|||||||
<CardContent className="pt-6 h-full">
|
<CardContent className="pt-6 h-full">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<Skeleton className="h-4 w-24 mb-4 dark:bg-gray-700" />
|
<Skeleton className="h-4 w-24 mb-4 bg-muted" />
|
||||||
<div className="flex items-baseline gap-2">
|
<div className="flex items-baseline gap-2">
|
||||||
<Skeleton className="h-8 w-20 dark:bg-gray-700" />
|
<Skeleton className="h-8 w-20 bg-muted" />
|
||||||
<Skeleton className="h-4 w-12 dark:bg-gray-700" />
|
<Skeleton className="h-4 w-12 bg-muted" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Skeleton className="h-5 w-5 rounded-full dark:bg-gray-700" />
|
<Skeleton className="h-5 w-5 rounded-full bg-muted" />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -184,19 +184,19 @@ const TableSkeleton = () => (
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="dark:border-gray-800">
|
<TableRow className="dark:border-gray-800">
|
||||||
<TableHead><Skeleton className="h-4 w-24 dark:bg-gray-700" /></TableHead>
|
<TableHead><Skeleton className="h-4 w-24 bg-muted" /></TableHead>
|
||||||
<TableHead className="text-right"><Skeleton className="h-4 w-16 ml-auto dark:bg-gray-700" /></TableHead>
|
<TableHead className="text-right"><Skeleton className="h-4 w-16 ml-auto bg-muted" /></TableHead>
|
||||||
<TableHead className="text-right"><Skeleton className="h-4 w-16 ml-auto dark:bg-gray-700" /></TableHead>
|
<TableHead className="text-right"><Skeleton className="h-4 w-16 ml-auto bg-muted" /></TableHead>
|
||||||
<TableHead className="text-right"><Skeleton className="h-4 w-16 ml-auto dark:bg-gray-700" /></TableHead>
|
<TableHead className="text-right"><Skeleton className="h-4 w-16 ml-auto bg-muted" /></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{[...Array(5)].map((_, i) => (
|
{[...Array(5)].map((_, i) => (
|
||||||
<TableRow key={i} className="dark:border-gray-800">
|
<TableRow key={i} className="dark:border-gray-800">
|
||||||
<TableCell><Skeleton className="h-4 w-32 dark:bg-gray-700" /></TableCell>
|
<TableCell><Skeleton className="h-4 w-32 bg-muted" /></TableCell>
|
||||||
<TableCell className="text-right"><Skeleton className="h-4 w-12 ml-auto dark:bg-gray-700" /></TableCell>
|
<TableCell className="text-right"><Skeleton className="h-4 w-12 ml-auto bg-muted" /></TableCell>
|
||||||
<TableCell className="text-right"><Skeleton className="h-4 w-12 ml-auto dark:bg-gray-700" /></TableCell>
|
<TableCell className="text-right"><Skeleton className="h-4 w-12 ml-auto bg-muted" /></TableCell>
|
||||||
<TableCell className="text-right"><Skeleton className="h-4 w-16 ml-auto dark:bg-gray-700" /></TableCell>
|
<TableCell className="text-right"><Skeleton className="h-4 w-16 ml-auto bg-muted" /></TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@@ -326,13 +326,23 @@ const GorgiasOverview = () => {
|
|||||||
|
|
||||||
console.log('Processed agents:', agents);
|
console.log('Processed agents:', agents);
|
||||||
|
|
||||||
if (error) return <p className="text-red-500">Error: {error}</p>;
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Card className="h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="p-4 m-6 text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/10 rounded-lg border border-red-200 dark:border-red-900/20">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-white dark:bg-gray-900">
|
<Card className="h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
Customer Service
|
Customer Service
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -414,180 +424,182 @@ const GorgiasOverview = () => {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="h-full">
|
||||||
|
<MetricCard
|
||||||
|
title="Customer Satisfaction"
|
||||||
|
value={`${satisfactionStats.average_rating?.value}/5`}
|
||||||
|
delta={satisfactionStats.average_rating?.delta}
|
||||||
|
suffix="%"
|
||||||
|
icon={Star}
|
||||||
|
colorClass="orange"
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="h-full">
|
||||||
|
<MetricCard
|
||||||
|
title="Survey Response Rate"
|
||||||
|
value={satisfactionStats.response_rate?.value}
|
||||||
|
delta={satisfactionStats.response_rate?.delta}
|
||||||
|
suffix="%"
|
||||||
|
icon={ClipboardCheck}
|
||||||
|
colorClass="pink"
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="h-full">
|
||||||
|
<MetricCard
|
||||||
|
title="Resolution Time"
|
||||||
|
value={formatDuration(stats.median_resolution_time?.value)}
|
||||||
|
delta={stats.median_resolution_time?.delta}
|
||||||
|
icon={Timer}
|
||||||
|
colorClass="teal"
|
||||||
|
more_is_better={false}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="h-full">
|
||||||
|
<MetricCard
|
||||||
|
title="Self-Service Rate"
|
||||||
|
value={selfServiceStats.self_service_automation_rate?.value}
|
||||||
|
delta={selfServiceStats.self_service_automation_rate?.delta}
|
||||||
|
suffix="%"
|
||||||
|
icon={Bot}
|
||||||
|
colorClass="cyan"
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Satisfaction & Efficiency */}
|
|
||||||
<div className="h-full">
|
|
||||||
<MetricCard
|
|
||||||
title="Customer Satisfaction"
|
|
||||||
value={`${satisfactionStats.average_rating?.value}/5`}
|
|
||||||
delta={satisfactionStats.average_rating?.delta}
|
|
||||||
suffix="%"
|
|
||||||
icon={Star}
|
|
||||||
colorClass="orange"
|
|
||||||
loading={loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="h-full">
|
|
||||||
<MetricCard
|
|
||||||
title="Survey Response Rate"
|
|
||||||
value={satisfactionStats.response_rate?.value}
|
|
||||||
delta={satisfactionStats.response_rate?.delta}
|
|
||||||
suffix="%"
|
|
||||||
icon={ClipboardCheck}
|
|
||||||
colorClass="pink"
|
|
||||||
loading={loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="h-full">
|
|
||||||
<MetricCard
|
|
||||||
title="Resolution Time"
|
|
||||||
value={formatDuration(stats.median_resolution_time?.value)}
|
|
||||||
delta={stats.median_resolution_time?.delta}
|
|
||||||
icon={Timer}
|
|
||||||
colorClass="teal"
|
|
||||||
more_is_better={false}
|
|
||||||
loading={loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="h-full">
|
|
||||||
<MetricCard
|
|
||||||
title="Self-Service Rate"
|
|
||||||
value={selfServiceStats.self_service_automation_rate?.value}
|
|
||||||
delta={selfServiceStats.self_service_automation_rate?.delta}
|
|
||||||
suffix="%"
|
|
||||||
icon={Bot}
|
|
||||||
colorClass="cyan"
|
|
||||||
loading={loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{/* Channel Distribution */}
|
{/* Channel Distribution */}
|
||||||
<div className="bg-white dark:bg-gray-900 rounded-lg border dark:border-gray-800 p-4 pt-0">
|
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<div className="p-4 pl-2">
|
<CardHeader className="pb-0">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||||
Channel Distribution
|
Channel Distribution
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</CardHeader>
|
||||||
{loading ? (
|
<CardContent className="overflow-y-auto max-h-[400px] scrollbar-thin scrollbar-thumb-gray-200 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent hover:scrollbar-thumb-gray-300 dark:hover:scrollbar-thumb-gray-600">
|
||||||
<TableSkeleton />
|
{loading ? (
|
||||||
) : (
|
<TableSkeleton />
|
||||||
<Table>
|
) : (
|
||||||
<TableHeader>
|
<Table>
|
||||||
<TableRow className="dark:border-gray-800">
|
<TableHeader>
|
||||||
<TableHead>Channel</TableHead>
|
<TableRow className="dark:border-gray-800">
|
||||||
<TableHead className="text-right">Total</TableHead>
|
<TableHead className="text-left font-medium text-gray-900 dark:text-gray-100">Channel</TableHead>
|
||||||
<TableHead className="text-right">%</TableHead>
|
<TableHead className="text-right font-medium text-gray-900 dark:text-gray-100">Total</TableHead>
|
||||||
<TableHead className="text-right">Change</TableHead>
|
<TableHead className="text-right font-medium text-gray-900 dark:text-gray-100">%</TableHead>
|
||||||
</TableRow>
|
<TableHead className="text-right font-medium text-gray-900 dark:text-gray-100">Change</TableHead>
|
||||||
</TableHeader>
|
</TableRow>
|
||||||
<TableBody>
|
</TableHeader>
|
||||||
{channels
|
<TableBody>
|
||||||
.sort((a, b) => b.total - a.total)
|
{channels
|
||||||
.map((channel, index) => (
|
.sort((a, b) => b.total - a.total)
|
||||||
<TableRow key={index} className="dark:border-gray-800">
|
.map((channel, index) => (
|
||||||
<TableCell className="dark:text-gray-300">
|
<TableRow key={index} className="dark:border-gray-800 hover:bg-muted/50 transition-colors">
|
||||||
{channel.name}
|
<TableCell className="text-gray-900 dark:text-gray-100">
|
||||||
</TableCell>
|
{channel.name}
|
||||||
<TableCell className="text-right dark:text-gray-300">
|
</TableCell>
|
||||||
{channel.total}
|
<TableCell className="text-right text-muted-foreground">
|
||||||
</TableCell>
|
{channel.total}
|
||||||
<TableCell className="text-right dark:text-gray-300">
|
</TableCell>
|
||||||
{channel.percentage}%
|
<TableCell className="text-right text-muted-foreground">
|
||||||
</TableCell>
|
{channel.percentage}%
|
||||||
<TableCell
|
</TableCell>
|
||||||
className={`text-right ${
|
<TableCell
|
||||||
channel.delta > 0
|
className={`text-right ${
|
||||||
? "text-green-600 dark:text-green-500"
|
channel.delta > 0
|
||||||
: channel.delta < 0
|
? "text-green-600 dark:text-green-500"
|
||||||
? "text-red-600 dark:text-red-500"
|
: channel.delta < 0
|
||||||
: "dark:text-gray-300"
|
? "text-red-600 dark:text-red-500"
|
||||||
}`}
|
: "text-muted-foreground"
|
||||||
>
|
}`}
|
||||||
<div className="flex items-center justify-end gap-0.5">
|
>
|
||||||
{channel.delta !== 0 && (
|
<div className="flex items-center justify-end gap-0.5">
|
||||||
<>
|
{channel.delta !== 0 && (
|
||||||
{channel.delta > 0 ? (
|
<>
|
||||||
<ArrowUp className="w-3 h-3" />
|
{channel.delta > 0 ? (
|
||||||
) : (
|
<ArrowUp className="w-3 h-3" />
|
||||||
<ArrowDown className="w-3 h-3" />
|
) : (
|
||||||
)}
|
<ArrowDown className="w-3 h-3" />
|
||||||
<span>{Math.abs(channel.delta)}%</span>
|
)}
|
||||||
</>
|
<span>{Math.abs(channel.delta)}%</span>
|
||||||
)}
|
</>
|
||||||
</div>
|
)}
|
||||||
</TableCell>
|
</div>
|
||||||
</TableRow>
|
</TableCell>
|
||||||
))}
|
</TableRow>
|
||||||
</TableBody>
|
))}
|
||||||
</Table>
|
</TableBody>
|
||||||
)}
|
</Table>
|
||||||
</div>
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Agent Performance */}
|
{/* Agent Performance */}
|
||||||
<div className="bg-white dark:bg-gray-900 rounded-lg border dark:border-gray-800 p-4 pt-0">
|
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<div className="p-4 pl-2">
|
<CardHeader className="pb-0">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||||
Agent Performance
|
Agent Performance
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</CardHeader>
|
||||||
{loading ? (
|
<CardContent className="overflow-y-auto max-h-[400px] scrollbar-thin scrollbar-thumb-gray-200 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent hover:scrollbar-thumb-gray-300 dark:hover:scrollbar-thumb-gray-600">
|
||||||
<TableSkeleton />
|
{loading ? (
|
||||||
) : (
|
<TableSkeleton />
|
||||||
<Table>
|
) : (
|
||||||
<TableHeader>
|
<Table>
|
||||||
<TableRow className="dark:border-gray-800">
|
<TableHeader>
|
||||||
<TableHead>Agent</TableHead>
|
<TableRow className="dark:border-gray-800">
|
||||||
<TableHead className="text-right">Closed</TableHead>
|
<TableHead className="text-left font-medium text-gray-900 dark:text-gray-100">Agent</TableHead>
|
||||||
<TableHead className="text-right">Rating</TableHead>
|
<TableHead className="text-right font-medium text-gray-900 dark:text-gray-100">Closed</TableHead>
|
||||||
<TableHead className="text-right">Change</TableHead>
|
<TableHead className="text-right font-medium text-gray-900 dark:text-gray-100">Rating</TableHead>
|
||||||
</TableRow>
|
<TableHead className="text-right font-medium text-gray-900 dark:text-gray-100">Change</TableHead>
|
||||||
</TableHeader>
|
</TableRow>
|
||||||
<TableBody>
|
</TableHeader>
|
||||||
{agents
|
<TableBody>
|
||||||
.filter((agent) => agent.name !== "Unassigned")
|
{agents
|
||||||
.map((agent, index) => (
|
.filter((agent) => agent.name !== "Unassigned")
|
||||||
<TableRow key={index} className="dark:border-gray-800">
|
.map((agent, index) => (
|
||||||
<TableCell className="dark:text-gray-300">
|
<TableRow key={index} className="dark:border-gray-800 hover:bg-muted/50 transition-colors">
|
||||||
{agent.name}
|
<TableCell className="text-gray-900 dark:text-gray-100">
|
||||||
</TableCell>
|
{agent.name}
|
||||||
<TableCell className="text-right dark:text-gray-300">
|
</TableCell>
|
||||||
{agent.closed}
|
<TableCell className="text-right text-muted-foreground">
|
||||||
</TableCell>
|
{agent.closed}
|
||||||
<TableCell className="text-right dark:text-gray-300">
|
</TableCell>
|
||||||
{agent.rating ? `${agent.rating}/5` : "-"}
|
<TableCell className="text-right text-muted-foreground">
|
||||||
</TableCell>
|
{agent.rating ? `${agent.rating}/5` : "-"}
|
||||||
<TableCell
|
</TableCell>
|
||||||
className={`text-right ${
|
<TableCell
|
||||||
agent.delta > 0
|
className={`text-right ${
|
||||||
? "text-green-600 dark:text-green-500"
|
agent.delta > 0
|
||||||
: agent.delta < 0
|
? "text-green-600 dark:text-green-500"
|
||||||
? "text-red-600 dark:text-red-500"
|
: agent.delta < 0
|
||||||
: "dark:text-gray-300"
|
? "text-red-600 dark:text-red-500"
|
||||||
}`}
|
: "text-muted-foreground"
|
||||||
>
|
}`}
|
||||||
<div className="flex items-center justify-end gap-0.5">
|
>
|
||||||
{agent.delta !== 0 && (
|
<div className="flex items-center justify-end gap-0.5">
|
||||||
<>
|
{agent.delta !== 0 && (
|
||||||
{agent.delta > 0 ? (
|
<>
|
||||||
<ArrowUp className="w-3 h-3" />
|
{agent.delta > 0 ? (
|
||||||
) : (
|
<ArrowUp className="w-3 h-3" />
|
||||||
<ArrowDown className="w-3 h-3" />
|
) : (
|
||||||
)}
|
<ArrowDown className="w-3 h-3" />
|
||||||
<span>{Math.abs(agent.delta)}%</span>
|
)}
|
||||||
</>
|
<span>{Math.abs(agent.delta)}%</span>
|
||||||
)}
|
</>
|
||||||
</div>
|
)}
|
||||||
</TableCell>
|
</div>
|
||||||
</TableRow>
|
</TableCell>
|
||||||
))}
|
</TableRow>
|
||||||
</TableBody>
|
))}
|
||||||
</Table>
|
</TableBody>
|
||||||
)}
|
</Table>
|
||||||
</div>
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -84,34 +84,26 @@ const summaryCard = (label, value, options = {}) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MetricCell = ({
|
const MetricCell = ({ value, label, sublabel, isMonetary = false, isPercentage = false, decimalPlaces = 0 }) => {
|
||||||
value,
|
const formattedValue = isMonetary
|
||||||
label,
|
? formatCurrency(value, decimalPlaces)
|
||||||
sublabel,
|
: isPercentage
|
||||||
isMonetary = false,
|
? formatPercent(value, decimalPlaces)
|
||||||
isPercentage = false,
|
: formatNumber(value, decimalPlaces);
|
||||||
decimalPlaces = 0,
|
|
||||||
}) => (
|
return (
|
||||||
<td className="p-2 text-center align-top">
|
<td className="p-2 text-center align-top">
|
||||||
<div className="text-blue-600 dark:text-blue-400 text-center font-semibold">
|
<div className="text-blue-600 dark:text-blue-400 text-lg font-semibold">
|
||||||
{isMonetary
|
{formattedValue}
|
||||||
? formatCurrency(value, decimalPlaces)
|
|
||||||
: isPercentage
|
|
||||||
? formatPercent(value, decimalPlaces)
|
|
||||||
: formatNumber(value, decimalPlaces)}
|
|
||||||
</div>
|
|
||||||
{label && (
|
|
||||||
<div className="text-xs text-center text-gray-500 dark:text-gray-400">
|
|
||||||
{label}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{(label || sublabel) && (
|
||||||
{sublabel && (
|
<div className="text-muted-foreground text-sm">
|
||||||
<div className="text-xs text-center text-gray-500 dark:text-gray-400">
|
{label || sublabel}
|
||||||
{sublabel}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</td>
|
||||||
</td>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
const getActionValue = (campaign, actionType) => {
|
const getActionValue = (campaign, actionType) => {
|
||||||
if (actionType === "impressions" || actionType === "reach") {
|
if (actionType === "impressions" || actionType === "reach") {
|
||||||
@@ -256,55 +248,57 @@ const SkeletonMetricCard = () => (
|
|||||||
<CardContent className="pt-6 h-full">
|
<CardContent className="pt-6 h-full">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<Skeleton className="h-4 w-24 mb-4 dark:bg-gray-700" />
|
<Skeleton className="h-4 w-24 mb-4 bg-muted" />
|
||||||
<div className="flex items-baseline gap-2">
|
<div className="flex items-baseline gap-2">
|
||||||
<Skeleton className="h-8 w-20 dark:bg-gray-700" />
|
<Skeleton className="h-8 w-20 bg-muted" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Skeleton className="h-5 w-5 rounded-full dark:bg-gray-700" />
|
<Skeleton className="h-5 w-5 rounded-full bg-muted" />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
const SkeletonTable = () => (
|
const SkeletonTable = () => (
|
||||||
<div className="grid overflow-x-auto">
|
<div className="h-full max-h-[400px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-200 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent hover:scrollbar-thumb-gray-300 dark:hover:scrollbar-thumb-gray-600 pr-2">
|
||||||
<div className="overflow-y-auto max-h-[400px]">
|
<table className="min-w-full">
|
||||||
<table className="min-w-full">
|
<thead>
|
||||||
<thead>
|
<tr className="border-b border-gray-200 dark:border-gray-800">
|
||||||
<tr className="border-b border-gray-200 dark:border-gray-800">
|
<th className="p-2 sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10">
|
||||||
<th className="p-2 sticky top-0 bg-white dark:bg-gray-900 z-10">
|
<Skeleton className="h-4 w-32 bg-muted" />
|
||||||
<Skeleton className="h-4 w-32 dark:bg-gray-700" />
|
</th>
|
||||||
|
{[...Array(8)].map((_, i) => (
|
||||||
|
<th key={i} className="p-2 text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10">
|
||||||
|
<Skeleton className="h-4 w-20 mx-auto bg-muted" />
|
||||||
</th>
|
</th>
|
||||||
{[...Array(8)].map((_, i) => (
|
))}
|
||||||
<th key={i} className="p-2 text-center sticky top-0 bg-white dark:bg-gray-900 z-10">
|
</tr>
|
||||||
<Skeleton className="h-4 w-20 mx-auto dark:bg-gray-700" />
|
</thead>
|
||||||
</th>
|
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
||||||
))}
|
{[...Array(5)].map((_, rowIndex) => (
|
||||||
</tr>
|
<tr key={rowIndex} className="hover:bg-muted/50 transition-colors">
|
||||||
</thead>
|
<td className="p-2">
|
||||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
<div className="flex items-center gap-2">
|
||||||
{[...Array(5)].map((_, rowIndex) => (
|
<Skeleton className="h-4 w-4 bg-muted" />
|
||||||
<tr key={rowIndex}>
|
|
||||||
<td className="p-2">
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Skeleton className="h-4 w-48 dark:bg-gray-700" />
|
<Skeleton className="h-4 w-48 bg-muted" />
|
||||||
<Skeleton className="h-3 w-24 dark:bg-gray-700" />
|
<Skeleton className="h-3 w-64 bg-muted" />
|
||||||
|
<Skeleton className="h-3 w-32 bg-muted" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{[...Array(8)].map((_, colIndex) => (
|
||||||
|
<td key={colIndex} className="p-2 text-center">
|
||||||
|
<div className="flex flex-col items-center gap-1">
|
||||||
|
<Skeleton className="h-4 w-16 bg-muted" />
|
||||||
|
<Skeleton className="h-3 w-24 bg-muted" />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{[...Array(8)].map((_, colIndex) => (
|
))}
|
||||||
<td key={colIndex} className="p-2 text-center">
|
</tr>
|
||||||
<div className="space-y-1">
|
))}
|
||||||
<Skeleton className="h-4 w-16 mx-auto dark:bg-gray-700" />
|
</tbody>
|
||||||
<Skeleton className="h-3 w-12 mx-auto dark:bg-gray-700" />
|
</table>
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -449,7 +443,7 @@ const MetaCampaigns = () => {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Card className="bg-white dark:bg-gray-900">
|
<Card className="h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<div className="flex justify-between items-start mb-6">
|
<div className="flex justify-between items-start mb-6">
|
||||||
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
@@ -479,16 +473,18 @@ const MetaCampaigns = () => {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Card className="bg-white dark:bg-gray-900">
|
<Card className="h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="text-red-500 dark:text-red-400">{error}</div>
|
<div className="p-4 m-6 text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/10 rounded-lg border border-red-200 dark:border-red-900/20">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="h-full bg-white dark:bg-gray-900">
|
<Card className="h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<div className="flex justify-between items-start mb-6">
|
<div className="flex justify-between items-start mb-6">
|
||||||
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
@@ -577,164 +573,162 @@ const MetaCampaigns = () => {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="p-4">
|
<CardContent className="overflow-y-auto pl-4 max-h-[400px] mb-4">
|
||||||
<div className="grid overflow-x-auto">
|
<table className="w-full">
|
||||||
<div className="overflow-y-auto max-h-[400px]">
|
<thead>
|
||||||
<table className="min-w-full">
|
<tr className="border-b border-gray-200 dark:border-gray-800">
|
||||||
<thead>
|
<th className="p-2 text-left font-medium sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 text-gray-900 dark:text-gray-100">
|
||||||
<tr className="border-b border-gray-200 dark:border-gray-800">
|
<Button
|
||||||
<th className="p-2 sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
|
variant="ghost"
|
||||||
<Button
|
className="pl-0 justify-start w-full h-8"
|
||||||
variant="ghost"
|
onClick={() => handleSort("date")}
|
||||||
className="pl-0 justify-start w-full"
|
>
|
||||||
onClick={() => handleSort("date")}
|
Campaign
|
||||||
>
|
</Button>
|
||||||
Campaign
|
</th>
|
||||||
</Button>
|
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 text-gray-900 dark:text-gray-100">
|
||||||
</th>
|
<Button
|
||||||
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
|
variant={sortConfig.key === "spend" ? "default" : "ghost"}
|
||||||
<Button
|
className="w-full justify-center h-8"
|
||||||
variant={sortConfig.key === "spend" ? "default" : "ghost"}
|
onClick={() => handleSort("spend")}
|
||||||
className="w-full justify-center"
|
>
|
||||||
onClick={() => handleSort("spend")}
|
Spend
|
||||||
>
|
</Button>
|
||||||
Spend
|
</th>
|
||||||
</Button>
|
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 text-gray-900 dark:text-gray-100">
|
||||||
</th>
|
<Button
|
||||||
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
|
variant={sortConfig.key === "reach" ? "default" : "ghost"}
|
||||||
<Button
|
className="w-full justify-center h-8"
|
||||||
variant={sortConfig.key === "reach" ? "default" : "ghost"}
|
onClick={() => handleSort("reach")}
|
||||||
className="w-full justify-center"
|
>
|
||||||
onClick={() => handleSort("reach")}
|
Reach
|
||||||
>
|
</Button>
|
||||||
Reach
|
</th>
|
||||||
</Button>
|
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 text-gray-900 dark:text-gray-100">
|
||||||
</th>
|
<Button
|
||||||
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
|
variant={sortConfig.key === "impressions" ? "default" : "ghost"}
|
||||||
<Button
|
className="w-full justify-center h-8"
|
||||||
variant={sortConfig.key === "impressions" ? "default" : "ghost"}
|
onClick={() => handleSort("impressions")}
|
||||||
className="w-full justify-center"
|
>
|
||||||
onClick={() => handleSort("impressions")}
|
Impressions
|
||||||
>
|
</Button>
|
||||||
Impressions
|
</th>
|
||||||
</Button>
|
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 text-gray-900 dark:text-gray-100">
|
||||||
</th>
|
<Button
|
||||||
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
|
variant={sortConfig.key === "cpm" ? "default" : "ghost"}
|
||||||
<Button
|
className="w-full justify-center h-8"
|
||||||
variant={sortConfig.key === "cpm" ? "default" : "ghost"}
|
onClick={() => handleSort("cpm")}
|
||||||
className="w-full justify-center"
|
>
|
||||||
onClick={() => handleSort("cpm")}
|
CPM
|
||||||
>
|
</Button>
|
||||||
CPM
|
</th>
|
||||||
</Button>
|
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 text-gray-900 dark:text-gray-100">
|
||||||
</th>
|
<Button
|
||||||
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
|
variant={sortConfig.key === "ctr" ? "default" : "ghost"}
|
||||||
<Button
|
className="w-full justify-center h-8"
|
||||||
variant={sortConfig.key === "ctr" ? "default" : "ghost"}
|
onClick={() => handleSort("ctr")}
|
||||||
className="w-full justify-center"
|
>
|
||||||
onClick={() => handleSort("ctr")}
|
CTR
|
||||||
>
|
</Button>
|
||||||
CTR
|
</th>
|
||||||
</Button>
|
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 text-gray-900 dark:text-gray-100">
|
||||||
</th>
|
<Button
|
||||||
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
|
variant={sortConfig.key === "results" ? "default" : "ghost"}
|
||||||
<Button
|
className="w-full justify-center h-8"
|
||||||
variant={sortConfig.key === "results" ? "default" : "ghost"}
|
onClick={() => handleSort("results")}
|
||||||
className="w-full justify-center"
|
>
|
||||||
onClick={() => handleSort("results")}
|
Results
|
||||||
>
|
</Button>
|
||||||
Results
|
</th>
|
||||||
</Button>
|
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 text-gray-900 dark:text-gray-100">
|
||||||
</th>
|
<Button
|
||||||
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
|
variant={sortConfig.key === "value" ? "default" : "ghost"}
|
||||||
<Button
|
className="w-full justify-center h-8"
|
||||||
variant={sortConfig.key === "value" ? "default" : "ghost"}
|
onClick={() => handleSort("value")}
|
||||||
className="w-full justify-center"
|
>
|
||||||
onClick={() => handleSort("value")}
|
Value
|
||||||
>
|
</Button>
|
||||||
Value
|
</th>
|
||||||
</Button>
|
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 text-gray-900 dark:text-gray-100">
|
||||||
</th>
|
<Button
|
||||||
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
|
variant={sortConfig.key === "engagements" ? "default" : "ghost"}
|
||||||
<Button
|
className="w-full justify-center h-8"
|
||||||
variant={sortConfig.key === "engagements" ? "default" : "ghost"}
|
onClick={() => handleSort("engagements")}
|
||||||
className="w-full justify-center"
|
>
|
||||||
onClick={() => handleSort("engagements")}
|
Engagements
|
||||||
>
|
</Button>
|
||||||
Engagements
|
</th>
|
||||||
</Button>
|
</tr>
|
||||||
</th>
|
</thead>
|
||||||
</tr>
|
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
||||||
</thead>
|
{sortedCampaigns.map((campaign) => (
|
||||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
<tr
|
||||||
{sortedCampaigns.map((campaign) => (
|
key={campaign.id}
|
||||||
<tr
|
className="hover:bg-muted/50 transition-colors"
|
||||||
key={campaign.id}
|
>
|
||||||
className="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
<td className="p-2 align-top">
|
||||||
>
|
<div>
|
||||||
<td className="p-2 align-top">
|
<div className="font-medium text-gray-900 dark:text-gray-100 break-words min-w-[200px] max-w-[300px]">
|
||||||
<div className="font-medium text-gray-900 dark:text-gray-100 break-words min-w-[200px] max-w-[300px]">
|
<CampaignName name={campaign.name} />
|
||||||
<CampaignName name={campaign.name} />
|
</div>
|
||||||
</div>
|
<div className="text-sm text-muted-foreground">
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
{campaign.objective}
|
||||||
{campaign.objective}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<MetricCell
|
<MetricCell
|
||||||
value={campaign.metrics.spend}
|
value={campaign.metrics.spend}
|
||||||
isMonetary
|
isMonetary
|
||||||
decimalPlaces={2}
|
decimalPlaces={2}
|
||||||
sublabel={
|
sublabel={
|
||||||
campaign.budget
|
campaign.budget
|
||||||
? `${formatCurrency(campaign.budget, 0)}/${campaign.budgetType}`
|
? `${formatCurrency(campaign.budget, 0)}/${campaign.budgetType}`
|
||||||
: "Budget: Ad set"
|
: "Budget: Ad set"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MetricCell
|
<MetricCell
|
||||||
value={campaign.metrics.reach}
|
value={campaign.metrics.reach}
|
||||||
label={`${formatNumber(campaign.metrics.frequency, 2)}x freq`}
|
label={`${formatNumber(campaign.metrics.frequency, 2)}x freq`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MetricCell
|
<MetricCell
|
||||||
value={campaign.metrics.impressions}
|
value={campaign.metrics.impressions}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MetricCell
|
<MetricCell
|
||||||
value={campaign.metrics.cpm}
|
value={campaign.metrics.cpm}
|
||||||
isMonetary
|
isMonetary
|
||||||
decimalPlaces={2}
|
decimalPlaces={2}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MetricCell
|
<MetricCell
|
||||||
value={campaign.metrics.ctr}
|
value={campaign.metrics.ctr}
|
||||||
isPercentage
|
isPercentage
|
||||||
decimalPlaces={2}
|
decimalPlaces={2}
|
||||||
label={`${formatCurrency(campaign.metrics.cpc, 2)} CPC`}
|
label={`${formatCurrency(campaign.metrics.cpc, 2)} CPC`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MetricCell
|
<MetricCell
|
||||||
value={getActionValue(campaign, campaign.objectiveActionType)}
|
value={getActionValue(campaign, campaign.objectiveActionType)}
|
||||||
label={campaign.objective}
|
label={campaign.objective}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MetricCell
|
<MetricCell
|
||||||
value={campaign.metrics.purchaseValue}
|
value={campaign.metrics.purchaseValue}
|
||||||
isMonetary
|
isMonetary
|
||||||
decimalPlaces={2}
|
decimalPlaces={2}
|
||||||
sublabel={campaign.metrics.costPerResult ? `${formatCurrency(campaign.metrics.costPerResult)}/result` : null}
|
sublabel={campaign.metrics.costPerResult ? `${formatCurrency(campaign.metrics.costPerResult)}/result` : null}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MetricCell
|
<MetricCell
|
||||||
value={campaign.metrics.totalPostEngagements}
|
value={campaign.metrics.totalPostEngagements}
|
||||||
/>
|
/>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -106,18 +106,18 @@ const ProductGrid = ({
|
|||||||
</td>
|
</td>
|
||||||
<td className="p-1 align-middle min-w-[200px]">
|
<td className="p-1 align-middle min-w-[200px]">
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<Skeleton className="h-4 w-[180px] bg-muted" />
|
<Skeleton className="h-4 w-[180px] bg-muted rounded-sm" />
|
||||||
<Skeleton className="h-3 w-[140px] bg-muted" />
|
<Skeleton className="h-3 w-[140px] bg-muted rounded-sm" />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-1 align-middle text-center">
|
<td className="p-1 align-middle text-center">
|
||||||
<Skeleton className="h-4 w-8 mx-auto bg-muted" />
|
<Skeleton className="h-4 w-8 mx-auto bg-muted rounded-sm" />
|
||||||
</td>
|
</td>
|
||||||
<td className="p-1 align-middle text-center">
|
<td className="p-1 align-middle text-center">
|
||||||
<Skeleton className="h-4 w-16 mx-auto bg-muted" />
|
<Skeleton className="h-4 w-16 mx-auto bg-muted rounded-sm" />
|
||||||
</td>
|
</td>
|
||||||
<td className="p-1 align-middle text-center">
|
<td className="p-1 align-middle text-center">
|
||||||
<Skeleton className="h-4 w-8 mx-auto bg-muted" />
|
<Skeleton className="h-4 w-8 mx-auto bg-muted rounded-sm" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -128,41 +128,41 @@ const ProductGrid = ({
|
|||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="hover:bg-transparent">
|
<tr className="hover:bg-transparent">
|
||||||
<th className="p-1.5 text-left font-medium sticky top-0 bg-white dark:bg-background z-10 w-[50px] min-w-[50px] border-b dark:border-gray-800" />
|
<th className="p-1.5 text-left font-medium sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 w-[50px] min-w-[50px] border-b dark:border-gray-800" />
|
||||||
<th className="p-1.5 text-left font-medium sticky top-0 bg-white dark:bg-background z-10 min-w-[200px] border-b dark:border-gray-800">
|
<th className="p-1.5 text-left font-medium sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 min-w-[200px] border-b dark:border-gray-800">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="w-full p-2 justify-start h-8 pointer-events-none"
|
className="w-full p-2 justify-start h-8 pointer-events-none"
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
<Skeleton className="h-4 w-16 bg-muted" />
|
<Skeleton className="h-4 w-16 bg-muted rounded-sm" />
|
||||||
</Button>
|
</Button>
|
||||||
</th>
|
</th>
|
||||||
<th className="p-1.5 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b dark:border-gray-800">
|
<th className="p-1.5 font-medium text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 border-b dark:border-gray-800">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="w-full p-2 justify-center h-8 pointer-events-none"
|
className="w-full p-2 justify-center h-8 pointer-events-none"
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
<Skeleton className="h-4 w-12 bg-muted" />
|
<Skeleton className="h-4 w-12 bg-muted rounded-sm" />
|
||||||
</Button>
|
</Button>
|
||||||
</th>
|
</th>
|
||||||
<th className="p-1.5 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b dark:border-gray-800">
|
<th className="p-1.5 font-medium text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 border-b dark:border-gray-800">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="w-full p-2 justify-center h-8 pointer-events-none"
|
className="w-full p-2 justify-center h-8 pointer-events-none"
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
<Skeleton className="h-4 w-12 bg-muted" />
|
<Skeleton className="h-4 w-12 bg-muted rounded-sm" />
|
||||||
</Button>
|
</Button>
|
||||||
</th>
|
</th>
|
||||||
<th className="p-1.5 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b dark:border-gray-800">
|
<th className="p-1.5 font-medium text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 border-b dark:border-gray-800">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="w-full p-2 justify-center h-8 pointer-events-none"
|
className="w-full p-2 justify-center h-8 pointer-events-none"
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
<Skeleton className="h-4 w-16 bg-muted" />
|
<Skeleton className="h-4 w-16 bg-muted rounded-sm" />
|
||||||
</Button>
|
</Button>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -185,17 +185,17 @@ const ProductGrid = ({
|
|||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
<Skeleton className="h-6 w-32 bg-muted" />
|
<Skeleton className="h-6 w-32 bg-muted rounded-sm" />
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
{description && (
|
{description && (
|
||||||
<CardDescription className="mt-1">
|
<CardDescription className="mt-1">
|
||||||
<Skeleton className="h-4 w-48 bg-muted" />
|
<Skeleton className="h-4 w-48 bg-muted rounded-sm" />
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Skeleton className="h-9 w-9 bg-muted" />
|
<Skeleton className="h-9 w-9 bg-muted rounded-sm" />
|
||||||
<Skeleton className="h-9 w-[130px] bg-muted" />
|
<Skeleton className="h-9 w-[130px] bg-muted rounded-sm" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,7 +218,7 @@ const ProductGrid = ({
|
|||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">{title}</CardTitle>
|
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">{title}</CardTitle>
|
||||||
{description && (
|
{description && (
|
||||||
<CardDescription className="mt-1">{description}</CardDescription>
|
<CardDescription className="mt-1 text-muted-foreground">{description}</CardDescription>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -280,7 +280,7 @@ const ProductGrid = ({
|
|||||||
<CardContent className="p-6 pt-0 flex-1 overflow-hidden -mt-1">
|
<CardContent className="p-6 pt-0 flex-1 overflow-hidden -mt-1">
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
{error ? (
|
{error ? (
|
||||||
<Alert variant="destructive" >
|
<Alert variant="destructive" className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
<AlertTitle>Error</AlertTitle>
|
<AlertTitle>Error</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
@@ -290,7 +290,7 @@ const ProductGrid = ({
|
|||||||
) : !products?.length ? (
|
) : !products?.length ? (
|
||||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||||
<Package className="h-12 w-12 text-muted-foreground mb-4" />
|
<Package className="h-12 w-12 text-muted-foreground mb-4" />
|
||||||
<p className="font-medium mb-2">No product data available</p>
|
<p className="font-medium mb-2 text-gray-900 dark:text-gray-100">No product data available</p>
|
||||||
<p className="text-sm text-muted-foreground">Try selecting a different time range</p>
|
<p className="text-sm text-muted-foreground">Try selecting a different time range</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -299,8 +299,8 @@ const ProductGrid = ({
|
|||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="hover:bg-transparent">
|
<tr className="hover:bg-transparent">
|
||||||
<th className="p-1 text-left font-medium sticky top-0 bg-white dark:bg-background z-10 h-[50px] min-h-[50px] w-[50px] min-w-[35px] border-b dark:border-gray-800" />
|
<th className="p-1 text-left font-medium sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 h-[50px] min-h-[50px] w-[50px] min-w-[35px] border-b dark:border-gray-800" />
|
||||||
<th className="p-1 text-left font-medium sticky top-0 bg-white dark:bg-background z-10 border-b dark:border-gray-800">
|
<th className="p-1 text-left font-medium sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 border-b dark:border-gray-800">
|
||||||
<Button
|
<Button
|
||||||
variant={sorting.column === "name" ? "default" : "ghost"}
|
variant={sorting.column === "name" ? "default" : "ghost"}
|
||||||
onClick={() => handleSort("name")}
|
onClick={() => handleSort("name")}
|
||||||
@@ -309,7 +309,7 @@ const ProductGrid = ({
|
|||||||
Product
|
Product
|
||||||
</Button>
|
</Button>
|
||||||
</th>
|
</th>
|
||||||
<th className="p-1 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b dark:border-gray-800">
|
<th className="p-1 font-medium text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 border-b dark:border-gray-800">
|
||||||
<Button
|
<Button
|
||||||
variant={sorting.column === "totalQuantity" ? "default" : "ghost"}
|
variant={sorting.column === "totalQuantity" ? "default" : "ghost"}
|
||||||
onClick={() => handleSort("totalQuantity")}
|
onClick={() => handleSort("totalQuantity")}
|
||||||
@@ -318,7 +318,7 @@ const ProductGrid = ({
|
|||||||
Sold
|
Sold
|
||||||
</Button>
|
</Button>
|
||||||
</th>
|
</th>
|
||||||
<th className="p-1 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b dark:border-gray-800">
|
<th className="p-1 font-medium text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 border-b dark:border-gray-800">
|
||||||
<Button
|
<Button
|
||||||
variant={sorting.column === "totalRevenue" ? "default" : "ghost"}
|
variant={sorting.column === "totalRevenue" ? "default" : "ghost"}
|
||||||
onClick={() => handleSort("totalRevenue")}
|
onClick={() => handleSort("totalRevenue")}
|
||||||
@@ -327,7 +327,7 @@ const ProductGrid = ({
|
|||||||
Rev
|
Rev
|
||||||
</Button>
|
</Button>
|
||||||
</th>
|
</th>
|
||||||
<th className="p-1 font-medium text-center sticky top-0 bg-white dark:bg-background z-10 border-b dark:border-gray-800">
|
<th className="p-1 font-medium text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 border-b dark:border-gray-800">
|
||||||
<Button
|
<Button
|
||||||
variant={sorting.column === "orderCount" ? "default" : "ghost"}
|
variant={sorting.column === "orderCount" ? "default" : "ghost"}
|
||||||
onClick={() => handleSort("orderCount")}
|
onClick={() => handleSort("orderCount")}
|
||||||
@@ -377,7 +377,7 @@ const ProductGrid = ({
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-1 align-middle text-center text-sm font-medium">
|
<td className="p-1 align-middle text-center text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
{product.totalQuantity}
|
{product.totalQuantity}
|
||||||
</td>
|
</td>
|
||||||
<td className="p-1 align-middle text-center text-emerald-600 dark:text-emerald-400 text-sm font-medium">
|
<td className="p-1 align-middle text-center text-emerald-600 dark:text-emerald-400 text-sm font-medium">
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ const QuotaInfo = ({ tokenQuota }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SkeletonSummaryCard = () => (
|
const SkeletonSummaryCard = () => (
|
||||||
<Card>
|
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 px-4 py-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 px-4 py-2">
|
||||||
<Skeleton className="h-4 w-24 bg-muted" />
|
<Skeleton className="h-4 w-24 bg-muted" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -247,10 +247,10 @@ const SkeletonTable = () => (
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{[...Array(8)].map((_, i) => (
|
{[...Array(8)].map((_, i) => (
|
||||||
<TableRow key={i} className="dark:border-gray-800">
|
<TableRow key={i} className="dark:border-gray-800">
|
||||||
<TableCell>
|
<TableCell className="py-2.5">
|
||||||
<Skeleton className="h-4 w-48 bg-muted" />
|
<Skeleton className="h-4 w-48 bg-muted" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right py-2.5">
|
||||||
<Skeleton className="h-4 w-12 ml-auto bg-muted" />
|
<Skeleton className="h-4 w-12 ml-auto bg-muted" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -438,14 +438,14 @@ export const RealtimeAnalytics = () => {
|
|||||||
<CardHeader className="p-6 pb-2">
|
<CardHeader className="p-6 pb-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
<Skeleton className="h-6 w-48 bg-muted" />
|
Real-Time Analytics
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Skeleton className="h-4 w-32 bg-muted" />
|
<Skeleton className="h-4 w-32 bg-muted" />
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="p-6 pt-0">
|
<CardContent className="p-6 pt-0">
|
||||||
<div className="grid grid-cols-2 gap-4 mt-1 mb-3">
|
<div className="grid grid-cols-2 gap-2 md:gap-3 mt-1 mb-3">
|
||||||
<SkeletonSummaryCard />
|
<SkeletonSummaryCard />
|
||||||
<SkeletonSummaryCard />
|
<SkeletonSummaryCard />
|
||||||
</div>
|
</div>
|
||||||
@@ -453,7 +453,7 @@ export const RealtimeAnalytics = () => {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{[...Array(3)].map((_, i) => (
|
{[...Array(3)].map((_, i) => (
|
||||||
<Skeleton key={i} className="h-8 w-20 bg-muted" />
|
<Skeleton key={i} className="h-8 w-20 bg-muted rounded-md" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<SkeletonBarChart />
|
<SkeletonBarChart />
|
||||||
|
|||||||
@@ -502,7 +502,7 @@ SummaryStats.displayName = "SummaryStats";
|
|||||||
|
|
||||||
// Add these skeleton components near the top of the file
|
// Add these skeleton components near the top of the file
|
||||||
const SkeletonChart = () => (
|
const SkeletonChart = () => (
|
||||||
<div className="h-[400px] w-full bg-card rounded-lg p-4">
|
<div className="h-[400px] w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm rounded-lg p-6">
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
{/* Grid lines */}
|
{/* Grid lines */}
|
||||||
@@ -516,13 +516,13 @@ const SkeletonChart = () => (
|
|||||||
{/* Y-axis labels */}
|
{/* Y-axis labels */}
|
||||||
<div className="absolute left-0 top-0 bottom-0 w-16 flex flex-col justify-between py-4">
|
<div className="absolute left-0 top-0 bottom-0 w-16 flex flex-col justify-between py-4">
|
||||||
{[...Array(6)].map((_, i) => (
|
{[...Array(6)].map((_, i) => (
|
||||||
<Skeleton key={i} className="h-4 w-12 bg-muted" />
|
<Skeleton key={i} className="h-4 w-12 bg-muted rounded-sm" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{/* X-axis labels */}
|
{/* X-axis labels */}
|
||||||
<div className="absolute left-16 right-0 bottom-0 flex justify-between px-4">
|
<div className="absolute left-16 right-0 bottom-0 flex justify-between px-4">
|
||||||
{[...Array(7)].map((_, i) => (
|
{[...Array(7)].map((_, i) => (
|
||||||
<Skeleton key={i} className="h-4 w-12 bg-muted" />
|
<Skeleton key={i} className="h-4 w-12 bg-muted rounded-sm" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{/* Chart line */}
|
{/* Chart line */}
|
||||||
@@ -540,16 +540,16 @@ const SkeletonChart = () => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const SkeletonStats = () => (
|
const SkeletonStats = () => (
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 py-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-2 md:gap-3">
|
||||||
{[...Array(4)].map((_, i) => (
|
{[...Array(4)].map((_, i) => (
|
||||||
<Card key={i}>
|
<Card key={i} className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-2">
|
||||||
<Skeleton className="h-4 w-24 bg-muted" />
|
<Skeleton className="h-4 w-24 bg-muted rounded-sm" />
|
||||||
<Skeleton className="h-4 w-16 bg-muted" />
|
<Skeleton className="h-4 w-8 bg-muted rounded-sm" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-4 pt-0">
|
<CardContent className="p-4 pt-0">
|
||||||
<Skeleton className="h-7 w-28 mb-1 bg-muted" />
|
<Skeleton className="h-7 w-32 bg-muted rounded-sm mb-1" />
|
||||||
<Skeleton className="h-4 w-20 bg-muted" />
|
<Skeleton className="h-4 w-24 bg-muted rounded-sm" />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
@@ -557,49 +557,15 @@ const SkeletonStats = () => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const SkeletonTable = () => (
|
const SkeletonTable = () => (
|
||||||
<div className="rounded-lg border bg-card">
|
<div className="rounded-lg border bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<Table>
|
<div className="p-4 space-y-4">
|
||||||
<TableHeader>
|
{[...Array(7)].map((_, i) => (
|
||||||
<TableRow className="hover:bg-transparent">
|
<div key={i} className="flex justify-between items-center">
|
||||||
<TableHead className="w-[120px]">
|
<Skeleton className="h-4 w-24 bg-muted rounded-sm" />
|
||||||
<Skeleton className="h-4 w-16 bg-muted" />
|
<Skeleton className="h-4 w-16 bg-muted rounded-sm" />
|
||||||
</TableHead>
|
</div>
|
||||||
<TableHead className="text-center">
|
))}
|
||||||
<Skeleton className="h-4 w-20 mx-auto bg-muted" />
|
</div>
|
||||||
</TableHead>
|
|
||||||
<TableHead className="text-center">
|
|
||||||
<Skeleton className="h-4 w-24 mx-auto bg-muted" />
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className="text-center">
|
|
||||||
<Skeleton className="h-4 w-16 mx-auto bg-muted" />
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className="text-center">
|
|
||||||
<Skeleton className="h-4 w-24 mx-auto bg-muted" />
|
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{[...Array(10)].map((_, i) => (
|
|
||||||
<TableRow key={i} className="hover:bg-muted/50">
|
|
||||||
<TableCell>
|
|
||||||
<Skeleton className="h-4 w-20 bg-muted" />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-center">
|
|
||||||
<Skeleton className="h-4 w-12 mx-auto bg-muted" />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-center">
|
|
||||||
<Skeleton className="h-4 w-20 mx-auto bg-muted" />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-center">
|
|
||||||
<Skeleton className="h-4 w-16 mx-auto bg-muted" />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-center">
|
|
||||||
<Skeleton className="h-4 w-20 mx-auto bg-muted" />
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -724,9 +690,9 @@ const SalesChart = ({
|
|||||||
Details
|
Details
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="min-w-[600px] max-w-[90vw] w-fit max-h-[85vh] overflow-hidden flex flex-col">
|
<DialogContent className="min-w-[600px] max-w-[90vw] w-fit max-h-[85vh] overflow-hidden flex flex-col bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<DialogHeader className="flex-none">
|
<DialogHeader className="flex-none">
|
||||||
<DialogTitle>Daily Details</DialogTitle>
|
<DialogTitle className="text-gray-900 dark:text-gray-100">Daily Details</DialogTitle>
|
||||||
<div className="flex items-center justify-center gap-2 pt-4">
|
<div className="flex items-center justify-center gap-2 pt-4">
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
<Button
|
<Button
|
||||||
@@ -796,7 +762,7 @@ const SalesChart = ({
|
|||||||
</div>
|
</div>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex-1 overflow-y-auto mt-6">
|
<div className="flex-1 overflow-y-auto mt-6">
|
||||||
<div className="rounded-lg border bg-card w-full">
|
<div className="rounded-lg border bg-white dark:bg-gray-900/60 backdrop-blur-sm w-full">
|
||||||
<Table className="w-full">
|
<Table className="w-full">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -984,12 +950,12 @@ const SalesChart = ({
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<SkeletonChart />
|
<SkeletonChart />
|
||||||
<div className="mt-4 flex justify-end">
|
<div className="mt-4 flex justify-end">
|
||||||
<Skeleton className="h-9 w-24" />
|
<Skeleton className="h-9 w-24 bg-muted rounded-sm" />
|
||||||
</div>
|
</div>
|
||||||
{showDailyTable && <SkeletonTable />}
|
{showDailyTable && <SkeletonTable />}
|
||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive" className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
<AlertTitle>Error</AlertTitle>
|
<AlertTitle>Error</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
@@ -1000,15 +966,15 @@ const SalesChart = ({
|
|||||||
<div className="flex items-center justify-center h-[400px] text-muted-foreground">
|
<div className="flex items-center justify-center h-[400px] text-muted-foreground">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<TrendingUp className="h-12 w-12 mx-auto mb-4" />
|
<TrendingUp className="h-12 w-12 mx-auto mb-4" />
|
||||||
<div className="font-medium mb-2">No sales data available</div>
|
<div className="font-medium mb-2 text-gray-900 dark:text-gray-100">No sales data available</div>
|
||||||
<div className="text-sm">
|
<div className="text-sm text-muted-foreground">
|
||||||
Try selecting a different time range
|
Try selecting a different time range
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="h-[400px] mt-4 bg-card rounded-lg p-0 relative">
|
<div className="h-[400px] mt-4 bg-white dark:bg-gray-900/60 backdrop-blur-sm rounded-lg p-0 relative">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<LineChart
|
<LineChart
|
||||||
data={data}
|
data={data}
|
||||||
@@ -1021,20 +987,20 @@ const SalesChart = ({
|
|||||||
<XAxis
|
<XAxis
|
||||||
dataKey="timestamp"
|
dataKey="timestamp"
|
||||||
tickFormatter={formatXAxis}
|
tickFormatter={formatXAxis}
|
||||||
className="text-xs"
|
className="text-xs text-muted-foreground"
|
||||||
tick={{ fill: "currentColor" }}
|
tick={{ fill: "currentColor" }}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
yAxisId="revenue"
|
yAxisId="revenue"
|
||||||
tickFormatter={(value) => formatCurrency(value, false)}
|
tickFormatter={(value) => formatCurrency(value, false)}
|
||||||
className="text-xs"
|
className="text-xs text-muted-foreground"
|
||||||
tick={{ fill: "currentColor" }}
|
tick={{ fill: "currentColor" }}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
yAxisId="orders"
|
yAxisId="orders"
|
||||||
orientation="right"
|
orientation="right"
|
||||||
tickFormatter={(value) => value.toLocaleString()}
|
tickFormatter={(value) => value.toLocaleString()}
|
||||||
className="text-xs"
|
className="text-xs text-muted-foreground"
|
||||||
tick={{ fill: "currentColor" }}
|
tick={{ fill: "currentColor" }}
|
||||||
/>
|
/>
|
||||||
<Tooltip content={<CustomTooltip />} />
|
<Tooltip content={<CustomTooltip />} />
|
||||||
|
|||||||
@@ -981,7 +981,7 @@ const StatCard = ({
|
|||||||
progress,
|
progress,
|
||||||
}) => (
|
}) => (
|
||||||
<Card
|
<Card
|
||||||
className={`${className} ${
|
className={`${className} bg-white dark:bg-gray-900/60 backdrop-blur-sm ${
|
||||||
onClick || onDetailsClick
|
onClick || onDetailsClick
|
||||||
? "cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
|
? "cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
|
||||||
: ""
|
: ""
|
||||||
@@ -1005,8 +1005,8 @@ const StatCard = ({
|
|||||||
<CardContent className="p-4 pt-0">
|
<CardContent className="p-4 pt-0">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<Skeleton className="h-8 w-32 mb-2" />
|
<Skeleton className="h-8 w-32 mb-2 bg-muted rounded-sm" />
|
||||||
<Skeleton className="h-4 w-24" />
|
<Skeleton className="h-4 w-24 bg-muted rounded-sm" />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -1094,7 +1094,7 @@ const useDebouncedEffect = (effect, deps, delay) => {
|
|||||||
|
|
||||||
// Add these skeleton components near the top of the file
|
// Add these skeleton components near the top of the file
|
||||||
const SkeletonCard = () => (
|
const SkeletonCard = () => (
|
||||||
<Card className="relative">
|
<Card className="relative bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Skeleton className="h-4 w-24 bg-muted" />
|
<Skeleton className="h-4 w-24 bg-muted" />
|
||||||
@@ -1107,7 +1107,7 @@ const SkeletonCard = () => (
|
|||||||
<Skeleton className="h-8 w-24 bg-muted" />
|
<Skeleton className="h-8 w-24 bg-muted" />
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Skeleton className="h-4 w-32 bg-muted" />
|
<Skeleton className="h-4 w-32 bg-muted" />
|
||||||
<Skeleton className="h-4 w-16 bg-muted" />
|
<Skeleton className="h-4 w-16 bg-muted rounded-md" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -1115,7 +1115,7 @@ const SkeletonCard = () => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const SkeletonChart = ({ type = "line" }) => (
|
const SkeletonChart = ({ type = "line" }) => (
|
||||||
<div className="h-[400px] w-full bg-card rounded-lg p-4">
|
<div className="h-[400px] w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm rounded-lg p-6">
|
||||||
<div className="h-full relative">
|
<div className="h-full relative">
|
||||||
{/* Grid lines */}
|
{/* Grid lines */}
|
||||||
{[...Array(5)].map((_, i) => (
|
{[...Array(5)].map((_, i) => (
|
||||||
@@ -1128,21 +1128,21 @@ const SkeletonChart = ({ type = "line" }) => (
|
|||||||
{/* Y-axis labels */}
|
{/* Y-axis labels */}
|
||||||
<div className="absolute left-0 top-0 bottom-0 w-8 flex flex-col justify-between py-4">
|
<div className="absolute left-0 top-0 bottom-0 w-8 flex flex-col justify-between py-4">
|
||||||
{[...Array(5)].map((_, i) => (
|
{[...Array(5)].map((_, i) => (
|
||||||
<Skeleton key={i} className="h-3 w-6 bg-muted" />
|
<Skeleton key={i} className="h-3 w-6 bg-muted rounded-sm" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{/* X-axis labels */}
|
{/* X-axis labels */}
|
||||||
<div className="absolute left-8 right-4 bottom-0 flex justify-between">
|
<div className="absolute left-8 right-4 bottom-0 flex justify-between">
|
||||||
{[...Array(6)].map((_, i) => (
|
{[...Array(6)].map((_, i) => (
|
||||||
<Skeleton key={i} className="h-3 w-8 bg-muted" />
|
<Skeleton key={i} className="h-3 w-8 bg-muted rounded-sm" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{type === "bar" ? (
|
{type === "bar" ? (
|
||||||
<div className="absolute inset-x-8 bottom-6 top-4 flex items-end justify-between">
|
<div className="absolute inset-x-8 bottom-6 top-4 flex items-end justify-between gap-1">
|
||||||
{[...Array(24)].map((_, i) => (
|
{[...Array(24)].map((_, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="w-2 bg-muted"
|
className="w-2 bg-muted rounded-sm"
|
||||||
style={{ height: `${Math.random() * 80 + 10}%` }}
|
style={{ height: `${Math.random() * 80 + 10}%` }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -1151,7 +1151,7 @@ const SkeletonChart = ({ type = "line" }) => (
|
|||||||
<div className="absolute inset-x-8 bottom-6 top-4">
|
<div className="absolute inset-x-8 bottom-6 top-4">
|
||||||
<div className="h-full w-full relative">
|
<div className="h-full w-full relative">
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-muted"
|
className="absolute inset-0 bg-muted rounded-sm"
|
||||||
style={{
|
style={{
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
clipPath: "polygon(0 50%, 100% 20%, 100% 100%, 0 100%)",
|
clipPath: "polygon(0 50%, 100% 20%, 100% 100%, 0 100%)",
|
||||||
@@ -1165,18 +1165,18 @@ const SkeletonChart = ({ type = "line" }) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const SkeletonTable = ({ rows = 8 }) => (
|
const SkeletonTable = ({ rows = 8 }) => (
|
||||||
<div className="rounded-lg border bg-card">
|
<div className="rounded-lg border bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="dark:border-gray-800">
|
<TableRow className="dark:border-gray-800">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<Skeleton className="h-4 w-32 bg-muted" />
|
<Skeleton className="h-4 w-32 bg-muted rounded-sm" />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="text-right">
|
<TableHead className="text-right">
|
||||||
<Skeleton className="h-4 w-24 ml-auto bg-muted" />
|
<Skeleton className="h-4 w-24 ml-auto bg-muted rounded-sm" />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="text-right">
|
<TableHead className="text-right">
|
||||||
<Skeleton className="h-4 w-24 ml-auto bg-muted" />
|
<Skeleton className="h-4 w-24 ml-auto bg-muted rounded-sm" />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -1184,13 +1184,13 @@ const SkeletonTable = ({ rows = 8 }) => (
|
|||||||
{[...Array(rows)].map((_, i) => (
|
{[...Array(rows)].map((_, i) => (
|
||||||
<TableRow key={i} className="dark:border-gray-800">
|
<TableRow key={i} className="dark:border-gray-800">
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Skeleton className="h-4 w-48 bg-muted" />
|
<Skeleton className="h-4 w-48 bg-muted rounded-sm" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<Skeleton className="h-4 w-16 ml-auto bg-muted" />
|
<Skeleton className="h-4 w-16 ml-auto bg-muted rounded-sm" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<Skeleton className="h-4 w-16 ml-auto bg-muted" />
|
<Skeleton className="h-4 w-16 ml-auto bg-muted rounded-sm" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
@@ -1731,7 +1731,7 @@ const StatCards = ({
|
|||||||
if (loading && !stats) {
|
if (loading && !stats) {
|
||||||
return (
|
return (
|
||||||
<Card className="w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
<Card className="w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
<CardHeader className="p-6">
|
<CardHeader className="p-6 pb-2">
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div>
|
<div>
|
||||||
@@ -1739,19 +1739,19 @@ const StatCards = ({
|
|||||||
{title}
|
{title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
{description && (
|
{description && (
|
||||||
<CardDescription className="mt-1">
|
<CardDescription className="mt-1 text-muted-foreground">
|
||||||
{description}
|
{description}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Skeleton className="h-4 w-32 bg-muted" />
|
<Skeleton className="h-4 w-32 bg-muted rounded-sm" />
|
||||||
<Skeleton className="h-9 w-[130px] bg-muted rounded-md" />
|
<Skeleton className="h-9 w-[130px] bg-muted rounded-md" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-6 pt-0">
|
<CardContent className="p-6 pt-0 space-y-4">
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-3 2xl:grid-cols-4 gap-2 md:gap-3">
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-3 2xl:grid-cols-4 gap-2 md:gap-3">
|
||||||
{[...Array(12)].map((_, i) => (
|
{[...Array(12)].map((_, i) => (
|
||||||
<SkeletonCard key={i} />
|
<SkeletonCard key={i} />
|
||||||
@@ -1773,7 +1773,7 @@ const StatCards = ({
|
|||||||
{title}
|
{title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
{description && (
|
{description && (
|
||||||
<CardDescription className="mt-1">
|
<CardDescription className="mt-1 text-muted-foreground">
|
||||||
{description}
|
{description}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -33,19 +33,19 @@ const SkeletonTable = ({ rows = 12 }) => (
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="dark:border-gray-800">
|
<TableRow className="dark:border-gray-800">
|
||||||
<TableHead><Skeleton className="h-4 w-48 dark:bg-gray-700" /></TableHead>
|
<TableHead><Skeleton className="h-4 w-48 bg-muted rounded-sm" /></TableHead>
|
||||||
<TableHead className="text-right"><Skeleton className="h-4 w-16 ml-auto dark:bg-gray-700" /></TableHead>
|
<TableHead className="text-right"><Skeleton className="h-4 w-16 ml-auto bg-muted rounded-sm" /></TableHead>
|
||||||
<TableHead className="text-right"><Skeleton className="h-4 w-16 ml-auto dark:bg-gray-700" /></TableHead>
|
<TableHead className="text-right"><Skeleton className="h-4 w-16 ml-auto bg-muted rounded-sm" /></TableHead>
|
||||||
<TableHead className="text-right"><Skeleton className="h-4 w-16 ml-auto dark:bg-gray-700" /></TableHead>
|
<TableHead className="text-right"><Skeleton className="h-4 w-16 ml-auto bg-muted rounded-sm" /></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{[...Array(rows)].map((_, i) => (
|
{[...Array(rows)].map((_, i) => (
|
||||||
<TableRow key={i} className="dark:border-gray-800">
|
<TableRow key={i} className="dark:border-gray-800">
|
||||||
<TableCell className="py-3"><Skeleton className="h-4 w-64 dark:bg-gray-700" /></TableCell>
|
<TableCell className="py-3"><Skeleton className="h-4 w-64 bg-muted rounded-sm" /></TableCell>
|
||||||
<TableCell className="text-right py-3"><Skeleton className="h-4 w-12 ml-auto dark:bg-gray-700" /></TableCell>
|
<TableCell className="text-right py-3"><Skeleton className="h-4 w-12 ml-auto bg-muted rounded-sm" /></TableCell>
|
||||||
<TableCell className="text-right py-3"><Skeleton className="h-4 w-12 ml-auto dark:bg-gray-700" /></TableCell>
|
<TableCell className="text-right py-3"><Skeleton className="h-4 w-12 ml-auto bg-muted rounded-sm" /></TableCell>
|
||||||
<TableCell className="text-right py-3"><Skeleton className="h-4 w-16 ml-auto dark:bg-gray-700" /></TableCell>
|
<TableCell className="text-right py-3"><Skeleton className="h-4 w-16 ml-auto bg-muted rounded-sm" /></TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@@ -56,13 +56,13 @@ const SkeletonTable = ({ rows = 12 }) => (
|
|||||||
const SkeletonPieChart = () => (
|
const SkeletonPieChart = () => (
|
||||||
<div className="h-60 relative">
|
<div className="h-60 relative">
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<div className="w-40 h-40 rounded-full bg-gray-100 dark:bg-gray-800 animate-pulse" />
|
<div className="w-40 h-40 rounded-full bg-muted animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 flex gap-4">
|
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 flex gap-4">
|
||||||
{[...Array(3)].map((_, i) => (
|
{[...Array(3)].map((_, i) => (
|
||||||
<div key={i} className="flex items-center gap-2">
|
<div key={i} className="flex items-center gap-2">
|
||||||
<Skeleton className="h-3 w-3 rounded-full dark:bg-gray-700" />
|
<Skeleton className="h-3 w-3 rounded-full bg-muted" />
|
||||||
<Skeleton className="h-4 w-16 dark:bg-gray-700" />
|
<Skeleton className="h-4 w-16 bg-muted rounded-sm" />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -73,7 +73,7 @@ const SkeletonTabs = () => (
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex gap-2 mb-4">
|
<div className="flex gap-2 mb-4">
|
||||||
{[...Array(3)].map((_, i) => (
|
{[...Array(3)].map((_, i) => (
|
||||||
<Skeleton key={i} className="h-8 w-24 dark:bg-gray-700" />
|
<Skeleton key={i} className="h-8 w-24 bg-muted rounded-sm" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -194,16 +194,16 @@ export const UserBehaviorDashboard = () => {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Card className="bg-white dark:bg-gray-900 h-full">
|
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm h-full">
|
||||||
<CardHeader>
|
<CardHeader className="p-6 pb-4">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
User Behavior Analysis
|
User Behavior Analysis
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Skeleton className="h-9 w-36 dark:bg-gray-700" />
|
<Skeleton className="h-9 w-36 bg-muted rounded-sm" />
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="p-6 pt-0">
|
||||||
<Tabs defaultValue="pages" className="w-full">
|
<Tabs defaultValue="pages" className="w-full">
|
||||||
<TabsList className="mb-4">
|
<TabsList className="mb-4">
|
||||||
<TabsTrigger value="pages" disabled>Top Pages</TabsTrigger>
|
<TabsTrigger value="pages" disabled>Top Pages</TabsTrigger>
|
||||||
@@ -245,21 +245,21 @@ export const UserBehaviorDashboard = () => {
|
|||||||
if (active && payload && payload.length) {
|
if (active && payload && payload.length) {
|
||||||
const data = payload[0].payload;
|
const data = payload[0].payload;
|
||||||
const percentage = ((data.pageViews / totalViews) * 100).toFixed(1);
|
const percentage = ((data.pageViews / totalViews) * 100).toFixed(1);
|
||||||
const sessionPercentage = ((data.sessions / totalSessions) * 100).toFixed(
|
const sessionPercentage = ((data.sessions / totalSessions) * 100).toFixed(1);
|
||||||
1
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-lg shadow-lg p-3">
|
<Card className="p-3 shadow-lg bg-white dark:bg-gray-900/60 backdrop-blur-sm border border-border">
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
<CardContent className="p-0 space-y-2">
|
||||||
{data.device}
|
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
</p>
|
{data.device}
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
</p>
|
||||||
{data.pageViews.toLocaleString()} views ({percentage}%)
|
<p className="text-sm text-muted-foreground">
|
||||||
</p>
|
{data.pageViews.toLocaleString()} views ({percentage}%)
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
</p>
|
||||||
{data.sessions.toLocaleString()} sessions ({sessionPercentage}%)
|
<p className="text-sm text-muted-foreground">
|
||||||
</p>
|
{data.sessions.toLocaleString()} sessions ({sessionPercentage}%)
|
||||||
</div>
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -272,14 +272,14 @@ export const UserBehaviorDashboard = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-white dark:bg-gray-900 h-full">
|
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm h-full">
|
||||||
<CardHeader>
|
<CardHeader className="p-6 pb-4">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
User Behavior Analysis
|
User Behavior Analysis
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Select value={timeRange} onValueChange={setTimeRange}>
|
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||||
<SelectTrigger className="w-36 bg-white dark:bg-gray-800">
|
<SelectTrigger className="w-36 h-9">
|
||||||
<SelectValue>
|
<SelectValue>
|
||||||
{timeRange === "7" && "Last 7 days"}
|
{timeRange === "7" && "Last 7 days"}
|
||||||
{timeRange === "14" && "Last 14 days"}
|
{timeRange === "14" && "Last 14 days"}
|
||||||
@@ -296,7 +296,7 @@ export const UserBehaviorDashboard = () => {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="p-6 pt-0">
|
||||||
<Tabs defaultValue="pages" className="w-full">
|
<Tabs defaultValue="pages" className="w-full">
|
||||||
<TabsList className="mb-4">
|
<TabsList className="mb-4">
|
||||||
<TabsTrigger value="pages">Top Pages</TabsTrigger>
|
<TabsTrigger value="pages">Top Pages</TabsTrigger>
|
||||||
@@ -311,33 +311,25 @@ export const UserBehaviorDashboard = () => {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="dark:border-gray-800">
|
<TableRow className="dark:border-gray-800">
|
||||||
<TableHead className="text-gray-900 dark:text-gray-100">
|
<TableHead className="text-foreground">Page Path</TableHead>
|
||||||
Page Path
|
<TableHead className="text-right text-foreground">Views</TableHead>
|
||||||
</TableHead>
|
<TableHead className="text-right text-foreground">Bounce Rate</TableHead>
|
||||||
<TableHead className="text-right text-gray-900 dark:text-gray-100">
|
<TableHead className="text-right text-foreground">Avg. Duration</TableHead>
|
||||||
Views
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className="text-right text-gray-900 dark:text-gray-100">
|
|
||||||
Bounce Rate
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className="text-right text-gray-900 dark:text-gray-100">
|
|
||||||
Avg. Duration
|
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data?.data?.pageData?.pageData.map((page, index) => (
|
{data?.data?.pageData?.pageData.map((page, index) => (
|
||||||
<TableRow key={index} className="dark:border-gray-800">
|
<TableRow key={index} className="dark:border-gray-800">
|
||||||
<TableCell className="font-medium text-gray-900 dark:text-gray-100">
|
<TableCell className="font-medium text-foreground">
|
||||||
{page.path}
|
{page.path}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right text-gray-600 dark:text-gray-300">
|
<TableCell className="text-right text-muted-foreground">
|
||||||
{page.pageViews.toLocaleString()}
|
{page.pageViews.toLocaleString()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right text-gray-600 dark:text-gray-300">
|
<TableCell className="text-right text-muted-foreground">
|
||||||
{page.bounceRate.toFixed(1)}%
|
{page.bounceRate.toFixed(1)}%
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right text-gray-600 dark:text-gray-300">
|
<TableCell className="text-right text-muted-foreground">
|
||||||
{formatDuration(page.avgSessionDuration)}
|
{formatDuration(page.avgSessionDuration)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -353,33 +345,25 @@ export const UserBehaviorDashboard = () => {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="dark:border-gray-800">
|
<TableRow className="dark:border-gray-800">
|
||||||
<TableHead className="text-gray-900 dark:text-gray-100">
|
<TableHead className="text-foreground w-[35%] min-w-[120px]">Source</TableHead>
|
||||||
Source
|
<TableHead className="text-right text-foreground w-[20%] min-w-[80px]">Sessions</TableHead>
|
||||||
</TableHead>
|
<TableHead className="text-right text-foreground w-[20%] min-w-[80px]">Conv.</TableHead>
|
||||||
<TableHead className="text-right text-gray-900 dark:text-gray-100">
|
<TableHead className="text-right text-foreground w-[25%] min-w-[80px]">Conv. Rate</TableHead>
|
||||||
Sessions
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className="text-right text-gray-900 dark:text-gray-100">
|
|
||||||
Conversions
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className="text-right text-gray-900 dark:text-gray-100">
|
|
||||||
Conv. Rate
|
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data?.data?.sourceData?.map((source, index) => (
|
{data?.data?.sourceData?.map((source, index) => (
|
||||||
<TableRow key={index} className="dark:border-gray-800">
|
<TableRow key={index} className="dark:border-gray-800">
|
||||||
<TableCell className="font-medium text-gray-900 dark:text-gray-100">
|
<TableCell className="font-medium text-foreground break-words max-w-[160px]">
|
||||||
{source.source}
|
{source.source}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right text-gray-600 dark:text-gray-300">
|
<TableCell className="text-right text-muted-foreground whitespace-nowrap">
|
||||||
{source.sessions.toLocaleString()}
|
{source.sessions.toLocaleString()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right text-gray-600 dark:text-gray-300">
|
<TableCell className="text-right text-muted-foreground whitespace-nowrap">
|
||||||
{source.conversions.toLocaleString()}
|
{source.conversions.toLocaleString()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right text-gray-600 dark:text-gray-300">
|
<TableCell className="text-right text-muted-foreground whitespace-nowrap">
|
||||||
{((source.conversions / source.sessions) * 100).toFixed(1)}%
|
{((source.conversions / source.sessions) * 100).toFixed(1)}%
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -392,7 +376,7 @@ export const UserBehaviorDashboard = () => {
|
|||||||
value="devices"
|
value="devices"
|
||||||
className="mt-4 space-y-2 h-full max-h-[540px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-200 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent hover:scrollbar-thumb-gray-300 dark:hover:scrollbar-thumb-gray-600 pr-2"
|
className="mt-4 space-y-2 h-full max-h-[540px] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-200 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent hover:scrollbar-thumb-gray-300 dark:hover:scrollbar-thumb-gray-600 pr-2"
|
||||||
>
|
>
|
||||||
<div className="h-60">
|
<div className="h-60 bg-white dark:bg-gray-900/60 backdrop-blur-sm rounded-lg p-4">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<PieChart>
|
<PieChart>
|
||||||
<Pie
|
<Pie
|
||||||
|
|||||||
Reference in New Issue
Block a user