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
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,5 @@ npm install && npm run dev
- Status: green/blue/yellow/red
- Charts: Custom SVG, CSS Grid for layouts
- No emojis in UI

Always document non-obvious logic changes with comments
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
10 changes: 10 additions & 0 deletions client/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,15 @@ export const api = {
async getPurchaseOrderByBacklogItem(backlogItemId) {
const response = await axios.get(`${API_BASE_URL}/purchase-orders/${backlogItemId}`)
return response.data
},

async getRestockingOrders() {
const response = await axios.get(`${API_BASE_URL}/restocking-orders`)
return response.data
},

async createRestockingOrder(orderData) {
const response = await axios.post(`${API_BASE_URL}/restocking-orders`, orderData)
return response.data
}
}
38 changes: 37 additions & 1 deletion 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,10 @@ export default {
title: 'Orders',
description: 'View and manage customer orders',
allOrders: 'All Orders',
submittedOrders: 'Submitted Orders',
noSubmittedOrders: 'No restocking orders submitted yet.',
leadTime: 'Lead Time',
leadTimeDays: '{count} days',
totalOrders: 'Total Orders',
totalRevenue: 'Total Revenue',
avgOrderValue: 'Avg Order Value',
Expand Down Expand Up @@ -188,6 +193,36 @@ export default {
}
},

// Restocking
restocking: {
title: 'Restocking',
description: 'Set a budget and order the items your forecast says you are short of',
budget: 'Available Budget',
budgetHint: 'Drag to set how much you can spend on restocking',
recommended: 'Recommended Restock',
itemsRecommended: 'Items Recommended',
unitsRecommended: 'Units Recommended',
totalCost: 'Total Cost',
budgetRemaining: 'Budget Remaining',
placeOrder: 'Place Order',
placingOrder: 'Placing Order...',
orderPlaced: 'Placed {count} order(s) successfully. See the Orders tab.',
orderFailed: 'Failed to place order',
noRecommendations: 'No restocking needed for the current budget and filters.',
noShortfall: 'Every forecast item is fully covered by current stock.',
shortBy: 'Short {count} units',
ordersAcross: 'Will create {count} order(s), one per warehouse',
table: {
sku: 'SKU',
itemName: 'Item Name',
warehouse: 'Warehouse',
shortfall: 'Shortfall',
quantity: 'Order Qty',
unitCost: 'Unit Cost',
lineTotal: 'Line Total'
}
},

