diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 00000000..77105961 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,37 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + id-token: write + actions: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} diff --git a/CLAUDE.md b/CLAUDE.md index d2086efa..8858b615 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,6 +67,9 @@ npm install && npm run dev - Data: `server/data/*.json` - Styles: `client/src/App.vue` +## Code Style +- Always document non-obvious logic changes with comments + ## Design System - Colors: Slate/gray (#0f172a, #64748b, #e2e8f0) - Status: green/blue/yellow/red diff --git a/architecture.html b/architecture.html new file mode 100644 index 00000000..ad0d9620 --- /dev/null +++ b/architecture.html @@ -0,0 +1,698 @@ + + + + + + Architecture — Factory Inventory Management + + + +
+ + +
+

Factory Inventory Management — System Architecture

+

Full-stack demo application with in-memory mock data, global filter system, and multi-view dashboard.

+
+ Vue 3 + FastAPI + No Database +
+
+ + +
+

Tech Stack

+
+
+
+
+

Vue 3

+

Composition API, Vue Router 4, scoped CSS, no UI library

+
+
+
+
+
+

FastAPI

+

Python, Pydantic models, CORS middleware, port 8001

+
+
+
+
+
+

Axios

+

HTTP client, centralized in api.js, query param filters

+
+
+
+
+
+

Vite

+

Dev server, port 3000, fast HMR, ES module builds

+
+
+
+
+ + +
+

System Overview

