fix: align frontend InventoryItem type with actual API response

InventoryItemResponse returns flat fields (product_name, barcode, category)
not a nested product object. Frontend interface and templates were using
item.product.name / item.product.brand which threw TypeError on render,
blanking the inventory tab.

- InventoryItem: remove product:Product, add product_name/barcode/category
- InventoryStats: fix total_products→available_items, expired→expired_items,
  items_by_location→locations
- item IDs are int not string — update deleteItem/updateItem/consumeItem sigs
- EditItemModal, RecipesView, InventoryList: fix all item.product.xxx refs
This commit is contained in:
pyr0ball 2026-04-01 17:30:21 -07:00
parent e11f91e14d
commit 828efede42
5 changed files with 26 additions and 25 deletions

View file

@ -10,8 +10,8 @@
<div class="form-group">
<label>Product</label>
<div class="product-info">
<strong>{{ item.product.name }}</strong>
<span v-if="item.product.brand" class="brand">({{ item.product.brand }})</span>
<strong>{{ item.product_name || 'Unknown Product' }}</strong>
<span v-if="item.category" class="brand">{{ item.category }}</span>
</div>
</div>

View file

@ -9,8 +9,8 @@
<div class="stat-label">Total Items</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ stats.total_products }}</div>
<div class="stat-label">Unique Products</div>
<div class="stat-value">{{ stats.available_items }}</div>
<div class="stat-label">Available</div>
</div>
<div class="stat-card expiry-soon">
<div class="stat-value">{{ store.expiringItems.length }}</div>
@ -263,8 +263,8 @@
:class="getItemClass(item)"
>
<div class="item-header">
<h3 class="item-name">{{ item.product.name }}</h3>
<span v-if="item.product.brand" class="item-brand">{{ item.product.brand }}</span>
<h3 class="item-name">{{ item.product_name || 'Unknown Product' }}</h3>
<span v-if="item.category" class="item-brand">{{ item.category }}</span>
</div>
<div class="item-details">
@ -485,11 +485,11 @@ async function handleSave() {
async function confirmDelete(item: InventoryItem) {
showConfirm(
`Are you sure you want to delete ${item.product.name}?`,
`Are you sure you want to delete ${item.product_name || 'item'}?`,
async () => {
try {
await store.deleteItem(item.id)
showToast(`${item.product.name} deleted successfully`, 'success')
showToast(`${item.product_name || 'item'} deleted successfully`, 'success')
} catch (err) {
showToast('Failed to delete item', 'error')
}
@ -504,12 +504,12 @@ async function confirmDelete(item: InventoryItem) {
async function markAsConsumed(item: InventoryItem) {
showConfirm(
`Mark ${item.product.name} as consumed?`,
`Mark ${item.product_name || 'item'} as consumed?`,
async () => {
try {
await inventoryAPI.consumeItem(item.id)
await refreshItems()
showToast(`${item.product.name} marked as consumed`, 'success')
showToast(`${item.product_name || 'item'} marked as consumed`, 'success')
} catch (err) {
showToast('Failed to mark item as consumed', 'error')
}
@ -542,7 +542,7 @@ async function handleScannerGunInput() {
const item = result.results[0]
scannerResults.value.push({
type: 'success',
message: `✓ Added: ${item.product.name}${item.product.brand ? ' (' + item.product.brand + ')' : ''} to ${scannerLocation.value}`,
message: `✓ Added: ${item.product_name || 'item'}${''} to ${scannerLocation.value}`,
})
await refreshItems()
} else {
@ -588,7 +588,7 @@ async function handleBarcodeImageSelect(e: Event) {
const item = result.results[0]
barcodeResults.value.push({
type: 'success',
message: `✓ Found: ${item.product.name}${item.product.brand ? ' (' + item.product.brand + ')' : ''}`,
message: `✓ Found: ${item.product_name || 'item'}${''}`,
})
await refreshItems()
} else {

View file

@ -291,7 +291,7 @@ const pantryItems = computed(() => {
if (!b.expiration_date) return -1
return new Date(a.expiration_date).getTime() - new Date(b.expiration_date).getTime()
})
return sorted.map((item) => item.product.name)
return sorted.map((item) => item.product_name).filter(Boolean) as string[]
})
// Grocery links relevant to a specific recipe's missing ingredients

View file

@ -80,9 +80,11 @@ export interface Tag {
}
export interface InventoryItem {
id: string
product_id: string
product: Product
id: number
product_id: number
product_name: string | null
barcode: string | null
category: string | null
quantity: number
unit: string
location: string
@ -109,11 +111,10 @@ export interface InventoryItemUpdate {
export interface InventoryStats {
total_items: number
total_products: number
available_items: number
expiring_soon: number
expired: number
items_by_location: Record<string, number>
items_by_status: Record<string, number>
expired_items: number
locations: Record<string, number>
}
export interface Receipt {
@ -185,7 +186,7 @@ export const inventoryAPI = {
/**
* Update an inventory item
*/
async updateItem(itemId: string, update: InventoryItemUpdate): Promise<InventoryItem> {
async updateItem(itemId: number, update: InventoryItemUpdate): Promise<InventoryItem> {
const response = await api.patch(`/inventory/items/${itemId}`, update)
return response.data
},
@ -193,7 +194,7 @@ export const inventoryAPI = {
/**
* Delete an inventory item
*/
async deleteItem(itemId: string): Promise<void> {
async deleteItem(itemId: number): Promise<void> {
await api.delete(`/inventory/items/${itemId}`)
},
@ -234,7 +235,7 @@ export const inventoryAPI = {
/**
* Mark item as consumed
*/
async consumeItem(itemId: string): Promise<void> {
async consumeItem(itemId: number): Promise<void> {
await api.post(`/inventory/items/${itemId}/consume`)
},

View file

@ -76,7 +76,7 @@ export const useInventoryStore = defineStore('inventory', () => {
}
}
async function updateItem(itemId: string, update: InventoryItemUpdate) {
async function updateItem(itemId: number, update: InventoryItemUpdate) {
loading.value = true
error.value = null
@ -99,7 +99,7 @@ export const useInventoryStore = defineStore('inventory', () => {
}
}
async function deleteItem(itemId: string) {
async function deleteItem(itemId: number) {
loading.value = true
error.value = null