More bugfixes
This commit is contained in:
@@ -298,9 +298,9 @@ func (h *Handler) ExportAWSalesHistory(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
cols := []export.Column{
|
cols := []export.Column{
|
||||||
{Key: "date", Label: "Date"},
|
{Key: "date", Label: "Date"},
|
||||||
{Key: "total_revenue", Label: "Total Revenue"},
|
{Key: "revenue", Label: "Revenue"},
|
||||||
{Key: "total_orders", Label: "Total Orders"},
|
{Key: "cost", Label: "Cost"},
|
||||||
{Key: "avg_order_value", Label: "Avg Order Value"},
|
{Key: "quantity", Label: "Quantity"},
|
||||||
}
|
}
|
||||||
b, err := export.ToXLSXBytes(r.Context(), "Sales History", cols, toMaps(data))
|
b, err := export.ToXLSXBytes(r.Context(), "Sales History", cols, toMaps(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -342,10 +342,12 @@ func (h *Handler) ExportAWRepScores(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
cols := []export.Column{
|
cols := []export.Column{
|
||||||
{Key: "rep_name", Label: "Sales Rep"},
|
{Key: "rep_name", Label: "Sales Rep"},
|
||||||
{Key: "total_revenue", Label: "Total Revenue"},
|
{Key: "territory", Label: "Territory"},
|
||||||
{Key: "total_orders", Label: "Total Orders"},
|
{Key: "revenue", Label: "Revenue"},
|
||||||
{Key: "avg_order_value", Label: "Avg Order Value"},
|
{Key: "orders", Label: "Orders"},
|
||||||
{Key: "performance_score", Label: "Performance Score"},
|
{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))
|
b, err := export.ToXLSXBytes(r.Context(), "Rep Scores", cols, toMaps(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -366,8 +368,10 @@ func (h *Handler) ExportAWProductDemand(w http.ResponseWriter, r *http.Request)
|
|||||||
cols := []export.Column{
|
cols := []export.Column{
|
||||||
{Key: "product_name", Label: "Product"},
|
{Key: "product_name", Label: "Product"},
|
||||||
{Key: "category", Label: "Category"},
|
{Key: "category", Label: "Category"},
|
||||||
{Key: "total_quantity", Label: "Total Quantity"},
|
{Key: "quantity", Label: "Quantity"},
|
||||||
{Key: "total_revenue", Label: "Total Revenue"},
|
{Key: "revenue", Label: "Revenue"},
|
||||||
|
{Key: "orders", Label: "Orders"},
|
||||||
|
{Key: "margin_pct", Label: "Margin %"},
|
||||||
{Key: "demand_score", Label: "Demand Score"},
|
{Key: "demand_score", Label: "Demand Score"},
|
||||||
}
|
}
|
||||||
b, err := export.ToXLSXBytes(r.Context(), "Product Demand", cols, toMaps(data))
|
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{
|
cols := []export.Column{
|
||||||
{Key: "supplier_name", Label: "Supplier"},
|
{Key: "supplier_name", Label: "Supplier"},
|
||||||
|
{Key: "category", Label: "Category"},
|
||||||
{Key: "total_orders", Label: "Total Orders"},
|
{Key: "total_orders", Label: "Total Orders"},
|
||||||
{Key: "on_time_delivery_rate", Label: "On-Time Delivery Rate"},
|
{Key: "fill_rate_pct", Label: "Fill Rate %"},
|
||||||
{Key: "avg_lead_time_days", Label: "Avg Lead Time (Days)"},
|
{Key: "finalization_rate_pct", Label: "Finalization Rate %"},
|
||||||
{Key: "performance_score", Label: "Performance Score"},
|
{Key: "score", Label: "Performance Score"},
|
||||||
}
|
}
|
||||||
b, err := export.ToXLSXBytes(r.Context(), "Supplier Scores", cols, toMaps(data))
|
b, err := export.ToXLSXBytes(r.Context(), "Supplier Scores", cols, toMaps(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -68,14 +68,14 @@ export const getWWIJobs = (limit = 50) =>
|
|||||||
get<JobExecution[]>(`/api/jobs/wwi?limit=${limit}`, "frontend.jobs.wwi");
|
get<JobExecution[]>(`/api/jobs/wwi?limit=${limit}`, "frontend.jobs.wwi");
|
||||||
|
|
||||||
export const triggerAWJob = (jobName: string) =>
|
export const triggerAWJob = (jobName: string) =>
|
||||||
post<{ triggered: boolean; job_name: string }>(
|
post<{ status: string; job: string }>(
|
||||||
`/api/jobs/aw/${jobName}/trigger`,
|
`/api/aw/jobs/${jobName}/trigger`,
|
||||||
"frontend.jobs.aw.trigger",
|
"frontend.jobs.aw.trigger",
|
||||||
);
|
);
|
||||||
|
|
||||||
export const triggerWWIJob = (jobName: string) =>
|
export const triggerWWIJob = (jobName: string) =>
|
||||||
post<{ triggered: boolean; job_name: string }>(
|
post<{ status: string; job: string }>(
|
||||||
`/api/jobs/wwi/${jobName}/trigger`,
|
`/api/wwi/jobs/${jobName}/trigger`,
|
||||||
"frontend.jobs.wwi.trigger",
|
"frontend.jobs.wwi.trigger",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -126,11 +126,12 @@ export type WWIScenario = {
|
|||||||
export type AWAnomalyPoint = {
|
export type AWAnomalyPoint = {
|
||||||
date: string;
|
date: string;
|
||||||
revenue: number;
|
revenue: number;
|
||||||
rolling_mean: number;
|
// null during the rolling-window warm-up period
|
||||||
lower_band: number;
|
rolling_mean: number | null;
|
||||||
upper_band: number;
|
lower_band: number | null;
|
||||||
|
upper_band: number | null;
|
||||||
is_anomaly: boolean;
|
is_anomaly: boolean;
|
||||||
z_score: number;
|
z_score: number | null;
|
||||||
direction: "high" | "low" | null;
|
direction: "high" | "low" | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -72,8 +72,16 @@ export default function AnomalyDetection() {
|
|||||||
anomalyRevenue: p.is_anomaly ? p.revenue : null,
|
anomalyRevenue: p.is_anomaly ? p.revenue : null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const domainMin = Math.min(...series.map((p) => p.lower_band)) * 0.95;
|
// Bands are null during the rolling-window warm-up; ignore those and fall
|
||||||
const domainMax = Math.max(...series.map((p) => p.upper_band)) * 1.05;
|
// 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 (
|
return (
|
||||||
<div className="flex flex-col gap-6 max-w-[1100px] mx-auto">
|
<div className="flex flex-col gap-6 max-w-[1100px] mx-auto">
|
||||||
@@ -228,16 +236,16 @@ export default function AnomalyDetection() {
|
|||||||
<td className="font-mono text-sm">{p.date}</td>
|
<td className="font-mono text-sm">{p.date}</td>
|
||||||
<td>{money.format(p.revenue)}</td>
|
<td>{money.format(p.revenue)}</td>
|
||||||
<td className="text-[rgba(233,244,255,0.5)]">
|
<td className="text-[rgba(233,244,255,0.5)]">
|
||||||
{money.format(p.rolling_mean)}
|
{p.rolling_mean != null ? money.format(p.rolling_mean) : "—"}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className={
|
className={
|
||||||
Math.abs(p.z_score) > 3
|
(p.z_score != null && Math.abs(p.z_score) > 3)
|
||||||
? "text-red-400 font-semibold"
|
? "text-red-400 font-semibold"
|
||||||
: "text-amber-400"
|
: "text-amber-400"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{p.z_score.toFixed(2)}
|
{p.z_score != null ? p.z_score.toFixed(2) : "—"}
|
||||||
</td>
|
</td>
|
||||||
<td>{directionBadge(p.direction)}</td>
|
<td>{directionBadge(p.direction)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ const AW_JOBS = [
|
|||||||
const WWI_JOBS = [
|
const WWI_JOBS = [
|
||||||
{ id: "reorder", label: "Reorder Recommendations" },
|
{ id: "reorder", label: "Reorder Recommendations" },
|
||||||
{ id: "supplier_scores", label: "Supplier Scores" },
|
{ id: "supplier_scores", label: "Supplier Scores" },
|
||||||
{ id: "events", label: "Business Events" },
|
|
||||||
{ id: "data_quality", label: "Data Quality" },
|
{ id: "data_quality", label: "Data Quality" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user