From c51405e6d7a488aa51062df5e3538018078eca65 Mon Sep 17 00:00:00 2001 From: Vengudi Selvaraj Date: Thu, 27 Aug 2026 14:30:45 -0400 Subject: [PATCH] Add Restocking Orders feature: view, API, and Orders integration - Add Restocking.vue: new page for browsing and submitting restocking orders; lets warehouse staff select low-stock items and submit a consolidated order with budget validation - Register /restocking route in main.js and add nav link in App.vue - api.js: add getRestockingOrders() and createRestockingOrder() helpers - server/main.py: add RestockingOrder/RestockingItem Pydantic models; POST /api/restocking-orders (201, 7-day fixed lead time, sequential order number RST-YYYY-NNNN); GET /api/restocking-orders - server/mock_data.py: initialise in-memory restocking_orders list (resets on restart, consistent with the no-persistence demo pattern) - Orders.vue: show submitted restocking orders in a summary card below the main orders table; errors fetching supplemental data are logged but do not block the main orders view - Add architecture.html: high-level system diagram Co-Authored-By: Claude Sonnet 4.6 --- architecture.html | 621 ++++++++++++++++++++++++++++++++ client/src/App.vue | 3 + client/src/api.js | 10 + client/src/main.js | 4 +- client/src/views/Orders.vue | 107 +++++- client/src/views/Restocking.vue | 567 +++++++++++++++++++++++++++++ server/main.py | 58 ++- server/mock_data.py | 4 + 8 files changed, 1371 insertions(+), 3 deletions(-) create mode 100644 architecture.html create mode 100644 client/src/views/Restocking.vue diff --git a/architecture.html b/architecture.html new file mode 100644 index 000000000..336c4d6ad --- /dev/null +++ b/architecture.html @@ -0,0 +1,621 @@ + + + + + + Architecture — Factory Inventory Management + + + +
+ + +
+

Factory Inventory Management — System Architecture

+

Full-stack demo application with Vue 3 frontend, Python FastAPI backend, and in-memory mock data. No database — all data lives in JSON files loaded at server startup.

+
+ Vue 3 + Vite + FastAPI + Python + In-Memory / JSON +
+
+ + +
+

System Architecture

+
+ + +
+
Frontend — Port 3000
+
Vue 3 + Composition API
+
Vite dev server
+
vue-router 4 (6 routes)
+
Axios HTTP client
+
Custom i18n (EN / JA)
+
Custom SVG charts
+
No Vuex / Pinia
+
No UI framework
+
+ + +
+ + + + HTTP
REST
+
+ + +
+
Backend — Port 8001
+
FastAPI (Python)
+
Pydantic v2 models
+
uv package manager
+
CORS enabled (all origins)
+
In-memory filtering
+
/docs — Swagger UI
+
No ORM / database
+
No auth middleware
+
+ + +
+ + + + load at
startup
+
+ + +
+
Data — JSON Files
+
inventory.json
+
orders.json
+
demand_forecasts.json
+
backlog_items.json
+
spending.json
+
transactions.json
+
purchase_orders.json
+
mock_data.py loader
+
+ +
+
+ + +
+

Tech Stack

+
+
+
UI Framework
+
Vue 3.4
+
Composition API, no Options API
+
+
+
Build Tool
+
Vite 5.2
+
HMR, port 3000
+
+
+
Routing
+
vue-router 4.3
+
6 client-side routes
+
+
+
HTTP Client
+
Axios 1.6
+
Centralized in api.js
+
+
+
Backend Framework
+
FastAPI
+
Python, Pydantic v2 models
+
+
+
Package Manager
+
uv
+
pyproject.toml, fast resolver
+
+
+
State Management
+
Singleton Composables
+
Module-scoped refs, no Pinia
+
+
+
Internationalisation
+
Custom i18n
+
EN / JA, localStorage persistence
+
+
+
Charts
+
Custom SVG
+
Donut, bar, stacked, horizontal
+
+
+
Testing
+
pytest + TestClient
+
Backend only, tests/backend/
+
+
+
+ + +
+

