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') }}

+ +
+
+

Submitted Restocking Orders

+ {{ restockingOrders.length }} +
+
+ + + + + + + + + + + + + + + + + + + + + +
Order #ItemsStatusOrder DateEst. Delivery (7 days)Total Value
{{ ro.order_number }} +
+ {{ ro.items.length }} item{{ ro.items.length !== 1 ? 's' : '' }} +
+
+ {{ item.name }} + SKU: {{ item.sku }}  ·  Qty: {{ item.quantity }} @ ${{ item.unit_cost.toFixed(2) }} +
+
+
+
+ {{ ro.status }} + {{ formatDate(ro.order_date) }}{{ formatDate(ro.expected_delivery) }}${{ ro.total_value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
+
+
+
{{ t('common.loading') }}
{{ error }}
@@ -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 + + + + +
+
+

Inventory Management System — Architecture

+

Full-stack factory inventory platform · Vue 3 + FastAPI · In-memory data

+
+
+ + Local dev running +
+
+ +
+ + +
+

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
+
+
+
+ +
+ +
+ 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)