diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..736ac9b80 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "github@claude-plugins-official": true + } +} diff --git a/CLAUDE.md b/CLAUDE.md index 89c307d15..dd6449d2e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,3 +74,7 @@ npm install && npm run dev - Status: green/blue/yellow/red - Charts: Custom SVG, CSS Grid for layouts - No emojis in UI + +## Code Standards + +- **Comments**: Always document non-obvious logic changes with comments. Only add a comment when the WHY is non-obvious: a hidden constraint, a subtle invariant, a workaround for a specific bug, behavior that would surprise a reader. diff --git a/client/src/App.vue b/client/src/App.vue index c2da05a5c..dd6849b1c 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -22,6 +22,9 @@ {{ t('nav.demandForecast') }} + + {{ t('nav.restocking') }} + Reports diff --git a/client/src/api.js b/client/src/api.js index 11cb9db70..7aaebe60e 100644 --- a/client/src/api.js +++ b/client/src/api.js @@ -102,5 +102,10 @@ export const api = { async getPurchaseOrderByBacklogItem(backlogItemId) { const response = await axios.get(`${API_BASE_URL}/purchase-orders/${backlogItemId}`) return response.data + }, + + async submitRestockingOrder(orderData) { + const response = await axios.post(`${API_BASE_URL}/orders`, orderData) + return response.data } } diff --git a/client/src/locales/en.js b/client/src/locales/en.js index 03a58fe6e..bf821a2a7 100644 --- a/client/src/locales/en.js +++ b/client/src/locales/en.js @@ -6,6 +6,7 @@ export default { orders: 'Orders', finance: 'Finance', demandForecast: 'Demand Forecast', + restocking: 'Restocking', companyName: 'Catalyst Components', subtitle: 'Inventory Management System' }, @@ -106,6 +107,9 @@ export default { title: 'Orders', description: 'View and manage customer orders', allOrders: 'All Orders', + submittedOrders: 'Submitted Orders', + historicalOrders: 'Historical Orders', + statusSubmitted: 'Submitted', totalOrders: 'Total Orders', totalRevenue: 'Total Revenue', avgOrderValue: 'Avg Order Value', @@ -129,6 +133,32 @@ export default { } }, + // Restocking + restocking: { + title: 'Restocking', + description: 'Plan and submit restocking orders based on demand forecasts', + warehouse: 'Select Warehouse', + budget: 'Available Budget', + recommendations: 'Recommended Items', + itemsRecommended: 'Items Recommended', + totalCost: 'Total Cost', + budgetRemaining: 'Budget Remaining', + sku: 'SKU', + productName: 'Product Name', + category: 'Category', + currentStock: 'Current Stock', + forecastedDemand: 'Forecasted Demand', + recommendedQty: 'Rec. Qty', + unitCost: 'Unit Cost', + placeOrder: 'Place Order', + submitting: 'Submitting...', + reset: 'Reset Selections', + noRecommendations: 'No recommendations available for this warehouse', + overBudget: 'Order exceeds budget', + selectAll: 'Select all items', + selectAtLeastOne: 'Please select at least one item' + }, + // Finance/Spending finance: { title: 'Finance Dashboard', diff --git a/client/src/locales/ja.js b/client/src/locales/ja.js index db33223ac..80142196d 100644 --- a/client/src/locales/ja.js +++ b/client/src/locales/ja.js @@ -6,6 +6,7 @@ export default { orders: '注文', finance: '財務', demandForecast: '需要予測', + restocking: '補充注文', companyName: '触媒コンポーネンツ', subtitle: '在庫管理システム' }, @@ -106,6 +107,9 @@ export default { title: '注文', description: '顧客注文の表示と管理', allOrders: 'すべての注文', + submittedOrders: '提出済み注文', + historicalOrders: '過去の注文', + statusSubmitted: '提出済み', totalOrders: '総注文数', totalRevenue: '総収益', avgOrderValue: '平均注文額', @@ -129,6 +133,32 @@ export default { } }, + // Restocking + restocking: { + title: '補充注文', + description: '需要予測に基づいて補充注文を計画・提出します', + warehouse: '倉庫を選択', + budget: '利用可能予算', + recommendations: '推奨品目', + itemsRecommended: '推奨品目数', + totalCost: '合計コスト', + budgetRemaining: '残る予算', + sku: 'SKU', + productName: '製品名', + category: 'カテゴリ', + currentStock: '現在の在庫', + forecastedDemand: '予測需要', + recommendedQty: '推奨数量', + unitCost: '単価', + placeOrder: '注文を提出', + submitting: '提出中...', + reset: 'リセット', + noRecommendations: 'この倉庫には推奨品目がありません', + overBudget: '注文が予算を超えています', + selectAll: 'すべてを選択', + selectAtLeastOne: '少なくとも1つの品目を選択してください' + }, + // Finance/Spending finance: { title: '財務ダッシュボード', diff --git a/client/src/main.js b/client/src/main.js index 477c2d966..8884eea63 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(), @@ -16,7 +17,8 @@ const router = createRouter({ { path: '/orders', component: Orders }, { path: '/demand', component: Demand }, { path: '/spending', component: Spending }, - { path: '/reports', component: Reports } + { path: '/reports', component: Reports }, + { path: '/restocking', component: Restocking } ] }) diff --git a/client/src/views/Orders.vue b/client/src/views/Orders.vue index 7413f6e66..f3d669064 100644 --- a/client/src/views/Orders.vue +++ b/client/src/views/Orders.vue @@ -27,9 +27,59 @@ -
+ +
-

