From 9c7be4b238c8ba3c85f2f104ee29376c33449fde Mon Sep 17 00:00:00 2001 From: Christopher Michael Date: Mon, 20 Jul 2026 10:39:41 +0100 Subject: [PATCH 1/6] Add coding convention to document non-obvious logic changes Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 89c307d15..0f1612775 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,6 +69,9 @@ npm install && npm run dev - Data: `server/data/*.json` - Styles: `client/src/App.vue` +## Coding Conventions +- Always document non-obvious logic changes with comments + ## Design System - Colors: Slate/gray (#0f172a, #64748b, #e2e8f0) - Status: green/blue/yellow/red From 09b2dc15a4e08f281d7cb215c0e4eaa477ad80c5 Mon Sep 17 00:00:00 2001 From: Christopher Michael Date: Mon, 20 Jul 2026 11:02:38 +0100 Subject: [PATCH 2/6] Add Restocking tab with budget-based restock recommendations Add a new Restocking tab where users set an available budget via a slider and receive restock recommendations derived from the demand forecast, filling the largest demand gaps first until the budget is exhausted. Submitted orders are stored in-memory on the backend and surfaced in a new "Submitted Orders" section of the Orders tab with delivery lead time. Backend (server/main.py): - GET /api/restock/candidates: demand-gap items enriched with estimated unit cost, recommended quantity, line total, and per-category lead time - POST /api/restock-orders: validates non-empty + within-budget, stamps order number/dates/lead time, stores in-memory - GET /api/restock-orders: lists submitted orders (newest first) Frontend: - Restocking.vue: budget slider, greedy recommendation table, summary cards, and Place Order flow - Orders.vue: new Submitted Orders section showing delivery lead time - api.js, main.js, App.vue: API methods, route, and nav link Co-Authored-By: Claude Opus 4.8 (1M context) --- client/src/App.vue | 3 + client/src/api.js | 15 ++ client/src/main.js | 2 + client/src/views/Orders.vue | 145 ++++++++++++- client/src/views/Restocking.vue | 352 ++++++++++++++++++++++++++++++++ server/main.py | 164 +++++++++++++++ 6 files changed, 680 insertions(+), 1 deletion(-) create mode 100644 client/src/views/Restocking.vue diff --git a/client/src/App.vue b/client/src/App.vue index c2da05a5c..e3ed2a692 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -16,6 +16,9 @@ {{ t('nav.orders') }} + + Restocking + {{ t('nav.finance') }} diff --git a/client/src/api.js b/client/src/api.js index 11cb9db70..7fb9ea4d1 100644 --- a/client/src/api.js +++ b/client/src/api.js @@ -102,5 +102,20 @@ export const api = { async getPurchaseOrderByBacklogItem(backlogItemId) { const response = await axios.get(`${API_BASE_URL}/purchase-orders/${backlogItemId}`) return response.data + }, + + async getRestockCandidates() { + const response = await axios.get(`${API_BASE_URL}/restock/candidates`) + return response.data + }, + + async submitRestockOrder(payload) { + const response = await axios.post(`${API_BASE_URL}/restock-orders`, payload) + return response.data + }, + + async getRestockOrders() { + const response = await axios.get(`${API_BASE_URL}/restock-orders`) + return response.data } } diff --git a/client/src/main.js b/client/src/main.js index 477c2d966..611c0a3b1 100644 --- a/client/src/main.js +++ b/client/src/main.js @@ -7,6 +7,7 @@ import Orders from './views/Orders.vue' import Demand from './views/Demand.vue' import Spending from './views/Spending.vue' import Reports from './views/Reports.vue' +import Restocking from './views/Restocking.vue' const router = createRouter({ history: createWebHistory(), @@ -14,6 +15,7 @@ const router = createRouter({ { path: '/', component: Dashboard }, { path: '/inventory', component: Inventory }, { path: '/orders', component: Orders }, + { path: '/restocking', component: Restocking }, { path: '/demand', component: Demand }, { path: '/spending', component: Spending }, { path: '/reports', component: Reports } diff --git a/client/src/views/Orders.vue b/client/src/views/Orders.vue index 7413f6e66..cd8298dde 100644 --- a/client/src/views/Orders.vue +++ b/client/src/views/Orders.vue @@ -5,6 +5,36 @@

