diff --git a/backend/innercontext/api/products.py b/backend/innercontext/api/products.py index 86b5979..25a1c82 100644 --- a/backend/innercontext/api/products.py +++ b/backend/innercontext/api/products.py @@ -101,6 +101,7 @@ class InventoryCreate(SQLModel): finished_at: Optional[date] = None expiry_date: Optional[date] = None current_weight_g: Optional[float] = None + last_weighed_at: Optional[date] = None notes: Optional[str] = None @@ -110,6 +111,7 @@ class InventoryUpdate(SQLModel): finished_at: Optional[date] = None expiry_date: Optional[date] = None current_weight_g: Optional[float] = None + last_weighed_at: Optional[date] = None notes: Optional[str] = None diff --git a/backend/innercontext/models/product.py b/backend/innercontext/models/product.py index 42a1755..9659826 100644 --- a/backend/innercontext/models/product.py +++ b/backend/innercontext/models/product.py @@ -103,6 +103,8 @@ class ProductBase(SQLModel): price_tier: PriceTier | None = None size_ml: float | None = Field(default=None, gt=0) + full_weight_g: float | None = Field(default=None, gt=0) + empty_weight_g: float | None = Field(default=None, gt=0) pao_months: int | None = Field(default=None, ge=1, le=60) inci: list[str] = Field(default_factory=list) @@ -383,6 +385,7 @@ class ProductInventory(SQLModel, table=True): finished_at: date | None = Field(default=None) expiry_date: date | None = Field(default=None) current_weight_g: float | None = Field(default=None, gt=0) + last_weighed_at: date | None = Field(default=None) notes: str | None = None created_at: datetime = Field(default_factory=utc_now, nullable=False) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 16e259b..5492162 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -69,6 +69,9 @@ export const createInventory = ( productId: string, body: Record ): Promise => api.post(`/products/${productId}/inventory`, body); +export const updateInventory = (id: string, body: Record): Promise => + api.patch(`/inventory/${id}`, body); +export const deleteInventory = (id: string): Promise => api.del(`/inventory/${id}`); // ─── Routines ──────────────────────────────────────────────────────────────── diff --git a/frontend/src/lib/components/ProductForm.svelte b/frontend/src/lib/components/ProductForm.svelte index 031b3b0..ab17872 100644 --- a/frontend/src/lib/components/ProductForm.svelte +++ b/frontend/src/lib/components/ProductForm.svelte @@ -668,6 +668,32 @@ /> +
+ + +
+ +
+ + +
+
= { is_opened: form.get('is_opened') === 'true' }; + const openedAt = form.get('opened_at'); + if (openedAt) body.opened_at = openedAt; + const finishedAt = form.get('finished_at'); + if (finishedAt) body.finished_at = finishedAt; const expiry = form.get('expiry_date'); if (expiry) body.expiry_date = expiry; const weight = form.get('current_weight_g'); if (weight) body.current_weight_g = Number(weight); + const lastWeighed = form.get('last_weighed_at'); + if (lastWeighed) body.last_weighed_at = lastWeighed; const notes = form.get('notes'); if (notes) body.notes = notes; try { @@ -211,5 +226,44 @@ export const actions: Actions = { } catch (e) { return fail(500, { error: (e as Error).message }); } + }, + + updateInventory: async ({ request }) => { + const form = await request.formData(); + const inventoryId = form.get('inventory_id') as string; + if (!inventoryId) return fail(400, { error: 'Missing inventory_id' }); + const body: Record = { + is_opened: form.get('is_opened') === 'true' + }; + const openedAt = form.get('opened_at'); + body.opened_at = openedAt || null; + const finishedAt = form.get('finished_at'); + body.finished_at = finishedAt || null; + const expiry = form.get('expiry_date'); + body.expiry_date = expiry || null; + const weight = form.get('current_weight_g'); + body.current_weight_g = weight ? Number(weight) : null; + const lastWeighed = form.get('last_weighed_at'); + body.last_weighed_at = lastWeighed || null; + const notes = form.get('notes'); + body.notes = notes || null; + try { + await apiUpdateInventory(inventoryId, body); + return { inventoryUpdated: true }; + } catch (e) { + return fail(500, { error: (e as Error).message }); + } + }, + + deleteInventory: async ({ request }) => { + const form = await request.formData(); + const inventoryId = form.get('inventory_id') as string; + if (!inventoryId) return fail(400, { error: 'Missing inventory_id' }); + try { + await apiDeleteInventory(inventoryId); + return { inventoryDeleted: true }; + } catch (e) { + return fail(500, { error: (e as Error).message }); + } } }; diff --git a/frontend/src/routes/products/[id]/+page.svelte b/frontend/src/routes/products/[id]/+page.svelte index 9379a4b..0843bc5 100644 --- a/frontend/src/routes/products/[id]/+page.svelte +++ b/frontend/src/routes/products/[id]/+page.svelte @@ -13,6 +13,7 @@ let { product } = $derived(data); let showInventoryForm = $state(false); + let editingInventoryId = $state(null); {product.name} — innercontext @@ -54,11 +55,29 @@ {#if form?.inventoryAdded}
Package added.
{/if} + {#if form?.inventoryUpdated} +
Package updated.
+ {/if} + {#if form?.inventoryDeleted} +
Package deleted.
+ {/if} {#if showInventoryForm}
+
+ + +
+
+ + +
+
+ + +
@@ -67,6 +86,10 @@
+
+ + +
@@ -74,7 +97,6 @@
- @@ -82,21 +104,141 @@ {#if product.inventory.length}
- {#each product.inventory as pkg} -
-
- - {pkg.is_opened ? 'Open' : 'Sealed'} - - {#if pkg.expiry_date} - Exp: {pkg.expiry_date} - {/if} - {#if pkg.current_weight_g} - {pkg.current_weight_g}g remaining - {/if} + {#each product.inventory as pkg (pkg.id)} +
+
+
+ + {pkg.is_opened ? 'Open' : 'Sealed'} + + {#if pkg.finished_at} + Finished + {/if} + {#if pkg.expiry_date} + Exp: {pkg.expiry_date.slice(0, 10)} + {/if} + {#if pkg.opened_at} + Opened: {pkg.opened_at.slice(0, 10)} + {/if} + {#if pkg.finished_at} + Finished: {pkg.finished_at.slice(0, 10)} + {/if} + {#if pkg.current_weight_g} + {pkg.current_weight_g}g remaining + {/if} + {#if pkg.last_weighed_at} + Weighed: {pkg.last_weighed_at.slice(0, 10)} + {/if} + {#if pkg.notes} + {pkg.notes} + {/if} +
+
+ +
{ if (!confirm('Delete this package?')) e.preventDefault(); }} + > + + +
+
- {#if pkg.notes} - {pkg.notes} + + {#if editingInventoryId === pkg.id} +
+
{ + return async ({ result, update }) => { + await update(); + if (result.type === 'success') editingInventoryId = null; + }; + }} + class="grid grid-cols-2 gap-4" + > + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
{/if}
{/each} diff --git a/frontend/src/routes/products/new/+page.server.ts b/frontend/src/routes/products/new/+page.server.ts index e61017e..094b0a8 100644 --- a/frontend/src/routes/products/new/+page.server.ts +++ b/frontend/src/routes/products/new/+page.server.ts @@ -122,6 +122,12 @@ export const actions: Actions = { const size_ml = parseOptionalFloat(form.get('size_ml') as string | null); if (size_ml !== undefined) payload.size_ml = size_ml; + const full_weight_g = parseOptionalFloat(form.get('full_weight_g') as string | null); + if (full_weight_g !== undefined) payload.full_weight_g = full_weight_g; + + const empty_weight_g = parseOptionalFloat(form.get('empty_weight_g') as string | null); + if (empty_weight_g !== undefined) payload.empty_weight_g = empty_weight_g; + const pao_months = parseOptionalInt(form.get('pao_months') as string | null); if (pao_months !== undefined) payload.pao_months = pao_months;