Filter Data Flow — End to End

+
+ +
+
Step 1
+
FilterBar.vue
+
4 dropdowns bound with v-model to singleton refs in useFilters.js:
Period · Location · Category · Status
+
+
+ +
+ +
+
Step 2
+
View Watchers
+
Each view watches the relevant filter refs. Change fires loadData(), which calls getCurrentFilters() to build a params object.
+
+
+ +
+ +
+
Step 3
+
api.js
+
Builds URLSearchParams, omitting any filter set to 'all'. Sends GET /api/orders?warehouse=Tokyo&month=2025-09.
+
+
+ +
+ +
+
Step 4
+
FastAPI
+
apply_filters() runs sequential list comprehensions on in-memory data. Then filter_by_month() does substring match on order_date.
+
+
+ +
+ +
+
Step 5
+
Pydantic Response
+
Filtered results validated by Pydantic models, serialized to JSON, returned as response. Vue computes derived state from raw response.
+
+ +
+ +
+
+ + + + + + + + + + + + +
FilterRefMaps to paramSupported by
Time PeriodselectedPeriodmonth (or Q1-2025)Orders, Dashboard
LocationselectedLocationwarehouseAll endpoints
CategoryselectedCategorycategoryAll endpoints
Order StatusselectedStatusstatusOrders, Dashboard
+
+
+ + +
+

API Endpoints

+
+
+ GET + /api/inventory + Filtered inventory list  ?warehouse &category +
+
+ GET + /api/inventory/{item_id} + Single inventory item by ID +
+
+ GET + /api/orders + Filtered orders list  ?warehouse &category &status &month +
+
+ GET + /api/orders/{order_id} + Single order by ID +
+
+ GET + /api/dashboard/summary + Aggregate KPIs  ?warehouse &category &status &month +
+
+ GET + /api/demand + All demand forecasts (no filtering) +
+
+ GET + /api/backlog + All backlog items + computed has_purchase_order flag +
+
+ GET + /api/spending/summary + Spending totals + % changes +
+
+ GET + /api/spending/monthly + Monthly breakdown by cost type (12 months × 4 categories) +
+
+ GET + /api/spending/categories + Category spending breakdown +
+
+ GET + /api/spending/transactions + Recent transactions list +
+
+ GET + /api/reports/quarterly + Quarterly stats computed on-the-fly from orders +
+
+ GET + /api/reports/monthly-trends + Month-over-month trends computed on-the-fly from orders +
+
+
+ + +
+

Views & Components

