diff --git a/.claude/agents/debugger.md b/.claude/agents/debugger.md new file mode 100644 index 000000000..1631fd4e8 --- /dev/null +++ b/.claude/agents/debugger.md @@ -0,0 +1,67 @@ +--- +name: debugger +description: Investigates runtime errors and stack traces, locates the root cause in the codebase, and proposes fixes without applying them +tools: Read, Grep, Glob, Bash +model: sonnet +color: red +--- + +# Debugger Agent + +You investigate runtime errors — a stack trace, an exception message, a browser console error, a failed request — and trace them back to a root cause in this codebase. You **diagnose and propose fixes; you do not apply them** (you have no Write/Edit access). Hand the fix back as a precise, reviewable recommendation. + +## Input You'll Typically Receive + +- A stack trace or exception message (Python/FastAPI or JS/Vue) +- A description of broken behavior ("the demand page shows NaN", "500 on POST /api/orders") +- Browser console errors or network request/response details +- Sometimes just "X is broken, figure out why" + +## Investigation Process + +1. **Parse the error** — identify the exact exception type, message, and the innermost frame that belongs to this codebase (skip framework/node_modules frames unless the app's usage of the framework is the actual bug). +2. **Locate the failing code** — use Grep/Glob to find the file:line named in the trace, or search by function/symbol name if the trace is vague (e.g., a Vue template error with no file reference). +3. **Read outward from the failure point** — read the full function, then its callers, then relevant data shapes (Pydantic models, API response shapes, component props) until you can state *why* the failure happens, not just *where*. +4. **Reproduce if possible** — use Bash to run the failing path: `curl` an endpoint, run a specific pytest test, check server logs, grep for the same error pattern elsewhere (it may be duplicated). Don't guess when you can confirm. +5. **Check for duplicates** — Grep for the same pattern elsewhere in the codebase; a bug in a hand-rolled pattern (e.g., an unvalidated date parse) is often repeated in multiple files. + +## Stack Trace Reading Cheatsheet + +**Python/FastAPI**: read bottom-to-top. The last frame in *your* code (not `site-packages`/`uv` internals) before the exception is usually the real site. Common culprits in this codebase: Pydantic validation mismatches against `server/data/*.json`, missing `None` checks on optional query params, KeyError from mismatched field names between mock data and models. + +**JS/Vue (browser console)**: read top-to-bottom for the immediate throw site; Vue often wraps it with a "at " trailer telling you which `.vue` file to open. Common culprits per this project's known patterns (see CLAUDE.md): +- `TypeError` from calling `.getMonth()`/date methods on an invalid `Date` (missing validation) +- `undefined` reads from data not yet loaded (missing loading-state guard, or a computed reading a ref before `onMounted` resolves) +- Reactivity issues (mutating a prop, reading a ref without `.value` in script but expecting reactivity, stale closure in an inline handler) +- Vue warns about duplicate/missing `:key` in `v-for`, usually not a hard crash but worth flagging if seen + +## Root Cause vs Symptom + +Don't stop at the line that throws — that's often a symptom. Example: a `TypeError: Cannot read properties of undefined` in a computed is the symptom; the root cause might be an API response shape that changed, or a filter param that's `undefined` instead of `'all'`. State both in your report. + +## Output Format + +Keep it tight — this is a diagnosis handoff, not a review: + +```markdown +## Root Cause +[One or two sentences: what actually breaks and why] + +## Evidence +- [file:line] — [what you found there] +- [command run / output that confirms it, if reproduced] + +## Proposed Fix +[file:line] — [specific change] +```suggested code or diff-style snippet``` + +## Other Occurrences (if any) +[Same pattern found elsewhere — file:line list] +``` + +If you cannot pin down a root cause with confidence, say so explicitly and list what you ruled out and what you'd check next — don't guess and present it as certain. + +## Boundaries + +- You do not edit files. If the fix is a `.vue` change, note that it should go through the **vue-expert** subagent per this project's CLAUDE.md rule; if it's backend, note the fix for the calling agent to apply directly. +- Don't expand scope into a general code review — stay focused on the reported failure and anything directly causing it. diff --git a/.claude/skills/vue-component-optimizer/SKILL.md b/.claude/skills/vue-component-optimizer/SKILL.md new file mode 100644 index 000000000..f8222ceb1 --- /dev/null +++ b/.claude/skills/vue-component-optimizer/SKILL.md @@ -0,0 +1,41 @@ +--- +name: vue-component-optimizer +description: Analyzes Vue 3 component structure in client/src for performance and code-reuse issues (reactivity misuse, unmemoized computations, unstable v-for keys, duplicated markup/logic across components) and applies the fixes. Use when asked to review, optimize, audit, or refactor Vue components. +--- + +# Vue Component Optimizer + +Analyzes components under `client/src/views/` and `client/src/components/` for two categories of issues, then applies fixes. + +## Process + +1. **Discover** — read every `.vue` file in `client/src/views/` and `client/src/components/`. +2. **Analyze** each file for the checks below, noting file:line for every finding. +3. **Cross-reference** across files to find duplication (same markup pattern, same formatting logic, same API-calling pattern repeated in 2+ components). +4. **Report** findings to the user first: group by category, cite file:line, state the concrete fix. Keep it short — a table or bullet list, not prose per finding. +5. **Apply fixes**, per [CLAUDE.md](../../../CLAUDE.md)'s mandatory rule: delegate all `.vue` edits to the **vue-expert** subagent. Batch related fixes into one vue-expert call per file rather than one call per finding. +6. **Verify** — after fixes land, run the app (`run` skill or dev servers already running) and check the affected pages still render and the browser console is clean. +7. **Summarize** what changed, file by file, and flag anything you deliberately left alone (e.g., a structural extraction that's high-risk enough to want the user's sign-off first). + +## Performance Checks + +- **Reactivity misuse** — derived values computed inside a method, watcher, or inline in the template instead of a `computed()`. This codebase's convention (see CLAUDE.md) is: raw data in `ref()`, derived data in `computed()`. +- **Watcher-should-be-computed** — a `watch()` whose only job is to recompute a value and assign it to another ref. Replace with `computed()`. +- **Unstable `v-for` keys** — `:key="index"` instead of a stable identifier (`sku`, `id`, `month`, order number). Array reordering/filtering will misrender with index keys. +- **Inline literals in templates** — object/array/function literals created directly in template expressions (`:style="{ color: x }"`, `@click="() => foo(x)"`) that get re-created every render. Hoist to a computed or method. +- **Unmemoized expensive work** — filtering/sorting/reducing large arrays inside the ` @@ -324,9 +299,6 @@ export default { const selectedProduct = ref(null) const showBacklogModal = ref(false) const selectedBacklogItem = ref(null) - const showPOModal = ref(false) - const selectedBacklogForPO = ref(null) - const poModalMode = ref('create') // Use shared filters const { @@ -650,28 +622,6 @@ export default { showBacklogModal.value = true } - const openPOModal = (item) => { - selectedBacklogForPO.value = item - poModalMode.value = 'create' - showPOModal.value = true - } - - const viewPO = (item) => { - selectedBacklogForPO.value = item - poModalMode.value = 'view' - showPOModal.value = true - } - - const handlePOCreated = (poData) => { - // Update the backlog item with the new PO ID - const item = allBacklogItems.value.find(b => b.id === poData.backlog_item_id) - if (item) { - item.purchase_order_id = poData.id - item.purchase_order = poData - } - showPOModal.value = false - } - // Watch for filter changes and reload data watch([selectedPeriod, selectedLocation, selectedCategory, selectedStatus], () => { loadData() @@ -715,12 +665,6 @@ export default { Math, translateProductName, translateWarehouse, - showPOModal, - selectedBacklogForPO, - poModalMode, - openPOModal, - viewPO, - handlePOCreated } } } @@ -1236,36 +1180,4 @@ export default { transform: scale(1.1); } -.po-button { - padding: 0.5rem 1rem; - border: none; - border-radius: 6px; - font-size: 0.813rem; - font-weight: 600; - cursor: pointer; - transition: all 0.2s ease; - white-space: nowrap; -} - -.po-button.create { - background: #3b82f6; - color: white; -} - -.po-button.create:hover { - background: #2563eb; - transform: translateY(-1px); - box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3); -} - -.po-button.view { - background: #64748b; - color: white; -} - -.po-button.view:hover { - background: #475569; - transform: translateY(-1px); - box-shadow: 0 2px 4px rgba(100, 116, 139, 0.3); -} diff --git a/client/src/views/Restocking.vue b/client/src/views/Restocking.vue new file mode 100644 index 000000000..7f77ba6f9 --- /dev/null +++ b/client/src/views/Restocking.vue @@ -0,0 +1,675 @@ + + + + + diff --git a/server/main.py b/server/main.py index a0c2d8c5a..acc71882b 100644 --- a/server/main.py +++ b/server/main.py @@ -1,8 +1,9 @@ from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware -from typing import List, Optional +from typing import List, Optional, Dict 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 datetime import datetime, timedelta app = FastAPI(title="Factory Inventory Management System") @@ -120,6 +121,44 @@ class CreatePurchaseOrderRequest(BaseModel): expected_delivery_date: str notes: Optional[str] = None +class RestockingItem(BaseModel): + sku: str + name: str + current_stock: int + forecasted_demand: int + unit_cost: float + recommended_qty: int + estimated_cost: float + warehouse: str + +class RestockingOrderItem(BaseModel): + sku: str + name: str + quantity: int + unit_cost: float + estimated_cost: float + +class RestockingOrderRequest(BaseModel): + items: List[RestockingOrderItem] + total_cost: float + budget: float + +class Task(BaseModel): + id: str + title: str + priority: str + dueDate: str + status: str + +class CreateTaskRequest(BaseModel): + title: str + priority: str + dueDate: str + +# In-memory task store (separate from mock_data.py - user-generated, not seed data) +tasks: List[dict] = [] +next_task_id = 1 + # API endpoints @app.get("/") def root(): @@ -304,6 +343,138 @@ def get_monthly_trends(): result.sort(key=lambda x: x['month']) return result +@app.get("/api/restocking/recommendations", response_model=List[RestockingItem]) +def get_restocking_recommendations(budget: float): + """Get recommended items to restock based on demand forecasts, prioritized by demand gap""" + recommendations = [] + + # Match demand with inventory and calculate gaps + for demand in demand_forecasts: + # Find matching inventory item by SKU + inventory = None + for inv in inventory_items: + if inv.get('sku') == demand.get('item_sku'): + inventory = inv + break + + # Only include if we have matching inventory and positive demand gap + if inventory: + demand_gap = demand.get('forecasted_demand', 0) - demand.get('current_demand', 0) + if demand_gap > 0: + recommendations.append({ + 'demand_gap': demand_gap, + 'demand': demand, + 'inventory': inventory + }) + + # Sort by demand gap (descending) - items with highest demand increase first + recommendations.sort(key=lambda x: x['demand_gap'], reverse=True) + + # Fill budget greedily + result = [] + remaining_budget = budget + for rec in recommendations: + unit_cost = rec['inventory'].get('unit_cost', 0) + if unit_cost > 0: + qty = int(remaining_budget / unit_cost) + if qty > 0: + estimated_cost = qty * unit_cost + result.append(RestockingItem( + sku=rec['inventory']['sku'], + name=rec['inventory']['name'], + current_stock=rec['inventory']['quantity_on_hand'], + forecasted_demand=rec['demand']['forecasted_demand'], + unit_cost=unit_cost, + recommended_qty=qty, + estimated_cost=estimated_cost, + warehouse=rec['inventory']['warehouse'] + )) + remaining_budget -= estimated_cost + if remaining_budget < 1: + break + + return result + +@app.post("/api/restocking/orders") +def create_restocking_order(order_request: RestockingOrderRequest): + """Create a new restocking order and add it to the orders list""" + # Validate budget + if order_request.total_cost > order_request.budget: + raise HTTPException( + status_code=400, + detail=f"Order total ${order_request.total_cost:.2f} exceeds budget ${order_request.budget:.2f}" + ) + + # Create order record + order_id = len(orders) + 1 + order_number = f"RST-2025-{order_id:04d}" + now = datetime.now() + delivery_date = now + timedelta(days=7) + + new_order = { + 'id': str(order_id), + 'order_number': order_number, + 'customer': 'Internal - Restocking', + 'items': [ + { + 'sku': item.sku, + 'name': item.name, + 'quantity': item.quantity, + 'unit_price': item.unit_cost + } + for item in order_request.items + ], + 'status': 'Submitted', + 'order_date': now.isoformat(), + 'expected_delivery': delivery_date.isoformat(), + 'total_value': order_request.total_cost, + 'warehouse': order_request.items[0].sku.split('-')[0] if order_request.items else 'Unknown', + 'category': 'Restocking' + } + + orders.append(new_order) + + return { + 'order_number': order_number, + 'status': 'Submitted', + 'total_cost': order_request.total_cost, + 'expected_delivery': delivery_date.isoformat() + } + +@app.get("/api/tasks", response_model=List[Task]) +def get_tasks(): + return tasks + +@app.post("/api/tasks", response_model=Task) +def create_task(task_request: CreateTaskRequest): + global next_task_id + new_task = { + 'id': str(next_task_id), + 'title': task_request.title, + 'priority': task_request.priority, + 'dueDate': task_request.dueDate, + 'status': 'pending' + } + next_task_id += 1 + tasks.append(new_task) + return new_task + +@app.patch("/api/tasks/{task_id}", response_model=Task) +def toggle_task(task_id: str): + task = next((t for t in tasks if t['id'] == task_id), None) + if not task: + raise HTTPException(status_code=404, detail=f"Task {task_id} not found") + task['status'] = 'completed' if task['status'] == 'pending' else 'pending' + return task + +@app.delete("/api/tasks/{task_id}") +def delete_task(task_id: str): + task = next((t for t in tasks if t['id'] == task_id), None) + if not task: + raise HTTPException(status_code=404, detail=f"Task {task_id} not found") + tasks.remove(task) + return {'message': 'Task deleted', 'id': task_id} + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8001)