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
1 change: 1 addition & 0 deletions client/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions client/src/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions client/src/locales/ja.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ export default {
skus: 'SKU',
searchPlaceholder: '品目名で検索...',
clearSearch: '検索をクリア',
stockStatusFilter: '在庫状況',
stockStatusAll: 'すべて',
stockStatusOutOfStock: '在庫切れ',
totalItems: '総品目数',
totalValue: '総価値',
lowStockItems: '在庫僅少品目',
Expand Down
56 changes: 54 additions & 2 deletions client/src/views/Inventory.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@
</svg>
</button>
</div>
<div class="stock-status-filter">
<label for="stock-status-select" class="visually-hidden">{{ t('inventory.stockStatusFilter') }}</label>
<select
id="stock-status-select"
v-model="stockStatusFilter"
class="stock-status-select"
>
<option value="all">{{ t('inventory.stockStatusAll') }}</option>
<option value="out_of_stock">{{ t('inventory.stockStatusOutOfStock') }}</option>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CONFIRMED] Out-of-stock rows are visually identical to ordinary low-stock rows.

getStockStatusKey/getStockStatusClass (further down in this file) only classify into lowStock/adequate/inStock — there's no outOfStock tier, and the status.* translation catalog (en.js/ja.js) has no outOfStock key either. So filtering to this new 'Out of Stock' option (e.g. SRV-302, quantity_on_hand=0) shows the same red 'Low Stock' badge as any other low-stock item (e.g. quantity_on_hand=5, reorder_point=200), with nothing distinguishing a true stockout from a merely-low item.

Worth adding a 4th tier (e.g. 'outOfStock' when quantity_on_hand === 0) to that classifier so the badge and this filter share one source of truth.

</select>
</div>
</div>
<div class="table-container">
<table>
Expand Down Expand Up @@ -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')

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CONFIRMED] Resetting filters won't clear this filter.

stockStatusFilter is a component-local ref, not part of the shared useFilters() composable. useFilters.js's resetFilters() and hasActiveFilters only cover selectedPeriod/selectedLocation/selectedCategory/selectedStatus, so clicking FilterBar's reset button leaves this stock-status filter untouched and doesn't reflect it as an 'active filter'. A user who filters to Out of Stock and hits 'reset' will still see a filtered list.

Consider adding this filter to useFilters.js (even if page-scoped in usage) so it participates in reset/hasActiveFilters, or wire an explicit reset here.


// Modal state
const showItemModal = ref(false)
Expand Down Expand Up @@ -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
Expand All @@ -166,7 +182,7 @@ export default {
}

// Watch for filter changes and reload data
watch([selectedLocation, selectedCategory], () => {
watch([selectedLocation, selectedCategory, stockStatusFilter], () => {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CONFIRMED] Unnecessary network round-trip for a client-computable filter.

stock_status only excludes items where quantity_on_hand === 0 — a field already present on every item in items.value after the initial load. Bundling stockStatusFilter into this watch triggers a full GET /api/inventory refetch (loading-state flip) on every toggle, even though the existing filteredItems computed already does equivalent client-side filtering (search) + sorting over the same in-memory data at zero network cost. Consider filtering client-side in filteredItems instead of round-tripping to the server.

loadInventory()
})

Expand Down Expand Up @@ -209,6 +225,7 @@ export default {
error,
items,
searchQuery,
stockStatusFilter,
filteredItems,
getStockStatus,
getStockStatusClass,
Expand Down Expand Up @@ -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 {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[PLAUSIBLE] Near-duplicate of FilterBar's .filter-select.

This new .stock-status-select rule shares the same structural intent (border color, focus border-color/box-shadow) as .filter-select in FilterBar.vue, though several concrete values (padding, border-radius, font-size, background) differ. Worth checking whether reusing/extending the existing class would keep this dropdown visually consistent with the other filter selects on the same page.

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;
Expand Down
2 changes: 1 addition & 1 deletion server/data/inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
16 changes: 14 additions & 2 deletions server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CONFIRMED] Unrecognized stock_status values are silently ignored.

Any value other than 'all'/falsy/'out_of_stock' falls through to return items (full unfiltered list, HTTP 200) instead of erroring. stock_status isn't constrained to a Literal/Enum in the endpoint signature, so a typo (e.g. out-of-stock) silently returns everything with no indication the filter wasn't applied.

return [item for item in items if item.get('quantity_on_hand', 0) == 0]

return items

# CORS middleware
app.add_middleware(
CORSMiddleware,
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CONFIRMED] New stock_status param has no test coverage.

server/CLAUDE.md's 'Adding New Endpoints' process (step 6) says 'Write tests in tests/backend/', and 'What to Test' explicitly lists 'Filters work correctly.' tests/backend/test_inventory.py currently has zero references to stock_status or out_of_stock — this new filter path is untested.

):
"""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)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CONFIRMED, currently latent] get_dashboard_summary (elsewhere in this file) doesn't apply this new stock_status filter.

That endpoint filters inventory_items via apply_filters(inventory_items, warehouse, category) but never calls filter_by_stock_status, unlike get_inventory here. Root CLAUDE.md documents 'GET /api/dashboard/summary - All filters', so this is a documented-contract gap — currently unreachable since the Dashboard UI doesn't expose a stock_status filter yet, but will silently diverge from Inventory's filtering if/when it does.


@app.get("/api/inventory/{item_id}", response_model=InventoryItem)
def get_inventory_item(item_id: str):
Expand Down