From 47c35f375bba0a24969cbec54eed840837e94d96 Mon Sep 17 00:00:00 2001 From: Preetam Mahapatra Date: Wed, 22 Jul 2026 11:30:27 +0100 Subject: [PATCH] new feature --- .github/workflows/claude.yml | 58 ++++ CLAUDE.md | 41 ++- client/CLAUDE.md | 474 ++---------------------------- client/src/App.vue | 3 + client/src/api.js | 10 + client/src/locales/en.js | 30 +- client/src/locales/ja.js | 30 +- client/src/main.js | 4 +- client/src/views/Orders.vue | 59 ++++ client/src/views/Restocking.vue | 247 ++++++++++++++++ docs/architecture.html | 368 +++++++++++++++++++++++ server/CLAUDE.md | 279 +----------------- server/data/demand_forecasts.json | 36 ++- server/main.py | 117 ++++++++ 14 files changed, 1020 insertions(+), 736 deletions(-) create mode 100644 .github/workflows/claude.yml create mode 100644 client/src/views/Restocking.vue create mode 100644 docs/architecture.html 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 @@ + + + + + +Architecture — Factory Inventory Management System + + + +
+ +
+

Factory Inventory Management System

+

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

+
+ + + + + + + + + + + + + + + + + + + +
MethodPathPurpose
GET/api/inventoryList inventory, filter by warehouse/category
GET/api/inventory/{id}Single inventory item
GET/api/ordersList orders, filter by warehouse/category/status/month
GET/api/orders/{id}Single order
GET/api/demandAll demand forecasts
GET/api/backlogBacklog items with purchase-order flag
GET/api/dashboard/summaryAggregated KPIs, filterable
GET/api/spending/summarySpending totals
GET/api/spending/monthlyMonthly spending breakdown
GET/api/spending/categoriesSpending by category
GET/api/spending/transactionsRecent transactions
GET/api/reports/quarterlyQuarterly order stats, computed on the fly
GET/api/reports/monthly-trendsMonth-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,