diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 00000000..aad6a23e --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,35 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize] + +jobs: + claude-review: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: | + Please review this pull request and provide feedback on: + - Code quality and best practices + - Potential bugs or issues + - Performance considerations + - Security concerns + - Test coverage + + Be constructive and specific in your feedback. Use inline comments + to highlight specific lines where relevant. diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 00000000..0c70af76 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,38 @@ +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: + # Only run when someone mentions @claude in a comment, issue, or review + 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: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + 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: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} diff --git a/CLAUDE.md b/CLAUDE.md index d2086efa..7fc95705 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,3 +72,6 @@ npm install && npm run dev - Status: green/blue/yellow/red - Charts: Custom SVG, CSS Grid for layouts - No emojis in UI + +## Code Style +- Always document non-obvious logic changes with comments diff --git a/client/src/App.vue b/client/src/App.vue index c2da05a5..e3ed2a69 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -16,6 +16,9 @@ {{ t('nav.orders') }} + + Restocking + {{ t('nav.finance') }} diff --git a/client/src/api.js b/client/src/api.js index 11cb9db7..8e58f351 100644 --- a/client/src/api.js +++ b/client/src/api.js @@ -102,5 +102,20 @@ export const api = { async getPurchaseOrderByBacklogItem(backlogItemId) { const response = await axios.get(`${API_BASE_URL}/purchase-orders/${backlogItemId}`) return response.data + }, + + async getRestockingRecommendations() { + const response = await axios.get(`${API_BASE_URL}/restocking/recommendations`) + return response.data + }, + + async submitRestockingOrder(items, budgetUsed) { + const response = await axios.post(`${API_BASE_URL}/restocking/orders`, { items, budget_used: budgetUsed }) + return response.data + }, + + async getRestockingOrders() { + const response = await axios.get(`${API_BASE_URL}/restocking/orders`) + return response.data } } diff --git a/client/src/main.js b/client/src/main.js index 477c2d96..8884eea6 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 7413f6e6..e4d390e0 100644 --- a/client/src/views/Orders.vue +++ b/client/src/views/Orders.vue @@ -8,6 +8,49 @@
{{ t('common.loading') }}
{{ error }}
+ + +
+
+

Submitted Orders {{ restockingOrders.length }}

