diff --git a/CLAUDE.md b/CLAUDE.md index 89c307d15..b301397e0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,7 @@ # CLAUDE.md +#### newest change directly file + Factory Inventory Management System Demo with GitHub integration - Full-stack application with Vue 3 frontend, Python FastAPI backend, and in-memory mock data (no database). > ⚠️ **This repository and any fork you create are PUBLIC.** Do not commit credentials, internal hostnames, or private registry URLs. `client/.npmrc` pins the public npm registry and `client/package-lock.json` is gitignored to prevent locally-configured registries from leaking into commits — leave both in place. @@ -55,6 +57,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/architecture.html b/architecture.html new file mode 100644 index 000000000..3c202821e --- /dev/null +++ b/architecture.html @@ -0,0 +1,959 @@ + + + + + + Architecture — Factory Inventory Management System + + + + +
+

Factory Inventory Management System

+

System Architecture & Technical Reference

+
+ + + +
+ + +
+

Overview

+
+
+
+

Vue 3 Frontend

+

7 page views with Composition API, reactive global filters, custom SVG charts, and English/Japanese i18n. Runs on port 3000.

+
+
+
+

FastAPI Backend

+

REST API with 13 endpoints, server-side filtering, Pydantic validation, and auto-generated OpenAPI docs. Runs on port 8001.

+
+
+
+

In-Memory Data

+

7 JSON files loaded at startup — 40+ inventory SKUs, 100+ orders, forecasts, backlog, and spending data. No database.

+
+
+
+ + +
+

Tech Stack

+
+
+

Frontend

+
+ Core + Vue 3 + Composition API + refs/computed +
+
+ Routing + Vue Router 4 + Client-side SPA routing +
+
+ HTTP + Axios + REST API client +
+
+ Build + Vite 5 + Dev server + bundler +
+
+ Charts + Custom SVG + Inline SVG donut & bar charts +
+
+ i18n + Custom composable + English / Japanese +
+
+
+

Backend

+
+ Core + FastAPI + Async REST framework +
+
+ Server + Uvicorn + ASGI server (0.0.0.0:8001) +
+
+ Validation + Pydantic v2 + Request & response models +
+
+ Runtime + Python 3.9+ + With uv package manager +
+
+ Data + JSON files + 7 files, loaded into memory +
+
+ Tests + Pytest + httpx + FastAPI TestClient integration +
+
+
+
+ + +
+

System Architecture

+
+ + + + + + + + + + + + + + + + BROWSER · PORT 3000 + + + + FilterBar.vue + useFilters() — Period · Warehouse · Category · Status + + + + Dashboard + + + Inventory + + + Orders + + + Demand + + + Spending + + + Reports + + + Backlog + + + + api.js — Axios HTTP Client + getInventory() · getOrders() · getDashboardSummary() · getDemand() · getBacklog() · getSpending*() + Builds query params → GET http://localhost:8001/api/... + + + + HTTP/JSON + + Response + + + + FASTAPI BACKEND · PORT 8001 + + + + Routes (main.py) + /api/inventory · /api/orders + /api/demand · /api/backlog + /api/dashboard/summary + /api/spending/* · /api/reports/* + + + + Filter Engine + apply_filters(warehouse, category, status) + filter_by_month(month) + Quarter expansion: Q1-2025 → [01,02,03] + Pydantic response validation + + + + + + + + + mock_data.py — JSON files loaded into memory at startup + + + + COMPOSABLES & STATE + + + useFilters.js + selectedPeriod · selectedLocation · selectedCategory · selectedStatus + + + useAuth.js + Mock user profile + + + useI18n.js + English / Japanese + + + + JSON Data Files (server/data/) + inventory.json · orders.json · demand_forecasts.json + backlog_items.json · spending.json · transactions.json + + + + VUE ROUTER 4 + / · /inventory · /orders · /demand · /spending · /reports · /backlog + Hash-based client-side routing, no server round-trip + + + + BACKEND TEST SUITE + pytest · httpx · FastAPI TestClient + test_dashboard · test_inventory · test_orders · test_misc + + + + CORS: allow_origins=["*"] — development only + +
+
+ + +
+

Data Flow

+
+
+
1
+
+

User selects a filter in FilterBar.vue

+

The global useFilters() composable holds singleton refs. Any view or component that calls it shares the same reactive state.

+ selectedLocation.value = "San Francisco" +selectedCategory.value = "Sensors" +selectedPeriod.value = "2025-09" +selectedStatus.value = "Delivered" +
+
+
+
2
+
+

