From e252bc44eb035c699b761a1fa62e7104f0937d60 Mon Sep 17 00:00:00 2001 From: Shravan Date: Thu, 18 Jun 2026 13:53:36 +1000 Subject: [PATCH] Add Restocking tab with budget-based demand recommendations and order submission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New Restocking Planner view (/restocking) with budget slider ($5K–$100K), greedy auto-selection of demand forecast items by priority, dimming of over-budget rows, sticky summary bar, and success/error banners - Two new FastAPI endpoints: GET /api/restocking/recommendations and POST /api/restocking/orders with 14-day lead time calculation - Submitted restocking orders appear in Orders tab under a new "Submitted Restocking Orders" section with expandable item details - New api.js methods: getRestockingRecommendations, getRestockingOrders, placeRestockingOrder - Added architecture.html reference doc for system overview Co-Authored-By: Claude Sonnet 4.6 --- architecture.html | 746 ++++++++++++++++++++++++++++++++ client/src/App.vue | 3 + client/src/api.js | 15 + client/src/main.js | 4 +- client/src/views/Orders.vue | 107 ++++- client/src/views/Restocking.vue | 471 ++++++++++++++++++++ server/main.py | 127 ++++++ 7 files changed, 1471 insertions(+), 2 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 00000000..d5f76101 --- /dev/null +++ b/architecture.html @@ -0,0 +1,746 @@ + + + + + + Factory Inventory Management — Architecture + + + +
+ + +
+
+ Demo + Vue 3 + FastAPI + In-Memory +
+

Factory Inventory Management

+

System architecture, tech stack, and data flow reference