{{ t('orders.description') }}

+ +
{{ t('common.loading') }}
{{ error }}
@@ -96,6 +126,10 @@ export default { const error = ref(null) const orders = ref([]) + const restockOrders = ref([]) + const restockLoading = ref(true) + const restockError = ref(null) + // Use shared filters const { selectedPeriod, @@ -129,6 +163,18 @@ export default { loadOrders() }) + const loadRestockOrders = async () => { + try { + restockLoading.value = true + restockError.value = null + restockOrders.value = await api.getRestockOrders() + } catch (err) { + restockError.value = 'Failed to load submitted restock orders: ' + err.message + } finally { + restockLoading.value = false + } + } + const getOrdersByStatus = (status) => { return orders.value.filter(order => order.status === status) } @@ -153,13 +199,19 @@ export default { }) } - onMounted(loadOrders) + onMounted(() => { + loadOrders() + loadRestockOrders() + }) return { t, loading, error, orders, + restockOrders, + restockLoading, + restockError, getOrdersByStatus, getOrderStatusClass, formatDate, @@ -172,6 +224,97 @@ export default { diff --git a/server/main.py b/server/main.py index a0c2d8c5a..5a62705a0 100644 --- a/server/main.py +++ b/server/main.py @@ -1,9 +1,14 @@ +import datetime 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 +# In-memory store for restock orders submitted from the Restocking tab. +# Matches the app's mock-data pattern: lives for the process lifetime, cleared on restart. +submitted_orders: List[dict] = [] + app = FastAPI(title="Factory Inventory Management System") # Quarter mapping for date filtering @@ -46,6 +51,57 @@ def apply_filters(items: list, warehouse: Optional[str] = None, category: Option return filtered +# --- Restocking helpers ----------------------------------------------------- +# Simulated delivery lead time (in days) per product category. Real supply-chain +# lead times vary by product type, so we model that instead of a single default. +CATEGORY_LEAD_TIME_DAYS = { + "Circuit Boards": 10, + "Sensors": 7, + "Actuators": 21, + "Controllers": 14, + "Power Supplies": 12, +} +DEFAULT_LEAD_TIME_DAYS = 9 + +def infer_category(item_name: str, sku: str) -> str: + """Best-effort category for a demand-forecast item. + + Most demand SKUs are not present in the inventory dataset, so we fall back to + keyword matching on the item name when no inventory record exists. + """ + inv = next((i for i in inventory_items if i["sku"] == sku), None) + if inv: + return inv["category"] + + name = item_name.lower() + if any(k in name for k in ("motor", "servo", "stepper", "actuator")): + return "Actuators" + if "sensor" in name: + return "Sensors" + if any(k in name for k in ("controller", "board", "logic")): + return "Controllers" + if "power supply" in name or "psu" in name: + return "Power Supplies" + if "pcb" in name or "circuit" in name: + return "Circuit Boards" + return "General" + +def estimate_unit_cost(sku: str) -> float: + """Unit cost from inventory when the SKU exists there; otherwise a stable estimate. + + The estimate is derived deterministically from the SKU string so the same item + always prices the same across requests (no randomness). + """ + inv = next((i for i in inventory_items if i["sku"] == sku), None) + if inv: + return float(inv["unit_cost"]) + base = sum(ord(c) for c in sku) + return round(15 + (base % 200), 2) + +def lead_time_for(category: str) -> int: + """Delivery lead time in days for a given category.""" + return CATEGORY_LEAD_TIME_DAYS.get(category, DEFAULT_LEAD_TIME_DAYS) + # CORS middleware app.add_middleware( CORSMiddleware, @@ -120,6 +176,42 @@ class CreatePurchaseOrderRequest(BaseModel): expected_delivery_date: str notes: Optional[str] = None +class RestockCandidate(BaseModel): + item_sku: str + item_name: str + category: str + current_demand: int + forecasted_demand: int + recommended_quantity: int + unit_cost: float + line_total: float + lead_time_days: int + +class RestockOrderItem(BaseModel): + item_sku: str + item_name: str + category: str + quantity: int + unit_cost: float + line_total: float + lead_time_days: int + +class CreateRestockOrderRequest(BaseModel): + budget: float + items: List[RestockOrderItem] + +class RestockOrder(BaseModel): + id: str + order_number: str + status: str + order_date: str + expected_delivery: str + lead_time_days: int + budget: float + total_value: float + item_count: int + items: List[RestockOrderItem] + # API endpoints @app.get("/") def root(): @@ -166,6 +258,78 @@ def get_demand_forecasts(): """Get demand forecasts""" return demand_forecasts +@app.get("/api/restock/candidates", response_model=List[RestockCandidate]) +def get_restock_candidates(): + """Restock recommendations derived from the demand forecast. + + Includes only items with a positive demand gap (forecasted > current). Each item + is enriched with an estimated unit cost, the recommended quantity (the gap), the + resulting line total, and a simulated delivery lead time. Sorted by largest gap + first so the frontend can greedily fill a budget starting with the biggest shortfalls. + """ + candidates = [] + for f in demand_forecasts: + gap = f["forecasted_demand"] - f["current_demand"] + if gap <= 0: + continue + category = infer_category(f["item_name"], f["item_sku"]) + unit_cost = estimate_unit_cost(f["item_sku"]) + candidates.append({ + "item_sku": f["item_sku"], + "item_name": f["item_name"], + "category": category, + "current_demand": f["current_demand"], + "forecasted_demand": f["forecasted_demand"], + "recommended_quantity": gap, + "unit_cost": unit_cost, + "line_total": round(gap * unit_cost, 2), + "lead_time_days": lead_time_for(category), + }) + candidates.sort(key=lambda c: c["recommended_quantity"], reverse=True) + return candidates + +@app.get("/api/restock-orders", response_model=List[RestockOrder]) +def get_restock_orders(): + """Get all submitted restock orders, newest first.""" + return list(reversed(submitted_orders)) + +@app.post("/api/restock-orders", response_model=RestockOrder, status_code=201) +def create_restock_order(request: CreateRestockOrderRequest): + """Submit a restock order. + + Validates the order is non-empty and within budget, then stamps it with an order + number, submission date, and expected delivery date. The order's overall lead time + is the longest lead time among its items (everything has arrived by then). The + resulting order is surfaced in the Orders tab's Submitted Orders section. + """ + if not request.items: + raise HTTPException(status_code=400, detail="Restock order must contain at least one item") + + total_value = round(sum(item.line_total for item in request.items), 2) + # Small epsilon guards against float rounding when the total exactly equals the budget. + if total_value > request.budget + 0.001: + raise HTTPException(status_code=400, detail="Order total exceeds the available budget") + + max_lead = max(item.lead_time_days for item in request.items) + order_date = datetime.date.today() + expected_delivery = order_date + datetime.timedelta(days=max_lead) + seq = len(submitted_orders) + 1 + + order = { + "id": f"restock-{seq}", + "order_number": f"RO-{1000 + seq}", + "status": "Submitted", + "order_date": order_date.isoformat(), + "expected_delivery": expected_delivery.isoformat(), + "lead_time_days": max_lead, + "budget": round(request.budget, 2), + "total_value": total_value, + "item_count": len(request.items), + "items": [item.model_dump() for item in request.items], + } + submitted_orders.append(order) + return order + @app.get("/api/backlog", response_model=List[BacklogItem]) def get_backlog(): """Get backlog items with purchase order status""" From 5986da46e60c28f47350c1c05036b14f9ccfdfdf Mon Sep 17 00:00:00 2001 From: Christopher Michael Date: Mon, 20 Jul 2026 11:03:53 +0100 Subject: [PATCH 3/6] Add HTML architecture reference page Standalone docs/architecture.html documenting the system architecture (three-layer diagram), tech stack, and data flow, generated from source inspection of server/ and client/. Uses the app's slate/gray design system. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/architecture.html | 403 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 403 insertions(+) create mode 100644 docs/architecture.html diff --git a/docs/architecture.html b/docs/architecture.html new file mode 100644 index 000000000..a40318ea3 --- /dev/null +++ b/docs/architecture.html @@ -0,0 +1,403 @@ + + + + + + Architecture — Factory Inventory Management + + + +
+
+

System Architecture

+

Factory Inventory Management System

+

A full-stack demo application for factory inventory, orders, demand, and spending analytics. Vue 3 single-page frontend, a stateless FastAPI backend, and in-memory mock data loaded from JSON at startup — no database.

+
+ Vue 3 + Vite + Python FastAPI + In-memory JSON + REST over HTTP + SPA +
+
+
+ + +
+
+
+

System Architecture

+

Three cleanly separated layers. The browser runs the SPA; the API server filters data in-process; JSON files seed the in-memory store at boot.

+
+ +
+
+ Client · Port 3000 +

Presentation — Vue 3 SPA

+

Composition API, client-side routing, hand-rolled SVG charts. Served by Vite in dev.

+
+ App.vue — layout + global FilterBar + vue-router — 6 routes + Dashboard + Inventory + Orders + Demand + Spending + Reports + useFilters — singleton state + api.js — axios client +
+
+ +
+ + HTTP · REST · JSON  (GET /api/* · CORS open) +
+ +
+ Server · Port 8001 +

Application — FastAPI

+

Stateless request handling. Pure-Python filtering over in-memory lists, Pydantic response validation, Swagger at /docs.

+
+ main.py — 14 GET endpoints + apply_filters() — warehouse / category / status + filter_by_month() — month + quarter + Pydantic models + CORSMiddleware +
+
+ +
+ + Loaded once at import  (mock_data.py) +
+ +
+ Data · In-memory +

Data — JSON Mock Store

+

Read from server/data/ into module-level globals at startup. No persistence; a restart reloads from disk.

+
+ inventory.json · 32 + orders.json · 250 + demand_forecasts.json · 9 + backlog_items.json · 4 + spending.json · 3 sets + transactions.json · 56 + purchase_orders.json · 0 +
+
+
+
+
+ + +
+
+
+

Tech Stack

+

Minimal, dependency-light stack. No database, no charting library, no CSS framework — charts and styles are hand-built.

+
+ +
+
+

Frontend

+

Client · Port 3000

+
FrameworkVue 3.4 (Composition API)
+
Build toolVite 5
+
Routingvue-router 4
+
HTTP clientaxios 1.6
+
ChartsInline SVG (custom)
+
StylingScoped CSS (no framework)
+
+ +
+

Backend

+

Server · Port 8001

+
FrameworkFastAPI 0.110+
+
ServerUvicorn
+
ValidationPydantic 2
+
RuntimePython 3.11+
+
CORSFully open (dev)
+
DocsSwagger UI at /docs
+
+ +
+

Data & Tooling

+

Storage · Dev

+
StoreIn-memory globals
+
Source7 JSON files
+
PersistenceNone
+
Py package mgruv
+
JS package mgrnpm
+
TransportREST / JSON
+
+
+
+
+ + +
+
+
+

Data Flow

+

A single filter change propagates through the singleton filter store, out to the API, and back into reactive views.

+
+ +
+
+
1
+

User selects a filter

+

<select> in FilterBar.vue — Time Period, Warehouse, Category, or Order Status.

+
+
+
2
+

Shared state mutates

+

v-model updates a module-level ref in useFilters (singleton), shared by every view.

+
+
+
3
+

Watchers fire

+

Each mounted view watches its filters and re-runs its loader via api.js.

+
+
+
4
+

API filters in-memory

+

FastAPI runs apply_filters() / filter_by_month() over the JSON-seeded lists, validates with Pydantic.

+
+
+
5
+

Reactive render

+

Response lands in a ref; computed props transform it into tables and SVG charts.

+
+
+ +
+

Primary API Surface

+
+ + + + + + + + + + + + + +
MethodEndpointFiltersReturns
GET/api/inventorywarehouse, categoryStock items (Pydantic-validated)
GET/api/orderswarehouse, category, status, monthCustomer orders
GET/api/dashboard/summarywarehouse, category, status, monthKPI aggregates
GET/api/demandDemand forecasts
GET/api/backlogBacklog + computed PO flag
GET/api/spending/*— (client-side filtering)Summary, monthly, categories, transactions
GET/api/reports/*— (inline bucketing)Quarterly & monthly trends
+
+
+ + +
+
+
+

Architectural Notes

+

Characteristics worth knowing before extending the system.

+
+
+
+

Stateless, in-process filtering

+

Every request filters in-memory lists with plain Python comprehensions — no caching, indexes, or pagination. Fine at demo scale (250 orders); the bottleneck under real load.

+
+
+

Singleton filter state

+

The 4 filters live as module-level refs in useFilters, so the FilterBar and all views share one source of truth without a store like Pinia.

+
+
+

No proxy — CORS bridges the gap

+

Vite has no dev proxy; the client hits http://localhost:8001/api directly. The backend's fully-open CORS (allow_origins=["*"]) is dev-only and must be tightened for production.

+
+
+

Read-only demo, mutations stubbed

+

Only GET endpoints are implemented. api.js declares task/purchase-order POST/PATCH/DELETE calls and CreatePurchaseOrderRequest exists, but no backend routes back them — purchase_orders.json is empty.

+
+
+
+
+ +
+
+

Factory Inventory Management System — architecture reference. Generated from source inspection of server/ and client/.

+
+
+ + From e98a6b6bb19ace0aba4230150a835c8d35b7fba5 Mon Sep 17 00:00:00 2001 From: Christopher Michael Date: Mon, 20 Jul 2026 11:52:55 +0100 Subject: [PATCH 4/6] Redesign UI into modern SaaS interface with left sidebar Replace the top navigation bar with a dark vertical sidebar and modernize the layout into a SaaS-style shell with a consistent design-token system. - Add AppSidebar.vue: dark sidebar with brand, icon+label nav (router-driven active state, aria-current, left accent bar), responsive icon-only collapse - Rewrite App.vue shell: sidebar + main column (top bar with page title, LanguageSwitcher, ProfileMenu), FilterBar strip, centered content region - Add design tokens (color, spacing, radius, shadow, type) and refactor the shared global utility classes (cards, stat cards, tables, badges) onto them, modernizing card layouts across all views at once - Retokenize FilterBar and fix its sticky offset to the new top-bar height Behavior preserved: routing, i18n, filters, profile/tasks modals, data flow. Co-Authored-By: Claude Opus 4.8 (1M context) --- client/src/App.vue | 388 +++++++++++++++------------ client/src/components/AppSidebar.vue | 199 ++++++++++++++ client/src/components/FilterBar.vue | 60 +++-- 3 files changed, 445 insertions(+), 202 deletions(-) create mode 100644 client/src/components/AppSidebar.vue diff --git a/client/src/App.vue b/client/src/App.vue index e3ed2a692..262911c51 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -1,45 +1,27 @@