diff --git a/CLAUDE.md b/CLAUDE.md index 89c307d15..c7ebc5549 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,6 +55,9 @@ npm install && npm run dev - `GET /api/demand`, `/api/backlog` - No filters - `GET /api/spending/*` - Summary, monthly, categories, transactions +## Code Style +- Always document non-obvious logic changes with comments + ## Common Issues 1. Use unique keys in v-for (not `index`) - use `sku`, `month`, etc. 2. Validate dates before `.getMonth()` calls 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 + + + + + + + + + + + diff --git a/client/src/locales/en.js b/client/src/locales/en.js index 03a58fe6e..b98c4807c 100644 --- a/client/src/locales/en.js +++ b/client/src/locales/en.js @@ -204,6 +204,7 @@ export default { shipped: 'Shipped', processing: 'Processing', backordered: 'Backordered', + submitted: 'Submitted', inStock: 'In Stock', lowStock: 'Low Stock', adequate: 'Adequate' diff --git a/client/src/locales/ja.js b/client/src/locales/ja.js index db33223ac..690ad20b9 100644 --- a/client/src/locales/ja.js +++ b/client/src/locales/ja.js @@ -204,6 +204,7 @@ export default { shipped: '出荷済み', processing: '処理中', backordered: 'バックオーダー', + submitted: '提出済み', inStock: '在庫あり', lowStock: '在庫僅少', adequate: '適量' 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/Dashboard.vue b/client/src/views/Dashboard.vue index 437da9c23..5d496419a 100644 --- a/client/src/views/Dashboard.vue +++ b/client/src/views/Dashboard.vue @@ -304,12 +304,14 @@ import { useI18n } from '../composables/useI18n' import { formatCurrency } from '../utils/currency' import ProductDetailModal from '../components/ProductDetailModal.vue' import BacklogDetailModal from '../components/BacklogDetailModal.vue' +import PurchaseOrderModal from '../components/PurchaseOrderModal.vue' export default { name: 'Dashboard', components: { ProductDetailModal, BacklogDetailModal, + PurchaseOrderModal, }, setup() { const { t, currentCurrency, translateProductName, translateWarehouse } = useI18n() diff --git a/client/src/views/Orders.vue b/client/src/views/Orders.vue index 7413f6e66..36dc2cbaa 100644 --- a/client/src/views/Orders.vue +++ b/client/src/views/Orders.vue @@ -25,6 +25,10 @@
{{ t('status.backordered') }}
{{ getOrdersByStatus('Backordered').length }}
+
+
Submitted
+
{{ submittedOrders.length }}
+
@@ -74,6 +78,45 @@
+
+
+

Submitted Orders ({{ submittedOrders.length }})

