import React, { useState, useEffect, useMemo } from 'react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; import { loadAllHistoricalItems } from '../services/supabaseClient'; import { LoadingSpinner } from './LoadingSpinner'; import type { MasterGroceryItem, FlyerItem } from '../types'; type HistoricalData = Record; // price is in cents type ChartData = { date: string; [itemName: string]: number | string }; const COLORS = ['#10B981', '#3B82F6', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899']; interface PriceHistoryChartProps { watchedItems: MasterGroceryItem[]; } export const PriceHistoryChart: React.FC = ({ watchedItems }) => { const [historicalData, setHistoricalData] = useState({}); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const watchedItemsMap = useMemo(() => new Map(watchedItems.map(item => [item.id, item.name])), [watchedItems]); useEffect(() => { if (watchedItems.length === 0) { setIsLoading(false); setHistoricalData({}); return; } const fetchData = async () => { setIsLoading(true); setError(null); try { const rawData: Pick[] = await loadAllHistoricalItems(watchedItems); if (rawData.length === 0) { setHistoricalData({}); return; } const processedData = rawData.reduce((acc, record) => { if (!record.master_item_id || record.price_in_cents === null || !record.created_at) return acc; const itemName = watchedItemsMap.get(record.master_item_id); if (!itemName) return acc; const priceInCents = record.price_in_cents; const date = new Date(record.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); if(priceInCents === 0) return acc; if (!acc[itemName]) { acc[itemName] = []; } // Ensure we only store the LOWEST price for a given day const existingEntryIndex = acc[itemName].findIndex(entry => entry.date === date); if (existingEntryIndex > -1) { if (priceInCents < acc[itemName][existingEntryIndex].price) { acc[itemName][existingEntryIndex].price = priceInCents; } } else { acc[itemName].push({ date, price: priceInCents }); } return acc; }, {}); // Filter out items that only have one data point for a meaningful trend line const filteredData = Object.entries(processedData).reduce((acc, [key, value]) => { if(value.length > 1){ acc[key] = value.sort((a,b) => new Date(a.date).getTime() - new Date(b.date).getTime()); } return acc; }, {}); setHistoricalData(filteredData); } catch (e: any) { setError(e.message || 'Failed to load price history.'); } finally { setIsLoading(false); } }; fetchData(); }, [watchedItems, watchedItemsMap]); const chartData = useMemo(() => { const availableItems = Object.keys(historicalData); if (availableItems.length === 0) return []; const dateMap: Map = new Map(); availableItems.forEach(itemName => { historicalData[itemName]?.forEach(({ date, price }) => { if (!dateMap.has(date)) { dateMap.set(date, { date }); } // Store price in cents dateMap.get(date)![itemName] = price; }); }); return Array.from(dateMap.values()).sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); }, [historicalData]); const availableItems = Object.keys(historicalData); const renderContent = () => { if (isLoading) { return (
Loading Price History...
); } if (error) { return (

Error: {error}

); } if (watchedItems.length === 0) { return (

Add items to your watchlist to see their price trends over time.

); } if (availableItems.length === 0) { return (

Not enough historical data for your watched items. Process more flyers to build a trend.

); } return ( `$${(Number(value) / 100).toFixed(2)}`} domain={['dataMin', 'auto']} /> `$${(value / 100).toFixed(2)}`} /> {availableItems.map((item, index) => ( ))} ) } return (

Historical Price Trends

{renderContent()}
); };