From caddbc4f79a5db3e21ca71cdf66de7fdec76dd12 Mon Sep 17 00:00:00 2001 From: agrimal Date: Thu, 21 May 2026 13:41:48 -0400 Subject: [PATCH] Add Restocking recommendation engine with budget optimization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New Restocking view with budget slider and demand-based recommendations - Greedy budget optimization sorted by trend (increasing → stable → decreasing) - Three new API endpoints: recommendations, order submission, order retrieval - Submitted restocking orders displayed in Orders page with expandable details - Full i18n support for EN and JA locales Co-Authored-By: Claude Sonnet 4.6 --- client/src/App.vue | 3 + client/src/api.js | 15 ++ client/src/locales/en.js | 29 ++++ client/src/locales/ja.js | 29 ++++ client/src/main.js | 4 +- client/src/views/Orders.vue | 101 ++++++++++- client/src/views/Restocking.vue | 270 ++++++++++++++++++++++++++++++ server/data/demand_forecasts.json | 32 ++-- server/main.py | 101 +++++++++++ 9 files changed, 566 insertions(+), 18 deletions(-) create mode 100644 client/src/views/Restocking.vue 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/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)