// src/features/charts/PriceHistoryChart.tsx import React, { useState, useEffect, useMemo } from 'react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, } from 'recharts'; import * as apiClient from '../../services/apiClient'; import { LoadingSpinner } from '../../components/LoadingSpinner'; // This path is correct import { useUserData } from '../../hooks/useUserData'; import type { HistoricalPriceDataPoint } from '../../types'; type HistoricalData = Record; type ChartData = { date: string; [itemName: string]: number | string }; const COLORS = ['#10B981', '#3B82F6', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899']; export const PriceHistoryChart: React.FC = () => { const { watchedItems, isLoading: isLoadingUserData } = useUserData(); const [historicalData, setHistoricalData] = useState({}); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const watchedItemsMap = useMemo( () => new Map(watchedItems.map((item) => [item.master_grocery_item_id, item.name])), [watchedItems], ); useEffect(() => { if (watchedItems.length === 0) { setIsLoading(false); setHistoricalData({}); // Clear data if watchlist becomes empty return; } const fetchData = async () => { setIsLoading(true); setError(null); try { const watchedItemIds = watchedItems .map((item) => item.master_grocery_item_id) .filter((id): id is number => id !== undefined); // Ensure only numbers are passed const response = await apiClient.fetchHistoricalPriceData(watchedItemIds); const rawData: HistoricalPriceDataPoint[] = await response.json(); if (rawData.length === 0) { setHistoricalData({}); return; } const processedData = rawData.reduce( (acc, record: HistoricalPriceDataPoint) => { if ( !record.master_item_id || record.avg_price_in_cents === null || !record.summary_date ) return acc; const itemName = watchedItemsMap.get(record.master_item_id); if (!itemName) return acc; const priceInCents = record.avg_price_in_cents; const date = new Date(`${record.summary_date}T00:00:00`).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) { // This is a type-safe way to handle errors. We check if the caught // object is an instance of Error before accessing its message property. setError(e instanceof Error ? 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 || isLoadingUserData) { 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 ( `$${(value / 100).toFixed(2)}`} domain={[(a: number) => Math.floor(a * 0.95), (b: number) => Math.ceil(b * 1.05)]} /> { if (typeof value === 'number') { return [`$${(value / 100).toFixed(2)}`]; } return [null]; }} /> {availableItems.map((item, index) => ( ))} ); }; return (

Historical Price Trends

{renderContent()}
); };