+
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
@@ -95,6 +138,8 @@ export default { const loading = ref(true) const error = ref(null) const orders = ref([]) + // allOrders holds unfiltered set so Submitted orders (from restock) always appear + const allOrders = ref([]) // Use shared filters const { @@ -109,14 +154,15 @@ export default { try { loading.value = true const filters = getCurrentFilters() - const fetchedOrders = await api.getOrders(filters) - - // Sort orders by order_date (earliest first) - orders.value = fetchedOrders.sort((a, b) => { - const dateA = new Date(a.order_date) - const dateB = new Date(b.order_date) - return dateA - dateB - }) + // Fetch filtered + unfiltered in parallel; unfiltered needed for Submitted orders section + const [fetchedOrders, allFetched] = await Promise.all([ + api.getOrders(filters), + api.getOrders({}) + ]) + + const sortByDate = (a, b) => new Date(a.order_date) - new Date(b.order_date) + orders.value = fetchedOrders.sort(sortByDate) + allOrders.value = allFetched.sort(sortByDate) } catch (err) { error.value = 'Failed to load orders: ' + err.message } finally { @@ -129,6 +175,15 @@ export default { loadOrders() }) + const submittedOrders = computed(() => + allOrders.value.filter(o => o.status === 'Submitted') + ) + + const getLeadTimeDays = (orderDate, deliveryDate) => { + const msPerDay = 1000 * 60 * 60 * 24 + return Math.round((new Date(deliveryDate) - new Date(orderDate)) / msPerDay) + } + const getOrdersByStatus = (status) => { return orders.value.filter(order => order.status === status) } @@ -138,7 +193,8 @@ export default { 'Delivered': 'success', 'Shipped': 'info', 'Processing': 'warning', - 'Backordered': 'danger' + 'Backordered': 'danger', + 'Submitted': 'stable' } return statusMap[status] || 'info' } @@ -160,9 +216,11 @@ export default { loading, error, orders, + submittedOrders, getOrdersByStatus, getOrderStatusClass, formatDate, + getLeadTimeDays, currencySymbol, translateProductName, translateCustomerName @@ -203,6 +261,17 @@ export default { width: 120px; } +.col-lead { + width: 100px; +} + +/* Submitted orders table has fewer columns so fixed layout needs different widths */ +.submitted-table .col-order-number { width: 160px; } +.submitted-table .col-items { width: 160px; } +.submitted-table .col-date { width: 160px; } +.submitted-table .col-lead { width: 100px; } +.submitted-table .col-value { width: 140px; } + /* 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..64f091ca3 --- /dev/null +++ b/client/src/views/Restocking.vue @@ -0,0 +1,390 @@ + + + + + diff --git a/server/main.py b/server/main.py index a0c2d8c5a..5559a3b15 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") @@ -120,6 +121,29 @@ class CreatePurchaseOrderRequest(BaseModel): expected_delivery_date: str notes: Optional[str] = None +class Task(BaseModel): + id: str + title: str + priority: str + dueDate: str + status: str + +class CreateTaskRequest(BaseModel): + title: str + priority: str + dueDate: str + +class RestockOrderItem(BaseModel): + sku: str + name: str + quantity: int + unit_price: float + +class RestockOrderRequest(BaseModel): + items: List[RestockOrderItem] + warehouse: Optional[str] = None + category: Optional[str] = None + # API endpoints @app.get("/") def root(): @@ -304,6 +328,103 @@ def get_monthly_trends(): result.sort(key=lambda x: x['month']) return result +@app.post("/api/restock-orders", status_code=201) +def create_restock_order(request: RestockOrderRequest): + """Create a restocking order from recommended items""" + now = datetime.utcnow() + year = now.year + # Zero-pad to match existing ORD-YYYY-NNNN format + order_number = f"RST-{year}-{str(len(orders) + 1).zfill(4)}" + total_value = sum(item.quantity * item.unit_price for item in request.items) + + new_order = { + "id": str(len(orders) + 1), + "order_number": order_number, + "customer": "Internal Restock", + "items": [ + { + "sku": item.sku, + "name": item.name, + "quantity": item.quantity, + "unit_price": item.unit_price, + } + for item in request.items + ], + "status": "Submitted", + "order_date": now.isoformat(), + "expected_delivery": (now + timedelta(days=14)).isoformat(), + "total_value": round(total_value, 2), + "actual_delivery": None, + "warehouse": request.warehouse, + "category": request.category, + } + + orders.append(new_order) + return new_order + + +# In-memory tasks store (resets on server restart, consistent with other mock data) +_tasks: list = [] +_task_id_counter = 100 # Start high to avoid collisions with client mock task IDs + + +@app.get("/api/tasks", response_model=List[Task]) +def get_tasks(): + return _tasks + + +@app.post("/api/tasks", response_model=Task, status_code=201) +def create_task(request: CreateTaskRequest): + global _task_id_counter + _task_id_counter += 1 + task = { + "id": str(_task_id_counter), + "title": request.title, + "priority": request.priority, + "dueDate": request.dueDate, + "status": "pending", + } + _tasks.append(task) + return task + + +@app.delete("/api/tasks/{task_id}", status_code=200) +def delete_task(task_id: str): + global _tasks + task = next((t for t in _tasks if t["id"] == task_id), None) + if not task: + raise HTTPException(status_code=404, detail="Task not found") + _tasks = [t for t in _tasks if t["id"] != task_id] + return {"deleted": task_id} + + +@app.patch("/api/tasks/{task_id}", response_model=Task) +def toggle_task(task_id: str): + task = next((t for t in _tasks if t["id"] == task_id), None) + if not task: + raise HTTPException(status_code=404, detail="Task not found") + task["status"] = "completed" if task["status"] == "pending" else "pending" + return task + + +@app.post("/api/purchase-orders", response_model=PurchaseOrder, status_code=201) +def create_purchase_order(request: CreatePurchaseOrderRequest): + now = datetime.utcnow() + new_po = { + "id": f"PO-{now.strftime('%Y%m%d%H%M%S')}-{len(purchase_orders) + 1}", + "backlog_item_id": request.backlog_item_id, + "supplier_name": request.supplier_name, + "quantity": request.quantity, + "unit_cost": request.unit_cost, + "expected_delivery_date": request.expected_delivery_date, + "status": "Pending", + "created_date": now.isoformat(), + "notes": request.notes, + } + purchase_orders.append(new_po) + return new_po + + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8001)