+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Order #ItemsStatusOrder DateExpected DeliveryLead TimeTotal Value
{{ order.order_number }} +
+ {{ order.items.length }} item{{ order.items.length !== 1 ? 's' : '' }} +
+
+ {{ item.name }} + Qty: {{ item.quantity }} @ ${{ item.unit_cost }} +
+
+
+
Processing{{ formatDate(order.order_date) }}{{ formatDate(order.expected_delivery) }}{{ order.lead_time_days }} days${{ order.total_value.toLocaleString() }}
+
+
{{ t('status.delivered') }}
@@ -95,6 +138,7 @@ export default { const loading = ref(true) const error = ref(null) const orders = ref([]) + const restockingOrders = ref([]) // Use shared filters const { @@ -109,7 +153,10 @@ export default { try { loading.value = true const filters = getCurrentFilters() - const fetchedOrders = await api.getOrders(filters) + const [fetchedOrders, fetchedRestocking] = await Promise.all([ + api.getOrders(filters), + api.getRestockingOrders(), + ]) // Sort orders by order_date (earliest first) orders.value = fetchedOrders.sort((a, b) => { @@ -117,6 +164,9 @@ export default { const dateB = new Date(b.order_date) return dateA - dateB }) + + // Most-recent restocking orders first + restockingOrders.value = fetchedRestocking.slice().reverse() } catch (err) { error.value = 'Failed to load orders: ' + err.message } finally { @@ -160,6 +210,7 @@ export default { loading, error, orders, + restockingOrders, getOrdersByStatus, getOrderStatusClass, formatDate, @@ -276,4 +327,35 @@ export default { font-size: 0.813rem; color: #64748b; } + +.restocking-section { + border-color: #bfdbfe; + background: #fafbff; +} + +.restocking-section .card-header { + border-bottom-color: #bfdbfe; +} + +.section-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 20px; + padding: 0 6px; + background: #2563eb; + color: #fff; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 700; + margin-left: 0.5rem; + vertical-align: middle; +} + +.lead-time { + font-size: 0.813rem; + color: #475569; + font-weight: 500; +} diff --git a/client/src/views/Restocking.vue b/client/src/views/Restocking.vue new file mode 100644 index 00000000..f206b32b --- /dev/null +++ b/client/src/views/Restocking.vue @@ -0,0 +1,449 @@ + + + + + diff --git a/docs/architecture.html b/docs/architecture.html new file mode 100644 index 00000000..b48c5660 --- /dev/null +++ b/docs/architecture.html @@ -0,0 +1,658 @@ + + + + + + Factory Inventory Management — System Architecture + + + + +

Factory Inventory Management — System Architecture

+

Full-stack demo · Vue 3 frontend · Python FastAPI backend · In-memory mock data

+ + +
+

Tech Stack

+
+ +
+
Frontend
+

Vue 3 + Vite

+ :3000 +
    +
  • Composition API
  • +
  • Vue Router (6 views)
  • +
  • Axios HTTP client
  • +
  • Custom SVG charts
  • +
  • EN / JA i18n
  • +
+
+ +
+
Backend
+

Python FastAPI

+ :8001 +
    +
  • Uvicorn ASGI server
  • +
  • Pydantic v2 models
  • +
  • Query-param filtering
  • +
  • CORS: all origins
  • +
  • OpenAPI docs at /docs
  • +
+
+ +
+
Data
+

JSON Files

+ server/data/ +
    +
  • 7 JSON data files
  • +
  • Loaded at startup
  • +
  • Filtered in-memory
  • +
  • No database
  • +
  • ~300+ mock records
  • +
+
+ +
+
+ + +
+

Architecture Layers

+
+ +
+
Browser
+
+
+
App.vue — Root Layout
+
Navigation sidebar · FilterBar (4 dropdowns) · router-view · ProfileMenu · LanguageSwitcher
+
+
+
Views (6 Pages)
+
Dashboard · Inventory · Orders · Spending · Demand · Reports
+
+
+
Modals & Composables
+
10 modal components · useFilters (global state) · useI18n (locale)
+
+
+
+ +
+
+
HTTP / Axios — query params (warehouse, category, status, month)
+
+
+ +
+
Server
+
+
+
FastAPI — main.py
+
14 GET endpoints · 2 POST endpoints · apply_filters() · filter_by_month()
+
+
+
Pydantic Models
+
InventoryItem · Order · DemandForecast · BacklogItem · PurchaseOrder · Summary
+
+
+
mock_data.py
+
Loads 7 JSON files at startup · Holds all data in-memory as Python dicts
+
+
+
+ +
+
+
File I/O at startup — loaded once into memory
+
+
+ +
+
Data
+
+
+
inventory.json
+
20+ items · 3 warehouses · 5 categories
+
+
+
orders.json
+
200+ orders · 12 months 2025 · 4 statuses
+
+
+
spending + transactions
+
Monthly breakdown · 50+ transactions · cost categories
+
+
+
backlog + demand + POs
+
10+ backlog items · 30+ forecasts · purchase orders
+
+
+
+ +
+
+ + +
+

Filter Data Flow

+
+
+
+
01
+
User picks filter
+
FilterBar dropdown — Time, Location, Category, or Status
+
+
+
+
02
+
useFilters updates
+
Global singleton ref (selectedPeriod, selectedLocation…) updates reactively
+
+
+
+
03
+
watch() triggers
+
Each view watches filter refs and calls loadData() on any change
+
+
+
+
04
+
api.js call
+
Builds URLSearchParams; Axios GET to FastAPI with filter query params
+
+
+
+
05
+
Backend filters
+
apply_filters() + filter_by_month() reduce in-memory dataset
+
+
+
+
06
+
Response → computed
+
ref[] updated → computed properties recalculate → template re-renders
+
+
+
+
+ + +
+

API Endpoints

+
+
+
+

Inventory & Orders

+
GET/api/inventory
+
GET/api/inventory/{id}
+
GET/api/orders
+
GET/api/orders/{id}
+
GET/api/demand
+
GET/api/backlog
+
+
+

Dashboard & Reports

+
GET/api/dashboard/summary
+
GET/api/reports/quarterly
+
GET/api/reports/monthly-trends
+
+
+

Spending

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

Tasks & Purchase Orders

+
GET/api/tasks
+
POST/api/tasks
+
POST/api/purchase-orders
+
GET/api/purchase-orders/{id}
+
+
+
+
+ + +
+

Frontend Views

+
+
+
Dashboard.vue
+
KPI cards, order health donut, inventory by category, backlog table, top products
+
+
+
Inventory.vue
+
Stock table with search, stock status badges, sorted by reorder level
+
+
+
Orders.vue
+
Order table, 4 status stat cards, expandable item details
+
+
+
Spending.vue
+
Revenue/cost KPIs, monthly bars, stacked cost chart, transactions table
+
+
+
Demand.vue
+
Trend cards (increasing / stable / decreasing), full forecast table
+
+
+
Reports.vue
+
Quarterly table, monthly revenue chart, month-over-month growth rates
+
+
+
+ + +
+

Data Files — server/data/

+
+
+
inventory.json
+
20+ items · 3 warehouses
+
+
+
orders.json
+
200+ orders · 12 months
+
+
+
backlog_items.json
+
10+ shortage items
+
+
+
demand_forecasts.json
+
30+ forecasts w/ trends
+
+
+
spending.json
+
Monthly cost breakdown
+
+
+
transactions.json
+
50+ vendor transactions
+
+
+
purchase_orders.json
+
POs linked to backlog
+
+
+
+ + + + + diff --git a/server/main.py b/server/main.py index a0c2d8c5..6f89aa21 100644 --- a/server/main.py +++ b/server/main.py @@ -2,8 +2,11 @@ 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 +restocking_orders = [] + app = FastAPI(title="Factory Inventory Management System") # Quarter mapping for date filtering @@ -14,6 +17,14 @@ 'Q4-2025': ['2025-10', '2025-11', '2025-12'] } +CATEGORY_LEAD_TIMES = { + "Circuit Boards": 14, + "Sensors": 7, + "Actuators": 10, + "Controllers": 12, + "Power Supplies": 8, +} + 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,29 @@ class CreatePurchaseOrderRequest(BaseModel): expected_delivery_date: str notes: Optional[str] = None +class RestockingOrderItem(BaseModel): + sku: str + name: str + category: str + quantity: int + unit_cost: float + total_cost: float + +class RestockingOrder(BaseModel): + id: str + order_number: str + items: List[RestockingOrderItem] + total_value: float + budget_used: float + order_date: str + expected_delivery: str + status: str + lead_time_days: int + +class CreateRestockingOrderRequest(BaseModel): + items: List[dict] + budget_used: float + # API endpoints @app.get("/") def root(): @@ -304,6 +338,90 @@ def get_monthly_trends(): result.sort(key=lambda x: x['month']) return result +@app.get("/api/restocking/recommendations") +def get_restocking_recommendations(): + """Get restocking recommendations: low-stock items enriched with demand forecast data""" + forecast_by_name = {df["item_name"].lower(): df for df in demand_forecasts} + + recommendations = [] + for item in inventory_items: + if item["quantity_on_hand"] <= item["reorder_point"]: + recommended_qty = (item["reorder_point"] * 2) - item["quantity_on_hand"] + estimated_cost = round(recommended_qty * item["unit_cost"], 2) + + forecast = forecast_by_name.get(item["name"].lower()) + if forecast and forecast["current_demand"] > 0: + demand_growth_pct = round( + (forecast["forecasted_demand"] - forecast["current_demand"]) / forecast["current_demand"] * 100, 1 + ) + else: + demand_growth_pct = 15.0 + + lead_days = CATEGORY_LEAD_TIMES.get(item["category"], 10) + + recommendations.append({ + "sku": item["sku"], + "item_name": item["name"], + "category": item["category"], + "warehouse": item["warehouse"], + "quantity_on_hand": item["quantity_on_hand"], + "reorder_point": item["reorder_point"], + "recommended_qty": recommended_qty, + "unit_cost": item["unit_cost"], + "estimated_cost": estimated_cost, + "demand_growth_pct": demand_growth_pct, + "lead_days": lead_days, + }) + + recommendations.sort(key=lambda x: x["demand_growth_pct"], reverse=True) + return recommendations + + +@app.post("/api/restocking/orders", response_model=RestockingOrder) +def create_restocking_order(request: CreateRestockingOrderRequest): + """Submit a restocking order; lead time determined by the longest category lead time""" + now = datetime.now() + max_lead_days = max( + (CATEGORY_LEAD_TIMES.get(item.get("category", ""), 10) for item in request.items), + default=10 + ) + expected_delivery = now + timedelta(days=max_lead_days) + order_id = f"RST-{len(restocking_orders) + 1:04d}" + + processed_items = [] + total_value = 0.0 + for item in request.items: + item_cost = round(item["quantity"] * item["unit_cost"], 2) + total_value += item_cost + processed_items.append({ + "sku": item["sku"], + "name": item["name"], + "category": item.get("category", ""), + "quantity": item["quantity"], + "unit_cost": item["unit_cost"], + "total_cost": item_cost, + }) + + order = { + "id": order_id, + "order_number": order_id, + "items": processed_items, + "total_value": round(total_value, 2), + "budget_used": request.budget_used, + "order_date": now.strftime("%Y-%m-%dT%H:%M:%S"), + "expected_delivery": expected_delivery.strftime("%Y-%m-%d"), + "status": "Processing", + "lead_time_days": max_lead_days, + } + + 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)