+
+ + +
+
At a Glance
+
+
6
Views
+
14
API Endpoints
+
7
JSON Data Files
+
~50
Inventory SKUs
+
2000+
Orders
+
3
Warehouses
+
4
Global Filters
+
2
Languages
+
+
+ + +
+
Tech Stack
+
+
+
Frontend Framework
+
Vue 3
+
Composition API, <script setup>, Reactivity
+
+
+
Build Tool
+
Vite
+
Port 3000, Hot module replacement
+
+
+
HTTP Client
+
Axios
+
Base URL: localhost:8001/api
+
+
+
Backend Framework
+
FastAPI
+
Uvicorn ASGI, Port 8001, auto Swagger docs
+
+
+
Data Validation
+
Pydantic v2
+
Response models for all endpoints
+
+
+
Data Layer
+
In-Memory JSON
+
7 JSON files loaded on startup, no DB
+
+
+
Python Runtime
+
uv / venv
+
Python 3.11+, pyproject.toml
+
+
+
i18n
+
Custom useI18n
+
English + Japanese via composable
+
+
+
+ + +
+
System Architecture
+
+ + +
+
Frontend — :3000
+
+
+
App.vue
+
Root, sidebar nav, global layout
+
+
+
FilterBar.vue
+
4 global filter dropdowns
+
+
+
useFilters.js
+
Shared filter state composable
+
+
+
6 Views
+
Dashboard, Inventory, Orders, Demand, Spending, Reports
+
+
+
9 Components
+
Modals, ProfileMenu, LanguageSwitcher
+
+
+
api.js
+
Axios client, all API methods
+
+
+
Vue Router
+
6 client-side routes
+
+
+
+ + +
+
HTTP
REST
+
+
+
JSON
CORS
+
+ + +
+
Backend — :8001
+
+
+
FastAPI App
+
main.py — 14 endpoints, CORS open
+
+
+
Filter Helpers
+
filter_by_month(), apply_filters()
+
+
+
Pydantic Models
+
InventoryItem, Order, DemandForecast, BacklogItem, PurchaseOrder
+
+
+
mock_data.py
+
JSON loader, data held in memory
+
+
+
Quarter Mapping
+
Q1–Q4 2025 → month arrays
+
+
+
/docs
+
Auto-generated Swagger UI
+
+
+
+ + +
+
Startup
load
+
+
+
Read
only
+
+ + +
+
Data — JSON Files
+
+
inventory.json
~50 SKUs, 3 warehouses, 5 categories
+
orders.json
2000+ orders, Jan–Sep 2025
+
demand_forecasts.json
9 items, trend forecasts
+
backlog_items.json
4 unfulfilled order items
+
spending.json
Summary + 12-month breakdown
+
transactions.json
Recent financial transactions
+
purchase_orders.json
Placeholder (empty array)
+
+
+ +
+
+ + +
+
Request Data Flow
+
+
+
+
1
+
+
User changes a filter
+
FilterBar.vue dropdowns are bound to reactive refs in useFilters.js — changing any filter updates shared state instantly.
+
+
+
+
2
+
+
View watches filter state
+
Each view watch()es the filter composable. On change it calls getCurrentFilters() to build { warehouse, category, status, month }.
+
+
+
+
3
+
+
API call via api.js
+
e.g. api.getOrders({ warehouse: "Tokyo", status: "shipped", month: "2025-03" }) — Axios serializes the object as query params.
+
+
+
+
4
+
+
FastAPI receives request
+
Endpoint reads in-memory data (loaded from JSON at startup). Calls apply_filters(items, warehouse, category, status) then filter_by_month() if month param present.
+
+
+
+
5
+
+
Pydantic validates & serializes
+
Response model (e.g. List[Order]) validates each item before serialization. Invalid fields raise 422 automatically.
+
+
+
+
6
+
+
Vue renders result
+
Raw data stored in ref() (e.g. allOrders). Derived stats live in computed() properties — recomputed automatically when raw data changes.
+
+
+
+
+
+ + +
+
Views & Routes
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RouteViewDescriptionAPI EndpointsFilters
/DashboardKPIs, order health donut, monthly trends/api/dashboard/summaryAll 4
/inventoryInventorySKU table, search, detail modal/api/inventoryWH + Cat
/ordersOrdersStatus cards, order table, expandable items/api/ordersAll 4
/demandDemandTrend cards, forecast table/api/demandNone
/spendingFinanceRevenue/cost KPIs, bar charts, transactions/api/spending/summary
/api/spending/monthly
/api/spending/categories
/api/spending/transactions
None
/reportsReportsQuarterly perf table, MoM analysis/api/reports/quarterly
/api/reports/monthly-trends
None
+
+
+ + +
+
API Endpoints
+
+ +
+
Inventory
+
GET/api/inventorywarehouse, category filters
+
GET/api/inventory/{item_id}single item by ID
+
+ +
+
Orders
+
GET/api/orderswarehouse, category, status, month filters
+
GET/api/orders/{order_id}single order by ID
+
+ +
+
Dashboard
+
GET/api/dashboard/summaryall 4 filters — KPI aggregates
+
+ +
+
Demand & Backlog
+
GET/api/demandno filters
+
GET/api/backlogno filters, includes PO status
+
+ +
+
Spending / Finance
+
GET/api/spending/summarytop-level cost totals
+
GET/api/spending/monthly12-month cost breakdown
+
GET/api/spending/categoriesspend by category
+
GET/api/spending/transactionsrecent transactions list
+
+ +
+
Reports
+
GET/api/reports/quarterlyQ1–Q4 performance aggregates
+
GET/api/reports/monthly-trendsmonth-by-month trends
+
+ +
+
Not Yet Implemented
+
GET/api/taskstask CRUD (frontend calls exist)
+
POST/api/purchase-orderscreate PO from backlog item
+
+ +
+
+ + +
+
Global Filter System
+
+ All 4 filters live in useFilters.js as shared reactive state. + FilterBar.vue renders them; views watch them and re-fetch on change. Backend applies them in order: warehouse → category → status → month. +
+
+
+
Time Period
+
selectedPeriod
+
+ all + 2025-01 … 2025-09 + Q1-2025 … Q4-2025 +
+
Applies to: Orders, Dashboard
+
Not on: Inventory (no time dim)
+
+
+
Warehouse / Location
+
selectedLocation
+
+ all + San Francisco + London + Tokyo +
+
Applies to: Inventory, Orders, Dashboard
+
+
+
Category
+
selectedCategory
+
+ all + circuit boards + sensors + actuators + controllers + power supplies +
+
Applies to: Inventory, Orders, Dashboard
+
+
+
Order Status
+
selectedStatus
+
+ all + delivered + shipped + processing + backordered +
+
Applies to: Orders, Dashboard
+
Not on: Inventory
+
+
+
+ + +
+
Data Files — server/data/
+
+
+
inventory.json
+
~50 inventory SKUs across 3 warehouses
+
Fields: id, sku, name, category, warehouse, quantity_on_hand, reorder_point, unit_cost, location, last_updated
+
+
+
orders.json
+
2000+ orders spanning Jan–Sep 2025
+
Fields: id, order_number, customer, items[], status, order_date, expected_delivery, total_value, warehouse, category, actual_delivery
+
+
+
demand_forecasts.json
+
9 forecast records, "next 30 days" horizon
+
Fields: id, item_sku, item_name, current_demand, forecasted_demand, trend (increasing / stable / decreasing), period
+
+
+
backlog_items.json
+
4 unfulfilled items due to stock shortage
+
Fields: id, order_id, item_sku, item_name, quantity_needed, quantity_available, days_delayed, priority (high / medium / low)
+
+
+
spending.json
+
Financial data — summary + 12-month breakdown
+
Keys: spending_summary, monthly_spending (12 mo), category_spending — procurement / operational / labor / overhead
+
+
+
transactions.json
+
Recent financial transactions for audit trail
+
Used by: /api/spending/transactions
+
+
+
purchase_orders.json
+
Placeholder — currently empty array []
+
Future fields: id, backlog_item_id, supplier_name, quantity, unit_cost, expected_delivery_date, status
+
+
+
+ + +
+
Pydantic Data Models
+
+
+
InventoryItem
+
+ id: int
sku: str
name: str
category: str
warehouse: str
quantity_on_hand: int
reorder_point: int
unit_cost: float
location: str
last_updated: str +
+
+
+
Order
+
+ id: int
order_number: str
customer: str
items: list[dict]
status: str
order_date: str
expected_delivery: str
total_value: float
warehouse: str
category: str
actual_delivery: str | None +
+
+
+
DemandForecast
+
+ id: int
item_sku: str
item_name: str
current_demand: int
forecasted_demand: int
trend: str
period: str +
+
+
+
BacklogItem
+
+ id: int
order_id: str
item_sku: str
item_name: str
quantity_needed: int
quantity_available: int
days_delayed: int
priority: str
has_purchase_order: bool | None +
+
+
+
+ + +
+
Key File Locations
+
+
+
Frontend — client/src/
+
+ views/*.vue — 6 page-level views
+ components/*.vue — 9 reusable components
+ composables/useFilters.js — global filter state
+ composables/useAuth.js — auth/user state
+ composables/useI18n.js — EN/JP translations
+ api.js — all Axios API methods
+ main.js — Vue app + router setup
+ App.vue — root layout, sidebar, filters +
+
+
+
Backend — server/
+
+ main.py — FastAPI app, all endpoints
+ mock_data.py — JSON file loader
+ data/inventory.json — ~50 SKUs
+ data/orders.json — 2000+ orders
+ data/spending.json — financial data
+ data/demand_forecasts.json
+ data/backlog_items.json
+ pyproject.toml — dependencies +
+
+
+
+ +
+ Factory Inventory Management — Architecture Reference  ·  Generated 2026-06-18 +
+ +
+ + 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 + + +
+
+

+ Submitted Restocking Orders + {{ restockingOrders.length }} +

+
+
+ No restocking orders submitted yet. +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Order #Order DateItemsStatusEst. DeliveryLead TimeTotal Cost
{{ order.order_number }}{{ formatDate(order.order_date) }} +
+ {{ order.items.length }} items +
+
+ {{ item.item_name }} + {{ item.sku }} — Qty: {{ item.quantity }} @ ${{ item.unit_cost.toFixed(2) }} +
+
+
+
+ {{ order.status }} + {{ formatDate(order.estimated_delivery) }}{{ order.lead_time_days }} days${{ order.total_cost.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
+
+
@@ -95,6 +145,7 @@ export default { const loading = ref(true) const error = ref(null) const orders = ref([]) + const restockingOrders = ref([]) // Use shared filters const { @@ -153,13 +204,26 @@ export default { }) } - onMounted(loadOrders) + const loadRestockingOrders = async () => { + try { + restockingOrders.value = await api.getRestockingOrders() + } catch (err) { + console.error('Failed to load restocking orders:', err) + } + } + + onMounted(() => { + loadOrders() + loadRestockingOrders() + }) return { t, loading, error, orders, + restockingOrders, + loadRestockingOrders, getOrdersByStatus, getOrderStatusClass, formatDate, @@ -276,4 +340,45 @@ export default { font-size: 0.813rem; color: #64748b; } + +/* Restocking orders table */ +.restock-orders-table { + table-layout: fixed; + width: 100%; +} + +.rst-col-number { + width: 180px; +} + +.rst-col-date { + width: 130px; +} + +.rst-col-items { + width: 160px; +} + +.rst-col-status { + width: 120px; +} + +.rst-col-delivery { + width: 130px; +} + +.rst-col-lead { + width: 100px; +} + +.rst-col-cost { + width: 140px; +} + +.empty-restock { + padding: 1.5rem 0.75rem; + color: #64748b; + font-size: 0.938rem; + font-style: italic; +} diff --git a/client/src/views/Restocking.vue b/client/src/views/Restocking.vue new file mode 100644 index 00000000..b139457d --- /dev/null +++ b/client/src/views/Restocking.vue @@ -0,0 +1,471 @@ + + + + + diff --git a/server/main.py b/server/main.py index a0c2d8c5..7dda6518 100644 --- a/server/main.py +++ b/server/main.py @@ -2,6 +2,7 @@ from fastapi.middleware.cors import CORSMiddleware from typing import List, Optional from pydantic import BaseModel +from datetime import datetime, timedelta from mock_data import inventory_items, orders, demand_forecasts, backlog_items, spending_summary, monthly_spending, category_spending, recent_transactions, purchase_orders app = FastAPI(title="Factory Inventory Management System") @@ -14,6 +15,26 @@ 'Q4-2025': ['2025-10', '2025-11', '2025-12'] } +# Supplier unit costs for demand forecast SKUs +RESTOCK_UNIT_COSTS = { + 'WDG-001': 12.50, + 'BRG-102': 28.75, + 'GSK-203': 8.99, + 'MTR-304': 245.00, + 'FLT-405': 15.50, + 'VLV-506': 67.25, + 'PSU-501': 45.00, + 'SNR-420': 22.50, + 'CTL-330': 89.99, +} + +TREND_PRIORITY = {'increasing': 'high', 'stable': 'medium', 'decreasing': 'low'} +TREND_SORT_ORDER = {'increasing': 0, 'stable': 1, 'decreasing': 2} + +# In-memory restocking orders store (persists for server lifetime) +restocking_orders: list = [] +_restocking_order_counter = [1] + def filter_by_month(items: list, month: Optional[str]) -> list: """Filter items by month/quarter based on order_date field""" if not month or month == 'all': @@ -120,6 +141,39 @@ class CreatePurchaseOrderRequest(BaseModel): expected_delivery_date: str notes: Optional[str] = None +# Restocking models +class RestockingRecommendation(BaseModel): + sku: str + item_name: str + trend: str + priority: str + current_demand: int + forecasted_demand: int + recommended_quantity: int + unit_cost: float + total_cost: float + period: str + +class RestockingOrderItem(BaseModel): + sku: str + item_name: str + quantity: int + unit_cost: float + total_cost: float + +class RestockingOrder(BaseModel): + id: str + order_number: str + order_date: str + estimated_delivery: str + lead_time_days: int + total_cost: float + status: str + items: List[RestockingOrderItem] + +class PlaceRestockingOrderRequest(BaseModel): + items: List[dict] + # API endpoints @app.get("/") def root(): @@ -304,6 +358,79 @@ def get_monthly_trends(): result.sort(key=lambda x: x['month']) return result +@app.get("/api/restocking/recommendations", response_model=List[RestockingRecommendation]) +def get_restocking_recommendations(): + """Get restocking recommendations ranked by demand trend priority""" + recommendations = [] + + for forecast in demand_forecasts: + sku = forecast['item_sku'] + unit_cost = RESTOCK_UNIT_COSTS.get(sku, 25.00) + recommended_qty = forecast['forecasted_demand'] + trend = forecast['trend'] + + recommendations.append({ + 'sku': sku, + 'item_name': forecast['item_name'], + 'trend': trend, + 'priority': TREND_PRIORITY.get(trend, 'medium'), + 'current_demand': forecast['current_demand'], + 'forecasted_demand': forecast['forecasted_demand'], + 'recommended_quantity': recommended_qty, + 'unit_cost': unit_cost, + 'total_cost': round(recommended_qty * unit_cost, 2), + 'period': forecast['period'] + }) + + recommendations.sort(key=lambda x: TREND_SORT_ORDER.get(x['trend'], 1)) + return recommendations + + +@app.get("/api/restocking/orders", response_model=List[RestockingOrder]) +def get_restocking_orders(): + """Get all submitted restocking orders""" + return restocking_orders + + +@app.post("/api/restocking/orders", response_model=RestockingOrder, status_code=201) +def place_restocking_order(request: PlaceRestockingOrderRequest): + """Submit a new restocking order""" + if not request.items: + raise HTTPException(status_code=400, detail="Order must contain at least one item") + + now = datetime.now() + lead_time_days = 14 + order_id = str(_restocking_order_counter[0]) + _restocking_order_counter[0] += 1 + + items = [] + total_cost = 0.0 + for item in request.items: + item_total = round(float(item['quantity']) * float(item['unit_cost']), 2) + total_cost += item_total + items.append({ + 'sku': item['sku'], + 'item_name': item['item_name'], + 'quantity': int(item['quantity']), + 'unit_cost': float(item['unit_cost']), + 'total_cost': item_total + }) + + order = { + 'id': order_id, + 'order_number': f"RST-{now.strftime('%Y%m%d')}-{order_id.zfill(4)}", + 'order_date': now.strftime('%Y-%m-%d'), + 'estimated_delivery': (now + timedelta(days=lead_time_days)).strftime('%Y-%m-%d'), + 'lead_time_days': lead_time_days, + 'total_cost': round(total_cost, 2), + 'status': 'Submitted', + 'items': items + } + + restocking_orders.append(order) + return order + + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8001)