diff --git a/.claude/skills/vue-component-audit/SKILL.md b/.claude/skills/vue-component-audit/SKILL.md new file mode 100644 index 000000000..52c11a458 --- /dev/null +++ b/.claude/skills/vue-component-audit/SKILL.md @@ -0,0 +1,245 @@ +--- +name: vue-component-audit +description: Analyzes Vue 3 component structure and suggests performance and code-reuse optimizations. Use this skill when auditing components in client/src, when asked to review Vue performance, reactivity, or duplication, or before extracting a shared composable or component. +--- + +# Vue Component Audit + +Analyze Vue 3 components in `client/src/` and report structural, performance, and reuse +problems with concrete fixes. + +**This skill reports; it does not rewrite.** Produce findings, let the user choose what to +apply. When they approve a fix that touches a `.vue` file, delegate the edit to the +**vue-expert** subagent per the mandate in CLAUDE.md. + +Distinct from the `/optimize` command, which sweeps the whole codebase for dead code and +edits in place. This skill is Vue-only, analysis-first, and focused on render cost and +duplication. + +## Scope + +| Path | What lives there | +|---|---| +| `client/src/views/*.vue` | Route-level views, one per tab | +| `client/src/components/*.vue` | Modals, filter bar, menus | +| `client/src/composables/*.js` | `useFilters`, `useI18n`, `useAuth` — singleton shared state | +| `client/src/api.js` | Every network call funnels through here | + +This project uses the **Options API `setup()`** form (`export default { setup() {...} }`), +**not** ` + + diff --git a/client/src/views/Orders.vue b/client/src/views/Orders.vue index 7413f6e66..27c5d4a3b 100644 --- a/client/src/views/Orders.vue +++ b/client/src/views/Orders.vue @@ -27,6 +27,61 @@ +
| {{ t('orders.table.orderNumber') }} | +{{ t('orders.table.status') }} | +{{ t('orders.table.items') }} | +{{ t('orders.submitted.submittedDate') }} | +{{ t('orders.table.expectedDelivery') }} | +{{ t('orders.submitted.leadTime') }} | +{{ t('orders.table.totalValue') }} | +
|---|---|---|---|---|---|---|
| {{ order.order_number }} | ++ {{ t('status.submitted') }} + | +
+
+
+ + {{ t('orders.itemsCount', { count: order.item_count }) }} ++
+
+
+ {{ translateProductName(item.item_name) }}
+
+
+ |
+ {{ formatDate(order.submitted_date) }} | +{{ formatDate(order.expected_delivery) }} | ++ {{ t('orders.submitted.leadTimeValue', { days: order.max_lead_time_days }) }} + | +{{ currencySymbol }}{{ order.total_cost.toLocaleString() }} | +
{{ t('restocking.description') }}
++ {{ t('restocking.budget.totalNeed', { amount: formatMoney(plan.total_need) }) }} +
+| {{ t('restocking.table.sku') }} | +{{ t('restocking.table.itemName') }} | +{{ t('restocking.table.supplier') }} | +{{ t('restocking.table.trend') }} | +{{ t('restocking.table.demandGap') }} | +{{ t('restocking.table.quantity') }} | +{{ t('restocking.table.unitCost') }} | +{{ t('restocking.table.lineTotal') }} | +{{ t('restocking.table.leadTime') }} | +
|---|---|---|---|---|---|---|---|---|
| {{ item.item_sku }} | +{{ translateProductName(item.item_name) }} | +{{ item.supplier }} | ++ {{ t(`trends.${item.trend}`) }} + | +{{ item.demand_gap.toLocaleString() }} | +{{ item.recommended_quantity.toLocaleString() }} | +{{ formatMoneyExact(item.unit_cost) }} | +{{ formatMoney(item.line_total) }} | +{{ item.lead_time_days }}{{ t('restocking.daysSuffix') }} | +
| {{ t('restocking.table.total') }} | +{{ formatMoney(plan.total_cost) }} | +{{ plan.max_lead_time_days }}{{ t('restocking.daysSuffix') }} | +||||||
{{ t('restocking.skipped.note') }}
+| {{ t('restocking.table.sku') }} | +{{ t('restocking.table.itemName') }} | +{{ t('restocking.table.trend') }} | +{{ t('restocking.table.quantity') }} | +{{ t('restocking.table.lineTotal') }} | +{{ t('restocking.skipped.shortBy') }} | +
|---|---|---|---|---|---|
| {{ item.item_sku }} | +{{ translateProductName(item.item_name) }} | ++ {{ t(`trends.${item.trend}`) }} + | +{{ item.recommended_quantity.toLocaleString() }} | +{{ formatMoney(item.line_total) }} | ++ {{ formatMoney(Math.max(0, item.line_total - plan.remaining_budget)) }} + | +
System Architecture
++ A two-tier demo application: a Vue 3 single-page frontend talking to a Python FastAPI service + over plain HTTP. There is no database — the API loads seven JSON files into memory at import + time and filters them in Python on every request. One feature, Restocking, also writes: + submitted orders accumulate in a module-level list for the lifetime of the process. +
+
+ mock_data.py calls json.load() at import, so every module shares the same
+ Python lists. Nothing writes back to disk; restarting the server resets all state. Filtering builds
+ new lists rather than mutating the originals, which keeps the shared data safe across requests.
+
| Layer | Technology | Version | Role |
|---|---|---|---|
| UI framework | Vue 3 (Composition API) | ^3.4.21 | Components, reactivity, computed derivations |
| Routing | vue-router | ^4.3.0 | History-mode client routing, 6 flat routes |
| HTTP client | axios | ^1.6.7 | All calls funnelled through api.js |
| Build / dev server | Vite + @vitejs/plugin-vue | ^5.2.0 | HMR on port 3000, production bundling |
| API framework | FastAPI | ≥0.110 | Routing, OpenAPI docs, dependency injection |
| ASGI server | uvicorn | ≥0.24 | Serves on 0.0.0.0:8001 |
| Validation | Pydantic | ≥2.5 | response_model enforcement on typed routes |
| Runtime | Python | ≥3.11 | Managed by uv (pyproject.toml) |
| Tests | pytest + FastAPI TestClient | ≥8.0 | tests/backend/ — 68 tests: dashboard 13, inventory 10, misc 17, restocking 28 |
| Charts | Hand-written SVG | — | No charting library; computed props feed SVG |
| i18n | Custom composable | — | useI18n + locales/en.js, ja.js, localStorage |
+ Every screen follows the same path. The four global filters are the only cross-cutting input, and they + live in a single shared module rather than a store. +
+ +FilterBar.vue binds to four refs in composables/useFilters.js —
+ period, location, category, status. The module holds them at top level, so every importer shares
+ one instance (singleton pattern, no Pinia/Vuex).
+ getCurrentFilters(), which maps UI names to API names —
+ selectedLocation becomes warehouse, and selectedPeriod is
+ emitted as month only when it is not all.
+ all via URLSearchParams, so a
+ default page load sends no filter params at all.
+ apply_filters() chains warehouse / category / status list comprehensions
+ (category and status compare case-insensitively). filter_by_month() then handles either
+ a direct YYYY-MM substring match or a Q1-2025-style key expanded through
+ QUARTER_MAP, both against the order_date field.
+ response_model, so a JSON shape that drifts from the model raises
+ a server-side error. Aggregate routes (/dashboard/summary, /reports/*,
+ /spending/*) return raw dicts with no model.
+ allOrders and inventoryItems; every
+ number on screen — totals, low-stock counts, chart series — is a
+ computed() over those refs, so the DOM updates without a second fetch.
+ + Restocking is the only feature that does not follow the flow above. It ignores the four global + filters entirely and drives off a single local input instead: a budget slider. +
+ +Restocking.vue watches one budget ref. A range input emits a value at
+ every step it crosses, so the watcher waits 250 ms for the drag to settle before
+ fetching — a full-track drag costs 2 requests, not 44. A planPending flag
+ disables Place Order while the on-screen plan belongs to an older budget.
+ build_restock_plan() takes the demand gap
+ (forecasted_demand − current_demand), drops any non-positive gap, and
+ weights it by trend through TREND_WEIGHTS — increasing ×1.5,
+ stable ×1.0, decreasing ×0.5. Candidates sort by that score descending, SKU
+ ascending as a deterministic tie-break.
+ line_total fits the remaining budget is funded in
+ full; one that does not is pushed to skipped and the budget passes to the next item
+ down. A partially funded line would not close its demand gap, so partial fills are never offered.
+ The plan quotes max_lead_time_days — the order lands when its slowest line lands.
+ create_restock_order() stamps RO-1001 upward by list length, sets
+ expected_delivery = today + max_lead_time_days, and appends to
+ submitted_restock_orders. No file is written.
+ Orders.vue calls getRestockOrders() once on mount, separately from its
+ filtered getOrders() call, and renders a Submitted Orders card above
+ All Orders. A failure there is logged and swallowed so it cannot blank out the customer
+ orders table beneath it.
+ | Method | Path | Filters accepted | Response model |
|---|---|---|---|
| GET | / | — | raw dict |
| GET | /api/inventory | warehouse, category | List[InventoryItem] |
| GET | /api/inventory/{id} | — (404 if absent) | InventoryItem |
| GET | /api/orders | warehouse, category, status, month | List[Order] |
| GET | /api/orders/{id} | — (404 if absent) | Order |
| GET | /api/demand | none | List[DemandForecast] |
| GET | /api/backlog | none | List[BacklogItem] |
| GET | /api/restock/recommendations | budget (≥0, 422 if negative) | RestockPlan |
| POST | /api/restock-orders | JSON body — 400 if items empty | SubmittedRestockOrder 201 |
| GET | /api/restock-orders | none — newest first | List[SubmittedRestockOrder] |
| GET | /api/dashboard/summary | all four | raw dict — 5 metrics |
| GET | /api/spending/summary | none | raw dict |
| GET | /api/spending/monthly | none | raw list |
| GET | /api/spending/categories | none | raw list |
| GET | /api/spending/transactions | none | raw list |
| GET | /api/reports/quarterly | none — derived from all orders | raw list |
| GET | /api/reports/monthly-trends | none — derived from all orders | raw list |
+ api.js exposes six methods whose endpoints are not defined in main.py.
+ They return 404 at runtime — the app requests /api/tasks on load, so this is visible
+ in the server log on every page view.
+
| Client method | Attempts | Server state |
|---|---|---|
| getTasks() | GET /api/tasks | 404 no route, no model, no data file |
| createTask() | POST /api/tasks | 404 tasks are hard-coded in useAuth.js instead |
| deleteTask() | DELETE /api/tasks/{id} | 404 |
| toggleTask() | PATCH /api/tasks/{id} | 404 |
| createPurchaseOrder() | POST /api/purchase-orders | 404 models exist, route does not |
| getPurchaseOrderByBacklogItem() | GET /api/purchase-orders/{id} | 404 |
+ Locale drives currency automatically: en → USD, ja → JPY.
+ The mock user's name, title and task list are all swapped per locale in
+ useAuth.js, and the choice persists in localStorage.
+
+ /api/inventory accepts only warehouse and category. Month and quarter filters apply to
+ orders, because filter_by_month() matches on order_date, a field inventory
+ records do not carry.
+
+ Both month and quarter filters use in against the raw date string rather than parsed
+ dates, and QUARTER_MAP hard-codes 2025 only. Data outside 2025 is silently excluded from
+ quarterly reports.
+
+ /api/reports/* derives its quarters from all 250 orders and ignores the global filters,
+ while /api/dashboard/summary honours all four. The same metric can differ between the
+ Reports and Overview screens by design.
+
+ No authentication, no rate limiting, and CORS is allow_origins=["*"]. The backend binds
+ 0.0.0.0, so it is reachable from the local network. Suitable for local demo only.
+
+ submitted_restock_orders is a plain module-level list, not a file. Restarting the server
+ empties it and RO- numbering restarts at 1001, which means order numbers are
+ not unique across process lifetimes. The backend tests reset the list in an autouse fixture so cases
+ stay independent.
+
+ A restock plan is derived from budget alone, and a submitted order carries no warehouse or category, so
+ the Submitted Orders card in the Orders view is always shown in full while the table below it
+ respects the filter bar. Recommendations also draw purely on
+ demand_forecasts.json — on-hand stock in inventory.json is never
+ consulted, and only PSU-501 appears in both files.
+
+ /api/backlog computes a has_purchase_order flag by scanning
+ purchase_orders.json — which currently holds zero records — so the flag is
+ always false. The PurchaseOrder and
+ CreatePurchaseOrderRequest models are defined but no route uses them.
+