{{ t('orders.allOrders') }} ({{ orders.length }})

+

{{ t('orders.submittedOrders') }} ({{ submittedOrders.length }})

+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
{{ t('orders.table.orderNumber') }}Warehouse{{ t('orders.table.items') }}{{ t('orders.table.status') }}{{ t('orders.table.orderDate') }}Lead Time (days){{ t('orders.table.totalValue') }}
{{ order.order_number }}{{ order.warehouse }} +
+ + {{ t('orders.itemsCount', { count: order.items.length }) }} + +
+
+ {{ translateProductName(item.name) }} + {{ t('orders.quantity') }}: {{ item.quantity }} @ {{ currencySymbol }}{{ item.unit_price }} +
+
+
+
+ + {{ t('orders.statusSubmitted') }} + + {{ formatDate(order.order_date) }}{{ calculateLeadTime(order.expected_delivery) }}{{ currencySymbol }}{{ order.total_value.toLocaleString() }}
+
+
+ + +
+
+

{{ t('orders.historicalOrders') }} ({{ regularOrders.length }})

@@ -45,7 +95,7 @@ - +
{{ order.order_number }} {{ translateCustomerName(order.customer) }} @@ -138,11 +188,28 @@ export default { 'Delivered': 'success', 'Shipped': 'info', 'Processing': 'warning', - 'Backordered': 'danger' + 'Backordered': 'danger', + 'Submitted Orders': 'info' } return statusMap[status] || 'info' } + const submittedOrders = computed(() => + orders.value.filter(order => order.status === 'Submitted Orders') + ) + + const regularOrders = computed(() => + orders.value.filter(order => order.status !== 'Submitted Orders') + ) + + const calculateLeadTime = (expectedDeliveryDate) => { + const today = new Date() + const delivery = new Date(expectedDeliveryDate) + const diffTime = Math.abs(delivery - today) + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + return diffDays + } + const formatDate = (dateString) => { const { currentLocale } = useI18n() const locale = currentLocale.value === 'ja' ? 'ja-JP' : 'en-US' @@ -160,12 +227,15 @@ export default { loading, error, orders, + submittedOrders, + regularOrders, getOrdersByStatus, getOrderStatusClass, formatDate, currencySymbol, translateProductName, - translateCustomerName + translateCustomerName, + calculateLeadTime } } } @@ -203,6 +273,24 @@ export default { width: 120px; } +.col-warehouse { + width: 140px; +} + +.col-leadtime { + width: 140px; + text-align: center; +} + +.submitted-section { + margin-bottom: 2rem; + border-top: 3px solid #3b82f6; +} + +.mock-section { + border-top: 3px solid #9ca3af; +} + /* Items details styling */ .items-details { position: relative; diff --git a/client/src/views/Restocking.vue b/client/src/views/Restocking.vue new file mode 100644 index 000000000..d5f66e16d --- /dev/null +++ b/client/src/views/Restocking.vue @@ -0,0 +1,476 @@ + + + + + diff --git a/server/main.py b/server/main.py index a0c2d8c5a..f89375a94 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 +from datetime import datetime, timedelta +import uuid app = FastAPI(title="Factory Inventory Management System") @@ -14,6 +16,15 @@ 'Q4-2025': ['2025-10', '2025-11', '2025-12'] } +# Lead time mapping by product category (in days) +CATEGORY_LEAD_TIMES = { + 'Circuit Boards': 14, + 'Sensors': 10, + 'Actuators': 12, + 'Controllers': 7, + 'Power Supplies': 10 +} + 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 +131,12 @@ class CreatePurchaseOrderRequest(BaseModel): expected_delivery_date: str notes: Optional[str] = None +class RestockingOrderRequest(BaseModel): + warehouse: str + items: List[dict] + budget: float + recommended_items: List[dict] + # API endpoints @app.get("/") def root(): @@ -304,6 +321,73 @@ def get_monthly_trends(): result.sort(key=lambda x: x['month']) return result +@app.post("/api/orders", response_model=Order) +def create_restocking_order(order_req: RestockingOrderRequest): + """Create a new restocking order""" + # Validate budget + total_cost = 0 + order_items = [] + + # Get inventory items to fetch product info + for item_req in order_req.items: + inventory_item = next( + (inv for inv in inventory_items if inv["sku"] == item_req["sku"]), + None + ) + if not inventory_item: + raise HTTPException(status_code=404, detail=f"Item {item_req['sku']} not found") + + unit_cost = inventory_item["unit_cost"] + quantity = item_req["quantity"] + item_total = unit_cost * quantity + total_cost += item_total + + order_items.append({ + "sku": item_req["sku"], + "name": inventory_item["name"], + "quantity": quantity, + "unit_price": unit_cost + }) + + # Validate budget constraint + if total_cost > order_req.budget: + raise HTTPException(status_code=400, detail=f"Order total (${total_cost:.2f}) exceeds budget (${order_req.budget:.2f})") + + # Calculate lead time based on first item's category + first_item_category = order_items[0].get("name", "") if order_items else "" + # Get category from first ordered item + first_sku = order_req.items[0]["sku"] if order_req.items else None + lead_time_days = 10 # default + if first_sku: + first_inv = next((inv for inv in inventory_items if inv["sku"] == first_sku), None) + if first_inv: + category = first_inv["category"] + lead_time_days = CATEGORY_LEAD_TIMES.get(category, 10) + + # Create new order + now = datetime.utcnow() + order_date = now.isoformat() + expected_delivery = (now + timedelta(days=lead_time_days)).isoformat() + + new_order = { + "id": str(uuid.uuid4()), + "order_number": f"ORD-RESTOCK-{len(orders) + 1001}", + "customer": "Internal Restocking", + "items": order_items, + "status": "Submitted Orders", + "order_date": order_date, + "expected_delivery": expected_delivery, + "total_value": round(total_cost, 2), + "warehouse": order_req.warehouse, + "category": order_items[0].get("name", "") if order_items else None, + "recommended_items": order_req.recommended_items + } + + # Add to orders list + orders.append(new_order) + + return new_order + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8001)