+
+
+
Browser
+
Vue 3 App
+
localhost:3000
+
+
+
State
+
Composables
+
useFilters · useAuth · useI18n
+
+
+
HTTP
+
api.js (Axios)
+
Query params → FastAPI
+
+
+
Server
+
FastAPI
+
localhost:8001
+
+
+
Data
+
JSON Files
+
server/data/*.json
loaded at startup
+
+
+
+ + +
+

Filter Data Flow

+
+
+
1. User
+
FilterBar.vue
+
Selects period, warehouse, category, status
+
+
+
2. State
+
useFilters()
+
Reactive singleton — updates shared state
+
+
+
3. View
+
watch() trigger
+
Page re-fetches on filter change
+
+
+
4. API
+
GET /api/...
+
?warehouse=&category=&status=&month=
+
+
+
5. Backend
+
apply_filters()
+
Python in-memory filter on JSON data
+
+
+
6. Render
+
Computed props
+
Vue re-renders tables & charts
+
+
+
+ Filter params: + month orders, dashboard + warehouse all endpoints + category inventory, orders, dashboard + status orders, dashboard +
+
+ + +
+

Frontend Views (client/src/views/)

+
+
+

Dashboard.vue

+

KPI overview — inventory turnover, fulfillment rate, low stock, backlog counts

+
+ /api/dashboard/summary + /api/backlog +
+
+
+

Inventory.vue

+

Stock levels, SKU search, warehouse/category filtering, detail modals

+
+ /api/inventory +
+
+
+

Orders.vue

+

Order status tracking — delivered, shipped, processing, backordered

+
+ /api/orders +
+
+
+

Demand.vue

+

Demand forecasts grouped by trend — increasing, stable, decreasing

+
+ /api/demand + /api/backlog +
+
+
+

Spending.vue

+

Revenue, costs, profit margin, monthly and category spending breakdowns

+
+ /api/spending/summary + /api/spending/monthly + /api/spending/categories + /api/spending/transactions +
+
+
+

Reports.vue

+

Quarterly metrics and monthly trend analysis — orders, revenue, fulfillment rate

+
+ /api/reports/quarterly + /api/reports/monthly-trends +
+
+
+
+ + +
+

Shared State (Composables)

+
+
+

useFilters.js

+ client/src/composables/ +
    +
  • Singleton reactive filter state
  • +
  • selectedPeriod, selectedLocation, selectedCategory, selectedStatus
  • +
  • getCurrentFilters() for API calls
  • +
  • resetFilters(), hasActiveFilters computed
  • +
+
+
+

useAuth.js

+ client/src/composables/ +
    +
  • currentUser profile data
  • +
  • tasks array with CRUD via /api/tasks
  • +
  • isAuthenticated flag
  • +
  • getInitials(name) helper
  • +
+
+
+

useI18n.js

+ client/src/composables/ +
    +
  • Locale switching: EN / JA
  • +
  • t(key) translate helper
  • +
  • Translations in locales/en.js, locales/ja.js
  • +
  • Translates labels, months, warehouses, categories
  • +
+
+
+
+ + +
+

API Endpoints (FastAPI — localhost:8001)

+
+ +
+

Inventory

+
+ GET + /api/inventory + warehousecategory — filtered stock list +
+
+ GET + /api/inventory/{id} + Single inventory item by ID +
+
+ +
+

Orders

+
+ GET + /api/orders + warehousecategorystatusmonth +
+
+ GET + /api/orders/{id} + Single order by ID +
+
+ +
+

Dashboard

+
+ GET + /api/dashboard/summary + warehousecategorystatusmonth — KPI aggregates +
+
+ +
+

Demand & Backlog

+
+ GET + /api/demand + All demand forecasts (no filters) +
+
+ GET + /api/backlog + All unfulfilled backlog items (no filters) +
+
+ +
+

Spending

+
+ GET + /api/spending/summary + Totals: procurement, operational, labor, overhead +
+
+ GET + /api/spending/monthly + Month-by-month cost breakdown +
+
+ GET + /api/spending/categories + Spending grouped by category +
+
+ GET + /api/spending/transactions + Historical transaction list +
+
+ +
+

Reports

+
+ GET + /api/reports/quarterly + Q1–Q4 aggregates: orders, revenue, fulfillment rate +
+
+ GET + /api/reports/monthly-trends + Per-month order count, revenue, delivered count +
+
+ +
+

Tasks

+
+ GET + /api/tasks + User task list +
+
+ POST + /api/tasks + Create new task +
+
+ PATCH + /api/tasks/{id} + Toggle task status +
+
+ DELETE + /api/tasks/{id} + Remove task +
+
+ +
+
+ + +
+

Core Data Models (Pydantic / JSON)

+
+
+

InventoryItem

+
id, sku, namestr
+
category, warehousestr
+
quantity_on_handint
+
reorder_pointint
+
unit_costfloat
+
location, last_updatedstr
+
+
+

Order

+
id, order_numberstr
+
customerstr
+
items[]sku, qty, price
+
statusDelivered | Shipped | Processing | Backordered
+
warehouse, categorystr
+
total_valuefloat
+
order_date, expected_delivery, actual_deliverydatetime
+
+
+

BacklogItem

+
id, order_idstr
+
item_sku, item_namestr
+
quantity_neededint
+
quantity_availableint
+
days_delayedint
+
prioritylow | medium | high
+
has_purchase_orderbool
+
+
+

DemandForecast

+
id, item_sku, item_namestr
+
current_demandint
+
forecasted_demandint
+
trendincreasing | stable | decreasing
+
periodstr
+
+
+

DashboardSummary

+
total_inventory_valuefloat
+
low_stock_itemsint
+
pending_ordersint
+
total_backlog_itemsint
+
total_orders_valuefloat
+
+
+

Transaction

+
id, datestr
+
description, vendorstr
+
category, warehousestr
+
amountfloat
+
typePurchase | Sale | ...
+
+
+
+ + +
+

File Structure

+
+
+ client/src/
+   views/
+     Dashboard.vue
+     Inventory.vue
+     Orders.vue
+     Demand.vue
+     Spending.vue
+     Reports.vue
+   components/
+     FilterBar.vue  ← global filter UI
+     ProfileMenu.vue
+     *DetailModal.vue  ← 5 modals
+     LanguageSwitcher.vue
+   composables/
+     useFilters.js  ← singleton filter state
+     useAuth.js
+     useI18n.js
+   locales/
+     en.js  ja.js
+   utils/  currency.js
+   App.vue  ← router-view + nav
+   main.js  ← Vue + Router init
+   api.js  ← all Axios calls +
+
+ server/
+   main.py  ← FastAPI app + all endpoints
+   mock_data.py  ← loads JSON at startup
+   data/
+     inventory.json
+     orders.json
+     backlog_items.json
+     demand_forecasts.json
+     spending.json
+     transactions.json
+     purchase_orders.json
+
+ client/
+   package.json
+   vite.config.js
+   index.html
+
+ CLAUDE.md  ← project instructions
+ architecture.html  ← this file +
+
+
+ + +
+

Reference Data

+
+
+

Warehouses

+
    +
  • San Francisco
  • +
  • London
  • +
  • Tokyo
  • +
+
+
+

Product Categories

+
    +
  • Circuit Boards
  • +
  • Sensors
  • +
  • Actuators
  • +
  • Controllers
  • +
  • Power Supplies
  • +
+
+
+

Order Statuses

+
    +
  • Delivered
  • +
  • Shipped
  • +
  • Processing
  • +
  • Backordered
  • +
+
+
+
+ + + +
+ + diff --git a/client/src/App.vue b/client/src/App.vue index c2da05a5..2c6081c3 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -25,6 +25,9 @@ Reports + + Restocking + + + + + + + + + + + diff --git a/client/src/main.js b/client/src/main.js index 477c2d96..383ff42b 100644 --- a/client/src/main.js +++ b/client/src/main.js @@ -7,6 +7,8 @@ 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' +import NotFound from './views/NotFound.vue' const router = createRouter({ history: createWebHistory(), @@ -16,7 +18,9 @@ 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 }, + { path: '/:pathMatch(.*)*', component: NotFound } ] }) diff --git a/client/src/views/Dashboard.vue b/client/src/views/Dashboard.vue index 437da9c2..5d496419 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/NotFound.vue b/client/src/views/NotFound.vue new file mode 100644 index 00000000..3240b9b7 --- /dev/null +++ b/client/src/views/NotFound.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/client/src/views/Orders.vue b/client/src/views/Orders.vue index 7413f6e6..606ee773 100644 --- a/client/src/views/Orders.vue +++ b/client/src/views/Orders.vue @@ -62,7 +62,10 @@ - + + Restocking + + {{ t(`status.${order.status.toLowerCase()}`) }} @@ -276,4 +279,9 @@ export default { font-size: 0.813rem; color: #64748b; } + +.restocking-badge { + background: #ede9fe; + color: #5b21b6; +} diff --git a/client/src/views/Restocking.vue b/client/src/views/Restocking.vue new file mode 100644 index 00000000..a9d5a8c1 --- /dev/null +++ b/client/src/views/Restocking.vue @@ -0,0 +1,545 @@ + + + + + diff --git a/dashboard.png b/dashboard.png new file mode 100644 index 00000000..3673c52c Binary files /dev/null and b/dashboard.png differ diff --git a/demand.png b/demand.png new file mode 100644 index 00000000..47510a1c Binary files /dev/null and b/demand.png differ diff --git a/finance.png b/finance.png new file mode 100644 index 00000000..a8a23247 Binary files /dev/null and b/finance.png differ diff --git a/inventory.png b/inventory.png new file mode 100644 index 00000000..d75f159d Binary files /dev/null and b/inventory.png differ diff --git a/orders.png b/orders.png new file mode 100644 index 00000000..b45f95b7 Binary files /dev/null and b/orders.png differ diff --git a/reports.png b/reports.png new file mode 100644 index 00000000..3b153883 Binary files /dev/null and b/reports.png differ diff --git a/restocking.png b/restocking.png new file mode 100644 index 00000000..5b93d10f Binary files /dev/null and b/restocking.png differ diff --git a/server/data/inventory.json b/server/data/inventory.json index 486fda81..60629de2 100644 --- a/server/data/inventory.json +++ b/server/data/inventory.json @@ -382,5 +382,101 @@ "unit_cost": 185.50, "location": "Warehouse C-11", "last_updated": "2025-09-21T15:20:00" + }, + { + "id": "33", + "sku": "WDG-001", + "name": "Industrial Widget Type A", + "category": "Actuators", + "warehouse": "San Francisco", + "quantity_on_hand": 300, + "reorder_point": 100, + "unit_cost": 45.00, + "location": "Warehouse A-18", + "last_updated": "2025-09-30T09:00:00" + }, + { + "id": "34", + "sku": "BRG-102", + "name": "Steel Bearing Assembly", + "category": "Actuators", + "warehouse": "Tokyo", + "quantity_on_hand": 150, + "reorder_point": 80, + "unit_cost": 28.50, + "location": "Warehouse C-12", + "last_updated": "2025-09-30T09:00:00" + }, + { + "id": "35", + "sku": "GSK-203", + "name": "High-Temperature Gasket", + "category": "Actuators", + "warehouse": "London", + "quantity_on_hand": 500, + "reorder_point": 200, + "unit_cost": 15.75, + "location": "Warehouse B-18", + "last_updated": "2025-09-30T09:00:00" + }, + { + "id": "36", + "sku": "MTR-304", + "name": "Electric Motor 5HP", + "category": "Actuators", + "warehouse": "Tokyo", + "quantity_on_hand": 50, + "reorder_point": 20, + "unit_cost": 89.00, + "location": "Warehouse C-13", + "last_updated": "2025-09-30T09:00:00" + }, + { + "id": "37", + "sku": "FLT-405", + "name": "Oil Filter Cartridge", + "category": "Actuators", + "warehouse": "San Francisco", + "quantity_on_hand": 800, + "reorder_point": 300, + "unit_cost": 22.00, + "location": "Warehouse A-19", + "last_updated": "2025-09-30T09:00:00" + }, + { + "id": "38", + "sku": "VLV-506", + "name": "Pressure Relief Valve", + "category": "Actuators", + "warehouse": "London", + "quantity_on_hand": 120, + "reorder_point": 60, + "unit_cost": 67.50, + "location": "Warehouse B-19", + "last_updated": "2025-09-30T09:00:00" + }, + { + "id": "39", + "sku": "SNR-420", + "name": "Temperature Sensor Module", + "category": "Sensors", + "warehouse": "Tokyo", + "quantity_on_hand": 180, + "reorder_point": 75, + "unit_cost": 35.25, + "location": "Warehouse C-14", + "last_updated": "2025-09-30T09:00:00" + }, + { + "id": "40", + "sku": "CTL-330", + "name": "Logic Controller Board", + "category": "Controllers", + "warehouse": "San Francisco", + "quantity_on_hand": 95, + "reorder_point": 40, + "unit_cost": 95.00, + "location": "Warehouse A-20", + "last_updated": "2025-09-30T09:00:00" } ] \ No newline at end of file diff --git a/server/main.py b/server/main.py index a0c2d8c5..9446ec1b 100644 --- a/server/main.py +++ b/server/main.py @@ -1,8 +1,10 @@ +import uuid +from datetime import datetime, timedelta from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware 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 mock_data import inventory_items, orders, demand_forecasts, backlog_items, spending_summary, monthly_spending, category_spending, recent_transactions, purchase_orders, restocking_orders app = FastAPI(title="Factory Inventory Management System") @@ -80,6 +82,7 @@ class Order(BaseModel): actual_delivery: Optional[str] = None warehouse: Optional[str] = None category: Optional[str] = None + source: Optional[str] = None # "restocking" for restocking orders, None for regular class DemandForecast(BaseModel): id: str @@ -120,6 +123,27 @@ class CreatePurchaseOrderRequest(BaseModel): expected_delivery_date: str notes: Optional[str] = None +class RestockingOrderItem(BaseModel): + sku: str + name: str + quantity: int + unit_price: float + +class CreateRestockingOrderRequest(BaseModel): + items: List[RestockingOrderItem] + +class Task(BaseModel): + id: str + title: str + status: str # 'pending' | 'completed' + created_date: str + +class CreateTaskRequest(BaseModel): + title: str + +# In-memory task storage +tasks_store: List[dict] = [] + # API endpoints @app.get("/") def root(): @@ -148,8 +172,9 @@ def get_orders( status: Optional[str] = None, month: Optional[str] = None ): - """Get all orders with optional filtering""" - filtered_orders = apply_filters(orders, warehouse, category, status) + """Get all orders (including restocking orders) with optional filtering""" + all_orders = list(orders) + list(restocking_orders) + filtered_orders = apply_filters(all_orders, warehouse, category, status) filtered_orders = filter_by_month(filtered_orders, month) return filtered_orders @@ -304,6 +329,67 @@ def get_monthly_trends(): result.sort(key=lambda x: x['month']) return result +@app.post("/api/restocking-orders", status_code=201) +def create_restocking_order(request: CreateRestockingOrderRequest): + """Create a new restocking order from selected demand-forecast items.""" + if not request.items: + raise HTTPException(status_code=400, detail="At least one item is required") + order_date = datetime.utcnow() + expected_delivery = order_date + timedelta(days=14) + order_number = f"RST-{order_date.year}-{len(restocking_orders) + 1:04d}" + total_value = sum(item.quantity * item.unit_price for item in request.items) + new_order = { + "id": str(uuid.uuid4()), + "order_number": order_number, + "customer": "Restocking Order", + "items": [item.dict() for item in request.items], + "status": "Processing", + "order_date": order_date.isoformat(), + "expected_delivery": expected_delivery.isoformat(), + "total_value": round(total_value, 2), + "actual_delivery": None, + "warehouse": "Multiple", + "category": "Multiple", + "source": "restocking" + } + restocking_orders.append(new_order) + return new_order + +@app.get("/api/restocking-orders", response_model=List[Order]) +def get_restocking_orders(): + """Get all submitted restocking orders.""" + return restocking_orders + +@app.get("/api/tasks", response_model=List[Task]) +def get_tasks(): + return tasks_store + +@app.post("/api/tasks", response_model=Task, status_code=201) +def create_task(request: CreateTaskRequest): + task = { + "id": str(uuid.uuid4()), + "title": request.title, + "status": "pending", + "created_date": datetime.now().strftime("%Y-%m-%d") + } + tasks_store.append(task) + return task + +@app.delete("/api/tasks/{task_id}", status_code=204) +def delete_task(task_id: str): + task = next((t for t in tasks_store if t["id"] == task_id), None) + if not task: + raise HTTPException(status_code=404, detail="Task not found") + tasks_store.remove(task) + +@app.patch("/api/tasks/{task_id}", response_model=Task) +def toggle_task(task_id: str): + task = next((t for t in tasks_store 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 + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8001) diff --git a/server/mock_data.py b/server/mock_data.py index 2a9cd7dc..d46b3d22 100644 --- a/server/mock_data.py +++ b/server/mock_data.py @@ -37,3 +37,6 @@ def load_json_file(filename): # All data is now loaded from JSON files in the data/ directory # This allows for easier maintenance and updates of the sample data + +# In-memory list for restocking orders created at runtime (not persisted to file) +restocking_orders = []