diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml
new file mode 100644
index 000000000..81c94302e
--- /dev/null
+++ b/.github/workflows/claude.yml
@@ -0,0 +1,58 @@
+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 # Required for Claude to read CI results on PRs
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 1
+
+ - name: Run Claude Code
+ id: claude
+ uses: anthropics/claude-code-action@v1
+ with:
+ anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
+
+ # Optional: Customize the trigger phrase (default: @claude)
+ # trigger_phrase: "/claude"
+
+ # Optional: Trigger when specific user is assigned to an issue
+ # assignee_trigger: "claude-bot"
+
+ # Optional: Configure Claude's behavior with CLI arguments
+ # claude_args: |
+ # --model claude-opus-4-1-20250805
+ # --max-turns 10
+ # --allowedTools "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
+ # --system-prompt "Follow our coding standards. Ensure all new code has tests. Use TypeScript for new files."
+
+ # Optional: Advanced settings configuration
+ # settings: |
+ # {
+ # "env": {
+ # "NODE_ENV": "test"
+ # }
+ # }
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
index 89c307d15..1a343282f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,5 +1,7 @@
# CLAUDE.md
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
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.
@@ -26,7 +28,7 @@ Use the Task tool with these specialized subagents for appropriate tasks:
- Test against: `http://localhost:3000` (frontend), `http://localhost:8001` (API)
## Stack
-- **Frontend**: Vue 3 + Composition API + Vite (port 3000)
+- **Frontend**: Vue 3 + Composition API (Options-style `setup()`) + Vue Router + Vite (port 3000)
- **Backend**: Python FastAPI (port 8001)
- **Data**: JSON files in `server/data/` loaded via `server/mock_data.py`
@@ -42,29 +44,54 @@ cd client
npm install && npm run dev
```
-## Key Patterns
+## Running Tests
+
+```bash
+cd tests
+uv run pytest -v # all tests
+uv run pytest backend/test_inventory.py -v # one file
+uv run pytest backend/test_inventory.py::TestInventoryEndpoints::test_get_all_inventory -v # one test
+```
+
+## Architecture
+
+**Filter System**: 4 filters (Time Period, Warehouse, Category, Order Status) live in the `useFilters` composable (`client/src/composables/useFilters.js`) as module-level refs — a singleton shared across every view. `getCurrentFilters()` maps this state to API query params.
-**Filter System**: 4 filters (Time Period, Warehouse, Category, Order Status) apply to all data via query params
-**Data Flow**: Vue filters → `client/src/api.js` → FastAPI → In-memory filtering → Pydantic validation → Computed properties
-**Reactivity**: Raw data in refs (`allOrders`, `inventoryItems`), derived data in computed properties
+**Data Flow**: Vue filters (`useFilters`) → `client/src/api.js` → FastAPI query params → `apply_filters`/`filter_by_month` in `server/main.py` → Pydantic response models → Vue computed properties.
+
+**Reactivity**: Raw data in refs (`allOrders`, `inventoryItems`), derived data in computed properties.
+
+**i18n**: `useI18n` composable (`client/src/composables/useI18n.js`) drives translations from `client/src/locales/{en,ja}.js`. Locale also determines currency (`en` → USD, `ja` → JPY); use `formatCurrency`/`convertAmount` from `client/src/utils/currency.js` rather than formatting amounts inline. Locale persists to `localStorage` under `app-locale`.
+
+**Auth**: `useAuth` composable (`client/src/composables/useAuth.js`) is fully mocked — a hardcoded current user, `isAuthenticated` always `true`, `logout()` just alerts. Task list in `App.vue` merges this mock user's tasks with real ones fetched via the API.
+
+**Mock data lifecycle**: JSON in `server/data/*.json` is loaded once into module-level Python lists in `server/mock_data.py` at import time. All mutations during a server run are in-memory only; restarting the server reloads from disk.
## API Endpoints
- `GET /api/inventory` - Filters: warehouse, category
-- `GET /api/orders` - Filters: warehouse, category, status, month
+- `GET /api/inventory/{item_id}`
+- `GET /api/orders` - Filters: warehouse, category, status, month (accepts `YYYY-MM` or `QN-YYYY` quarters)
+- `GET /api/orders/{order_id}`
- `GET /api/dashboard/summary` - All filters
- `GET /api/demand`, `/api/backlog` - No filters
- `GET /api/spending/*` - Summary, monthly, categories, transactions
+- `GET /api/reports/quarterly`, `/api/reports/monthly-trends` - Computed on the fly from `orders`, not filterable
+
+**Known gap**: `client/src/api.js` also calls `/api/tasks` (GET/POST/PATCH/DELETE) and `/api/purchase-orders` (GET/POST) — these have no matching routes in `server/main.py`. Calls to them will 404 until implemented.
## Common Issues
1. Use unique keys in v-for (not `index`) - use `sku`, `month`, etc.
2. Validate dates before `.getMonth()` calls
-3. Update Pydantic models when changing JSON data structure
+3. Update Pydantic models in `server/main.py` when changing JSON data structure in `server/data/`
4. Inventory filters don't support month (no time dimension)
5. Revenue goals: $800K/month single, $9.6M YTD all months
+6. `Backlog.vue` exists under `client/src/views/` but is not registered in `client/src/main.js` router
## File Locations
- Views: `client/src/views/*.vue`
+- Composables: `client/src/composables/*.js` (`useFilters`, `useAuth`, `useI18n`)
- API Client: `client/src/api.js`
+- Router: `client/src/main.js`
- Backend: `server/main.py`, `server/mock_data.py`
- Data: `server/data/*.json`
- Styles: `client/src/App.vue`
diff --git a/client/CLAUDE.md b/client/CLAUDE.md
index bb9960e72..e2d2e97db 100644
--- a/client/CLAUDE.md
+++ b/client/CLAUDE.md
@@ -5,480 +5,52 @@ This file provides guidance to Claude Code (claude.ai/code) when working with th
## Running the Client
```bash
-# From client directory
-npm run dev
-# Runs on http://localhost:3000
+npm run dev # http://localhost:3000
+npm run build # output: client/dist/
```
-## Development Best Practices
+## Component Pattern
-### Vue 3 Composition API Patterns
+This codebase uses Composition API via `export default { setup() {...} }`, **not** `
-
-
-```
-
-**Why Composition API:**
-- Better code organization by feature
-- Easier to extract and reuse logic
-- TypeScript support
-- Smaller bundle size
-- More flexible than Options API
-
-### Reactive Data Best Practices
-
-**refs vs computed:**
-- Use `ref()` for values that change via assignment
-- Use `computed()` for values derived from other reactive data
-- computed properties are cached until dependencies change
-- Never mutate computed properties
-
-**Example:**
-```javascript
-// refs - mutable state
-const searchQuery = ref('')
-const items = ref([])
-
-// computed - derived from refs
-const filteredItems = computed(() => {
- if (!searchQuery.value) return items.value
- return items.value.filter(item =>
- item.name.toLowerCase().includes(searchQuery.value.toLowerCase())
- )
-})
-```
-
-**Accessing ref values:**
-- In `
+
+
diff --git a/docs/architecture.html b/docs/architecture.html
new file mode 100644
index 000000000..d2aca3b54
--- /dev/null
+++ b/docs/architecture.html
@@ -0,0 +1,368 @@
+
+
+
System architecture, tech stack, and data flow reference
+
+
+
+
Tech Stack
+
+
+ Frontend
+
Vue 3 SPA :3000
+
+
Vue 3.4 + Composition API
+
Vite 5 dev server / bundler
+
vue-router 4 (6 routes)
+
axios for HTTP calls
+
Custom composables for state (no Vuex/Pinia)
+
Custom i18n (en / ja)
+
+
+
+ Backend
+
FastAPI :8001
+
+
Python 3.11+, FastAPI
+
Uvicorn ASGI server
+
Pydantic models for validation
+
Single-file main.py, no routers
+
CORS open (dev only)
+
uv for dependency management
+
+
+
+ Data
+
In-Memory Mock Data
+
+
Flat JSON files in server/data/
+
Loaded once at process startup
+
No database, no ORM
+
Changes don't persist across restarts
+
Seeded via generate_data.py
+
+
+
+
+
+
+
System Architecture
+
+
+
Browser
+
Vue 3 SPA — port 3000
+
+
6 routed views
+
Global FilterBar
+
Shared composable state
+
+
+
→
+
+
api.js
+
axios client
+
+
Hardcoded base URL
+
http://localhost:8001/api
+
Builds query params from filters
+
+
+
→
+
+
FastAPI
+
main.py — port 8001
+
+
17 GET endpoints
+
Pydantic response models
+
In-request filtering & aggregation
+
+
+
→
+
+
mock_data.py
+
In-memory lists/dicts
+
+
Loaded from server/data/*.json
+
Read once at import time
+
No persistence
+
+
+
+
+
+
+
Data Flow — Example: Dashboard Load
+
+
+
+
1
+
Route mountsUser navigates to / → Dashboard.vue renders, triggering onMounted(loadData).
+
+
+
2
+
Filters readCurrent filter values (period, warehouse, category, status) pulled from the shared useFilters composable.
+
+
+
3
+
Parallel API callsapi.getDashboardSummary(), getOrders(), getInventory(), getBacklog() fire together via axios.
+
+
+
4
+
HTTP requestaxios sends GET /api/dashboard/summary?warehouse=&category=&status=&month= to the FastAPI server.
+
+
+
5
+
Filter & aggregateFastAPI handler applies apply_filters() / filter_by_month() over the in-memory inventory_items and orders lists, then computes KPIs.
+
+
+
6
+
JSON responseResult dict is serialized and returned to axios, resolved back through api.js into a reactive summary ref.
+
+
+
7
+
Re-renderVue reactivity updates the KPI cards. A watch() on the filter refs re-triggers this whole flow on any filter change.
+
+
+
+
+
+
+
Frontend Views
+
+
/
Dashboard
KPIs, revenue vs. goal, order status breakdown, low-stock & backlog highlights.
+
/inventory
Inventory
Stock levels table with search and warehouse/category filtering.
+
/orders
Orders
Order list with status counters and filterable table.
+
/demand
Demand
Forecasts grouped by trend: increasing, stable, decreasing.
+
/spending
Spending
Revenue/cost/profit KPIs and monthly revenue-vs-cost chart.
+
/reports
Reports
Quarterly performance and monthly trend tables.
+
unrouted
Backlog.vue
Backlog/shortage tracking by priority — built, but not wired into router or nav.
+
+
+
+
+
API Endpoints
+
+
+
Method
Path
Purpose
+
+
GET
/api/inventory
List inventory, filter by warehouse/category
+
GET
/api/inventory/{id}
Single inventory item
+
GET
/api/orders
List orders, filter by warehouse/category/status/month
+
GET
/api/orders/{id}
Single order
+
GET
/api/demand
All demand forecasts
+
GET
/api/backlog
Backlog items with purchase-order flag
+
GET
/api/dashboard/summary
Aggregated KPIs, filterable
+
GET
/api/spending/summary
Spending totals
+
GET
/api/spending/monthly
Monthly spending breakdown
+
GET
/api/spending/categories
Spending by category
+
GET
/api/spending/transactions
Recent transactions
+
GET
/api/reports/quarterly
Quarterly order stats, computed on the fly
+
GET
/api/reports/monthly-trends
Month-over-month order stats
+
GET
/api/tasks*
Called by frontend NOT IMPLEMENTED
+
GET
/api/purchase-orders*
Called by frontend NOT IMPLEMENTED
+
+
+
+
+
+
+
+
Known Gaps
+
+
/api/tasks* and /api/purchase-orders* are called by the frontend (task modal, backlog purchase actions) but have no route handlers in main.py — requests 404 and are silently caught in App.vue.
+
Backlog.vue is a fully built view that isn't registered in the router or nav, so it's currently unreachable in the UI.
+
No reverse proxy: the Vite dev server and FastAPI server run independently, and the frontend calls the backend's absolute URL directly rather than through a Vite proxy.
+
No database — all data is flat JSON loaded into memory once at startup; edits don't persist across a server restart.
+
+
+
+
+
+
+
+
+
diff --git a/server/CLAUDE.md b/server/CLAUDE.md
index bca8f7870..1699dd501 100644
--- a/server/CLAUDE.md
+++ b/server/CLAUDE.md
@@ -5,282 +5,29 @@ This file provides guidance to Claude Code (claude.ai/code) when working with th
## Running the Server
```bash
-# From server directory
uv run python main.py
-# Server runs on http://localhost:8001
-# API docs at http://localhost:8001/docs
+# http://localhost:8001, docs at /docs
```
-## Development Best Practices
+## Running Tests
-### API Design Principles
-
-**RESTful Design:**
-- Use appropriate HTTP methods (GET for retrieval, POST for creation, etc.)
-- Return proper status codes (200, 201, 404, 400, 500)
-- Use plural nouns for resource endpoints (`/api/orders`, not `/api/order`)
-- Keep URLs simple and predictable
-
-**Request/Response:**
-- Always validate input with Pydantic models
-- Return consistent response structure
-- Include error details in error responses
-- Use ISO 8601 for dates (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS)
-
-### Adding New Endpoints
-
-**Process:**
-1. Define Pydantic model for data validation
-2. Create endpoint function with clear name
-3. Add route decorator with explicit path
-4. Implement business logic
-5. Handle errors appropriately
-6. Write tests in `tests/backend/`
-
-**Example Pattern:**
-```python
-class MyModel(BaseModel):
- id: str
- name: str
- value: float
-
-@app.get("/api/resource", response_model=List[MyModel])
-def get_resources(
- filter_param: Optional[str] = None,
- category: Optional[str] = None
-):
- """Get resources with optional filtering."""
- results = all_resources
-
- if filter_param and filter_param != 'all':
- results = [r for r in results if r['field'] == filter_param]
-
- if category and category != 'all':
- results = [r for r in results if r['category'].lower() == category.lower()]
-
- return results
-```
-
-### Data Model Best Practices
-
-**Pydantic Models:**
-- Define once, use everywhere
-- Make optional fields explicitly `Optional[Type]`
-- Use descriptive field names
-- Add default values where appropriate
-- Keep models close to their usage
-
-**Model Updates:**
-- When adding fields to JSON data, update Pydantic models
-- When removing fields, mark as Optional first, then remove
-- Consider backwards compatibility
-- Update tests when models change
-
-### Filtering Best Practices
-
-**Standard Pattern:**
-- Accept filter parameters as optional query params
-- Check for 'all' value and skip that filter
-- Use lowercase comparison for case-insensitive matching
-- Apply filters sequentially for code clarity
-- Don't mutate original data - filter on copies
-
-**Filter Implementation:**
-```python
-def filter_data(data, warehouse=None, category=None):
- """Filter data by multiple criteria."""
- filtered = data
-
- if warehouse and warehouse != 'all':
- filtered = [item for item in filtered
- if item.get('warehouse') == warehouse]
-
- if category and category != 'all':
- filtered = [item for item in filtered
- if item.get('category', '').lower() == category.lower()]
-
- return filtered
-```
-
-**Date/Time Filtering:**
-- Support both direct month match (2025-01) and quarters (Q1-2025)
-- Parse date strings safely
-- Handle missing/null dates gracefully
-- Consider timezone if adding real database
-
-### Error Handling
-
-**Use HTTPException:**
-```python
-from fastapi import HTTPException
-
-@app.get("/api/item/{item_id}")
-def get_item(item_id: str):
- item = find_item(item_id)
- if not item:
- raise HTTPException(
- status_code=404,
- detail=f"Item {item_id} not found"
- )
- return item
-```
-
-**Best Practices:**
-- Return 404 for "not found" errors
-- Return 400 for bad input/validation errors
-- Return 500 for server errors (let FastAPI handle these)
-- Include helpful error messages
-- Log errors for debugging
-
-### Mock Data Management
-
-**Pattern:**
-- Load all data from JSON files at startup
-- Data lives in memory during server runtime
-- Changes don't persist (restart reloads from files)
-- Keep JSON files well-formatted and validated
-
-**Adding New Data:**
-1. Update JSON file in `server/data/`
-2. Update Pydantic model if structure changed
-3. Restart server to reload data
-4. Verify with API docs (/docs endpoint)
-
-**Data Consistency:**
-- Ensure SKUs in orders reference valid inventory items
-- Keep category names consistent across data files
-- Use same date format everywhere
-- Validate JSON structure before committing
-
-### CORS Configuration
-
-**Development:**
-- Allow all origins during development (`allow_origins=["*"]`)
-- Useful for frontend dev server on different port
-
-**Production:**
-- Restrict to specific origins only
-- Example: `allow_origins=["https://yourdomain.com"]`
-- Never use wildcard (*) in production
-- Configure based on deployment environment
-
-### Testing API Endpoints
-
-**Using FastAPI Docs:**
-1. Start server
-2. Navigate to http://localhost:8001/docs
-3. Click endpoint to expand
-4. Click "Try it out"
-5. Fill in parameters
-6. Execute and verify response
-
-**Using pytest:**
-```python
-def test_endpoint(client):
- response = client.get("/api/endpoint?param=value")
- assert response.status_code == 200
- data = response.json()
- assert isinstance(data, list)
- assert len(data) > 0
-```
-
-**What to Test:**
-- Successful requests return 200
-- Invalid IDs return 404
-- Filters work correctly
-- Response structure matches model
-- Calculations are accurate
-- Edge cases (empty results, invalid input)
-
-### Performance Considerations
-
-**In-Memory Data:**
-- Fast reads (no database queries)
-- No indexing needed for demo
-- All filtering happens in Python
-- Reasonable for small datasets (<10K items)
-
-**If Scaling:**
-- Add database (PostgreSQL, MongoDB)
-- Implement pagination
-- Add caching layer (Redis)
-- Use database indexes for common filters
-- Consider async database queries
-
-### Code Organization
-
-**When to Extract:**
-- Filtering logic used in multiple endpoints → Extract to utility function
-- Complex business logic → Move to separate module
-- Data validation beyond Pydantic → Create custom validators
-- Repeated calculations → Extract to helper functions
-
-**Module Structure for Growth:**
-```
-server/
-├── main.py # API endpoints only
-├── models.py # Pydantic models
-├── services/ # Business logic
-│ ├── inventory.py
-│ └── orders.py
-├── utils/ # Helper functions
-│ └── filters.py
-└── data/ # JSON data files
+```bash
+cd ../tests
+uv run pytest -v
```
-### Common Pitfalls
-
-**Avoid:**
-- ❌ Mutating global data (filter on copies)
-- ❌ Missing Pydantic model updates when JSON changes
-- ❌ Inconsistent filter parameter names across endpoints
-- ❌ Returning raw dict instead of Pydantic model
-- ❌ Not handling None/null values in data
-
-**Do:**
-- ✅ Validate all input with Pydantic
-- ✅ Return typed responses (response_model)
-- ✅ Handle optional parameters gracefully
-- ✅ Keep endpoints focused and simple
-- ✅ Write tests for new endpoints
-
-### Debugging
+## Architecture
-**Techniques:**
-- Use FastAPI's automatic docs for quick testing
-- Print statements in endpoint functions (shows in terminal)
-- Check Pydantic validation errors in response
-- Use Python debugger (`import pdb; pdb.set_trace()`)
-- Review JSON data files for structure issues
+Everything lives in `main.py` — there's no `routers/`, `services/`, or `models.py` split. Data is loaded once at import time in `mock_data.py` (JSON files in `data/` → module-level Python lists) and imported directly into `main.py`. All filtering happens in-memory on plain dicts; Pydantic models (defined at the top of `main.py`) only validate the response shape.
-**Common Issues:**
-- Data not loading → Check JSON file path
-- Validation errors → Verify Pydantic model matches data
-- Empty results → Check filter logic and data
-- 404 errors → Verify route path and HTTP method
+**Filtering**: two shared helpers in `main.py` — `apply_filters(items, warehouse, category, status)` for the common three, and `filter_by_month(items, month)` for date filtering on `order_date` (accepts `YYYY-MM` or `QN-YYYY` quarter strings via `QUARTER_MAP`). New filterable endpoints should compose these rather than reimplementing filter logic. Any `'all'` value means "don't filter on this field."
-### Security Notes
+**Reports endpoints** (`/api/reports/quarterly`, `/api/reports/monthly-trends`) don't use `apply_filters` — they bucket the full unfiltered `orders` list by date extracted with string slicing, not `filter_by_month`.
-**For Production:**
-- Add authentication/authorization
-- Validate and sanitize all input
-- Use HTTPS only
-- Implement rate limiting
-- Add input size limits
-- Use environment variables for sensitive config
-- Never commit secrets to git
+## Data Model Changes
-**Current State:**
-- No authentication (demo only)
-- CORS allows all origins
-- No rate limiting
-- No input validation beyond types
-- Suitable for local development only
+When changing the shape of a JSON file in `data/`, update the matching Pydantic model in `main.py` in the same change — response validation will fail otherwise. Keep SKUs in `orders`/`backlog_items` consistent with `inventory.json`, and category names consistent across all data files (comparisons are case-insensitive but not fuzzy).
-## Quick Reference
+## Known Gap
-**Start server:** `uv run python main.py`
-**API docs:** http://localhost:8001/docs
-**Run tests:** `cd ../tests && uv run pytest backend/ -v`
-**Add endpoint:** Define model → Add route → Write tests
-**Add filter:** Add query param → Check 'all' value → Filter data
+`purchase_orders` data (`data/purchase_orders.json`) is loaded in `mock_data.py` and used internally to compute `has_purchase_order` on backlog items, but there's no `/api/purchase-orders` route. The frontend's `api.js` calls it anyway (`createPurchaseOrder`, `getPurchaseOrderByBacklogItem`) — those requests currently 404. Same for `/api/tasks`.
diff --git a/server/data/demand_forecasts.json b/server/data/demand_forecasts.json
index e1b388385..a2672a30a 100644
--- a/server/data/demand_forecasts.json
+++ b/server/data/demand_forecasts.json
@@ -6,7 +6,9 @@
"current_demand": 300,
"forecasted_demand": 450,
"trend": "increasing",
- "period": "Next 30 days"
+ "period": "Next 30 days",
+ "unit_cost": 45.00,
+ "category": "Actuators"
},
{
"id": "2",
@@ -15,7 +17,9 @@
"current_demand": 150,
"forecasted_demand": 152,
"trend": "stable",
- "period": "Next 30 days"
+ "period": "Next 30 days",
+ "unit_cost": 32.50,
+ "category": "Actuators"
},
{
"id": "3",
@@ -24,7 +28,9 @@
"current_demand": 500,
"forecasted_demand": 600,
"trend": "increasing",
- "period": "Next 30 days"
+ "period": "Next 30 days",
+ "unit_cost": 8.75,
+ "category": "Actuators"
},
{
"id": "4",
@@ -33,7 +39,9 @@
"current_demand": 50,
"forecasted_demand": 35,
"trend": "decreasing",
- "period": "Next 30 days"
+ "period": "Next 30 days",
+ "unit_cost": 425.00,
+ "category": "Actuators"
},
{
"id": "5",
@@ -42,7 +50,9 @@
"current_demand": 800,
"forecasted_demand": 950,
"trend": "increasing",
- "period": "Next 30 days"
+ "period": "Next 30 days",
+ "unit_cost": 14.25,
+ "category": "Actuators"
},
{
"id": "6",
@@ -51,7 +61,9 @@
"current_demand": 120,
"forecasted_demand": 121,
"trend": "stable",
- "period": "Next 30 days"
+ "period": "Next 30 days",
+ "unit_cost": 67.00,
+ "category": "Actuators"
},
{
"id": "7",
@@ -60,7 +72,9 @@
"current_demand": 250,
"forecasted_demand": 252,
"trend": "stable",
- "period": "Next 30 days"
+ "period": "Next 30 days",
+ "unit_cost": 18.99,
+ "category": "Power Supplies"
},
{
"id": "8",
@@ -69,7 +83,9 @@
"current_demand": 180,
"forecasted_demand": 182,
"trend": "stable",
- "period": "Next 30 days"
+ "period": "Next 30 days",
+ "unit_cost": 22.50,
+ "category": "Sensors"
},
{
"id": "9",
@@ -78,6 +94,8 @@
"current_demand": 95,
"forecasted_demand": 96,
"trend": "stable",
- "period": "Next 30 days"
+ "period": "Next 30 days",
+ "unit_cost": 89.00,
+ "category": "Controllers"
}
]
diff --git a/server/main.py b/server/main.py
index a0c2d8c5a..51baa3d2e 100644
--- a/server/main.py
+++ b/server/main.py
@@ -1,9 +1,13 @@
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from typing import List, Optional
+from datetime import datetime, timedelta
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
+RESTOCK_LEAD_TIME_DAYS = 14
+TREND_WEIGHT = {'increasing': 2, 'stable': 1, 'decreasing': 0}
+
app = FastAPI(title="Factory Inventory Management System")
# Quarter mapping for date filtering
@@ -80,6 +84,7 @@ class Order(BaseModel):
actual_delivery: Optional[str] = None
warehouse: Optional[str] = None
category: Optional[str] = None
+ lead_time_days: Optional[int] = None
class DemandForecast(BaseModel):
id: str
@@ -89,6 +94,8 @@ class DemandForecast(BaseModel):
forecasted_demand: int
trend: str
period: str
+ unit_cost: float
+ category: str
class BacklogItem(BaseModel):
id: str
@@ -120,6 +127,27 @@ class CreatePurchaseOrderRequest(BaseModel):
expected_delivery_date: str
notes: Optional[str] = None
+class RestockRecommendation(BaseModel):
+ sku: str
+ name: str
+ category: str
+ current_demand: int
+ forecasted_demand: int
+ trend: str
+ unit_cost: float
+ recommended_quantity: int
+ line_total: float
+
+class RestockOrderItem(BaseModel):
+ sku: str
+ name: str
+ quantity: int
+ unit_cost: float
+
+class RestockOrderRequest(BaseModel):
+ budget: float
+ items: List[RestockOrderItem]
+
# API endpoints
@app.get("/")
def root():
@@ -179,6 +207,95 @@ def get_backlog():
result.append(item_dict)
return result
+@app.get("/api/restocking/recommendations")
+def get_restocking_recommendations(budget: float = 0):
+ """Recommend items to restock within budget, prioritized by demand urgency"""
+ candidates = []
+ for forecast in demand_forecasts:
+ gap = forecast["forecasted_demand"] - forecast["current_demand"]
+ recommended_quantity = max(gap, 1)
+ urgency = TREND_WEIGHT.get(forecast["trend"], 0) * 1000 + gap
+
+ candidates.append({
+ "sku": forecast["item_sku"],
+ "name": forecast["item_name"],
+ "category": forecast["category"],
+ "current_demand": forecast["current_demand"],
+ "forecasted_demand": forecast["forecasted_demand"],
+ "trend": forecast["trend"],
+ "unit_cost": forecast["unit_cost"],
+ "recommended_quantity": recommended_quantity,
+ "urgency": urgency
+ })
+
+ candidates.sort(key=lambda c: c["urgency"], reverse=True)
+
+ recommendations = []
+ remaining_budget = budget
+ for candidate in candidates:
+ line_total = round(candidate["recommended_quantity"] * candidate["unit_cost"], 2)
+ if line_total > remaining_budget:
+ continue
+
+ remaining_budget -= line_total
+ recommendations.append({
+ "sku": candidate["sku"],
+ "name": candidate["name"],
+ "category": candidate["category"],
+ "current_demand": candidate["current_demand"],
+ "forecasted_demand": candidate["forecasted_demand"],
+ "trend": candidate["trend"],
+ "unit_cost": candidate["unit_cost"],
+ "recommended_quantity": candidate["recommended_quantity"],
+ "line_total": line_total
+ })
+
+ total_cost = round(budget - remaining_budget, 2)
+ return {
+ "recommendations": recommendations,
+ "budget": budget,
+ "total_cost": total_cost,
+ "remaining_budget": round(remaining_budget, 2),
+ "item_count": len(recommendations)
+ }
+
+@app.post("/api/restocking/orders", response_model=Order)
+def create_restocking_order(request: RestockOrderRequest):
+ """Submit a restocking order built from recommended items"""
+ if not request.items:
+ raise HTTPException(status_code=400, detail="Restocking order must include at least one item")
+
+ now = datetime.now()
+ expected_delivery = now + timedelta(days=RESTOCK_LEAD_TIME_DAYS)
+ total_value = round(sum(item.quantity * item.unit_cost for item in request.items), 2)
+ order_id = str(len(orders) + 1)
+
+ new_order = {
+ "id": order_id,
+ "order_number": f"RESTOCK-{order_id.zfill(4)}",
+ "customer": "Internal Restocking",
+ "items": [
+ {
+ "sku": item.sku,
+ "name": item.name,
+ "quantity": item.quantity,
+ "unit_price": item.unit_cost
+ }
+ for item in request.items
+ ],
+ "status": "Processing",
+ "order_date": now.isoformat(),
+ "expected_delivery": expected_delivery.isoformat(),
+ "total_value": total_value,
+ "actual_delivery": None,
+ "warehouse": None,
+ "category": None,
+ "lead_time_days": RESTOCK_LEAD_TIME_DAYS
+ }
+
+ orders.append(new_order)
+ return new_order
+
@app.get("/api/dashboard/summary")
def get_dashboard_summary(
warehouse: Optional[str] = None,