+
+
+
Dashboard
+
views/Dashboard.vue
+
KPI cards, order health donut, inventory by category bar chart, backlog table, top products. Calls 4 APIs in parallel via Promise.all.
+
+
+
Inventory
+
views/Inventory.vue
+
Inventory table with client-side search. Sorts by stock status: Low Stock → Adequate → In Stock. Opens InventoryDetailModal.
+
+
+
Orders
+
views/Orders.vue
+
Orders table with status stat cards. Expandable item list per order using details/summary. Respects all 4 filters.
+
+
+
Demand
+
views/Demand.vue
+
Forecast cards grouped by trend: increasing / stable / decreasing. Top 5 items per group with % change. No filter support.
+
+
+
Spending
+
views/Spending.vue
+
Revenue vs costs bar chart, stacked monthly cost flow (procurement / operational / labor / overhead), financial KPI tiles.
+
+
+
Reports
+
views/Reports.vue
+
Quarterly performance table + monthly revenue bar chart. Data computed server-side from orders on each request.
+
+
+
FilterBar
+
components/FilterBar.vue
+
Global sticky filter bar. 4 dropdowns v-model bound to singleton refs. Rendered in App.vue, affects all views simultaneously.
+
+
+
Modals (5)
+
components/*Modal.vue
+
InventoryDetail, ProductDetail, BacklogDetail, CostDetail, ProfileDetails. All scoped and opened via parent state.
+
+
+
+ + +
+

Composables — Shared State

+
+
+
+
useFilters.js
+
+
Singleton filter state. Refs declared at module scope so all components share one instance. Exports selectedPeriod, selectedLocation, selectedCategory, selectedStatus, getCurrentFilters(), and resetFilters().
+
+
+
+
useI18n.js
+
+
Custom i18n engine. Locale stored in localStorage. t('key.nested') traverses nested objects with dot notation, falls back to English. Auto-switches currency: USD (EN) ↔ JPY at hardcoded rate of 150.
+
+
+
+
useAuth.js
+
+
Mock auth — always authenticated. User: Operations Manager, Supply Chain. Name and tasks switch to Japanese when locale is ja. logout() shows an alert only.
+
+
+
+ + +
+

Key Patterns

+
+
+
Singleton Composable Pattern
+
Reactive refs in useFilters and useI18n are declared at module scope, outside the exported function. Every component that imports the composable shares the exact same ref objects — no Vuex or Pinia needed.
+
+
+
In-Memory Filtering
+
All JSON data is loaded once at server startup into Python lists. Every request re-filters the full in-memory list using apply_filters() (sequential list comprehensions) and filter_by_month() (substring match on order_date).
+
+
+
Raw Refs + Computed Derivation
+
Views store raw API responses in ref() and expose all derived values (totals, grouped data, chart points) as computed() properties. This keeps re-renders minimal — Vue only recalculates what the template actually uses.
+
+
+
Client-Side Backlog Filtering
+
/api/backlog always returns all items. The view first fetches filtered inventory, builds a Set of SKUs, then keeps only backlog items whose item_sku appears in that set — warehouse/category filtering happens implicitly.
+
+
+
Quarter Expansion
+
When selectedPeriod is Q1-2025 through Q4-2025, filter_by_month() maps the quarter to its 3 month strings via QUARTER_MAP and checks any(m in order_date …), allowing filtering by full quarters.
+
+
+
Reports Computed On-the-Fly
+
/api/reports/quarterly and /api/reports/monthly-trends have no stored data — they scan the entire unfiltered orders list on every request and aggregate dynamically, so they always reflect the live data file.
+
+
+
+ + +
+

Known Gaps — Frontend Calls Missing Backend Endpoints

+
+
+
+
Tasks API
+
GET/POST/DELETE/PATCH /api/tasks — called by App.vue for the TasksModal (add, delete, toggle tasks). No handler exists in main.py. Calls return 404.
+
+
+
+
+
+
Purchase Orders API
+
POST /api/purchase-orders and GET /api/purchase-orders/{id} — called by Dashboard "Create PO" / "View PO" buttons. Not defined in main.py. purchase_orders.json exists but is empty [].
+
+
+
+
+
+
PurchaseOrderModal Component
+
Referenced in Dashboard.vue template but no corresponding file exists in src/components/. Will throw a Vue runtime warning.
+
+
+
+ +
+ + diff --git a/client/src/App.vue b/client/src/App.vue index c2da05a5c..2c6081c38 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -25,6 +25,9 @@ Reports + + Restocking + + +
+
+

Submitted Restocking Orders ({{ restockingOrders.length }})

+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Order #ItemsTotal CostBudgetSubmittedExpected DeliveryStatus
{{ order.order_number }}{{ order.items.length }} items{{ formatCurrency(order.total_cost) }}{{ formatCurrency(order.budget) }}{{ formatDate(order.submitted_date) }} + {{ formatDate(order.expected_delivery) }} + 7-day lead + + Submitted +
+
+
@@ -124,6 +161,17 @@ export default { } } + const restockingOrders = ref([]) + const loadRestockingOrders = async () => { + try { + restockingOrders.value = await api.getRestockingOrders() + } catch (err) { + // Restocking orders are supplemental data — a failure here should not block or + // disrupt the main orders view, so we swallow the error silently after logging it. + console.error('Failed to load restocking orders:', err) + } + } + // Watch for filter changes and reload data watch([selectedPeriod, selectedLocation, selectedCategory, selectedStatus], () => { loadOrders() @@ -153,16 +201,24 @@ export default { }) } - onMounted(loadOrders) + const formatCurrency = (val) => + Number(val).toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }) + + onMounted(() => { + loadOrders() + loadRestockingOrders() + }) return { t, loading, error, orders, + restockingOrders, getOrdersByStatus, getOrderStatusClass, formatDate, + formatCurrency, currencySymbol, translateProductName, translateCustomerName @@ -276,4 +332,53 @@ export default { font-size: 0.813rem; color: #64748b; } + +/* Restocking orders card */ +.restocking-card { + margin-top: 1.5rem; +} + +.restocking-table { + table-layout: fixed; + width: 100%; +} + +.rcol-order-number { + width: 140px; +} + +.rcol-items { + width: 90px; +} + +.rcol-cost { + width: 120px; +} + +.rcol-budget { + width: 120px; +} + +.rcol-date { + width: 130px; +} + +.rcol-delivery { + width: 160px; +} + +.rcol-status { + width: 110px; +} + +.mono { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; +} + +.delivery-sublabel { + display: block; + font-size: 0.75rem; + color: #94a3b8; + margin-top: 0.125rem; +} diff --git a/client/src/views/Restocking.vue b/client/src/views/Restocking.vue new file mode 100644 index 000000000..ca3f68a62 --- /dev/null +++ b/client/src/views/Restocking.vue @@ -0,0 +1,567 @@ + + + + + diff --git a/server/main.py b/server/main.py index a0c2d8c5a..632b2ff28 100644 --- a/server/main.py +++ b/server/main.py @@ -2,7 +2,7 @@ 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, restocking_orders app = FastAPI(title="Factory Inventory Management System") @@ -120,6 +120,28 @@ class CreatePurchaseOrderRequest(BaseModel): expected_delivery_date: str notes: Optional[str] = None +class RestockingItem(BaseModel): + sku: str + name: str + quantity: int + unit_cost: float + total_cost: float + +class RestockingOrder(BaseModel): + id: str + order_number: str + items: List[RestockingItem] + total_cost: float + budget: float + submitted_date: str + expected_delivery: str + status: str + +class CreateRestockingOrderRequest(BaseModel): + items: List[RestockingItem] + total_cost: float + budget: float + # API endpoints @app.get("/") def root(): @@ -304,6 +326,40 @@ def get_monthly_trends(): result.sort(key=lambda x: x['month']) return result +@app.post("/api/restocking-orders", response_model=RestockingOrder, status_code=201) +def create_restocking_order(request: CreateRestockingOrderRequest): + """Submit a new restocking order; delivery is always 7 days from submission.""" + from datetime import datetime, timedelta + + now = datetime.now() + # Fixed 7-day lead time across all categories (as per product spec) + delivery_date = now + timedelta(days=7) + + # Zero-padded sequential order number within the current year + seq = len(restocking_orders) + 1 + order_number = f"RST-{now.year}-{seq:04d}" + + new_order = { + "id": f"rst-{seq:04d}", + "order_number": order_number, + "items": [item.dict() for item in request.items], + "total_cost": round(request.total_cost, 2), + "budget": round(request.budget, 2), + "submitted_date": now.strftime("%Y-%m-%dT%H:%M:%S"), + "expected_delivery": delivery_date.strftime("%Y-%m-%d"), + "status": "Submitted", + } + + 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 (in-memory, resets on restart).""" + return restocking_orders + + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8001) diff --git a/server/mock_data.py b/server/mock_data.py index 2a9cd7dcb..8f4fb3f39 100644 --- a/server/mock_data.py +++ b/server/mock_data.py @@ -35,5 +35,9 @@ def load_json_file(filename): # Load purchase orders purchase_orders = load_json_file('purchase_orders.json') +# In-memory store for restocking orders — not backed by a JSON file; +# resets on server restart (consistent with the no-persistence demo pattern) +restocking_orders = [] + # All data is now loaded from JSON files in the data/ directory # This allows for easier maintenance and updates of the sample data