From 8cc18395dd8541913b5c117bc431143346efb5ce Mon Sep 17 00:00:00 2001
From: upen7
Date: Thu, 21 May 2026 10:29:59 -0700
Subject: [PATCH] Add Restocking tab with budget-aware order recommendations
- New Restocking view with a $1K-$100K budget slider, demand-forecast-driven
recommendations (sorted by forecasted demand), and a Place Order button.
Auto-selects items greedily within the budget and trims selections when
the budget shrinks.
- Backend: three new endpoints - GET /api/restocking/recommendations enriches
demand forecasts with unit cost (live inventory cost when available, static
fallback otherwise) and recommended quantity; POST /api/restocking-orders
creates an order with a fixed 7-day delivery lead time; GET returns all
submitted orders newest-first.
- Orders tab now shows a "Submitted Restocking Orders" section at the top
with delivery date highlighted.
- Adds /restocking route + nav link, an interactive architecture page in
docs/architecture.html, and a Coding Standards note in CLAUDE.md.
Co-Authored-By: Claude Sonnet 4.6
---
CLAUDE.md | 3 +
client/src/App.vue | 3 +
client/src/api.js | 18 +-
client/src/main.js | 4 +-
client/src/views/Orders.vue | 71 +++-
client/src/views/Restocking.vue | 522 ++++++++++++++++++++++++
docs/architecture.html | 701 ++++++++++++++++++++++++++++++++
server/main.py | 113 +++++
8 files changed, 1431 insertions(+), 4 deletions(-)
create mode 100644 client/src/views/Restocking.vue
create mode 100644 docs/architecture.html
diff --git a/CLAUDE.md b/CLAUDE.md
index d2086efa..c924f5eb 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -53,6 +53,9 @@ npm install && npm run dev
- `GET /api/demand`, `/api/backlog` - No filters
- `GET /api/spending/*` - Summary, monthly, categories, transactions
+## Coding Standards
+- Always document non-obvious logic changes with comments
+
## Common Issues
1. Use unique keys in v-for (not `index`) - use `sku`, `month`, etc.
2. Validate dates before `.getMonth()` calls
diff --git a/client/src/App.vue b/client/src/App.vue
index c2da05a5..2c6081c3 100644
--- a/client/src/App.vue
+++ b/client/src/App.vue
@@ -25,6 +25,9 @@
Reports
+
+ Restocking
+
{{ t('orders.description') }}
+
+
@@ -95,6 +139,7 @@ export default {
const loading = ref(true)
const error = ref(null)
const orders = ref([])
+ const restockingOrders = ref([])
// Use shared filters
const {
@@ -109,7 +154,10 @@ export default {
try {
loading.value = true
const filters = getCurrentFilters()
- const fetchedOrders = await api.getOrders(filters)
+ const [fetchedOrders, fetchedRestocking] = await Promise.all([
+ api.getOrders(filters),
+ api.getRestockingOrders()
+ ])
// Sort orders by order_date (earliest first)
orders.value = fetchedOrders.sort((a, b) => {
@@ -117,6 +165,7 @@ export default {
const dateB = new Date(b.order_date)
return dateA - dateB
})
+ restockingOrders.value = fetchedRestocking
} catch (err) {
error.value = 'Failed to load orders: ' + err.message
} finally {
@@ -165,13 +214,31 @@ export default {
formatDate,
currencySymbol,
translateProductName,
- translateCustomerName
+ translateCustomerName,
+ restockingOrders
}
}
}
diff --git a/docs/architecture.html b/docs/architecture.html
new file mode 100644
index 00000000..6ae156e4
--- /dev/null
+++ b/docs/architecture.html
@@ -0,0 +1,701 @@
+
+
+
+
+
+
Inventory Management — System Architecture
+
+
+
+
+
+
+
+
+
+
+ Tech Stack
+
+
+
💚
+
+
Vue 3
+
Composition API · Reactive state · SFC components
+
Frontend
+
+
+
+
⚡
+
+
Vite 5
+
Dev server on :3000 · HMR · ES module builds
+
Build Tool
+
+
+
+
🔀
+
+
Vue Router 4
+
6 client-side routes · History mode
+
Routing
+
+
+
+
🌐
+
+
Axios
+
HTTP client · Query param builder · Promise API
+
HTTP
+
+
+
+
🐍
+
+
FastAPI
+
Python backend · 16 endpoints · Auto Swagger docs
+
Backend
+
+
+
+
✅
+
+
Pydantic v2
+
Data validation · Response models · Type enforcement
+
Validation
+
+
+
+
🚀
+
+
Uvicorn
+
ASGI server on :8000 · Auto-reload · StatReload
+
Server
+
+
+
+
🗂️
+
+
In-Memory Data
+
7 JSON files · No database · Loaded at startup
+
Data
+
+
+
+
🌍
+
+
i18n (EN / JA)
+
English + Japanese · Currency formatting · localStorage
+
Locale
+
+
+
+
+
+
+
+ System Architecture
+
+
+
+
+
+
Browser
+
+
Vue 3 SPA
+
localhost:3000
+
Vite Dev Server
+
+
+
Vue Router
+
6 routes / history mode
+
+
+
useFilters
+
Global filter state
+
+
+
useI18n
+
EN / JA translations
+
+
+
+
+
+
+
+
Views / Components
+
+
Dashboard.vue
+
KPIs · Charts · Tables
+
+
+
Inventory.vue
+
Stock levels table
+
+
+
Orders.vue
+
Order tracking
+
+
+
Spending · Reports · Demand
+
Analytics views
+
+
+
9 Modal Components
+
Detail · Filter · Profile · Tasks
+
+
+
+
+
+
+
+
API Client
+
+
api.js (Axios)
+
Base: localhost:8000/api
+
HTTP/JSON
+
+
+
URLSearchParams
+
warehouse · category status · month
+
+
+
20+ API Methods
+
GET · POST · PATCH · DELETE
+
+
+
+
+
+
+
+
Backend
+
+
FastAPI
+
localhost:8000
+
Python / Uvicorn
+
+
+
CORS Middleware
+
Allow all origins
+
+
+
apply_filters()
+
warehouse · category · status
+
+
+
filter_by_month()
+
month / Q1–Q4 ranges
+
+
+
Pydantic Models
+
Type validation · Serialization
+
+
+
+
+
+
+
+
Data Layer
+
+
mock_data.py
+
JSON loader · In-memory
+
No Database
+
+
+
inventory.json
+
50+ items · 3 warehouses
+
+
+
orders.json
+
1000+ orders · 148 KB
+
+
+
spending · backlog · demand
+
4 additional JSON files
+
+
+
+
+
+
+
+
+
+ Request Data Flow
+
+
+
1
+
+
User Interaction
+
User navigates to a route or changes a filter (warehouse, category, period, status) in FilterBar.vue
+
+
+
+
2
+
+
Vue Router
+
Route resolved, component mounts. onMounted() or a watch() on filter state triggers loadData()
+
+
+
+
3
+
+
useFilters Composable
+
getCurrentFilters() returns current state: {warehouse, category, status, month} as query params
+
+
+
+
4
+
+
Axios HTTP Request
+
api.js builds URL with URLSearchParams and fires GET /api/inventory?warehouse=Tokyo
+
+
+
+
5
+
+
FastAPI Handler
+
CORS validated. Route matched. apply_filters() and filter_by_month() applied to in-memory data
+
+
+
+
6
+
+
Pydantic Serialization
+
Filtered data validated against response model (e.g. List[InventoryItem]) and serialized to JSON
+
+
+
+
7
+
+
Vue State Update
+
Axios promise resolves. inventoryItems.value = data triggers Vue reactivity, re-rendering the template
+
+
+
+
8
+
+
Computed + Render
+
Computed properties recalculate (charts, aggregates). Template re-renders tables, KPIs, and SVG charts with new data
+
+
+
+
+
+
+
+ API Endpoints — localhost:8000
+
+
GET /api/inventory All items · filters: warehouse, category
+
GET /api/inventory/{id} Single inventory item
+
GET /api/orders All orders · filters: warehouse, category, status, month
+
GET /api/orders/{id} Single order
+
GET /api/dashboard/summary Aggregated KPI metrics · all filters
+
GET /api/demand Demand forecasts · no filters
+
GET /api/backlog Backlog items with PO flags
+
GET /api/spending/summary Cost totals by type
+
GET /api/spending/monthly Month-by-month breakdown
+
GET /api/spending/categories Cost split by category
+
GET /api/spending/transactions Recent 100+ transactions
+
GET /api/reports/quarterly Quarterly performance stats
+
GET /api/reports/monthly-trends Month-over-month trends
+
POST /api/purchase-orders Create a purchase order
+
GET /api/purchase-orders/{id} PO by backlog item ID
+
GET / Health check
+
+
+
+
+
+ Frontend Component Structure
+
+
+
+
Views (Pages)
+
Dashboard.vue KPIs, charts, tables
+
Inventory.vue Stock levels
+
Orders.vue Order tracking
+
Demand.vue Forecasts
+
Spending.vue Cost analytics
+
Reports.vue Quarterly / monthly
+
Backlog.vue Shortage tracking
+
+
+
+
UI Components
+
FilterBar.vue 4 global filters
+
TasksModal.vue CRUD task manager
+
ProductDetailModal Product deep dive
+
InventoryDetailModal Stock item details
+
BacklogDetailModal Shortage details
+
CostDetailModal Cost breakdown
+
ProfileMenu.vue User dropdown
+
LanguageSwitcher EN / JA toggle
+
+
+
+
Composables (Shared State)
+
useFilters.js Global filter state
+
useI18n.js Translations + currency
+
useAuth.js Mock user + tasks
+
Data Files
+
inventory.json 50+ SKUs
+
orders.json 1000+ orders
+
spending.json Cost summaries
+
backlog + demand + tx Supporting data
+
+
+
+
+
+
+
+ Project File Structure
+
+
inventory-management/
+
+
client/ — Vue 3 frontend (port 3000)
+
+
src/
+
+
main.js — App entry, router config
+
api.js — Axios client, 20+ methods
+
App.vue — Root layout, nav, global styles
+
views/ — 7 page components
+
components/ — 9 reusable UI/modal components
+
composables/ — useFilters · useI18n · useAuth
+
locales/ — en.js · ja.js
+
utils/ — currency.js
+
+
vite.config.js
+
package.json — Vue 3, Vue Router, Axios
+
+
server/ — FastAPI backend (port 8000)
+
+
main.py — 16 endpoints, Pydantic models, filters
+
mock_data.py — JSON loader, exports data globals
+
requirements.txt — fastapi, uvicorn, pydantic
+
data/ — inventory · orders · spending · backlog · demand · transactions
+
+
tests/backend/ — pytest + FastAPI TestClient, 15+ tests
+
scripts/ — start.sh · stop.sh
+
docs/ — Architecture, screenshots
+
CLAUDE.md — Claude Code project config
+
README.md
+
+
+
+
+
+
+
+ Inventory Management System · Vue 3 + FastAPI · Generated 2026-05-21
+
+
+
+
diff --git a/server/main.py b/server/main.py
index a0c2d8c5..3e60748c 100644
--- a/server/main.py
+++ b/server/main.py
@@ -304,6 +304,119 @@ def get_monthly_trends():
result.sort(key=lambda x: x['month'])
return result
+# ── Restocking ──────────────────────────────────────────────────────────────
+
+# Fallback unit costs for demand-forecast SKUs not in inventory
+FALLBACK_UNIT_COSTS = {
+ "WDG-001": 45.00,
+ "BRG-102": 28.50,
+ "GSK-203": 12.00,
+ "MTR-304": 850.00,
+ "FLT-405": 8.25,
+ "VLV-506": 65.00,
+ "PSU-501": 35.00,
+ "SNR-420": 18.00,
+ "CTL-330": 95.00,
+}
+
+# In-memory store for submitted restocking orders
+restocking_orders: list = []
+
+class RestockingItem(BaseModel):
+ sku: str
+ name: str
+ quantity: int
+ unit_cost: float
+ total_cost: float
+ trend: str
+ forecasted_demand: int
+
+class CreateRestockingOrderRequest(BaseModel):
+ items: List[RestockingItem]
+ total_value: float
+ notes: Optional[str] = None
+
+class RestockingOrder(BaseModel):
+ id: str
+ order_number: str
+ items: List[RestockingItem]
+ total_value: float
+ status: str
+ order_date: str
+ expected_delivery: str
+ notes: Optional[str] = None
+
+@app.get("/api/restocking/recommendations")
+def get_restocking_recommendations():
+ """
+ Return demand forecasts enriched with unit cost and recommended restock quantity.
+ Unit cost is sourced from inventory when the SKU matches; otherwise falls back
+ to a static lookup table so the frontend always has pricing data.
+ Sorted by forecasted_demand descending (highest priority first).
+ """
+ # Build a quick SKU → unit_cost map from live inventory
+ inventory_cost_map = {item.get("sku"): item.get("unit_cost", 0) for item in inventory_items}
+
+ enriched = []
+ for forecast in demand_forecasts:
+ sku = forecast.get("item_sku", "")
+ # Prefer live inventory cost, fall back to static table, then 0
+ unit_cost = inventory_cost_map.get(sku) or FALLBACK_UNIT_COSTS.get(sku, 0.0)
+
+ # Recommended quantity = gap between forecasted and current demand (min 1)
+ recommended_qty = max(1, forecast.get("forecasted_demand", 0) - forecast.get("current_demand", 0))
+
+ enriched.append({
+ "id": forecast.get("id"),
+ "sku": sku,
+ "name": forecast.get("item_name", ""),
+ "current_demand": forecast.get("current_demand", 0),
+ "forecasted_demand": forecast.get("forecasted_demand", 0),
+ "trend": forecast.get("trend", "stable"),
+ "period": forecast.get("period", ""),
+ "unit_cost": unit_cost,
+ "recommended_qty": recommended_qty,
+ "estimated_cost": round(unit_cost * recommended_qty, 2),
+ })
+
+ # Sort by forecasted_demand descending — highest demand items first
+ enriched.sort(key=lambda x: x["forecasted_demand"], reverse=True)
+ return enriched
+
+
+@app.post("/api/restocking-orders", response_model=RestockingOrder)
+def create_restocking_order(request: CreateRestockingOrderRequest):
+ """Submit a restocking order. Assigns a 7-day delivery lead time."""
+ from datetime import datetime, timedelta
+
+ now = datetime.utcnow()
+ order_date = now.strftime("%Y-%m-%d")
+ expected_delivery = (now + timedelta(days=7)).strftime("%Y-%m-%d")
+
+ # Generate order number: RST-
+ order_id = f"rst-{int(now.timestamp())}"
+ order_number = f"RST-{now.strftime('%Y%m%d%H%M%S')}"
+
+ new_order = {
+ "id": order_id,
+ "order_number": order_number,
+ "items": [item.dict() for item in request.items],
+ "total_value": round(request.total_value, 2),
+ "status": "Processing",
+ "order_date": order_date,
+ "expected_delivery": expected_delivery,
+ "notes": request.notes,
+ }
+ restocking_orders.append(new_order)
+ return new_order
+
+
+@app.get("/api/restocking-orders", response_model=List[RestockingOrder])
+def get_restocking_orders():
+ """Return all submitted restocking orders, newest first."""
+ return list(reversed(restocking_orders))
+
+
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001)