A Vue 3 single-page app and a FastAPI backend, talking over plain REST/JSON. Each side keeps its own copy of the data in memory — there is no database and no shared process.
+
+
+
Frontend
Vue 3.4 · Vite 5.2 :3000
+
Backend
FastAPI · Python ≥3.11 :8001
+
Data
7 JSON files loaded to memory at boot
+
Status
Local dev only wildcard CORS, no auth
+
+
+
+
Architecture
+
The Vite dev server and the FastAPI app are two independent processes on two ports. The browser is the only thing that talks to both — restart either side without touching the other.
+
+
+
+
+
+ Nothing is shared between the two sides except HTTP requests: no session, no socket, no shared memory. The JSON files are only ever read — once, at process start.
+
+
+
+
+
Tech stack
+
+
+
Frontend · client/
+
+
UI framework
Vue 3.4, Composition API only
+
Routing
vue-router 4.3, client-side (createWebHistory)
+
HTTP client
Axios 1.6
+
Build tool
Vite 5.2 + @vitejs/plugin-vue
+
State
No store library — shared refs inside composables
+
i18n
Hand-rolled composable, 2 locales (en, ja)
+
+
+
+
Backend · server/
+
+
Web framework
FastAPI ≥0.110
+
ASGI server
Uvicorn ≥0.24
+
Validation
Pydantic ≥2.5
+
Runtime
Python ≥3.11, managed with uv (pyproject.toml)
+
Data
mock_data.py loads server/data/*.json into module-level lists at import
+
Tests
pytest + FastAPI TestClient (tests/backend/)
+
+
+
+
+
+
+
Request lifecycle
+
Every view shares four filters — Time Period, Warehouse, Category, Order Status — held as refs in useFilters.js and sent as query params on every request. This is what happens for one filter change, in order.
+
+
+
+
+
+ The two lanes are two separate processes: everything left of the dashed line runs in the browser's JS engine, everything right of it runs in the Python interpreter. The only thing that crosses is the HTTP request and its JSON response.
+
+
+
+
+
Routes
+
Registered once in client/src/main.js, matched to the nav bar in App.vue.
+
+
+
Path
Component
Shows
+
+
/
Dashboard.vue
KPIs, order health, top products, inventory shortages
+
/inventory
Inventory.vue
Inventory list — warehouse & category filters
+
/orders
Orders.vue
Order list — warehouse, category, status & month filters
+
/demand
Demand.vue
Demand forecasts & trend
+
/spending
Spending.vue
Spending summary, monthly & category breakdowns
+
/reports
Reports.vue
Quarterly & month-over-month reports
+
+
+
+
+ Orphaned view
+ views/Backlog.vue exists but is never registered as a route or linked from the nav. Backlog items instead surface inline, inside the Dashboard's “Inventory Shortages” table, opened through BacklogDetailModal.vue.
+
+
+
+
+
API surface
+
All defined in server/main.py. Filters are optional query params; a value of all (or an omitted param) skips that filter.
+
+
+
Resource
Endpoint
Filters
+
+
Inventory
GET/api/inventory
warehouse, category
+
GET/api/inventory/{id}
—
+
Orders
GET/api/orders
warehouse, category, status, month
+
GET/api/orders/{id}
—
+
Demand
GET/api/demand
—
+
Backlog
GET/api/backlog
—
+
Dashboard
GET/api/dashboard/summary
warehouse, category, status, month
+
Spending
GET/api/spending/summary
—
+
GET/api/spending/monthly
—
+
GET/api/spending/categories
—
+
GET/api/spending/transactions
—
+
Reports
GET/api/reports/quarterly
—
+
GET/api/reports/monthly-trends
—
+
+
+
+
+ Client calls with no matching route
+ client/src/api.js also defines getTasks, createTask, deleteTask, toggleTask, createPurchaseOrder and getPurchaseOrderByBacklogItem, targeting /api/tasks and /api/purchase-orders. Neither path is defined in server/main.py — those calls will 404 until a matching route is added.
+
+
+
+
+
+
diff --git a/server/main.py b/server/main.py
index a0c2d8c5a..c724568ca 100644
--- a/server/main.py
+++ b/server/main.py
@@ -1,11 +1,15 @@
+from datetime import datetime, timedelta
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from typing import List, Optional
from pydantic import BaseModel
-from mock_data import inventory_items, orders, demand_forecasts, backlog_items, spending_summary, monthly_spending, category_spending, recent_transactions, purchase_orders
+from mock_data import inventory_items, orders, demand_forecasts, backlog_items, spending_summary, monthly_spending, category_spending, recent_transactions, purchase_orders, restock_orders
app = FastAPI(title="Factory Inventory Management System")
+# Fixed demo lead time — there's no supplier/lead-time data anywhere in the mock dataset
+RESTOCK_LEAD_TIME_DAYS = 14
+
# Quarter mapping for date filtering
QUARTER_MAP = {
'Q1-2025': ['2025-01', '2025-02', '2025-03'],
@@ -120,6 +124,39 @@ class CreatePurchaseOrderRequest(BaseModel):
expected_delivery_date: str
notes: Optional[str] = None
+class RestockOrderItem(BaseModel):
+ item_sku: str
+ item_name: str
+ quantity: int
+ unit_cost: float
+
+class RestockOrderRequest(BaseModel):
+ items: List[RestockOrderItem]
+ budget: float
+
+class RestockOrder(BaseModel):
+ id: str
+ order_number: str
+ items: List[RestockOrderItem]
+ total_cost: float
+ budget: float
+ created_date: str
+ lead_time_days: int
+ expected_delivery_date: str
+ status: str
+
+class RestockRecommendation(BaseModel):
+ item_sku: str
+ item_name: str
+ category: str
+ warehouse: str
+ unit_cost: float
+ current_demand: int
+ forecasted_demand: int
+ trend: str
+ recommended_quantity: int
+ line_total: float
+
# API endpoints
@app.get("/")
def root():
@@ -179,6 +216,87 @@ def get_backlog():
result.append(item_dict)
return result
+@app.get("/api/restocking/recommendations", response_model=List[RestockRecommendation])
+def get_restock_recommendations(
+ budget: float,
+ warehouse: Optional[str] = None,
+ category: Optional[str] = None
+):
+ """Recommend items to restock from demand forecasts, greedily filling the given budget"""
+ inventory_by_sku = {item["sku"]: item for item in apply_filters(inventory_items, warehouse, category)}
+
+ candidates = []
+ for forecast in demand_forecasts:
+ if forecast["trend"] == "decreasing":
+ continue
+ item = inventory_by_sku.get(forecast["item_sku"])
+ if not item:
+ continue
+ shortfall = forecast["forecasted_demand"] - forecast["current_demand"]
+ if shortfall <= 0:
+ continue
+ candidates.append((forecast, item, shortfall))
+
+ # Increasing trend first, then by largest shortfall
+ candidates.sort(key=lambda c: (c[0]["trend"] != "increasing", -c[2]))
+
+ recommendations = []
+ remaining_budget = budget
+ for forecast, item, shortfall in candidates:
+ unit_cost = item["unit_cost"]
+ if remaining_budget < unit_cost:
+ continue
+ affordable_qty = int(remaining_budget // unit_cost)
+ quantity = min(shortfall, affordable_qty)
+ if quantity <= 0:
+ continue
+ line_total = round(quantity * unit_cost, 2)
+ remaining_budget -= line_total
+
+ recommendations.append({
+ "item_sku": forecast["item_sku"],
+ "item_name": forecast["item_name"],
+ "category": item["category"],
+ "warehouse": item["warehouse"],
+ "unit_cost": unit_cost,
+ "current_demand": forecast["current_demand"],
+ "forecasted_demand": forecast["forecasted_demand"],
+ "trend": forecast["trend"],
+ "recommended_quantity": quantity,
+ "line_total": line_total
+ })
+
+ return recommendations
+
+@app.post("/api/restocking/orders", response_model=RestockOrder)
+def create_restock_order(request: RestockOrderRequest):
+ """Submit a restocking order built from the recommendations"""
+ if not request.items:
+ raise HTTPException(status_code=400, detail="Order must contain at least one item")
+
+ total_cost = round(sum(item.quantity * item.unit_cost for item in request.items), 2)
+ created_date = datetime.now()
+ expected_delivery = created_date + timedelta(days=RESTOCK_LEAD_TIME_DAYS)
+
+ order = {
+ "id": str(len(restock_orders) + 1),
+ "order_number": f"RSO-{created_date.year}-{len(restock_orders) + 1:04d}",
+ "items": [item.model_dump() for item in request.items],
+ "total_cost": total_cost,
+ "budget": request.budget,
+ "created_date": created_date.strftime("%Y-%m-%d"),
+ "lead_time_days": RESTOCK_LEAD_TIME_DAYS,
+ "expected_delivery_date": expected_delivery.strftime("%Y-%m-%d"),
+ "status": "Submitted"
+ }
+ restock_orders.append(order)
+ return order
+
+@app.get("/api/restocking/orders", response_model=List[RestockOrder])
+def get_restock_orders():
+ """Get all submitted restocking orders"""
+ return restock_orders
+
@app.get("/api/dashboard/summary")
def get_dashboard_summary(
warehouse: Optional[str] = None,
diff --git a/server/mock_data.py b/server/mock_data.py
index 2a9cd7dcb..fd105fb8c 100644
--- a/server/mock_data.py
+++ b/server/mock_data.py
@@ -35,5 +35,8 @@ def load_json_file(filename):
# Load purchase orders
purchase_orders = load_json_file('purchase_orders.json')
+# Restocking orders submitted via POST /api/restocking/orders — in-memory only, no backing JSON file
+restock_orders = []
+
# All data is now loaded from JSON files in the data/ directory
# This allows for easier maintenance and updates of the sample data
diff --git a/tests/backend/test_restocking.py b/tests/backend/test_restocking.py
new file mode 100644
index 000000000..8c7d79706
--- /dev/null
+++ b/tests/backend/test_restocking.py
@@ -0,0 +1,104 @@
+"""
+Tests for restocking API endpoints.
+"""
+import pytest
+
+
+class TestRestockingEndpoints:
+ """Test suite for restocking-related endpoints."""
+
+ def test_get_recommendations_within_budget(self, client):
+ """Test that recommendations never exceed the given budget."""
+ response = client.get("/api/restocking/recommendations?budget=5000")
+ assert response.status_code == 200
+
+ data = response.json()
+ assert isinstance(data, list)
+
+ total = sum(item["line_total"] for item in data)
+ assert total <= 5000
+
+ def test_get_recommendations_zero_budget(self, client):
+ """Test that a zero budget returns no recommendations."""
+ response = client.get("/api/restocking/recommendations?budget=0")
+ assert response.status_code == 200
+ assert response.json() == []
+
+ def test_recommendations_exclude_decreasing_trend(self, client):
+ """Test that items with decreasing demand are never recommended."""
+ response = client.get("/api/restocking/recommendations?budget=1000000")
+ assert response.status_code == 200
+
+ data = response.json()
+ for item in data:
+ assert item["trend"] != "decreasing"
+
+ def test_recommendations_respect_warehouse_filter(self, client):
+ """Test filtering recommendations by warehouse."""
+ response = client.get("/api/restocking/recommendations?budget=1000000&warehouse=Tokyo")
+ assert response.status_code == 200
+
+ data = response.json()
+ for item in data:
+ assert item["warehouse"] == "Tokyo"
+
+ def test_recommendations_respect_category_filter(self, client):
+ """Test filtering recommendations by category."""
+ response = client.get("/api/restocking/recommendations?budget=1000000&category=Sensors")
+ assert response.status_code == 200
+
+ data = response.json()
+ for item in data:
+ assert item["category"].lower() == "sensors"
+
+ def test_recommendation_fields(self, client):
+ """Test that recommendations have all required fields with correct types."""
+ response = client.get("/api/restocking/recommendations?budget=1000000")
+ data = response.json()
+ assert len(data) > 0
+
+ required_fields = [
+ "item_sku", "item_name", "category", "warehouse", "unit_cost",
+ "current_demand", "forecasted_demand", "trend",
+ "recommended_quantity", "line_total"
+ ]
+ for item in data:
+ for field in required_fields:
+ assert field in item, f"Missing field: {field}"
+ assert isinstance(item["recommended_quantity"], int)
+ assert item["recommended_quantity"] > 0
+
+ def test_place_order_creates_submitted_order(self, client):
+ """Test that placing a restock order adds it to the submitted orders list."""
+ request_body = {
+ "items": [
+ {"item_sku": "PCB-001", "item_name": "Single Layer PCB Assembly", "quantity": 10, "unit_cost": 24.99}
+ ],
+ "budget": 500
+ }
+ response = client.post("/api/restocking/orders", json=request_body)
+ assert response.status_code == 200
+
+ order = response.json()
+ assert order["total_cost"] == pytest.approx(249.9)
+ assert order["lead_time_days"] == 14
+ assert order["status"] == "Submitted"
+ assert "order_number" in order
+ assert "expected_delivery_date" in order
+
+ # Confirm it shows up in the submitted orders list
+ list_response = client.get("/api/restocking/orders")
+ assert list_response.status_code == 200
+ order_ids = [o["id"] for o in list_response.json()]
+ assert order["id"] in order_ids
+
+ def test_place_order_rejects_empty_items(self, client):
+ """Test that an order with no items is rejected."""
+ response = client.post("/api/restocking/orders", json={"items": [], "budget": 500})
+ assert response.status_code == 400
+
+ def test_get_restock_orders_returns_list(self, client):
+ """Test that the submitted orders endpoint always returns a list."""
+ response = client.get("/api/restocking/orders")
+ assert response.status_code == 200
+ assert isinstance(response.json(), list)