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/inventoryAll items · filters: warehouse, category
+
GET/api/inventory/{id}Single inventory item
+
GET/api/ordersAll orders · filters: warehouse, category, status, month
+
GET/api/orders/{id}Single order
+
GET/api/dashboard/summaryAggregated KPI metrics · all filters
+
GET/api/demandDemand forecasts · no filters
+
GET/api/backlogBacklog items with PO flags
+
GET/api/spending/summaryCost totals by type
+
GET/api/spending/monthlyMonth-by-month breakdown
+
GET/api/spending/categoriesCost split by category
+
GET/api/spending/transactionsRecent 100+ transactions
+
GET/api/reports/quarterlyQuarterly performance stats
+
GET/api/reports/monthly-trendsMonth-over-month trends
+
POST/api/purchase-ordersCreate a purchase order
+
GET/api/purchase-orders/{id}PO by backlog item ID
+
GET/Health check
+
+
+
+
+
+ Frontend Component Structure
+
+
+
+
Views (Pages)
+
Dashboard.vueKPIs, charts, tables
+
Inventory.vueStock levels
+
Orders.vueOrder tracking
+
Demand.vueForecasts
+
Spending.vueCost analytics
+
Reports.vueQuarterly / monthly
+
Backlog.vueShortage tracking
+
+
+
+
UI Components
+
FilterBar.vue4 global filters
+
TasksModal.vueCRUD task manager
+
ProductDetailModalProduct deep dive
+
InventoryDetailModalStock item details
+
BacklogDetailModalShortage details
+
CostDetailModalCost breakdown
+
ProfileMenu.vueUser dropdown
+
LanguageSwitcherEN / JA toggle
+
+
+
+
Composables (Shared State)
+
useFilters.jsGlobal filter state
+
useI18n.jsTranslations + currency
+
useAuth.jsMock user + tasks
+
Data Files
+
inventory.json50+ SKUs
+
orders.json1000+ orders
+
spending.jsonCost summaries
+
backlog + demand + txSupporting 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
+
+
+
+
+
+
+
+
+
+
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)