// Filters
filters: {
timePeriod: 'Time Period',
Expand All @@ -206,7 +241,8 @@ export default {
backordered: 'Backordered',
inStock: 'In Stock',
lowStock: 'Low Stock',
adequate: 'Adequate'
adequate: 'Adequate',
submitted: 'Submitted'
},

// Trends
Expand Down
38 changes: 37 additions & 1 deletion 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,10 @@ export default {
title: '注文',
description: '顧客注文の表示と管理',
allOrders: 'すべての注文',
submittedOrders: '送信済み注文',
noSubmittedOrders: '送信済みの補充注文はまだありません。',
leadTime: 'リードタイム',
leadTimeDays: '{count}日',
totalOrders: '総注文数',
totalRevenue: '総収益',
avgOrderValue: '平均注文額',
Expand Down Expand Up @@ -188,6 +193,36 @@ export default {
}
},

// Restocking
restocking: {
title: '補充',
description: '予算を設定し、予測で不足している品目を発注します',
budget: '利用可能予算',
budgetHint: 'ドラッグして補充に使える金額を設定します',
recommended: '推奨補充',
itemsRecommended: '推奨品目数',
unitsRecommended: '推奨数量',
totalCost: '合計金額',
budgetRemaining: '残り予算',
placeOrder: '発注する',
placingOrder: '発注中...',
orderPlaced: '{count}件の注文を送信しました。注文タブをご確認ください。',
orderFailed: '発注に失敗しました',
noRecommendations: '現在の予算とフィルターでは補充は不要です。',
noShortfall: 'すべての予測品目は現在の在庫で充足しています。',
shortBy: '{count}個不足',
ordersAcross: '倉庫ごとに{count}件の注文を作成します',
table: {
sku: 'SKU',
itemName: '品目名',
warehouse: '倉庫',
shortfall: '不足数',
quantity: '発注数量',
unitCost: '単価',
lineTotal: '小計'
}
},

// Filters
filters: {
timePeriod: '期間',
Expand All @@ -206,7 +241,8 @@ export default {
backordered: 'バックオーダー',
inStock: '在庫あり',
lowStock: '在庫僅少',
adequate: '適量'
adequate: '適量',
submitted: '送信済み'
},

// Trends
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
107 changes: 106 additions & 1 deletion client/src/views/Orders.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,59 @@
</div>
</div>

<div class="card">
<div class="card-header">
<h3 class="card-title">{{ t('orders.submittedOrders') }} ({{ submittedOrders.length }})</h3>
</div>
<div v-if="submittedOrders.length === 0" class="empty-state">
{{ t('orders.noSubmittedOrders') }}
</div>
<div v-else class="table-container">
<table class="submitted-orders-table">
<thead>
<tr>
<th class="scol-order-number">{{ t('orders.table.orderNumber') }}</th>
<th class="scol-warehouse">{{ t('orders.table.warehouse') }}</th>
<th class="scol-items">{{ t('orders.table.items') }}</th>
<th class="scol-status">{{ t('orders.table.status') }}</th>
<th class="scol-date">{{ t('orders.table.orderDate') }}</th>
<th class="scol-date">{{ t('orders.table.expectedDelivery') }}</th>
<th class="scol-lead-time">{{ t('orders.leadTime') }}</th>
<th class="scol-value">{{ t('orders.table.totalValue') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="order in submittedOrders" :key="order.id">
<td class="scol-order-number"><strong>{{ order.order_number }}</strong></td>
<td class="scol-warehouse">{{ order.warehouse }}</td>
<td class="scol-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="scol-status">
<span :class="['badge', 'info']">{{ t('status.submitted') }}</span>
</td>
<td class="scol-date">{{ formatDate(order.order_date) }}</td>
<td class="scol-date">{{ formatDate(order.expected_delivery) }}</td>
<td class="scol-lead-time">{{ t('orders.leadTimeDays', { count: order.lead_time_days }) }}</td>
<!-- Restocking totals carry cents, so pin to 2 decimals - a bare
toLocaleString() renders 17993.5 as "17,993.5". -->
<td class="scol-value"><strong>{{ currencySymbol }}{{ order.total_value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}</strong></td>
</tr>
</tbody>
</table>
</div>
</div>

<div class="card">
<div class="card-header">
<h3 class="card-title">{{ t('orders.allOrders') }} ({{ orders.length }})</h3>
Expand Down Expand Up @@ -95,6 +148,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 +163,23 @@ export default {
try {
loading.value = true
const filters = getCurrentFilters()
const fetchedOrders = await api.getOrders(filters)

// Restocking orders are a separate list and are NOT affected by the
// global filter bar - they always show in full, regardless of the
// selected period/warehouse/category/status. The call is wrapped in
// its own catch so a failure there doesn't blank the main orders table.
const [fetchedOrders, fetchedSubmitted] = await Promise.all([
api.getOrders(filters),
api.getRestockingOrders().catch(() => [])
])

// 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
})
submittedOrders.value = fetchedSubmitted
} catch (err) {
error.value = 'Failed to load orders: ' + err.message
} finally {
Expand Down Expand Up @@ -160,6 +223,7 @@ export default {
loading,
error,
orders,
submittedOrders,
getOrdersByStatus,
getOrderStatusClass,
formatDate,
Expand Down Expand Up @@ -276,4 +340,45 @@ export default {
font-size: 0.813rem;
color: #64748b;
}

/* Submitted orders empty state */
.empty-state {
padding: 3rem;
text-align: center;
color: #64748b;
}

/* Submitted orders table - fixed layout with its own column widths */
.submitted-orders-table {
table-layout: fixed;
width: 100%;
}

.scol-order-number {
width: 130px;
}

.scol-warehouse {
width: 120px;
}

.scol-items {
width: 160px;
}

.scol-status {
width: 110px;
}

.scol-date {
width: 140px;
}

.scol-lead-time {
width: 110px;
}

.scol-value {
width: 120px;
}
</style>
Loading