Files
ember-market-frontend/components/analytics/ProfitAnalyticsChart.tsx
NotII 130ecac208 Add Chromebook compatibility fixes and optimizations
Implemented comprehensive Chromebook-specific fixes including viewport adjustments, enhanced touch and keyboard detection, improved scrolling and keyboard navigation hooks, and extensive CSS optimizations for better usability. Updated chat and dashboard interfaces for larger touch targets, better focus management, and responsive layouts. Added documentation in docs/CHROMEBOOK-FIXES.md and new hooks for Chromebook scroll and keyboard handling.
2025-10-26 18:29:23 +00:00

356 lines
12 KiB
TypeScript

"use client"
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Alert, AlertDescription } from "@/components/ui/alert";
import {
TrendingUp,
TrendingDown,
DollarSign,
PieChart,
Calculator,
Info,
AlertTriangle
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { formatGBP } from "@/utils/format";
import { getProfitOverview, type ProfitOverview } from "@/lib/services/profit-analytics-service";
import { Skeleton } from "@/components/ui/skeleton";
interface ProfitAnalyticsChartProps {
timeRange: string;
hideNumbers?: boolean;
}
export default function ProfitAnalyticsChart({ timeRange, hideNumbers = false }: ProfitAnalyticsChartProps) {
const [data, setData] = useState<ProfitOverview | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { toast } = useToast();
const maskValue = (value: string): string => {
if (!hideNumbers) return value;
if (value.includes('£')) return '£***';
if (value.match(/^\d/)) {
const numLength = value.replace(/[,\.%]/g, '').length;
return '*'.repeat(Math.min(numLength, 4));
}
return value;
};
useEffect(() => {
const fetchData = async () => {
try {
setIsLoading(true);
setError(null);
const response = await getProfitOverview(timeRange);
setData(response);
} catch (error) {
console.error('Error fetching profit data:', error);
setError('Failed to load profit analytics');
toast({
title: "Error",
description: "Failed to load profit analytics data.",
variant: "destructive",
});
} finally {
setIsLoading(false);
}
};
fetchData();
}, [timeRange, toast]);
if (isLoading) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Calculator className="h-5 w-5" />
Profit Analytics
</CardTitle>
<CardDescription>
Track your actual profits based on sales and cost data
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Summary Cards Skeleton */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<Card key={i}>
<CardHeader className="pb-2">
<Skeleton className="h-4 w-24" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-20 mb-2" />
<Skeleton className="h-3 w-16" />
</CardContent>
</Card>
))}
</div>
{/* Coverage Card Skeleton */}
<Card>
<CardHeader>
<Skeleton className="h-6 w-40" />
<Skeleton className="h-4 w-60" />
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
<Skeleton className="h-8 w-16" />
<div className="flex-1">
<Skeleton className="h-2 w-full rounded-full" />
</div>
<Skeleton className="h-6 w-16" />
</div>
</CardContent>
</Card>
{/* Products List Skeleton */}
<Card>
<CardHeader>
<Skeleton className="h-6 w-48" />
<Skeleton className="h-4 w-64" />
</CardHeader>
<CardContent>
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<div key={i} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-3">
<Skeleton className="h-8 w-8 rounded-full" />
<div>
<Skeleton className="h-4 w-32 mb-2" />
<Skeleton className="h-3 w-24" />
</div>
</div>
<div className="text-right">
<Skeleton className="h-4 w-20 mb-2" />
<Skeleton className="h-3 w-16" />
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
);
}
if (error || !data) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Calculator className="h-5 w-5" />
Profit Analytics
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-center py-8">
<AlertTriangle className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
<p className="text-muted-foreground">Failed to load profit data</p>
</div>
</CardContent>
</Card>
);
}
if (!data.hasCostData) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Calculator className="h-5 w-5" />
Profit Analytics
</CardTitle>
<CardDescription>
Track your actual profits based on sales and cost data
</CardDescription>
</CardHeader>
<CardContent>
<Alert>
<Info className="h-4 w-4" />
<AlertDescription>
<strong>No cost data available</strong><br />
Add cost prices to your products to see profit analytics. Go to Products Edit Cost & Profit Tracking section.
</AlertDescription>
</Alert>
</CardContent>
</Card>
);
}
const profitDirection = data.summary.totalProfit >= 0;
// Fallback for backwards compatibility
const revenueFromTracked = data.summary.revenueFromTrackedProducts || data.summary.totalRevenue || 0;
const totalRevenue = data.summary.totalRevenue || 0;
const totalCost = data.summary.totalCost || 0;
const totalProfit = data.summary.totalProfit || 0;
const productsWithCostData = data.summary.productsWithCostData || 0;
const totalProductsSold = data.summary.totalProductsSold || 0;
return (
<div className="space-y-6">
{/* Summary Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Revenue (Tracked)</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">
{maskValue(formatGBP(revenueFromTracked))}
</div>
<p className="text-xs text-muted-foreground">
From {productsWithCostData} tracked items
</p>
<p className="text-xs text-muted-foreground mt-1">
Total revenue: {maskValue(formatGBP(totalRevenue))}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Total Cost</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">
{maskValue(formatGBP(totalCost))}
</div>
<p className="text-xs text-muted-foreground">
From {productsWithCostData} tracked items
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Total Profit</CardTitle>
</CardHeader>
<CardContent>
<div className={`text-2xl font-bold flex items-center gap-2 ${
profitDirection ? 'text-green-600' : 'text-red-600'
}`}>
{profitDirection ? (
<TrendingUp className="h-5 w-5" />
) : (
<TrendingDown className="h-5 w-5" />
)}
{maskValue(formatGBP(totalProfit))}
</div>
<p className="text-xs text-muted-foreground">
{maskValue(`${(data.summary.overallProfitMargin || 0).toFixed(1)}%`)} margin
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Avg Profit/Unit</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-600">
{maskValue(formatGBP(data.summary.averageProfitPerUnit || 0))}
</div>
<p className="text-xs text-muted-foreground">
Per unit sold
</p>
</CardContent>
</Card>
</div>
{/* Cost Data Coverage */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<PieChart className="h-5 w-5" />
Cost Data Coverage
</CardTitle>
<CardDescription>
Percentage of sold items that have cost data for profit calculation
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
<div className="text-3xl font-bold">
{hideNumbers ? "**%" : `${(data.summary.costDataCoverage || 0).toFixed(1)}%`}
</div>
<div className="flex-1">
<div className="w-full bg-secondary rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all duration-300"
style={{ width: hideNumbers ? "0%" : `${data.summary.costDataCoverage || 0}%` }}
/>
</div>
</div>
<Badge variant="secondary">
{hideNumbers ? "** / **" : `${productsWithCostData} / ${totalProductsSold}`}
</Badge>
</div>
</CardContent>
</Card>
{/* Top Profitable Products */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<DollarSign className="h-5 w-5" />
Most Profitable Products
</CardTitle>
<CardDescription>
Products generating the highest total profit (last {timeRange} days)
</CardDescription>
</CardHeader>
<CardContent>
{data.topProfitableProducts.length === 0 ? (
<div className="text-center py-8">
<Calculator className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
<p className="text-muted-foreground">No profitable products data available</p>
</div>
) : (
<div className="space-y-4">
{data.topProfitableProducts.map((product, index) => {
const profitPositive = product.totalProfit >= 0;
return (
<div
key={product.productId}
className="flex items-center justify-between p-4 border rounded-lg"
>
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary font-semibold text-sm">
{index + 1}
</div>
<div>
<p className="font-medium">{product.productName}</p>
<p className="text-sm text-muted-foreground">
{product.totalQuantitySold} units sold
</p>
</div>
</div>
<div className="text-right">
<div className={`font-medium ${profitPositive ? 'text-green-600' : 'text-red-600'}`}>
{maskValue(formatGBP(product.totalProfit))}
</div>
<div className="text-sm text-muted-foreground">
{maskValue(`${product.profitMargin.toFixed(1)}%`)} margin
</div>
</div>
</div>
);
})}
</div>
)}
</CardContent>
</Card>
</div>
);
}