diff --git a/backend/analytics/internal/handler/handler.go b/backend/analytics/internal/handler/handler.go index 45dc4af..5891008 100644 --- a/backend/analytics/internal/handler/handler.go +++ b/backend/analytics/internal/handler/handler.go @@ -298,9 +298,9 @@ func (h *Handler) ExportAWSalesHistory(w http.ResponseWriter, r *http.Request) { } cols := []export.Column{ {Key: "date", Label: "Date"}, - {Key: "total_revenue", Label: "Total Revenue"}, - {Key: "total_orders", Label: "Total Orders"}, - {Key: "avg_order_value", Label: "Avg Order Value"}, + {Key: "revenue", Label: "Revenue"}, + {Key: "cost", Label: "Cost"}, + {Key: "quantity", Label: "Quantity"}, } b, err := export.ToXLSXBytes(r.Context(), "Sales History", cols, toMaps(data)) if err != nil { @@ -342,10 +342,12 @@ func (h *Handler) ExportAWRepScores(w http.ResponseWriter, r *http.Request) { } cols := []export.Column{ {Key: "rep_name", Label: "Sales Rep"}, - {Key: "total_revenue", Label: "Total Revenue"}, - {Key: "total_orders", Label: "Total Orders"}, - {Key: "avg_order_value", Label: "Avg Order Value"}, - {Key: "performance_score", Label: "Performance Score"}, + {Key: "territory", Label: "Territory"}, + {Key: "revenue", Label: "Revenue"}, + {Key: "orders", Label: "Orders"}, + {Key: "avg_deal_size", Label: "Avg Deal Size"}, + {Key: "margin_pct", Label: "Margin %"}, + {Key: "score", Label: "Performance Score"}, } b, err := export.ToXLSXBytes(r.Context(), "Rep Scores", cols, toMaps(data)) if err != nil { @@ -366,8 +368,10 @@ func (h *Handler) ExportAWProductDemand(w http.ResponseWriter, r *http.Request) cols := []export.Column{ {Key: "product_name", Label: "Product"}, {Key: "category", Label: "Category"}, - {Key: "total_quantity", Label: "Total Quantity"}, - {Key: "total_revenue", Label: "Total Revenue"}, + {Key: "quantity", Label: "Quantity"}, + {Key: "revenue", Label: "Revenue"}, + {Key: "orders", Label: "Orders"}, + {Key: "margin_pct", Label: "Margin %"}, {Key: "demand_score", Label: "Demand Score"}, } b, err := export.ToXLSXBytes(r.Context(), "Product Demand", cols, toMaps(data)) @@ -411,10 +415,11 @@ func (h *Handler) ExportWWISupplierScores(w http.ResponseWriter, r *http.Request } cols := []export.Column{ {Key: "supplier_name", Label: "Supplier"}, + {Key: "category", Label: "Category"}, {Key: "total_orders", Label: "Total Orders"}, - {Key: "on_time_delivery_rate", Label: "On-Time Delivery Rate"}, - {Key: "avg_lead_time_days", Label: "Avg Lead Time (Days)"}, - {Key: "performance_score", Label: "Performance Score"}, + {Key: "fill_rate_pct", Label: "Fill Rate %"}, + {Key: "finalization_rate_pct", Label: "Finalization Rate %"}, + {Key: "score", Label: "Performance Score"}, } b, err := export.ToXLSXBytes(r.Context(), "Supplier Scores", cols, toMaps(data)) if err != nil { diff --git a/frontend/src/api/gateway.ts b/frontend/src/api/gateway.ts index a4971fd..4c8063a 100644 --- a/frontend/src/api/gateway.ts +++ b/frontend/src/api/gateway.ts @@ -68,14 +68,14 @@ export const getWWIJobs = (limit = 50) => get(`/api/jobs/wwi?limit=${limit}`, "frontend.jobs.wwi"); export const triggerAWJob = (jobName: string) => - post<{ triggered: boolean; job_name: string }>( - `/api/jobs/aw/${jobName}/trigger`, + post<{ status: string; job: string }>( + `/api/aw/jobs/${jobName}/trigger`, "frontend.jobs.aw.trigger", ); export const triggerWWIJob = (jobName: string) => - post<{ triggered: boolean; job_name: string }>( - `/api/jobs/wwi/${jobName}/trigger`, + post<{ status: string; job: string }>( + `/api/wwi/jobs/${jobName}/trigger`, "frontend.jobs.wwi.trigger", ); diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index f837fe6..b43c043 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -126,11 +126,12 @@ export type WWIScenario = { export type AWAnomalyPoint = { date: string; revenue: number; - rolling_mean: number; - lower_band: number; - upper_band: number; + // null during the rolling-window warm-up period + rolling_mean: number | null; + lower_band: number | null; + upper_band: number | null; is_anomaly: boolean; - z_score: number; + z_score: number | null; direction: "high" | "low" | null; }; diff --git a/frontend/src/pages/aw/AnomalyDetection.tsx b/frontend/src/pages/aw/AnomalyDetection.tsx index 1510055..f52707b 100644 --- a/frontend/src/pages/aw/AnomalyDetection.tsx +++ b/frontend/src/pages/aw/AnomalyDetection.tsx @@ -72,8 +72,16 @@ export default function AnomalyDetection() { anomalyRevenue: p.is_anomaly ? p.revenue : null, })); - const domainMin = Math.min(...series.map((p) => p.lower_band)) * 0.95; - const domainMax = Math.max(...series.map((p) => p.upper_band)) * 1.05; + // Bands are null during the rolling-window warm-up; ignore those and fall + // back to revenue so anomaly points are never clipped off the chart. + const lows = series + .map((p) => (p.lower_band ?? p.revenue)) + .filter((v): v is number => v != null); + const highs = series + .map((p) => (p.upper_band ?? p.revenue)) + .filter((v): v is number => v != null); + const domainMin = lows.length ? Math.min(...lows) * 0.95 : 0; + const domainMax = highs.length ? Math.max(...highs) * 1.05 : 0; return (
@@ -228,16 +236,16 @@ export default function AnomalyDetection() { {p.date} {money.format(p.revenue)} - {money.format(p.rolling_mean)} + {p.rolling_mean != null ? money.format(p.rolling_mean) : "—"} 3 + (p.z_score != null && Math.abs(p.z_score) > 3) ? "text-red-400 font-semibold" : "text-amber-400" } > - {p.z_score.toFixed(2)} + {p.z_score != null ? p.z_score.toFixed(2) : "—"} {directionBadge(p.direction)} diff --git a/frontend/src/pages/ops/OperationsPage.tsx b/frontend/src/pages/ops/OperationsPage.tsx index 82dc2d6..99a5bc9 100644 --- a/frontend/src/pages/ops/OperationsPage.tsx +++ b/frontend/src/pages/ops/OperationsPage.tsx @@ -13,7 +13,6 @@ const AW_JOBS = [ const WWI_JOBS = [ { id: "reorder", label: "Reorder Recommendations" }, { id: "supplier_scores", label: "Supplier Scores" }, - { id: "events", label: "Business Events" }, { id: "data_quality", label: "Data Quality" }, ];