Views watch filter changes and reload data

+

Each view (Dashboard, Inventory, Orders, etc.) has a watcher that fires loadData() whenever any filter ref changes.

+ watch( + [selectedLocation, selectedCategory, selectedPeriod, selectedStatus], + () => loadData() +) +
+
+
+
3
+
+

api.js builds the HTTP request

+

Filter values are mapped to API param names and appended to the URL. Values of 'all' are omitted from params so the backend skips that filter.

+ // GET /api/orders?warehouse=San+Francisco&category=Sensors&status=Delivered&month=2025-09 +const params = new URLSearchParams() +if (filters.warehouse !== 'all') params.append('warehouse', filters.warehouse) +if (filters.category !== 'all') params.append('category', filters.category) +if (filters.status !== 'all') params.append('status', filters.status) +if (filters.month !== 'all') params.append('month', filters.month) +
+
+
+
4
+
+

FastAPI applies filters server-side

+

apply_filters() handles warehouse/category/status. filter_by_month() handles the date dimension. Quarter strings like Q1-2025 are expanded to the three month strings.

+ filtered = apply_filters(orders, warehouse, category, status) +filtered = filter_by_month(filtered, month) # "Q1-2025" → ["2025-01","2025-02","2025-03"] +return filtered # Pydantic serialises to JSON +
+
+
+
5
+
+

Filtered JSON is returned and rendered

+

The Vue view receives the array, assigns it to a ref, and computed properties (totals, chart data, KPIs) auto-recalculate from the new data.

+ orders.value = await api.getOrders(filters) +// → computed totals, chart slices, status counts all update automatically +
+
+
+
+ + +
+

API Endpoints

+
+
+ GET + /api/inventory +
+ warehouse + category +
+
+
+ GET + /api/inventory/{item_id} +
path param
+
+
+ GET + /api/orders +
+ warehouse + category + status + month +
+
+
+ GET + /api/orders/{order_id} +
path param
+
+
+ GET + /api/dashboard/summary +
+ warehouse + category + status + month +
+
+
+ GET + /api/demand +
no filters
+
+
+ GET + /api/backlog +
no filters
+
+
+ GET + /api/spending/summary +
no filters
+
+
+ GET + /api/spending/monthly +
no filters
+
+
+ GET + /api/spending/categories +
no filters
+
+
+ GET + /api/spending/transactions +
no filters
+
+
+ GET + /api/reports/quarterly +
no filters
+
+
+ GET + /api/reports/monthly-trends +
no filters
+
+
+
+ + +
+

Data Models

+
+
+

InventoryItem

+
idstring
+
skustring
+
namestring
+
categorystring
+
warehousestring
+
quantity_on_handint
+
reorder_pointint
+
unit_costfloat
+
locationstring
+
last_updateddatetime
+
+
+

Order

+
idstring
+
order_numberstring
+
customerstring
+
itemsOrderItem[]
+
statusenum
+
warehousestring
+
categorystring
+
order_datedatetime
+
expected_deliverydatetime
+
total_valuefloat
+
+
+

BacklogItem

+
idstring
+
order_idstring
+
item_skustring
+
item_namestring
+
quantity_neededint
+
quantity_availableint
+
days_delayedint
+
priorityenum
+
has_purchase_orderbool
+
+
+

DemandForecast

+
idstring
+
item_skustring
+
item_namestring
+
current_demandint
+
forecasted_demandint
+
trendenum
+
periodstring
+
+
+

PurchaseOrder

+
idstring
+
backlog_item_idstring
+
supplier_namestring
+
quantityint
+
unit_costfloat
+
expected_delivery_datedate
+
statusstring
+
created_datedate
+
+
+

SpendingSummary

+
total_procurement_costfloat
+
total_operational_costfloat
+
total_labor_costfloat
+
total_overheadfloat
+
procurement_changefloat %
+
operational_changefloat %
+
labor_changefloat %
+
overhead_changefloat %
+
+
+
+ + +
+

Filter Matrix

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FilterAPI ParamValuesInventoryOrdersDashboardDemandBacklogSpending
Warehouse / Locationwarehouseall · San Francisco · London · Tokyo
Categorycategoryall · Circuit Boards · Sensors · Actuators · Controllers · Power Supplies
Order Statusstatusall · Delivered · Shipped · Processing · Backordered
Time Periodmonthall · 2025-01…2025-12 · Q1-2025…Q4-2025
+
+
+ + +
+

