Restore ForecastView UI, Fix default YTD logic, and Refine Seasonality (Specific > Global fallback)

This commit is contained in:
Christian Vidal Wolf
2026-01-28 14:55:35 +01:00
parent cfff95bff2
commit 9d732eeba3
2 changed files with 9 additions and 27 deletions
+7 -19
View File
@@ -147,6 +147,7 @@ const ForecastRow: React.FC<{
); );
}); });
const ForecastView: React.FC<ForecastViewProps> = ({ const ForecastView: React.FC<ForecastViewProps> = ({
data, data,
filters, filters,
@@ -164,7 +165,6 @@ const ForecastView: React.FC<ForecastViewProps> = ({
const [displayCount, setDisplayCount] = useState(50); const [displayCount, setDisplayCount] = useState(50);
const lastDataMonthIdx = useMemo(() => { const lastDataMonthIdx = useMemo(() => {
// Find the last month with actual data
let maxDataMonth = 0; let maxDataMonth = 0;
for (let i = MONTH_ORDER.length - 1; i >= 0; i--) { for (let i = MONTH_ORDER.length - 1; i >= 0; i--) {
if (data.some(p => (p.monthlyData?.[MONTH_ORDER[i]]?.actualUnits || 0) > 0)) { if (data.some(p => (p.monthlyData?.[MONTH_ORDER[i]]?.actualUnits || 0) > 0)) {
@@ -172,12 +172,7 @@ const ForecastView: React.FC<ForecastViewProps> = ({
break; break;
} }
} }
// Clamp to current month to ensure we don't show future months in "Period" view
// This fixes the issue where "Period" shows Annual forecast if there is any stray future data
const currentMonth = new Date().getMonth(); // 0 for Jan const currentMonth = new Date().getMonth(); // 0 for Jan
// We assume the data is for the current year if we are in FC 26 view.
// If we want to support past years, we'd need to check the year context, but for now this fixes the user's immediate issue.
return Math.min(maxDataMonth, currentMonth); return Math.min(maxDataMonth, currentMonth);
}, [data]); }, [data]);
@@ -185,8 +180,10 @@ const ForecastView: React.FC<ForecastViewProps> = ({
if (filters.month && filters.month.length > 0) { if (filters.month && filters.month.length > 0) {
return filters.month.map(m => m.split('-')[0]); return filters.month.map(m => m.split('-')[0]);
} }
return MONTH_ORDER.slice(0, lastDataMonthIdx + 1); // Default to YTD: Current month and all before it this year
}, [filters.month, lastDataMonthIdx]); const currentMonthIdx = new Date().getMonth();
return MONTH_ORDER.slice(0, currentMonthIdx + 1);
}, [filters.month]);
const baseFilteredData = useMemo(() => { const baseFilteredData = useMemo(() => {
let result = data; let result = data;
@@ -198,7 +195,6 @@ const ForecastView: React.FC<ForecastViewProps> = ({
const globalSummary = useMemo(() => { const globalSummary = useMemo(() => {
return baseFilteredData.reduce((acc, curr) => { return baseFilteredData.reduce((acc, curr) => {
// Calculate Forecast Period (YTD) for this item
const itemPeriodForecast = activeMonths.reduce((sum, month) => { const itemPeriodForecast = activeMonths.reduce((sum, month) => {
return sum + (curr.monthlyData?.[month]?.forecastUnits || 0); return sum + (curr.monthlyData?.[month]?.forecastUnits || 0);
}, 0); }, 0);
@@ -260,26 +256,20 @@ const ForecastView: React.FC<ForecastViewProps> = ({
return ( return (
<div className="flex flex-col gap-6 animate-fade-in p-6 h-full overflow-hidden"> <div className="flex flex-col gap-6 animate-fade-in p-6 h-full overflow-hidden">
{/* Top Section: Metrics & Graph */}
<div className="flex flex-col xl:flex-row gap-6 h-[400px] shrink-0"> <div className="flex flex-col xl:flex-row gap-6 h-[400px] shrink-0">
{/* Metrics Cards */}
<div className="flex flex-col gap-4 min-w-[300px] xl:w-[25%]"> <div className="flex flex-col gap-4 min-w-[300px] xl:w-[25%]">
{/* Annual Forecast */}
<div className="bg-slate-900 border border-white/5 p-4 rounded-xl shadow-lg flex-1 flex flex-col justify-center"> <div className="bg-slate-900 border border-white/5 p-4 rounded-xl shadow-lg flex-1 flex flex-col justify-center">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Annual Forecast Total</span> <span className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Annual Forecast Total</span>
<div className="text-2xl font-black text-white">{(globalSummary.annualForecast || 0).toLocaleString('de-DE')} <span className="text-sm font-bold text-slate-500">Units</span></div> <div className="text-2xl font-black text-white">{(globalSummary.annualForecast || 0).toLocaleString('de-DE')} <span className="text-sm font-bold text-slate-500">Units</span></div>
</div> </div>
{/* Period Forecast */}
<div className="bg-slate-900 border border-white/5 p-4 rounded-xl shadow-lg flex-1 flex flex-col justify-center"> <div className="bg-slate-900 border border-white/5 p-4 rounded-xl shadow-lg flex-1 flex flex-col justify-center">
<span className="text-[10px] font-black text-indigo-400 uppercase tracking-widest mb-1">Total Forecast (Period)</span> <span className="text-[10px] font-black text-indigo-400 uppercase tracking-widest mb-1">Total Forecast (Period)</span>
<div className="text-2xl font-black text-white">{(globalSummary.forecastUnits || 0).toLocaleString('de-DE')} <span className="text-sm font-bold text-slate-500">Units</span></div> <div className="text-2xl font-black text-white">{(globalSummary.forecastUnits || 0).toLocaleString('de-DE')} <span className="text-sm font-bold text-slate-500">Units</span></div>
</div> </div>
{/* Actual Sales */}
<div className="bg-slate-900 border border-white/5 p-4 rounded-xl shadow-lg flex-1 flex flex-col justify-center"> <div className="bg-slate-900 border border-white/5 p-4 rounded-xl shadow-lg flex-1 flex flex-col justify-center">
<span className="text-[10px] font-black text-emerald-400 uppercase tracking-widest mb-1">Total Actual Sales (Period)</span> <span className="text-[10px] font-black text-emerald-400 uppercase tracking-widest mb-1">Total Actual Sales (Period)</span>
<div className="text-2xl font-black text-emerald-400">{(globalSummary.actualUnits || 0).toLocaleString('de-DE')} <span className="text-sm font-bold text-emerald-600/70">Units</span></div> <div className="text-2xl font-black text-emerald-400">{(globalSummary.actualUnits || 0).toLocaleString('de-DE')} <span className="text-sm font-bold text-emerald-600/70">Units</span></div>
</div> </div>
{/* Fulfillment */}
<div className="flex gap-4 flex-1"> <div className="flex gap-4 flex-1">
<div className="bg-slate-900 border border-white/5 p-4 rounded-xl shadow-lg flex-1 flex flex-col justify-center"> <div className="bg-slate-900 border border-white/5 p-4 rounded-xl shadow-lg flex-1 flex flex-col justify-center">
<span className="text-[10px] font-black text-blue-400 uppercase tracking-widest mb-1">Fulfillment (Period)</span> <span className="text-[10px] font-black text-blue-400 uppercase tracking-widest mb-1">Fulfillment (Period)</span>
@@ -295,7 +285,6 @@ const ForecastView: React.FC<ForecastViewProps> = ({
</div> </div>
</div> </div>
{/* Graph */}
<div className="flex-1 bg-slate-900 border border-white/5 rounded-xl shadow-lg p-6 flex flex-col"> <div className="flex-1 bg-slate-900 border border-white/5 rounded-xl shadow-lg p-6 flex flex-col">
<h3 className="flex items-center gap-2 text-sm font-bold text-white mb-6"> <h3 className="flex items-center gap-2 text-sm font-bold text-white mb-6">
<TrendingIcon /> Monthly Evolution: Forecast vs Actual <TrendingIcon /> Monthly Evolution: Forecast vs Actual
@@ -329,11 +318,10 @@ const ForecastView: React.FC<ForecastViewProps> = ({
</div> </div>
</div> </div>
{/* Bottom Section: Table */}
<div className="bg-slate-900 border border-white/10 rounded-2xl overflow-hidden shadow-2xl flex-1 flex flex-col min-h-0"> <div className="bg-slate-900 border border-white/10 rounded-2xl overflow-hidden shadow-2xl flex-1 flex flex-col min-h-0">
<div className="p-4 border-b border-white/5 flex flex-col md:flex-row justify-between gap-4 bg-slate-800/20 shrink-0"> <div className="p-4 border-b border-white/5 flex flex-col md:flex-row justify-between gap-4 bg-slate-800/20 shrink-0">
<h3 className="text-lg font-bold text-white uppercase tracking-tight"> <h3 className="text-lg font-bold text-white uppercase tracking-tight">
Product Performance Comparison <span className="text-xs text-slate-500 font-normal normal-case ml-2">(v2.2 Updated)</span> Product Performance Comparison <span className="text-xs text-slate-500 font-normal normal-case ml-2">(v2.4 Final)</span>
</h3> </h3>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{top50Ranking && (top50Ranking.eu.size > 0 || top50Ranking.uk.size > 0) && ( {top50Ranking && (top50Ranking.eu.size > 0 || top50Ranking.uk.size > 0) && (
@@ -374,7 +362,7 @@ const ForecastView: React.FC<ForecastViewProps> = ({
<ForecastRow <ForecastRow
key={item.asin} key={item.asin}
item={item} item={item}
activeMonths={filters.month.length > 0 ? filters.month : MONTH_ORDER} activeMonths={activeMonths}
top50Ranking={top50Ranking} top50Ranking={top50Ranking}
top50Mode={top50Mode} top50Mode={top50Mode}
stockMap={stockMap} stockMap={stockMap}
+2 -8
View File
@@ -1628,7 +1628,6 @@ export const calculateForecastViewData = (
const getWeights = (records: SalesRecord[]): number[] | null => { const getWeights = (records: SalesRecord[]): number[] | null => {
const weights = new Array(12).fill(0); const weights = new Array(12).fill(0);
let total = 0; let total = 0;
const seenMonths = new Set<string>();
records.forEach(r => { records.forEach(r => {
const m = r.month.split('-')[0]; const m = r.month.split('-')[0];
@@ -1636,18 +1635,13 @@ export const calculateForecastViewData = (
if (idx !== -1) { if (idx !== -1) {
weights[idx] += r.units; weights[idx] += r.units;
total += r.units; total += r.units;
if (r.units > 0) seenMonths.add(m);
} }
}); });
// 1. No data -> Fallback // If ASIN has any 2025 sales, we trust its specific seasonality.
// Return null ONLY if there's no data at all for this ASIN in 2025.
if (total === 0) return null; if (total === 0) return null;
// 2. Sparse Data Check (< 4 months)
// If an ASIN has very little history (e.g. only Jan), using its own curve implies 100% seasonality in Jan.
// The user requested to use the "General Catalog Seasonality" in these cases.
if (seenMonths.size < 4) return null;
return weights.map(w => w / total); return weights.map(w => w / total);
}; };