mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 15:35:22 +02:00
Restore ForecastView UI, Fix default YTD logic, and Refine Seasonality (Specific > Global fallback)
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user