diff --git a/client/src/App.vue b/client/src/App.vue index c2da05a5..72f6b070 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -25,6 +25,9 @@ Reports + + {{ t('nav.restocking') }} + {{ t('common.loading') }}
{{ error }}
+
+
+

+ {{ t('restocking.submittedOrders') }} + {{ restockingOrders.length }} +

+
+

{{ t('restocking.submittedOrdersDescription') }}

+
+ + + + + + + + + + + + + + + + + + + + +
+
+
{{ t('status.delivered') }}
@@ -95,6 +146,7 @@ export default { const loading = ref(true) const error = ref(null) const orders = ref([]) + const restockingOrders = ref([]) // Use shared filters const { @@ -153,16 +205,41 @@ export default { }) } - onMounted(loadOrders) + const formatEstimatedDelivery = (createdDate, leadTimeDays) => { + const { currentLocale } = useI18n() + const locale = currentLocale.value === 'ja' ? 'ja-JP' : 'en-US' + const date = new Date(createdDate) + date.setDate(date.getDate() + leadTimeDays) + return date.toLocaleDateString(locale, { + year: 'numeric', + month: 'short', + day: 'numeric' + }) + } + + 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, getOrdersByStatus, getOrderStatusClass, formatDate, + formatEstimatedDelivery, currencySymbol, translateProductName, translateCustomerName @@ -276,4 +353,26 @@ export default { font-size: 0.813rem; color: #64748b; } + +/* Submitted restocking orders table */ +.submitted-orders-table { + table-layout: fixed; + width: 100%; +} + +.col-total { + width: 140px; +} + +.submitted-item-entry { + display: flex; + flex-direction: column; + gap: 0.2rem; + padding: 0.5rem; + border-bottom: 1px solid #f1f5f9; +} + +.submitted-item-entry:last-child { + border-bottom: none; +} diff --git a/client/src/views/Restocking.vue b/client/src/views/Restocking.vue new file mode 100644 index 00000000..65bdbec6 --- /dev/null +++ b/client/src/views/Restocking.vue @@ -0,0 +1,270 @@ + + + + + diff --git a/docs/architecture.html b/docs/architecture.html new file mode 100644 index 00000000..d6b4aba0 --- /dev/null +++ b/docs/architecture.html @@ -0,0 +1,796 @@ + + + + + +Inventory Management — Architecture + + + +
+ + +
+

Factory Inventory Management System

+

Full-stack SPA — Vue 3 frontend · Python FastAPI backend · In-memory JSON data · No database

+
+ Vue 3.4 + FastAPI + Pydantic v2 + Vite 5 + Vue Router 4 +
+
+ + +
+
Tech Stack
+
+
+

Frontend — client/ (port 3000)

+
Vue3.4.21
+
Vue Router4.3.0
+
Axios1.6.7
+
Vite5.2.0
+
Composition APIrefs + computed
+
i18nEN / JA
+
+
+

Backend — server/ (port 8001)

+
FastAPI≥ 0.110.0
+
Uvicorn≥ 0.24.0 (ASGI)
+
Pydantic≥ 2.5.0
+
Python3.11+
+
pytest + httpxtest suite
+
CORSall origins (*)
+
+
+
+ + +
+
System Architecture
+
+
+
+
User
+
Browser
+
localhost:3000
+
+
+
+
Frontend SPA
+
Vue 3 App
+
Vite dev server
+
+
→ HTTP/JSON →
+
+
REST API
+
FastAPI
+
localhost:8001
+
+
+
+
Data layer
+
In-memory
+
7 JSON files
+
+
+ +
+
+
Vue App Layers
+
+
Views (7 routes)
+
+
Composables (useFilters, useAuth, useI18n)
+
+
api.js (Axios — 16 methods)
+
+
Components (9 modals/UI)
+
+
+
+
FastAPI Layers
+
+
Endpoints (15) — main.py
+
+
Pydantic models (validation)
+
+
mock_data.py (filter logic)
+
+
data/*.json (in-memory arrays)
+
+
+
+
+
+ + +
+
Data Flow — Filter Interaction
+
+
+
+
1
+
+
User changes a filter
+
FilterBar.vue dropdown updates useFilters() composable reactive state (selectedPeriod, selectedLocation, selectedCategory, selectedStatus)
+
+
+
+
2
+
+
View detects change
+
watch() in the active view triggers, calls getCurrentFilters() which maps composable state to API params (location → warehouse, period → month)
+
+
+
+
3
+
+
API call via Axios
+
api.js builds URLSearchParams and calls e.g. GET /api/orders?warehouse=Tokyo&status=Delivered&month=2025-01
+
+
+
+
4
+
+
FastAPI endpoint receives request
+
main.py endpoint calls apply_filters(items, warehouse, category, status) and filter_by_month(items, month) on in-memory arrays
+
+
+
+
5
+
+
Pydantic validation + JSON response
+
Filtered list is validated against response model (e.g. List[Order]) and returned as JSON with HTTP 200
+
+
+
+
6
+
+
Vue re-renders
+
View updates its ref with response data; computed properties recalculate; DOM updates reactively
+
+
+
+
+
+ + +
+
Views & Routes
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RouteView FilePurposeAPI Calls
/Dashboard.vueKPI cards, order health donut chart, stock analysisgetDashboardSummary
/inventoryInventory.vueStock levels table, search, SKU detail modalgetInventory, getInventoryItem
/ordersOrders.vueOrder list with status, dates, and valuegetOrders, getOrder
/demandDemand.vueForecast trends grouped by increasing/stable/decreasinggetDemandForecasts
/spendingSpending.vueCost breakdown, monthly bar chart, category analysisgetSpendingSummary, getMonthlySpending, getCategorySpending, getTransactions
/reportsReports.vueQuarterly performance table, monthly revenue trendsgetQuarterlyReports, getMonthlyTrends
/backlogBacklog.vueShortage items by priority, PO creation workflowgetBacklog, createPurchaseOrder
+
+
+ + +
+
API Endpoints (15)
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodPathDescriptionFilters
GET/api/inventoryAll inventory itemswarehouse, category
GET/api/inventory/{id}Single item by ID
GET/api/ordersAll orderswarehouse, category, status, month
GET/api/orders/{id}Single order by ID
GET/api/demandDemand forecasts
GET/api/backlogBacklog items (with has_purchase_order flag)
GET/api/dashboard/summaryAggregated KPIswarehouse, category, status, month
GET/api/spending/summaryCost category totals + % change
GET/api/spending/monthlyMonth-by-month breakdown (12 months)
GET/api/spending/categoriesSpending by category
GET/api/spending/transactionsRecent purchase transactions
GET/api/reports/quarterlyQuarterly performance stats
GET/api/reports/monthly-trendsMonthly revenue trend
POST/api/purchase-ordersCreate a purchase orderbody: PurchaseOrder
GET/api/purchase-orders/{backlog_id}Get PO by backlog item
+
+
+ Filter values: warehouse — "all" | "San Francisco" | "Tokyo" | "London" | "Singapore"    + month — "2025-01"…"2025-12" | "Q1-2025"…"Q4-2025" | "all"    + status — "Delivered" | "Shipped" | "Processing" | "Backordered" | "all" +
+
+ + +
+
Data Models (Pydantic)
+
+ +
+

InventoryItem

+
+
idstr
+
skustr
+
namestr
+
categorystr
+
warehousestr
+
quantity_on_handint
+
reorder_pointint
+
unit_costfloat
+
locationstr
+
last_updateddatetime
+
+
+ +
+

Order

+
+
idstr
+
order_numberstr
+
customerstr
+
itemsList[dict]
+
statusstr
+
warehousestr
+
categorystr
+
order_datedatetime
+
expected_deliverydatetime
+
total_valuefloat
+
+
+ +
+

DemandForecast

+
+
idstr
+
item_skustr
+
item_namestr
+
current_demandint
+
forecasted_demandint
+
trendincreasing | stable | decreasing
+
periodstr
+
+
+ +
+

BacklogItem

+
+
idstr
+
order_idstr
+
item_skustr
+
item_namestr
+
quantity_neededint
+
quantity_availableint
+
days_delayedint
+
priorityhigh | medium | low
+
has_purchase_orderbool (computed)
+
+
+ +
+

PurchaseOrder

+
+
idstr
+
backlog_item_idstr
+
supplier_namestr
+
quantityint
+
unit_costfloat
+
expected_delivery_datedatetime
+
statusstr
+
created_datedatetime
+
notesstr?
+
+
+ +
+
+ + +
+
Frontend Shared State (Composables)
+
+
+
useFilters()
+
Singleton filter state shared across all views. FilterBar writes, views read.
+
+ selectedPeriod + selectedLocation + selectedCategory + selectedStatus + getCurrentFilters() +
+
+
+
useAuth()
+
User session management — authentication state and user profile.
+
+ currentUser + isAuthenticated +
+
+
+
useI18n()
+
Language switching between English and Japanese (en.js / ja.js).
+
+ currentLang + t(key) + setLang() +
+
+
+
+ + +
+
Data Files — server/data/
+
+
+ inventory.json + Stock items — SKU, name, category, warehouse, quantity_on_hand, reorder_point, unit_cost + 30+ items +
+
+ orders.json + Customer orders — order_number, customer, items[], status, warehouse, category, order_date, total_value + ~2000 orders (2025) +
+
+ demand_forecasts.json + Demand predictions — item_sku, current_demand, forecasted_demand, trend, period + 9 forecasts +
+
+ backlog_items.json + Unfulfilled line items — order_id, item_sku, quantity_needed, quantity_available, days_delayed, priority + 4 items +
+
+ spending.json + Cost data — spending_summary{}, monthly_spending[], category_spending[] — split at load time + 12-month breakdown +
+
+ transactions.json + Purchase records — TXN id, date, description, category, warehouse, amount, vendor, type + recent transactions +
+
+ purchase_orders.json + Created POs — backlog_item_id, supplier_name, quantity, unit_cost, expected_delivery_date, status + [] empty (runtime only) +
+
+
+ + +
+ inventory-management — branch: new_features + Generated 2026-05-21 +
+ +
+ + diff --git a/server/data/demand_forecasts.json b/server/data/demand_forecasts.json index e1b38838..d1e65e7a 100644 --- a/server/data/demand_forecasts.json +++ b/server/data/demand_forecasts.json @@ -1,8 +1,8 @@ [ { "id": "1", - "item_sku": "WDG-001", - "item_name": "Industrial Widget Type A", + "item_sku": "PCB-001", + "item_name": "Single Layer PCB Assembly", "current_demand": 300, "forecasted_demand": 450, "trend": "increasing", @@ -10,8 +10,8 @@ }, { "id": "2", - "item_sku": "BRG-102", - "item_name": "Steel Bearing Assembly", + "item_sku": "TMP-201", + "item_name": "Temperature Sensor Module", "current_demand": 150, "forecasted_demand": 152, "trend": "stable", @@ -19,8 +19,8 @@ }, { "id": "3", - "item_sku": "GSK-203", - "item_name": "High-Temperature Gasket", + "item_sku": "PRS-203", + "item_name": "Pressure Sensor Module", "current_demand": 500, "forecasted_demand": 600, "trend": "increasing", @@ -28,8 +28,8 @@ }, { "id": "4", - "item_sku": "MTR-304", - "item_name": "Electric Motor 5HP", + "item_sku": "STP-303", + "item_name": "Stepper Motor NEMA 17", "current_demand": 50, "forecasted_demand": 35, "trend": "decreasing", @@ -37,8 +37,8 @@ }, { "id": "5", - "item_sku": "FLT-405", - "item_name": "Oil Filter Cartridge", + "item_sku": "MCU-401", + "item_name": "8-bit Microcontroller", "current_demand": 800, "forecasted_demand": 950, "trend": "increasing", @@ -46,8 +46,8 @@ }, { "id": "6", - "item_sku": "VLV-506", - "item_name": "Pressure Relief Valve", + "item_sku": "PSU-502", + "item_name": "12V 5A Power Supply Module", "current_demand": 120, "forecasted_demand": 121, "trend": "stable", @@ -64,8 +64,8 @@ }, { "id": "8", - "item_sku": "SNR-420", - "item_name": "Temperature Sensor Module", + "item_sku": "MCU-402", + "item_name": "32-bit ARM Microcontroller", "current_demand": 180, "forecasted_demand": 182, "trend": "stable", @@ -73,8 +73,8 @@ }, { "id": "9", - "item_sku": "CTL-330", - "item_name": "Logic Controller Board", + "item_sku": "LED-406", + "item_name": "LED Driver IC", "current_demand": 95, "forecasted_demand": 96, "trend": "stable", diff --git a/server/main.py b/server/main.py index a0c2d8c5..3ab99cea 100644 --- a/server/main.py +++ b/server/main.py @@ -3,6 +3,8 @@ 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 +import uuid +from datetime import datetime app = FastAPI(title="Factory Inventory Management System") @@ -120,6 +122,39 @@ class CreatePurchaseOrderRequest(BaseModel): expected_delivery_date: str notes: Optional[str] = None +class RestockingItem(BaseModel): + item_sku: str + item_name: str + trend: str + forecasted_demand: int + unit_cost: float + total_cost: float + category: str + lead_time_days: int + +class RestockingOrder(BaseModel): + id: str + order_number: str + items: List[RestockingItem] + total_cost: float + status: str + created_date: str + +class SubmitRestockingOrderRequest(BaseModel): + items: List[RestockingItem] + +# Lead time by category (days) +LEAD_TIMES = { + "Circuit Boards": 7, + "Sensors": 3, + "Actuators": 5, + "Controllers": 4, + "Power Supplies": 2, +} + +# In-memory storage for restocking orders +restocking_orders: List[dict] = [] + # API endpoints @app.get("/") def root(): @@ -304,6 +339,72 @@ def get_monthly_trends(): result.sort(key=lambda x: x['month']) return result +@app.get("/api/restocking/recommendations") +def get_restocking_recommendations(budget: float = 0): + """Get restocking recommendations based on available budget""" + # Build a lookup of inventory by SKU for unit_cost and category + inventory_by_sku = {item["sku"]: item for item in inventory_items} + + # Enrich forecasts with unit_cost, category, lead_time_days, total_cost + enriched = [] + for forecast in demand_forecasts: + inv = inventory_by_sku.get(forecast["item_sku"]) + if not inv: + continue + category = inv["category"] + unit_cost = inv["unit_cost"] + total_cost = round(forecast["forecasted_demand"] * unit_cost, 2) + lead_time = LEAD_TIMES.get(category, 5) + enriched.append({ + "item_sku": forecast["item_sku"], + "item_name": forecast["item_name"], + "trend": forecast["trend"], + "forecasted_demand": forecast["forecasted_demand"], + "unit_cost": unit_cost, + "total_cost": total_cost, + "category": category, + "lead_time_days": lead_time, + }) + + # Sort: increasing first, then stable, then decreasing; within each group by total_cost ascending + trend_order = {"increasing": 0, "stable": 1, "decreasing": 2} + enriched.sort(key=lambda x: (trend_order.get(x["trend"], 9), x["total_cost"])) + + # Select items that fit within budget + selected = [] + remaining = budget + for item in enriched: + if item["total_cost"] <= remaining: + selected.append(item) + remaining = round(remaining - item["total_cost"], 2) + + total_cost = round(sum(i["total_cost"] for i in selected), 2) + return { + "items": selected, + "total_cost": total_cost, + "remaining_budget": round(budget - total_cost, 2), + } + +@app.post("/api/restocking/orders", response_model=RestockingOrder) +def submit_restocking_order(request: SubmitRestockingOrderRequest): + """Submit a restocking order""" + order_number = f"RESTOCK-2026-{str(len(restocking_orders) + 1).zfill(4)}" + order = { + "id": str(uuid.uuid4()), + "order_number": order_number, + "items": [item.model_dump() for item in request.items], + "total_cost": round(sum(item.total_cost for item in request.items), 2), + "status": "Submitted", + "created_date": datetime.utcnow().isoformat() + "Z", + } + restocking_orders.append(order) + return order + +@app.get("/api/restocking/orders", response_model=List[RestockingOrder]) +def get_restocking_orders(): + """Get all submitted restocking orders""" + return restocking_orders + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8001)