Frontend Views

+
+
+
+
+

Dashboard.vue

+

Executive KPIs — inventory turnover, order fulfillment rate, fill rate, revenue. Order health donut chart, inventory value by category, monthly trend charts.

+
+
+
+
+
+

Inventory.vue

+

Full SKU table with search, warehouse filter, category filter, and stock status badges (Low Stock / Adequate / In Stock). Click-to-open detail modal.

+
+
+
+
+
+

Orders.vue

+

Order list with status summary cards, collapsible item rows, delivery dates, and total values. Supports all 4 filters including status and time period.

+
+
+
+
+
+

Demand.vue

+

Demand forecast table showing current vs forecasted demand per SKU, trend direction (increasing / stable / decreasing), and percentage change.

+
+
+
+
+
+

Spending.vue

+

Financial analytics with revenue/cost/profit metrics, monthly Revenue vs Cost bar chart, and cost breakdown by category (Procurement, Labor, Operational, Overhead).

+
+
+
+
+
+

Reports.vue

+

Quarterly performance reports (revenue, fulfillment rate) and monthly trend tables (order count, revenue, on-time delivery rate).

+
+
+
+
+
+

Backlog.vue

+

Unmet demand tracking by priority (High / Medium / Low), shortage quantities, days delayed, and purchase order creation for backlog items.

+
+
+
+
+ +
+ + + + + diff --git a/client/src/App.vue b/client/src/App.vue index c2da05a5c..b61aae150 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -16,6 +16,9 @@ {{ t('nav.orders') }} + + {{ t('nav.restocking') }} + {{ t('nav.finance') }} diff --git a/client/src/api.js b/client/src/api.js index 11cb9db70..c2176eea0 100644 --- a/client/src/api.js +++ b/client/src/api.js @@ -102,5 +102,15 @@ export const api = { async getPurchaseOrderByBacklogItem(backlogItemId) { const response = await axios.get(`${API_BASE_URL}/purchase-orders/${backlogItemId}`) return response.data + }, + + async createRestockOrder(orderData) { + const response = await axios.post(`${API_BASE_URL}/orders/restock`, orderData) + return response.data + }, + + async getRestockOrders() { + const response = await axios.get(`${API_BASE_URL}/orders/restock`) + return response.data } } diff --git a/client/src/components/PurchaseOrderModal.vue b/client/src/components/PurchaseOrderModal.vue new file mode 100644 index 000000000..0b2434eac --- /dev/null +++ b/client/src/components/PurchaseOrderModal.vue @@ -0,0 +1,591 @@ + + + + + diff --git a/client/src/locales/en.js b/client/src/locales/en.js index 03a58fe6e..4dc0592e5 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' }, @@ -311,6 +312,34 @@ export default { selectLanguage: 'Select Language' }, + // Restocking + restocking: { + title: 'Restocking', + description: 'Recommend and place restocking orders based on demand forecasts and inventory levels', + budget: 'Available Budget', + recommendedItems: 'Recommended Items', + withinBudget: 'Within Budget', + overBudget: 'Over Budget', + quantity: 'Quantity to Order', + estimatedCost: 'Estimated Cost', + totalCost: 'Total Cost', + selectedItems: 'items selected', + placeOrder: 'Place Order', + orderPlaced: 'Order Placed', + submittedOrders: 'Submitted Restocking Orders', + noRecommendations: 'No restocking recommendations based on current demand and inventory data', + successMessage: 'Restocking order submitted successfully', + orderNumber: 'Order Number', + placedDate: 'Placed', + expectedDelivery: 'Expected Delivery', + trend: 'Trend', + currentStock: 'Current Stock', + reorderPoint: 'Reorder Point', + budgetUsed: 'of budget used', + selectAll: 'Select All', + clearAll: 'Clear All' + }, + // Common common: { loading: 'Loading...', diff --git a/client/src/locales/ja.js b/client/src/locales/ja.js index db33223ac..7241b250f 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: '在庫管理システム' }, @@ -311,6 +312,34 @@ export default { selectLanguage: '言語を選択' }, + // Restocking + restocking: { + title: '再入荷', + description: '需要予測と在庫レベルに基づいて再入荷注文を推奨・発注します', + budget: '利用可能予算', + recommendedItems: '推奨品目', + withinBudget: '予算内', + overBudget: '予算超過', + quantity: '注文数量', + estimatedCost: '推定費用', + totalCost: '合計費用', + selectedItems: '件選択中', + placeOrder: '注文する', + orderPlaced: '注文完了', + submittedOrders: '提出済み再入荷注文', + noRecommendations: '現在の需要と在庫データに基づく再入荷推奨はありません', + successMessage: '再入荷注文が正常に提出されました', + orderNumber: '注文番号', + placedDate: '発注日', + expectedDelivery: '予定納期', + trend: 'トレンド', + currentStock: '現在庫', + reorderPoint: '再注文点', + budgetUsed: '予算使用率', + selectAll: 'すべて選択', + clearAll: 'すべてクリア' + }, + // Common common: { loading: '読み込み中...', diff --git a/client/src/main.js b/client/src/main.js index 477c2d966..3347ae013 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(), @@ -15,6 +16,7 @@ const router = createRouter({ { path: '/inventory', component: Inventory }, { path: '/orders', component: Orders }, { path: '/demand', component: Demand }, + { path: '/restocking', component: Restocking }, { path: '/spending', component: Spending }, { path: '/reports', component: Reports } ] 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..e70dcd770 100644 --- a/client/src/views/Orders.vue +++ b/client/src/views/Orders.vue @@ -27,7 +27,7 @@ -
+

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

