diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 000000000..ce75e31c7 --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,41 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + +jobs: + claude-review: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + track_progress: true + prompt: | + REPO: ${{ github.repository }} + PR NUMBER: ${{ github.event.pull_request.number }} + + Review this pull request and focus on: + + - Correctness: logic errors, unhandled edge cases, error handling + - Code quality: readability, maintainability, adherence to the + conventions already used in this repo + - Security: input validation, injection risks, exposed secrets + - Performance: obvious bottlenecks or wasteful work + - Tests: is the new behaviour covered? + + Leave inline comments for specific issues and a top-level comment + for general observations. Be concise and skip nitpicks. + claude_args: | + --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)" diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 000000000..5da751990 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,37 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} diff --git a/client/src/App.vue b/client/src/App.vue index c2da05a5c..45e78a8c6 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -22,8 +22,11 @@ {{ t('nav.demandForecast') }} + + {{ t('nav.restocking') }} + - Reports + {{ t('nav.reports') }} @@ -467,6 +470,15 @@ tbody tr:hover { color: #1e40af; } +.badge.restocking_order { + background: #e0e7ff; + color: #3730a3; +} + +.stat-card.info-alt .stat-value { + color: #8b5cf6; +} + .loading { text-align: center; padding: 3rem; diff --git a/client/src/api.js b/client/src/api.js index 11cb9db70..9acf9553f 100644 --- a/client/src/api.js +++ b/client/src/api.js @@ -102,5 +102,30 @@ export const api = { async getPurchaseOrderByBacklogItem(backlogItemId) { const response = await axios.get(`${API_BASE_URL}/purchase-orders/${backlogItemId}`) return response.data + }, + + async getRestockingRecommendations(budget, filters = {}) { + const params = new URLSearchParams() + params.append('budget', budget) + if (filters.warehouse && filters.warehouse !== 'all') params.append('warehouse', filters.warehouse) + if (filters.category && filters.category !== 'all') params.append('category', filters.category) + + 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-orders`, orderData) + return response.data + }, + + async getQuarterlyReports() { + const response = await axios.get(`${API_BASE_URL}/reports/quarterly`) + return response.data + }, + + async getMonthlyTrends() { + const response = await axios.get(`${API_BASE_URL}/reports/monthly-trends`) + return response.data } } diff --git a/client/src/components/OrderSubmittedModal.vue b/client/src/components/OrderSubmittedModal.vue new file mode 100644 index 000000000..2d73c1d71 --- /dev/null +++ b/client/src/components/OrderSubmittedModal.vue @@ -0,0 +1,371 @@ + + + + + diff --git a/client/src/locales/en.js b/client/src/locales/en.js index 03a58fe6e..17bf8c13f 100644 --- a/client/src/locales/en.js +++ b/client/src/locales/en.js @@ -6,6 +6,8 @@ export default { orders: 'Orders', finance: 'Finance', demandForecast: 'Demand Forecast', + restocking: 'Restocking', + reports: 'Reports', companyName: 'Catalyst Components', subtitle: 'Inventory Management System' }, @@ -204,6 +206,7 @@ export default { shipped: 'Shipped', processing: 'Processing', backordered: 'Backordered', + restocking_order: 'Restocking Order', inStock: 'In Stock', lowStock: 'Low Stock', adequate: 'Adequate' @@ -304,6 +307,105 @@ export default { noTasks: 'No tasks yet. Add your first task above!' }, + // Restocking + restocking: { + title: 'Smart Restocking', + description: 'Set a budget and get data-driven recommendations for what to reorder next', + budgetLabel: 'Restocking Budget', + stats: { + recommendedSpend: 'Recommended Spend', + budgetRemaining: 'Budget Remaining', + itemsSelected: 'Items Selected' + }, + recommendationsTitle: 'Recommendations', + table: { + sku: 'SKU', + itemName: 'Item Name', + currentStock: 'Current Stock', + recommendedQty: 'Recommended Qty', + unitCost: 'Unit Cost', + totalCost: 'Total Cost', + demandTrend: 'Demand Trend', + urgencyScore: 'Urgency Score', + reason: 'Reason' + }, + selectAll: 'Select All', + refresh: 'Refresh', + placeOrder: 'Place Order ({count} items)', + clearSelection: 'Clear Selection', + noRecommendations: 'No restocking recommendations match your current budget and filters.', + budgetExceeded: 'Adding this item would exceed your budget.', + loadError: 'Failed to load restocking recommendations', + orderError: 'Failed to submit restocking order' + }, + + // Order Submitted Modal + orderSubmitted: { + title: 'Restocking Order Submitted', + orderNumber: 'Order Number', + totalItems: 'Total Items', + totalValue: 'Total Value', + expectedDelivery: 'Expected Delivery', + forecastNote: 'Based on Next 30 days demand forecast period', + orderItems: 'Order Items:', + viewInOrders: 'View in Orders Tab', + close: 'Close' + }, + + // Reports + reports: { + title: 'Performance Reports', + description: 'View quarterly performance metrics and monthly trends', + error: 'Failed to load reports', + quarterly: { + title: 'Quarterly Performance', + quarter: 'Quarter', + totalOrders: 'Total Orders', + totalRevenue: 'Total Revenue', + avgOrderValue: 'Avg Order Value', + fulfillmentRate: 'Fulfillment Rate' + }, + monthly: { + trendTitle: 'Monthly Revenue Trend', + analysisTitle: 'Month-over-Month Analysis', + month: 'Month', + orders: 'Orders', + revenue: 'Revenue', + change: 'Change', + growthRate: 'Growth Rate' + }, + summary: { + totalRevenue: 'Total Revenue (YTD)', + avgMonthlyRevenue: 'Avg Monthly Revenue', + totalOrders: 'Total Orders (YTD)', + bestQuarter: 'Best Performing Quarter' + } + }, + + // Backlog + backlog: { + title: 'Backlog Management', + description: 'Track and resolve inventory shortages', + highPriority: 'High Priority', + mediumPriority: 'Medium Priority', + lowPriority: 'Low Priority', + totalItems: 'Total Backlog Items', + itemsTitle: 'Backlog Items', + noItems: 'No backlog items - all orders can be fulfilled!', + unitsShort: 'units short', + days: 'days', + table: { + orderId: 'Order ID', + sku: 'SKU', + itemName: 'Item Name', + quantityNeeded: 'Quantity Needed', + quantityAvailable: 'Quantity Available', + shortage: 'Shortage', + daysDelayed: 'Days Delayed', + priority: 'Priority' + } + }, + // Language language: { english: 'English', @@ -322,7 +424,7 @@ export default { cancel: 'Cancel', search: 'Search', filter: 'Filter', - export: 'Export', + export: 'Export CSV', items: 'items' } } diff --git a/client/src/locales/ja.js b/client/src/locales/ja.js index db33223ac..f6df92a16 100644 --- a/client/src/locales/ja.js +++ b/client/src/locales/ja.js @@ -6,6 +6,8 @@ export default { orders: '注文', finance: '財務', demandForecast: '需要予測', + restocking: '補充', + reports: 'レポート', companyName: '触媒コンポーネンツ', subtitle: '在庫管理システム' }, @@ -204,6 +206,7 @@ export default { shipped: '出荷済み', processing: '処理中', backordered: 'バックオーダー', + restocking_order: '補充注文', inStock: '在庫あり', lowStock: '在庫僅少', adequate: '適量' @@ -304,6 +307,105 @@ export default { noTasks: 'タスクがありません。上記からタスクを追加してください!' }, + // Restocking + restocking: { + title: 'スマート補充', + description: '予算を設定し、データに基づいた再発注の推奨を確認できます', + budgetLabel: '補充予算', + stats: { + recommendedSpend: '推奨支出額', + budgetRemaining: '残り予算', + itemsSelected: '選択品目数' + }, + recommendationsTitle: '推奨品目', + table: { + sku: 'SKU', + itemName: '品目名', + currentStock: '現在庫数', + recommendedQty: '推奨数量', + unitCost: '単価', + totalCost: '合計金額', + demandTrend: '需要動向', + urgencyScore: '緊急度スコア', + reason: '理由' + }, + selectAll: 'すべて選択', + refresh: '更新', + placeOrder: '注文する({count}件)', + clearSelection: '選択を解除', + noRecommendations: '現在の予算とフィルターに一致する補充推奨品目はありません。', + budgetExceeded: 'この品目を追加すると予算を超えてしまいます。', + loadError: '補充推奨の読み込みに失敗しました', + orderError: '補充注文の送信に失敗しました' + }, + + // Order Submitted Modal + orderSubmitted: { + title: '補充注文が送信されました', + orderNumber: '注文番号', + totalItems: '総品目数', + totalValue: '合計金額', + expectedDelivery: '予定配達日', + forecastNote: '今後30日間の需要予測期間に基づく', + orderItems: '注文品目:', + viewInOrders: '注文タブで表示', + close: '閉じる' + }, + + // Reports + reports: { + title: 'パフォーマンスレポート', + description: '四半期別パフォーマンス指標と月間トレンドを表示', + error: 'レポートの読み込みに失敗しました', + quarterly: { + title: '四半期別パフォーマンス', + quarter: '四半期', + totalOrders: '総注文数', + totalRevenue: '総収益', + avgOrderValue: '平均注文額', + fulfillmentRate: '履行率' + }, + monthly: { + trendTitle: '月別収益トレンド', + analysisTitle: '前月比分析', + month: '月', + orders: '注文数', + revenue: '収益', + change: '変化', + growthRate: '成長率' + }, + summary: { + totalRevenue: '総収益(年初来)', + avgMonthlyRevenue: '平均月間収益', + totalOrders: '総注文数(年初来)', + bestQuarter: 'パフォーマンス最高四半期' + } + }, + + // Backlog + backlog: { + title: 'バックログ管理', + description: '在庫不足を追跡して解決', + highPriority: '高優先度', + mediumPriority: '中優先度', + lowPriority: '低優先度', + totalItems: 'バックログ総件数', + itemsTitle: 'バックログ品目', + noItems: 'バックログ品目なし - すべての注文を履行できます!', + unitsShort: '単位不足', + days: '日', + table: { + orderId: '注文ID', + sku: 'SKU', + itemName: '品目名', + quantityNeeded: '必要数量', + quantityAvailable: '在庫数量', + shortage: '不足', + daysDelayed: '遅延日数', + priority: '優先度' + } + }, + // Language language: { english: 'English', @@ -322,7 +424,7 @@ export default { cancel: 'キャンセル', search: '検索', filter: 'フィルター', - export: 'エクスポート', + export: 'CSVをエクスポート', items: '件' }, diff --git a/client/src/main.js b/client/src/main.js index 477c2d966..2940c1ee9 100644 --- a/client/src/main.js +++ b/client/src/main.js @@ -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(), @@ -16,6 +17,7 @@ const router = createRouter({ { path: '/orders', component: Orders }, { path: '/demand', component: Demand }, { path: '/spending', component: Spending }, + { path: '/restocking', component: Restocking }, { path: '/reports', component: Reports } ] }) diff --git a/client/src/utils/csvExport.js b/client/src/utils/csvExport.js new file mode 100644 index 000000000..62c0003ba --- /dev/null +++ b/client/src/utils/csvExport.js @@ -0,0 +1,34 @@ +export function exportToCSV(data, filename, headers) { + if (!data || data.length === 0) return + + // Build CSV header row + const headerRow = headers.map(h => `"${h}"`).join(',') + + // Build CSV data rows + const rows = data.map(row => { + return headers.map(header => { + const value = row[header] + // Handle special cases: null, undefined, objects + if (value === null || value === undefined) return '""' + if (typeof value === 'object') return `"${JSON.stringify(value).replace(/"/g, '""')}"` + // Escape quotes in strings + return `"${String(value).replace(/"/g, '""')}"` + }).join(',') + }) + + // Combine header and rows + const csv = [headerRow, ...rows].join('\n') + + // Create blob and download + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }) + const link = document.createElement('a') + const url = URL.createObjectURL(blob) + + link.setAttribute('href', url) + link.setAttribute('download', `${filename}.csv`) + link.style.visibility = 'hidden' + + document.body.appendChild(link) + link.click() + document.body.removeChild(link) +} diff --git a/client/src/views/Backlog.vue b/client/src/views/Backlog.vue index bb3bac21d..8cd8e8b87 100644 --- a/client/src/views/Backlog.vue +++ b/client/src/views/Backlog.vue @@ -1,53 +1,53 @@