diff --git a/client/src/api.js b/client/src/api.js index 11cb9db70..1d374309f 100644 --- a/client/src/api.js +++ b/client/src/api.js @@ -7,6 +7,7 @@ export const api = { const params = new URLSearchParams() if (filters.warehouse && filters.warehouse !== 'all') params.append('warehouse', filters.warehouse) if (filters.category && filters.category !== 'all') params.append('category', filters.category) + if (filters.stock_status && filters.stock_status !== 'all') params.append('stock_status', filters.stock_status) const response = await axios.get(`${API_BASE_URL}/inventory?${params.toString()}`) return response.data diff --git a/client/src/locales/en.js b/client/src/locales/en.js index 03a58fe6e..346590003 100644 --- a/client/src/locales/en.js +++ b/client/src/locales/en.js @@ -80,6 +80,9 @@ export default { skus: 'SKUs', searchPlaceholder: 'Search by item name...', clearSearch: 'Clear search', + stockStatusFilter: 'Stock Status', + stockStatusAll: 'All', + stockStatusOutOfStock: 'Out of Stock', totalItems: 'Total Items', totalValue: 'Total Value', lowStockItems: 'Low Stock Items', diff --git a/client/src/locales/ja.js b/client/src/locales/ja.js index db33223ac..e24c0b0a1 100644 --- a/client/src/locales/ja.js +++ b/client/src/locales/ja.js @@ -80,6 +80,9 @@ export default { skus: 'SKU', searchPlaceholder: '品目名で検索...', clearSearch: '検索をクリア', + stockStatusFilter: '在庫状況', + stockStatusAll: 'すべて', + stockStatusOutOfStock: '在庫切れ', totalItems: '総品目数', totalValue: '総価値', lowStockItems: '在庫僅少品目', diff --git a/client/src/views/Inventory.vue b/client/src/views/Inventory.vue index 4819326eb..0a70e14f2 100644 --- a/client/src/views/Inventory.vue +++ b/client/src/views/Inventory.vue @@ -32,6 +32,17 @@ +
+ + +
@@ -106,6 +117,10 @@ export default { const error = ref(null) const items = ref([]) const searchQuery = ref('') + // Local filter for stock status - scoped to this component only. + // The global useFilters `selectedStatus` is for Order Status; inventory + // intentionally does not use it (see comment below in loadInventory). + const stockStatusFilter = ref('all') // Modal state const showItemModal = ref(false) @@ -156,7 +171,8 @@ export default { // Inventory doesn't support month/status filters, only warehouse and category items.value = await api.getInventory({ warehouse: filters.warehouse, - category: filters.category + category: filters.category, + stock_status: stockStatusFilter.value }) } catch (err) { error.value = 'Failed to load inventory: ' + err.message @@ -166,7 +182,7 @@ export default { } // Watch for filter changes and reload data - watch([selectedLocation, selectedCategory], () => { + watch([selectedLocation, selectedCategory, stockStatusFilter], () => { loadInventory() }) @@ -209,6 +225,7 @@ export default { error, items, searchQuery, + stockStatusFilter, filteredItems, getStockStatus, getStockStatusClass, @@ -317,6 +334,41 @@ export default { height: 18px; } +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.stock-status-filter { + display: flex; + align-items: center; +} + +.stock-status-select { + padding: 0.5rem 0.75rem; + border: 1px solid #cbd5e1; + border-radius: 8px; + font-size: 0.875rem; + color: #0f172a; + background: #f8fafc; + transition: all 0.2s; + cursor: pointer; +} + +.stock-status-select:focus { + outline: none; + border-color: #3b82f6; + background: white; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + .loading, .error { padding: 2rem; diff --git a/server/data/inventory.json b/server/data/inventory.json index 486fda819..9deeda74f 100644 --- a/server/data/inventory.json +++ b/server/data/inventory.json @@ -113,7 +113,7 @@ "name": "Standard Servo Motor", "category": "Actuators", "warehouse": "Tokyo", - "quantity_on_hand": 28, + "quantity_on_hand": 0, "reorder_point": 30, "unit_cost": 725.0, "location": "Warehouse C-03", diff --git a/server/main.py b/server/main.py index a0c2d8c5a..efad7d80a 100644 --- a/server/main.py +++ b/server/main.py @@ -46,6 +46,16 @@ def apply_filters(items: list, warehouse: Optional[str] = None, category: Option return filtered +def filter_by_stock_status(items: list, stock_status: Optional[str] = None) -> list: + """Filter inventory items by stock status derived from quantity_on_hand""" + if not stock_status or stock_status == 'all': + return items + + if stock_status == 'out_of_stock': + return [item for item in items if item.get('quantity_on_hand', 0) == 0] + + return items + # CORS middleware app.add_middleware( CORSMiddleware, @@ -128,10 +138,12 @@ def root(): @app.get("/api/inventory", response_model=List[InventoryItem]) def get_inventory( warehouse: Optional[str] = None, - category: Optional[str] = None + category: Optional[str] = None, + stock_status: Optional[str] = None ): """Get all inventory items with optional filtering""" - return apply_filters(inventory_items, warehouse, category) + filtered = apply_filters(inventory_items, warehouse, category) + return filter_by_stock_status(filtered, stock_status) @app.get("/api/inventory/{item_id}", response_model=InventoryItem) def get_inventory_item(item_id: str):