Skip to content

Commit 7e7fe86

Browse files
Phase 1 + Phase 2: M1 stabilize, M2 build — 13 defects closed, 48/48 E2E green (#1)
* Phase 1 M1: stabilize Reports module, document architecture, baseline E2E Delivers the Phase 1 milestone (M1) of the Meridian engagement (RFP MC-2026-0417): the foundation for every subsequent change — Reports module remediation, current-state architecture documentation, and a Playwright E2E framework that gates future regressions. R1 — Reports module remediation (closes 9 of 15 logged defects) - Reports.vue rewritten in Composition API (was Options API) - Filters now wired through useFilters composable + watch reload - Backend /api/reports/* endpoints accept warehouse, category, status, month - i18n integration via new reports.* namespace (English + Japanese) - api.js centralises calls (no more direct axios from views) - Stable v-for keys, console-log spam removed - W3 cleanup: brand corrected to Meridian Components, "1 day" pluralization helper, Spending alert() removed and toLocaleString -> formatCurrency R3 — Browser test framework (Playwright, standalone) - tests/e2e/ self-contained: package.json, config, 17 specs across 2 files - Flow #1 smoke (shell + 6 routes + interactive nav) - Flow lindsey-anthropic#4 Reports (existing-behaviour + R1 regression contract) - Trace/screenshot/video on failure for IT debuggability - README documents stack, conventions, sample CI R4 — Architecture documentation - proposal/architecture.html: 9 sections covering system overview, request lifecycle, API reference, frontend/backend patterns, data layer, 10-item technical-debt catalogue, and 5 ADRs Act 1 proposal package (was already produced; included in this milestone for repo coherence): executive summary, technical approach, timeline + Gantt, pricing, relevant-experience scaffold, clarifying questions, capabilities deck. Verification - 17/17 Playwright tests pass against the running app - Manual snapshot via Playwright MCP confirms brand, pluralization, no Reports console noise - Defect log (docs/r1-defect-log.md) updated with disposition for all 15 findings; remaining 6 are deferred (4 await Tanaka decisions, 2 absorbed into R2 build) — within the +50% pricing trigger, no T&M conversion Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Phase 2 W4-W5: extend E2E coverage to 4 more critical flows Adds Playwright specs for the four remaining non-gated critical flows in the proposal's R3 plan, raising suite coverage from 17 to 35 green tests. R3 — Critical flow coverage (4 new flows) - Flow #2 Inventory: Category and Location filtering, intersection, Reset-button behaviour. 5 tests. - Flow #3 Orders: status stat cards, header/row-count consistency, items <details> drill-down, Status + Time Period intersection. 4 tests. - Flow lindsey-anthropic#5 Demand: three trend cards (increasing/stable/decreasing), forecasts table columns, per-row trend value validation, summary-vs-table consistency check. 4 tests. - Flow lindsey-anthropic#7 Spending: 4 KPI cards with currency values, Revenue vs Costs chart bar groups, 4 cost categories with name/amount/percentage, Recent Transactions structure, post-cleanup non-clickability. 6 tests. Tooling - Authoring convention reinforced: page.waitForRequest must be armed BEFORE the action; selectOption fires synchronously and the request is gone by the time a post-action listener attaches. Defect log - lindsey-anthropic#17 added (Low): Spending category percentages labelled "% of total" but sum to ~124%. Discovered while writing the Flow lindsey-anthropic#7 spec. Open; resolution requires Operations input on the intended denominator. Verification - 35/35 Playwright tests pass against the running app (was 17/17) - Defect-log severity counts and disposition table updated to reflect lindsey-anthropic#17 and the four newly-shipped flows in tests/e2e/README.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Phase 2 W5-W7: ship R2 Restocking feature end-to-end The R2 deliverable from RFP §3.1: a new Restocking view that recommends purchase orders given current stock, demand forecast, and an operator-supplied budget ceiling. Built without a Discovery session (Tanaka unavailable in the workshop simulation), using the assumptions from the proposal — (s, S) policy with newsvendor service-level calibration (Silver, Pyke & Thomas, 2017). Backend (server/main.py) - GET /api/restocking/recommendations?budget=&service_level=&warehouse=&category= - For each inventory item with a matching demand forecast: shortfall = max(0, forecast - on_hand) qty = shortfall (or reorder_point - on_hand if below ROP) cost = qty * unit_cost criticality = shortfall * unit_cost (urgency × value at stake) - Greedy budget allocation: sort by criticality desc, mark in_budget while cumulative cost <= ceiling. - Service level snaps to the closest tabulated z-score (0.80 .. 0.99). Today z is informational; when forecasts add σ_LT, it moves into the formula. - Documented limitation: only SKUs with a forecast in server/data/demand_forecasts.json get scored (currently 9 of 32). Frontend (client/src/views/Restocking.vue) - Operator controls: budget input, service-level slider (80–99%), "Generate" button. - Five summary cards: Candidates / In budget / Out of budget / Total selected cost / Budget remaining. - Recommendations table: 11 columns including operator-overridable Qty input. Editing Qty recomputes Est. Cost client-side; status badges distinguish in-budget vs out-of-budget rows. - Auto-loads on mount; integrates with useFilters? — no, this is a single-purpose tool that uses its own controls (budget/service-level) rather than the global filter bar. - Composition API throughout, useI18n for all strings. Routing (client/src/main.js, App.vue) - /restocking route registered. - "Restocking" added to the top nav after "Reports". Internationalisation - Full restocking.* namespace added to en.js and ja.js (37 keys × 2 locales). Browser tests (tests/e2e/specs/08-restocking.spec.js) - 6 specs covering: page render, table columns, summary↔table consistency, sort-by-criticality invariant, budget-tightening behaviour, operator override propagation. Verification - 41/41 Playwright tests pass (was 35/35; +6 for Flow lindsey-anthropic#8). - Manual probe via Playwright MCP confirms: 4 candidates ranked by Est. Cost desc, all in budget at $100k ($65,965 total, $34,035 remaining), overrides update the table in real time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * W7: route Backlog (close #DEBT-05) + i18n cleanup + Flow lindsey-anthropic#6 Closes the last gated R1 cleanup items by registering a route for the Backlog view (which had been dead code in the previous vendor's handoff) and translating it through the same useI18n + pluralize plumbing used elsewhere. Routing - /backlog route registered in main.js - "Backlog" link added to App.vue nav after "Restocking" Backlog.vue rewrite - Adds useI18n; all hardcoded English strings move to a new backlog.* namespace (en + ja, 21 keys) - Days Delayed renders via pluralize() — "1 day" vs "n days" - Priority badges use t(`priority.${item.priority}`) so the lowercase raw value never reaches the DOM (defect lindsey-anthropic#9) - Empty-state copy moved out of inline styles into scoped CSS - useFilters integration preserved (Backlog already had this part right) Browser tests - tests/e2e/specs/06-backlog.spec.js — 7 specs covering routing, priority stats, table structure, translated badges, "1 day" pluralization, EN→JA translation contract - Locale-leak fix: tests that switch the language now reset localStorage in afterEach (specs 04 + 06). Without this the JP UI leaks across worker boundaries and breaks subsequent tests that assert English text — surfaced by a flaky run during this commit's development. Defect log + README - lindsey-anthropic#8, lindsey-anthropic#9 marked closed (were gated by #DEBT-05) - #DEBT-05 closed implicitly (route now exists) - Flow lindsey-anthropic#6 status flipped to shipped in tests/e2e/README.md Verification - 48/48 Playwright tests pass (was 41) - Manual probe via Playwright MCP confirms: page reachable from nav, "1 day" vs "5 days" rendered correctly, priority badges translated, JA switch translates the title to バックログ管理 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6b71605 commit 7e7fe86

36 files changed

Lines changed: 4405 additions & 248 deletions

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,21 @@ logs/
5050
# Playwright MCP artifacts
5151
.playwright-mcp/
5252

53+
# Playwright test runner artifacts
54+
tests/e2e/playwright-report/
55+
tests/e2e/test-results/
56+
57+
# Local Claude Code settings (per-developer)
58+
.claude/settings.local.json
59+
60+
# Workshop-host scratch files (kept out of the engagement repo)
61+
/Anthropic FDE Training FAQ.pdf
62+
/bedrock_check.stderr.txt
63+
/bedrock_check.stdout.txt
64+
/claude-trainee-setup-guide.md
65+
/testclaude.py
66+
/testclaude.stderr.txt
67+
/testclaude.stdout.txt
68+
5369
# Environment variables
5470
.env

client/src/App.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@
2525
<router-link to="/reports" :class="{ active: $route.path === '/reports' }">
2626
Reports
2727
</router-link>
28+
<router-link to="/restocking" :class="{ active: $route.path === '/restocking' }">
29+
Restocking
30+
</router-link>
31+
<router-link to="/backlog" :class="{ active: $route.path === '/backlog' }">
32+
Backlog
33+
</router-link>
2834
</nav>
2935
<LanguageSwitcher />
3036
<ProfileMenu

client/src/api.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,39 @@ export const api = {
7474
return response.data
7575
},
7676

77+
async getReportsQuarterly(filters = {}) {
78+
const params = new URLSearchParams()
79+
if (filters.warehouse && filters.warehouse !== 'all') params.append('warehouse', filters.warehouse)
80+
if (filters.category && filters.category !== 'all') params.append('category', filters.category)
81+
if (filters.status && filters.status !== 'all') params.append('status', filters.status)
82+
if (filters.month && filters.month !== 'all') params.append('month', filters.month)
83+
84+
const response = await axios.get(`${API_BASE_URL}/reports/quarterly?${params.toString()}`)
85+
return response.data
86+
},
87+
88+
async getRestockingRecommendations({ budget, service_level = 0.95, warehouse = 'all', category = 'all' } = {}) {
89+
const params = new URLSearchParams()
90+
if (budget != null) params.append('budget', budget)
91+
params.append('service_level', service_level)
92+
if (warehouse && warehouse !== 'all') params.append('warehouse', warehouse)
93+
if (category && category !== 'all') params.append('category', category)
94+
95+
const response = await axios.get(`${API_BASE_URL}/restocking/recommendations?${params.toString()}`)
96+
return response.data
97+
},
98+
99+
async getReportsMonthlyTrends(filters = {}) {
100+
const params = new URLSearchParams()
101+
if (filters.warehouse && filters.warehouse !== 'all') params.append('warehouse', filters.warehouse)
102+
if (filters.category && filters.category !== 'all') params.append('category', filters.category)
103+
if (filters.status && filters.status !== 'all') params.append('status', filters.status)
104+
if (filters.month && filters.month !== 'all') params.append('month', filters.month)
105+
106+
const response = await axios.get(`${API_BASE_URL}/reports/monthly-trends?${params.toString()}`)
107+
return response.data
108+
},
109+
77110
async getTasks() {
78111
const response = await axios.get(`${API_BASE_URL}/tasks`)
79112
return response.data

client/src/components/BacklogDetailModal.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
</div>
3737
<div class="summary-card warning">
3838
<div class="summary-label">Days Delayed</div>
39-
<div class="summary-value">{{ backlogItem.days_delayed }} days</div>
39+
<div class="summary-value">{{ pluralize(backlogItem.days_delayed, t('dashboard.inventoryShortages.day'), t('dashboard.inventoryShortages.days')) }}</div>
4040
</div>
4141
</div>
4242

@@ -87,8 +87,9 @@
8787
<script setup>
8888
import { computed } from 'vue'
8989
import { useI18n } from '../composables/useI18n'
90+
import { pluralize } from '../utils/pluralize'
9091
91-
const { translateProductName } = useI18n()
92+
const { t, translateProductName } = useI18n()
9293
9394
const props = defineProps({
9495
isOpen: {

client/src/locales/en.js

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default {
66
orders: 'Orders',
77
finance: 'Finance',
88
demandForecast: 'Demand Forecast',
9-
companyName: 'Catalyst Components',
9+
companyName: 'Meridian Components',
1010
subtitle: 'Inventory Management System'
1111
},
1212

@@ -55,6 +55,7 @@ export default {
5555
daysDelayed: 'Days Delayed',
5656
priority: 'Priority',
5757
unitsShort: 'units short',
58+
day: 'day',
5859
days: 'days'
5960
},
6061
topProducts: {
@@ -188,6 +189,93 @@ export default {
188189
}
189190
},
190191

192+
// Backlog view
193+
backlog: {
194+
title: 'Backlog Management',
195+
subtitle: 'Track and resolve inventory shortages',
196+
loading: 'Loading backlog...',
197+
loadError: 'Failed to load backlog',
198+
highPriority: 'High Priority',
199+
mediumPriority: 'Medium Priority',
200+
lowPriority: 'Low Priority',
201+
totalItems: 'Total Backlog Items',
202+
sectionTitle: 'Backlog Items',
203+
empty: '✓ No backlog items - all orders can be fulfilled!',
204+
table: {
205+
orderId: 'Order ID',
206+
sku: 'SKU',
207+
itemName: 'Item Name',
208+
quantityNeeded: 'Quantity Needed',
209+
quantityAvailable: 'Quantity Available',
210+
shortage: 'Shortage',
211+
daysDelayed: 'Days Delayed',
212+
priority: 'Priority'
213+
},
214+
unitsShort: 'units short'
215+
},
216+
217+
// Restocking view (R2)
218+
restocking: {
219+
title: 'Restocking Recommendations',
220+
subtitle: 'Recommended purchase orders given current stock, demand forecast, and your budget ceiling.',
221+
budgetLabel: 'Budget ceiling',
222+
serviceLevelLabel: 'Service level',
223+
generate: 'Generate recommendations',
224+
loading: 'Calculating recommendations...',
225+
loadError: 'Failed to load recommendations',
226+
noResults: 'No restocking actions needed at the current stock and forecast.',
227+
summary: {
228+
candidates: 'Candidates',
229+
inBudget: 'In budget',
230+
outOfBudget: 'Out of budget',
231+
totalCost: 'Total selected cost',
232+
remaining: 'Budget remaining'
233+
},
234+
table: {
235+
sku: 'SKU',
236+
itemName: 'Item Name',
237+
category: 'Category',
238+
warehouse: 'Warehouse',
239+
onHand: 'On Hand',
240+
reorderPoint: 'Reorder Point',
241+
forecast: 'Forecast',
242+
recommendedQty: 'Recommended Qty',
243+
unitCost: 'Unit Cost',
244+
estCost: 'Est. Cost',
245+
status: 'Status'
246+
},
247+
status: {
248+
inBudget: 'In budget',
249+
outOfBudget: 'Out of budget'
250+
},
251+
note: 'Operator-overridable. Recommendations are computed via an (s, S) policy with newsvendor service-level calibration; coverage is currently limited to SKUs with an active demand forecast.'
252+
},
253+
254+
// Reports view
255+
reports: {
256+
title: 'Performance Reports',
257+
subtitle: 'View quarterly performance metrics and monthly trends',
258+
loading: 'Loading reports...',
259+
loadError: 'Failed to load reports',
260+
quarterlyPerformance: 'Quarterly Performance',
261+
monthlyRevenueTrend: 'Monthly Revenue Trend',
262+
monthOverMonth: 'Month-over-Month Analysis',
263+
quarter: 'Quarter',
264+
totalOrders: 'Total Orders',
265+
totalRevenue: 'Total Revenue',
266+
avgOrderValue: 'Avg Order Value',
267+
fulfillmentRate: 'Fulfillment Rate',
268+
month: 'Month',
269+
orders: 'Orders',
270+
revenue: 'Revenue',
271+
change: 'Change',
272+
growthRate: 'Growth Rate',
273+
totalRevenueYtd: 'Total Revenue (YTD)',
274+
avgMonthlyRevenue: 'Avg Monthly Revenue',
275+
totalOrdersYtd: 'Total Orders (YTD)',
276+
bestPerformingQuarter: 'Best Performing Quarter'
277+
},
278+
191279
// Filters
192280
filters: {
193281
timePeriod: 'Time Period',

client/src/locales/ja.js

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default {
66
orders: '注文',
77
finance: '財務',
88
demandForecast: '需要予測',
9-
companyName: '触媒コンポーネンツ',
9+
companyName: 'メリディアン・コンポーネンツ',
1010
subtitle: '在庫管理システム'
1111
},
1212

@@ -55,6 +55,7 @@ export default {
5555
daysDelayed: '遅延日数',
5656
priority: '優先度',
5757
unitsShort: '単位不足',
58+
day: '日',
5859
days: '日'
5960
},
6061
topProducts: {
@@ -188,6 +189,93 @@ export default {
188189
}
189190
},
190191

192+
// Backlog view
193+
backlog: {
194+
title: 'バックログ管理',
195+
subtitle: '在庫不足を追跡し解決',
196+
loading: 'バックログを読み込み中...',
197+
loadError: 'バックログの読み込みに失敗しました',
198+
highPriority: '優先度: 高',
199+
mediumPriority: '優先度: 中',
200+
lowPriority: '優先度: 低',
201+
totalItems: 'バックログアイテム総数',
202+
sectionTitle: 'バックログアイテム',
203+
empty: '✓ バックログアイテムなし - すべての注文を履行できます!',
204+
table: {
205+
orderId: '注文ID',
206+
sku: 'SKU',
207+
itemName: '品目名',
208+
quantityNeeded: '必要数量',
209+
quantityAvailable: '在庫数量',
210+
shortage: '不足',
211+
daysDelayed: '遅延日数',
212+
priority: '優先度'
213+
},
214+
unitsShort: '単位不足'
215+
},
216+
217+
// Restocking view (R2)
218+
restocking: {
219+
title: '補充推奨',
220+
subtitle: '現在の在庫、需要予測、予算上限に基づいて推奨される発注書。',
221+
budgetLabel: '予算上限',
222+
serviceLevelLabel: 'サービスレベル',
223+
generate: '推奨を生成',
224+
loading: '推奨を計算中...',
225+
loadError: '推奨の読み込みに失敗しました',
226+
noResults: '現在の在庫と予測では、補充は必要ありません。',
227+
summary: {
228+
candidates: '候補',
229+
inBudget: '予算内',
230+
outOfBudget: '予算外',
231+
totalCost: '選択合計コスト',
232+
remaining: '予算残高'
233+
},
234+
table: {
235+
sku: 'SKU',
236+
itemName: '品目名',
237+
category: 'カテゴリ',
238+
warehouse: '倉庫',
239+
onHand: '在庫数',
240+
reorderPoint: '発注点',
241+
forecast: '予測',
242+
recommendedQty: '推奨数量',
243+
unitCost: '単価',
244+
estCost: '見積コスト',
245+
status: 'ステータス'
246+
},
247+
status: {
248+
inBudget: '予算内',
249+
outOfBudget: '予算外'
250+
},
251+
note: 'オペレータが調整可能。推奨は(s, S)ポリシーとニュースベンダーモデルのサービスレベル較正で計算されます。現在、需要予測が存在するSKUに限定されています。'
252+
},
253+
254+
// Reports view
255+
reports: {
256+
title: 'パフォーマンスレポート',
257+
subtitle: '四半期パフォーマンス指標と月次トレンドを表示',
258+
loading: 'レポートを読み込み中...',
259+
loadError: 'レポートの読み込みに失敗しました',
260+
quarterlyPerformance: '四半期パフォーマンス',
261+
monthlyRevenueTrend: '月次収益トレンド',
262+
monthOverMonth: '前月比分析',
263+
quarter: '四半期',
264+
totalOrders: '合計注文数',
265+
totalRevenue: '合計収益',
266+
avgOrderValue: '平均注文金額',
267+
fulfillmentRate: '達成率',
268+
month: '月',
269+
orders: '注文数',
270+
revenue: '収益',
271+
change: '変化',
272+
growthRate: '成長率',
273+
totalRevenueYtd: '累計収益 (YTD)',
274+
avgMonthlyRevenue: '月平均収益',
275+
totalOrdersYtd: '累計注文数 (YTD)',
276+
bestPerformingQuarter: '最高業績四半期'
277+
},
278+
191279
// Filters
192280
filters: {
193281
timePeriod: '期間',

client/src/main.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import Orders from './views/Orders.vue'
77
import Demand from './views/Demand.vue'
88
import Spending from './views/Spending.vue'
99
import Reports from './views/Reports.vue'
10+
import Restocking from './views/Restocking.vue'
11+
import Backlog from './views/Backlog.vue'
1012

1113
const router = createRouter({
1214
history: createWebHistory(),
@@ -16,7 +18,9 @@ const router = createRouter({
1618
{ path: '/orders', component: Orders },
1719
{ path: '/demand', component: Demand },
1820
{ path: '/spending', component: Spending },
19-
{ path: '/reports', component: Reports }
21+
{ path: '/reports', component: Reports },
22+
{ path: '/restocking', component: Restocking },
23+
{ path: '/backlog', component: Backlog }
2024
]
2125
})
2226

client/src/utils/pluralize.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Tiny pluralization helper.
3+
*
4+
* Returns "<n> <singular|plural>" depending on the value of n.
5+
* Designed for English, where 1 takes the singular form and everything else
6+
* (including 0) takes the plural. For locales without grammatical number
7+
* (e.g. Japanese) the i18n layer should resolve to a single string and this
8+
* helper is unnecessary.
9+
*
10+
* pluralize(1, 'day', 'days') // "1 day"
11+
* pluralize(0, 'day', 'days') // "0 days"
12+
* pluralize(3, 'item', 'items') // "3 items"
13+
*/
14+
export function pluralize(count, singular, plural) {
15+
const n = Number(count) || 0
16+
return `${n} ${n === 1 ? singular : plural}`
17+
}

0 commit comments

Comments
 (0)