Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file modified .claude/hooks/post-tool-use.sh
100755 → 100644
Empty file.
31 changes: 28 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# 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).

> **Nested guidance**: `client/CLAUDE.md` (detailed Vue 3 patterns) and `server/CLAUDE.md` (detailed FastAPI patterns) hold deeper, directory-specific best practices. Read them when doing substantial frontend or backend work.

## Critical Tool Usage Rules

### Subagents
Expand Down Expand Up @@ -38,20 +42,34 @@ uv run python main.py
# Frontend
cd client
npm install && npm run dev

# Frontend production build
cd client && npm run build # output: client/dist/

# Backend tests (pytest + FastAPI TestClient; deps in server/pyproject.toml)
cd server && uv run pytest ../tests/backend -v
cd server && uv run pytest ../tests/backend/test_inventory.py::test_name -v # single test
```

On macOS/Linux, `./scripts/start.sh` and `./scripts/stop.sh` run both servers. On Windows, use the manual commands above.

## Key Patterns

**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
**Composables** (`client/src/composables/`): `useFilters` (global filter state + `getCurrentFilters()`), `useI18n` (translations), `useAuth` (auth state)
**i18n**: English/Japanese via `useI18n` + `client/src/locales/{en,ja}.js`; `LanguageSwitcher` component. Add UI strings to both locale files
**Routing**: 6 views via vue-router (`client/src/main.js`): `/` Dashboard, `/inventory`, `/orders`, `/demand`, `/spending`, `/reports`

## API Endpoints
- `GET /api/inventory` - Filters: warehouse, category
- `GET /api/orders` - Filters: warehouse, category, status, month
- `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` - Reports view data
- `GET /api/inventory/{item_id}`, `/api/orders/{order_id}` - Single item (404 if missing)

## Common Issues
1. Use unique keys in v-for (not `index`) - use `sku`, `month`, etc.
Expand All @@ -61,11 +79,18 @@ npm install && npm run dev
5. Revenue goals: $800K/month single, $9.6M YTD all months

## File Locations
- Views: `client/src/views/*.vue`
- Views: `client/src/views/*.vue` (one per route)
- Components: `client/src/components/*.vue` (FilterBar, detail modals, ProfileMenu, LanguageSwitcher)
- Composables: `client/src/composables/*.js`
- Locales: `client/src/locales/{en,ja}.js`
- API Client: `client/src/api.js`
- Backend: `server/main.py`, `server/mock_data.py`
- Backend: `server/main.py` (endpoints), `server/mock_data.py` (JSON loader)
- Data: `server/data/*.json`
- Styles: `client/src/App.vue`
- Tests: `tests/backend/*.py`
- Styles: `client/src/App.vue` (global)

## Conventions
- Always document non-obvious logic changes with comments

## Design System
- Colors: Slate/gray (#0f172a, #64748b, #e2e8f0)
Expand Down
1 change: 1 addition & 0 deletions client/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<router-link to="/demand" :class="{ active: $route.path === '/demand' }">
{{ t('nav.demandForecast') }}
</router-link>
<router-link to="/restocking" :class="{ active: $route.path === '/restocking' }">{{ t('nav.restocking') }}</router-link>
<router-link to="/reports" :class="{ active: $route.path === '/reports' }">
Reports
</router-link>
Expand Down
12 changes: 12 additions & 0 deletions client/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,17 @@ export const api = {
async getPurchaseOrderByBacklogItem(backlogItemId) {
const response = await axios.get(`${API_BASE_URL}/purchase-orders/${backlogItemId}`)
return response.data
},

async getRestockingRecommendations(budget) {
const params = new URLSearchParams()
params.append('budget', budget)
const response = await axios.get(`${API_BASE_URL}/restocking/recommendations?${params.toString()}`)
return response.data
},

async createRestockingOrder(orderData) {
const response = await axios.post(`${API_BASE_URL}/restocking/order`, orderData)
return response.data
}
}
29 changes: 29 additions & 0 deletions client/src/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default {
orders: 'Orders',
finance: 'Finance',
demandForecast: 'Demand Forecast',
restocking: 'Restocking',
companyName: 'Catalyst Components',
subtitle: 'Inventory Management System'
},
Expand Down Expand Up @@ -106,6 +107,9 @@ export default {
title: 'Orders',
description: 'View and manage customer orders',
allOrders: 'All Orders',
submittedOrders: 'Submitted Orders',
leadTime: 'Lead Time',
leadTimeDays: '{days} days',
totalOrders: 'Total Orders',
totalRevenue: 'Total Revenue',
avgOrderValue: 'Avg Order Value',
Expand Down Expand Up @@ -188,6 +192,30 @@ export default {
}
},

// Restocking
restocking: {
title: 'Restocking',
description: 'Set a budget to get recommended restock items from the demand forecast',
budget: 'Available Budget',
recommendedItems: 'Recommended Items',
placeOrder: 'Place Order',
placing: 'Placing Order...',
totalCost: 'Total Cost',
remainingBudget: 'Remaining Budget',
itemCount: '{count} items',
noRecommendations: 'Increase the budget to see recommended items',
skippedNotice: '{count} forecasted item(s) skipped (no inventory cost data)',
orderPlaced: 'Restock order {orderNumber} created',
table: {
sku: 'SKU',
itemName: 'Item Name',
trend: 'Trend',
gapQuantity: 'Restock Qty',
unitCost: 'Unit Cost',
lineCost: 'Line Cost'
}
},

// Filters
filters: {
timePeriod: 'Time Period',
Expand All @@ -204,6 +232,7 @@ export default {
shipped: 'Shipped',
processing: 'Processing',
backordered: 'Backordered',
submitted: 'Submitted',
inStock: 'In Stock',
lowStock: 'Low Stock',
adequate: 'Adequate'
Expand Down
30 changes: 30 additions & 0 deletions client/src/locales/ja.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default {
orders: '注文',
finance: '財務',
demandForecast: '需要予測',
restocking: '補充',
companyName: '触媒コンポーネンツ',
subtitle: '在庫管理システム'
},
Expand Down Expand Up @@ -106,6 +107,9 @@ export default {
title: '注文',
description: '顧客注文の表示と管理',
allOrders: 'すべての注文',
submittedOrders: '送信済み注文',
leadTime: 'リードタイム',
leadTimeDays: '{days}日',
totalOrders: '総注文数',
totalRevenue: '総収益',
avgOrderValue: '平均注文額',
Expand Down Expand Up @@ -188,6 +192,30 @@ export default {
}
},

// Restocking
restocking: {
title: '補充',
description: '予算を設定して、需要予測から推奨される補充品目を取得します',
budget: '利用可能な予算',
recommendedItems: '推奨品目',
placeOrder: '発注する',
placing: '発注中...',
totalCost: '合計コスト',
remainingBudget: '残り予算',
itemCount: '{count}件',
noRecommendations: '予算を増やすと推奨品目が表示されます',
skippedNotice: '{count}件の予測品目をスキップしました(在庫コストデータなし)',
orderPlaced: '補充注文 {orderNumber} を作成しました',
table: {
sku: 'SKU',
itemName: '品目名',
trend: 'トレンド',
gapQuantity: '補充数量',
unitCost: '単価',
lineCost: '小計'
}
},

// Filters
filters: {
timePeriod: '期間',
Expand All @@ -204,6 +232,7 @@ export default {
shipped: '出荷済み',
processing: '処理中',
backordered: 'バックオーダー',
submitted: '送信済み',
inStock: '在庫あり',
lowStock: '在庫僅少',
adequate: '適量'
Expand Down Expand Up @@ -373,6 +402,7 @@ export default {
'Superior Manufacturing': 'スーペリアマニュファクチャリング',
'Cascade Manufacturing': 'カスケードマニュファクチャリング',
'Acme Manufacturing Corp': 'アクメ製造',
'Internal Restock': '社内補充',
'TechBuild Industries': 'テックビルド工業',
'Advanced Components Inc': 'アドバンストコンポーネンツ',
'Premier Industries': 'プレミア工業',
Expand Down
4 changes: 3 additions & 1 deletion client/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Orders from './views/Orders.vue'
import Demand from './views/Demand.vue'
import Spending from './views/Spending.vue'
import Reports from './views/Reports.vue'
import Restocking from './views/Restocking.vue'

const router = createRouter({
history: createWebHistory(),
Expand All @@ -16,7 +17,8 @@ const router = createRouter({
{ path: '/orders', component: Orders },
{ path: '/demand', component: Demand },
{ path: '/spending', component: Spending },
{ path: '/reports', component: Reports }
{ path: '/reports', component: Reports },
{ path: '/restocking', component: Restocking }
]
})

Expand Down
79 changes: 76 additions & 3 deletions client/src/views/Orders.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@
<div class="stat-label">{{ t('status.backordered') }}</div>
<div class="stat-value">{{ getOrdersByStatus('Backordered').length }}</div>
</div>
<div class="stat-card info">
<div class="stat-label">{{ t('status.submitted') }}</div>
<div class="stat-value">{{ getOrdersByStatus('Submitted').length }}</div>
</div>
</div>

<div class="card">
<div class="card-header">
<h3 class="card-title">{{ t('orders.allOrders') }} ({{ orders.length }})</h3>
<h3 class="card-title">{{ t('orders.allOrders') }} ({{ otherOrders.length }})</h3>
</div>
<div class="table-container">
<table class="orders-table">
Expand All @@ -45,7 +49,7 @@
</tr>
</thead>
<tbody>
<tr v-for="order in orders" :key="order.id">
<tr v-for="order in otherOrders" :key="order.id">
<td class="col-order-number"><strong>{{ order.order_number }}</strong></td>
<td class="col-customer">{{ translateCustomerName(order.customer) }}</td>
<td class="col-items">
Expand Down Expand Up @@ -74,6 +78,56 @@
</table>
</div>
</div>

<div class="card" v-if="submittedOrders.length">
<div class="card-header">
<h3 class="card-title">{{ t('orders.submittedOrders') }} ({{ submittedOrders.length }})</h3>
</div>
<div class="table-container">
<table class="orders-table orders-table-submitted">
<thead>
<tr>
<th class="col-order-number">{{ t('orders.table.orderNumber') }}</th>
<th class="col-customer">{{ t('orders.table.customer') }}</th>
<th class="col-items">{{ t('orders.table.items') }}</th>
<th class="col-status">{{ t('orders.table.status') }}</th>
<th class="col-date">{{ t('orders.table.orderDate') }}</th>
<th class="col-date">{{ t('orders.table.expectedDelivery') }}</th>
<th class="col-value">{{ t('orders.table.totalValue') }}</th>
<th class="col-lead-time">{{ t('orders.leadTime') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="order in submittedOrders" :key="order.id">
<td class="col-order-number"><strong>{{ order.order_number }}</strong></td>
<td class="col-customer">{{ translateCustomerName(order.customer) }}</td>
<td class="col-items">
<details class="items-details">
<summary class="items-summary">
{{ t('orders.itemsCount', { count: order.items.length }) }}
</summary>
<div class="items-dropdown">
<div v-for="(item, idx) in order.items" :key="idx" class="item-entry">
<span class="item-name">{{ translateProductName(item.name) }}</span>
<span class="item-meta">{{ t('orders.quantity') }}: {{ item.quantity }} @ {{ currencySymbol }}{{ item.unit_price }}</span>
</div>
</div>
</details>
</td>
<td class="col-status">
<span :class="['badge', getOrderStatusClass(order.status)]">
{{ t(`status.${order.status.toLowerCase()}`) }}
</span>
</td>
<td class="col-date">{{ formatDate(order.order_date) }}</td>
<td class="col-date">{{ formatDate(order.expected_delivery) }}</td>
<td class="col-value"><strong>{{ currencySymbol }}{{ order.total_value.toLocaleString() }}</strong></td>
<td class="col-lead-time">{{ getLeadTimeDays(order) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
Expand Down Expand Up @@ -133,16 +187,28 @@ export default {
return orders.value.filter(order => order.status === status)
}

const submittedOrders = computed(() => orders.value.filter(o => o.status === 'Submitted'))
const otherOrders = computed(() => orders.value.filter(o => o.status !== 'Submitted'))

const getOrderStatusClass = (status) => {
const statusMap = {
'Delivered': 'success',
'Shipped': 'info',
'Processing': 'warning',
'Backordered': 'danger'
'Backordered': 'danger',
'Submitted': 'info'
}
return statusMap[status] || 'info'
}

const getLeadTimeDays = (order) => {
const orderDate = new Date(order.order_date)
const deliveryDate = new Date(order.expected_delivery)
if (isNaN(orderDate.getTime()) || isNaN(deliveryDate.getTime())) return '-'
const days = Math.round((deliveryDate - orderDate) / 86400000)
return t('orders.leadTimeDays', { days })
}

const formatDate = (dateString) => {
const { currentLocale } = useI18n()
const locale = currentLocale.value === 'ja' ? 'ja-JP' : 'en-US'
Expand All @@ -160,8 +226,11 @@ export default {
loading,
error,
orders,
submittedOrders,
otherOrders,
getOrdersByStatus,
getOrderStatusClass,
getLeadTimeDays,
formatDate,
currencySymbol,
translateProductName,
Expand Down Expand Up @@ -203,6 +272,10 @@ export default {
width: 120px;
}

.col-lead-time {
width: 100px;
}

/* Items details styling */
.items-details {
position: relative;
Expand Down
Loading