@@ -74,6 +74,36 @@
+ +
+
+

{{ t('restocking.submittedOrders') }} ({{ restockOrders.length }})

+
+
+ + + + + + + + + + + + + + + + + + + + + +
{{ t('restocking.orderNumber') }}{{ t('orders.table.items') }}{{ t('restocking.placedDate') }}{{ t('restocking.expectedDelivery') }}{{ t('orders.table.totalValue') }}{{ t('orders.table.status') }}
{{ order.order_number }}{{ t('orders.itemsCount', { count: order.items.length }) }}{{ formatDate(order.order_date) }}{{ formatDate(order.expected_delivery) }}{{ currencySymbol }}{{ order.total_value.toLocaleString() }}{{ order.status }}
+
+
@@ -95,6 +125,7 @@ export default { const loading = ref(true) const error = ref(null) const orders = ref([]) + const restockOrders = ref([]) // Use shared filters const { @@ -129,6 +160,14 @@ export default { loadOrders() }) + const loadRestockOrders = async () => { + try { + restockOrders.value = await api.getRestockOrders() + } catch (err) { + console.error('Failed to load restock orders:', err) + } + } + const getOrdersByStatus = (status) => { return orders.value.filter(order => order.status === status) } @@ -153,19 +192,24 @@ export default { }) } - onMounted(loadOrders) + onMounted(() => { + loadOrders() + loadRestockOrders() + }) return { t, loading, error, orders, + restockOrders, getOrdersByStatus, getOrderStatusClass, formatDate, currencySymbol, translateProductName, - translateCustomerName + translateCustomerName, + loadRestockOrders } } } @@ -276,4 +320,13 @@ export default { font-size: 0.813rem; color: #64748b; } + +.restock-card { + border-top: 3px solid #2563eb; +} + +.restock-table { + width: 100%; + border-collapse: collapse; +} diff --git a/client/src/views/Restocking.vue b/client/src/views/Restocking.vue new file mode 100644 index 000000000..d38b6ed47 --- /dev/null +++ b/client/src/views/Restocking.vue @@ -0,0 +1,598 @@ + + + + + diff --git a/server/main.py b/server/main.py index a0c2d8c5a..5e9920d83 100644 --- a/server/main.py +++ b/server/main.py @@ -2,6 +2,8 @@ from fastapi.middleware.cors import CORSMiddleware from typing import List, Optional from pydantic import BaseModel +from datetime import datetime, timedelta +import mock_data 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") @@ -100,6 +102,7 @@ class BacklogItem(BaseModel): days_delayed: int priority: str has_purchase_order: Optional[bool] = False + purchase_order_id: Optional[str] = None class PurchaseOrder(BaseModel): id: str @@ -120,6 +123,23 @@ class CreatePurchaseOrderRequest(BaseModel): expected_delivery_date: str notes: Optional[str] = None +class RestockOrderRequest(BaseModel): + items: List[dict] + total_value: float + warehouse: Optional[str] = "San Francisco" + +class Task(BaseModel): + id: str + title: str + priority: str + dueDate: str + status: str + +class CreateTaskRequest(BaseModel): + title: str + priority: str + dueDate: str + # API endpoints @app.get("/") def root(): @@ -153,6 +173,36 @@ def get_orders( filtered_orders = filter_by_month(filtered_orders, month) return filtered_orders +@app.get("/api/orders/restock") +def get_restock_orders(): + """Get all submitted restocking orders""" + return mock_data.restock_orders + +@app.post("/api/orders/restock", status_code=201) +def create_restock_order(request: RestockOrderRequest): + """Submit a restocking order from the Restocking tab""" + now = datetime.now() + new_id = f"restock-{len(mock_data.restock_orders) + 1}" + order_number = f"RST-{now.year}-{len(mock_data.restock_orders) + 1:04d}" + expected_delivery = (now + timedelta(days=14)).isoformat() + + new_order = { + "id": new_id, + "order_number": order_number, + "customer": "Internal Restocking", + "items": request.items, + "status": "Processing", + "warehouse": request.warehouse, + "category": "Restocking", + "order_date": now.isoformat(), + "expected_delivery": expected_delivery, + "total_value": request.total_value, + "actual_delivery": None, + "is_restock": True + } + mock_data.restock_orders.append(new_order) + return new_order + @app.get("/api/orders/{order_id}", response_model=Order) def get_order(order_id: str): """Get a specific order""" @@ -161,6 +211,71 @@ def get_order(order_id: str): raise HTTPException(status_code=404, detail="Order not found") return order +@app.get("/api/tasks", response_model=List[Task]) +def get_tasks(): + """Get all API-created tasks (mock user tasks live client-side)""" + return mock_data.tasks + +@app.post("/api/tasks", response_model=Task, status_code=201) +def create_task(request: CreateTaskRequest): + """Create a new task from the profile menu's Tasks modal""" + new_task = { + "id": f"task-{len(mock_data.tasks) + 1}", + "title": request.title, + "priority": request.priority, + "dueDate": request.dueDate, + "status": "pending" + } + mock_data.tasks.append(new_task) + return new_task + +@app.delete("/api/tasks/{task_id}") +def delete_task(task_id: str): + """Delete a task""" + task = next((t for t in mock_data.tasks if t["id"] == task_id), None) + if not task: + raise HTTPException(status_code=404, detail="Task not found") + mock_data.tasks.remove(task) + return {"message": "Task deleted"} + +@app.patch("/api/tasks/{task_id}", response_model=Task) +def toggle_task(task_id: str): + """Toggle a task's status between pending and completed""" + task = next((t for t in mock_data.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): + """Create a purchase order for a backlog item""" + backlog_item = next((item for item in backlog_items if item["id"] == request.backlog_item_id), None) + if not backlog_item: + raise HTTPException(status_code=404, detail="Backlog item not found") + + new_po = { + "id": f"po-{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": datetime.now().isoformat(), + "notes": request.notes + } + purchase_orders.append(new_po) + return new_po + +@app.get("/api/purchase-orders/{backlog_item_id}", response_model=PurchaseOrder) +def get_purchase_order_by_backlog_item(backlog_item_id: str): + """Get the purchase order associated with a backlog item""" + po = next((po for po in purchase_orders if po["backlog_item_id"] == backlog_item_id), None) + if not po: + raise HTTPException(status_code=404, detail="Purchase order not found") + return po + @app.get("/api/demand", response_model=List[DemandForecast]) def get_demand_forecasts(): """Get demand forecasts""" @@ -174,8 +289,9 @@ def get_backlog(): for item in backlog_items: item_dict = dict(item) # Check if this backlog item has a purchase order - has_po = any(po["backlog_item_id"] == item["id"] for po in purchase_orders) - item_dict["has_purchase_order"] = has_po + po = next((po for po in purchase_orders if po["backlog_item_id"] == item["id"]), None) + item_dict["has_purchase_order"] = po is not None + item_dict["purchase_order_id"] = po["id"] if po else None result.append(item_dict) return result diff --git a/server/mock_data.py b/server/mock_data.py index 2a9cd7dcb..6e92adf64 100644 --- a/server/mock_data.py +++ b/server/mock_data.py @@ -35,5 +35,11 @@ def load_json_file(filename): # Load purchase orders purchase_orders = load_json_file('purchase_orders.json') +# In-memory list for restocking orders submitted via the Restocking tab +restock_orders = [] + +# In-memory list for tasks created via the profile menu's Tasks modal +tasks = [] + # All data is now loaded from JSON files in the data/ directory # This allows for easier maintenance and updates of the sample data