import { useCallback, useEffect, useRef, useState } from 'react'; import { useOutletContext } from 'react-router-dom'; import moment from 'moment'; // components import BarCharts from '../../common/charting/BarCharts'; import LineCharts from '../../common/charting/LineCharts'; // hooks import useDebounceLogic from '../useDebounceLogic'; import useLoadingMessages from '../useLoadingMessages'; // functions import apiLoader from '../../../services/utilities/apiLoader'; // constants const errorNumber = 367; const nowRunning = 'hooks/inventory/useSkuHistory.jsx'; function useSkuHistory({ sku, inStock }) { const [state, setState] = useState({ allItems: {}, // All items from the API filteredItems: {}, // Filtered items based on `inStock` averagesByYear: {}, // Averages by year for all items durations: [], chartLabel: '', average: 0, overallAverage: 0, tooltipContent: [], // Tooltip content for BarCharts }); const { handleError, level, setLoadingMessages } = useOutletContext(); const { addLoadingMessage, removeLoadingMessage } = useLoadingMessages(setLoadingMessages); const previousSkuRef = useRef(null); const noAccess = useCallback(() => null, []); // Calculate averages by year (independent of inStock filtering). const calculateAveragesByYear = useCallback(() => { const context = `${nowRunning}.calculateAveragesByYear`; try { const yearGroups = {}; Object.values(state.allItems).forEach((item) => { const stockedYear = moment(item.stocked).year(); const stockedDate = moment(item.stocked); let endDate; if (item.condition < 11) { endDate = moment(); // Still in stock. } else if (item.condition === 99 && item.orders.length > 0) { endDate = moment( Math.max(...item.orders.map((order) => new Date(order.orderTime).getTime())) ); // Sold. } else { endDate = moment(item.verified); // Otherwise, verified date. } const timeInStock = endDate.diff(stockedDate, 'days'); if (!yearGroups[stockedYear]) { yearGroups[stockedYear] = { totalDays: 0, itemCount: 0 }; } yearGroups[stockedYear].totalDays += timeInStock; yearGroups[stockedYear].itemCount += 1; }); const averagesByYear = Object.keys(yearGroups).reduce((acc, year) => { const { totalDays, itemCount } = yearGroups[year]; acc[year] = totalDays / itemCount; return acc; }, {}); setState((prevState) => ({ ...prevState, averagesByYear, })); } catch (error) { handleError(error, context, errorNumber); } }, [ handleError, state.allItems ]); // Filter items based on `inStock` and calculate durations. const filterItems = useCallback(() => { try { const filteredItems = Object.values(state.allItems).filter((item) => inStock ? item.condition < 11 : true ); const durations = []; let sum = 0; let count = 0; const tooltipContent = filteredItems.map((item) => { const days = item.duration || 0; const sunkCost = item.sunkCost || 0; // Default to 0 if `sunkCost` is not defined. durations.push({ key: `#${item.itemNumber}`, days }); sum += days; count += 1; return `Item ${item.itemNumber}, in stock ${days} day${days !== 1 ? 's' : ''}, Sunk Cost: $${sunkCost.toFixed(2)}`; }); const average = count > 0 ? +(sum / count).toFixed(0) : 0; const chartLabel = `days in stock (average: ${average})`; setState((prevState) => ({ ...prevState, filteredItems: filteredItems.reduce((acc, item) => { acc[item.itemId] = item; return acc; }, {}), durations, average, chartLabel, tooltipContent, })); } catch (error) { handleError(error, `${nowRunning}.filterItems`, errorNumber); } }, [ handleError, inStock, state.allItems ]); // Fetch data from the API. const runSearch = useCallback(async () => { const context = `${nowRunning}.runSearch`; const loadingMessage = 'Loading SKU history...'; try { if (!sku || sku.length < 5 || sku === previousSkuRef.current) { return; } previousSkuRef.current = sku; addLoadingMessage(loadingMessage); // Get SKU history data first. let api = 'analysis/inventory/sku-history'; let payload = { sku }; const { data } = await apiLoader({ api, payload }); const { failure, items, success } = data; if (!success) { handleError(failure, context, errorNumber, true); return; } if (Object.keys(items).length === 0) { return; } else { const api = 'analysis/cost_sources/unrecovered-costs'; const payload = {}; const { data } = await apiLoader({ api, payload }); const { failure, success, unrecoveredItems } = data; if (!success) { handleError(failure, context, errorNumber, true); return; } Object.entries(unrecoveredItems).forEach(([itemId, details]) => { if (items[itemId]) { items[itemId].sunkCost = details.cost; } }); setState((prevState) => ({ ...prevState, allItems: items, })); } } catch (error) { handleError(error, context, errorNumber); } finally { removeLoadingMessage(loadingMessage); } }, [ addLoadingMessage, handleError, removeLoadingMessage, sku ]); const debouncedRunSearch = useDebounceLogic(runSearch, 500); // Update charts and filtered items whenever state changes. useEffect(() => { calculateAveragesByYear(); }, [state.allItems, calculateAveragesByYear]); useEffect(() => { filterItems(); }, [inStock, state.allItems, filterItems]); useEffect(() => { if (sku && sku.length >= 5) { debouncedRunSearch(); } }, [sku, debouncedRunSearch]); if (level < 5) { return { debouncedRunSearch: noAccess, filteredItems: {}, renderItemsChart: noAccess, renderLineChart: noAccess }; } try { return { debouncedRunSearch, filteredItems: state.filteredItems, renderItemsChart: () => { const { chartLabel, durations, tooltipContent } = state; if (!durations.length) return null; return ( ); }, renderLineChart: () => { const dataset = Object.entries(state.averagesByYear).map(([year, average]) => ({ year, average, })); if (!dataset.length) return null; return ( ); } }; } catch (error) { handleError(error, `${nowRunning}.return`, errorNumber); } } export default useSkuHistory;