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.
27 changes: 25 additions & 2 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` (Vue 3 patterns) and `server/CLAUDE.md` (FastAPI patterns) are auto-loaded when you work in those directories — consult them for subsystem detail.

## Critical Tool Usage Rules

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

# Tests (backend, 51 tests via pytest + FastAPI TestClient)
cd tests
uv run pytest -v
uv run pytest backend/test_inventory.py::TestInventoryEndpoints::test_get_all_inventory -v # single test
```

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

## 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
**Routing**: `client/src/main.js` defines vue-router routes — `/` Dashboard, `/inventory`, `/orders`, `/demand`, `/spending`, `/reports`
**Shared state (composables)**: `useFilters` (global filters), `useAuth` (auth/profile), `useI18n` (locale)
**i18n**: English/Japanese locales in `client/src/locales/{en,ja}.js`, toggled via `LanguageSwitcher.vue`
**Currency**: Format via `client/src/utils/currency.js` (don't hand-roll `toLocaleString` per component)

## API Endpoints
All routes are defined in `server/main.py`. Shared helpers: `apply_filters()` (warehouse/category/status) and `filter_by_month()` (month `2025-01` or quarter `Q1-2025` via `QUARTER_MAP`).
- `GET /api/inventory` - Filters: warehouse, category
- `GET /api/inventory/{item_id}` - Single item (404 if missing)
- `GET /api/orders` - Filters: warehouse, category, status, month
- `GET /api/orders/{order_id}` - Single order (404 if missing)
- `GET /api/dashboard/summary` - All filters
- `GET /api/demand`, `/api/backlog` - No filters
- `GET /api/spending/*` - Summary, monthly, categories, transactions
- `GET /api/demand`, `/api/backlog` - No filters (backlog injects `has_purchase_order` flag)
- `GET /api/spending/*` - summary, monthly, categories, transactions
- `GET /api/reports/quarterly`, `/api/reports/monthly-trends` - Computed from orders

## 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
4. Inventory filters don't support month (no time dimension)
5. Revenue goals: $800K/month single, $9.6M YTD all months
6. `client/src/api.js` calls `/api/tasks` (CRUD) and `/api/purchase-orders` (POST/GET) endpoints that are **not implemented** in `server/main.py` — add the backend routes before wiring up those features

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

## File Locations
- Views: `client/src/views/*.vue`
Expand Down
3 changes: 3 additions & 0 deletions client/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
<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
5 changes: 5 additions & 0 deletions client/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export const api = {
return response.data
},

async placeRestockOrder(payload) {
const response = await axios.post(`${API_BASE_URL}/restock-orders`, payload)
return response.data
},

async getDemandForecasts() {
const response = await axios.get(`${API_BASE_URL}/demand`)
return response.data
Expand Down
27 changes: 27 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 @@ -204,11 +205,37 @@ export default {
shipped: 'Shipped',
processing: 'Processing',
backordered: 'Backordered',
submitted: 'Submitted',
inStock: 'In Stock',
lowStock: 'Low Stock',
adequate: 'Adequate'
},

// Restocking
restocking: {
title: 'Restocking',
description: 'Set a budget and get restock recommendations from demand forecasts',
budgetLabel: 'Available Budget',
recommendations: 'Recommended Restock Items',
runningTotal: 'Order Total',
remaining: 'Remaining Budget',
placeOrder: 'Place Order',
placing: 'Placing Order...',
orderPlaced: 'Order {number} submitted successfully',
noRecommendations: 'No items fit within the selected budget',
submittedOrders: 'Submitted Orders',
leadTime: 'Lead Time',
leadTimeDays: '{days} days',
table: {
sku: 'SKU',
itemName: 'Item Name',
trend: 'Trend',
recommendedQty: 'Recommended Qty',
estUnitCost: 'Est. Unit Cost',
lineTotal: 'Line Total'
}
},

// Trends
trends: {
increasing: 'increasing',
Expand Down
27 changes: 27 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 @@ -204,11 +205,37 @@ export default {
shipped: '出荷済み',
processing: '処理中',
backordered: 'バックオーダー',
submitted: '送信済み',
inStock: '在庫あり',
lowStock: '在庫僅少',
adequate: '適量'
},

// Restocking
restocking: {
title: '補充',
description: '予算を設定し、需要予測から補充推奨を取得します',
budgetLabel: '利用可能な予算',
recommendations: '推奨補充品目',
runningTotal: '注文合計',
remaining: '残りの予算',
placeOrder: '注文する',
placing: '注文中...',
orderPlaced: '注文 {number} が正常に送信されました',
noRecommendations: '選択した予算内に収まる品目がありません',
submittedOrders: '送信済み注文',
leadTime: 'リードタイム',
leadTimeDays: '{days}日間',
table: {
sku: 'SKU',
itemName: '品目名',
trend: 'トレンド',
recommendedQty: '推奨数量',
estUnitCost: '推定単価',
lineTotal: '小計'
}
},

// Trends
trends: {
increasing: '増加',
Expand Down
2 changes: 2 additions & 0 deletions 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 @@ -15,6 +16,7 @@ const router = createRouter({
{ path: '/inventory', component: Inventory },
{ path: '/orders', component: Orders },
{ path: '/demand', component: Demand },
{ path: '/restocking', component: Restocking },
{ path: '/spending', component: Spending },
{ path: '/reports', component: Reports }
]
Expand Down
66 changes: 64 additions & 2 deletions client/src/views/Orders.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,52 @@
<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">{{ submittedOrders.length }}</div>
</div>
</div>

<div v-if="submittedOrders.length" class="card">
<div class="card-header">
<h3 class="card-title">{{ t('restocking.submittedOrders') }} ({{ submittedOrders.length }})</h3>
</div>
<div class="table-container">
<table class="orders-table">
<thead>
<tr>
<th class="col-order-number">{{ t('orders.table.orderNumber') }}</th>
<th class="col-items">{{ t('orders.table.items') }}</th>
<th class="col-date">{{ t('orders.table.orderDate') }}</th>
<th class="col-date">{{ t('orders.table.expectedDelivery') }}</th>
<th class="col-leadtime">{{ t('restocking.leadTime') }}</th>
<th class="col-value">{{ t('orders.table.totalValue') }}</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-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-date">{{ formatDate(order.order_date) }}</td>
<td class="col-date">{{ formatDate(order.expected_delivery) }}</td>
<td class="col-leadtime">{{ t('restocking.leadTimeDays', { days: leadTimeDays(order) }) }}</td>
<td class="col-value"><strong>{{ currencySymbol }}{{ order.total_value.toLocaleString() }}</strong></td>
</tr>
</tbody>
</table>
</div>
</div>

<div class="card">
Expand Down Expand Up @@ -95,6 +141,7 @@ export default {
const loading = ref(true)
const error = ref(null)
const orders = ref([])
const submittedOrders = ref([])

// Use shared filters
const {
Expand All @@ -109,14 +156,20 @@ export default {
try {
loading.value = true
const filters = getCurrentFilters()
const fetchedOrders = await api.getOrders(filters)
const [fetchedOrders, submitted] = await Promise.all([
api.getOrders(filters),
api.getOrders({ status: 'submitted' })
])

// Sort orders by order_date (earliest first)
orders.value = fetchedOrders.sort((a, b) => {
const dateA = new Date(a.order_date)
const dateB = new Date(b.order_date)
return dateA - dateB
})

// Sort submitted orders newest first
submittedOrders.value = submitted.sort((a, b) => new Date(b.order_date) - new Date(a.order_date))
} catch (err) {
error.value = 'Failed to load orders: ' + err.message
} finally {
Expand All @@ -138,11 +191,14 @@ export default {
'Delivered': 'success',
'Shipped': 'info',
'Processing': 'warning',
'Backordered': 'danger'
'Backordered': 'danger',
'Submitted': 'info'
}
return statusMap[status] || 'info'
}

const leadTimeDays = (o) => Math.round((new Date(o.expected_delivery) - new Date(o.order_date)) / 86400000)

const formatDate = (dateString) => {
const { currentLocale } = useI18n()
const locale = currentLocale.value === 'ja' ? 'ja-JP' : 'en-US'
Expand All @@ -160,8 +216,10 @@ export default {
loading,
error,
orders,
submittedOrders,
getOrdersByStatus,
getOrderStatusClass,
leadTimeDays,
formatDate,
currencySymbol,
translateProductName,
Expand Down Expand Up @@ -199,6 +257,10 @@ export default {
width: 140px;
}

.col-leadtime {
width: 110px;
}

.col-value {
width: 120px;
}
Expand Down
Loading