feat(products): improve replenishment-aware shopping suggestions
Replace product weight and repurchase intent fields with per-package remaining levels and inventory-first restock signals. Enrich shopping suggestions with usage-aware replenishment scoring so the frontend and LLM can prioritize real gaps and near-empty staples more reliably.
This commit is contained in:
parent
bb5d402c15
commit
d91d06455b
18 changed files with 587 additions and 210 deletions
|
|
@ -95,8 +95,11 @@
|
|||
"inventory_openedDate": "Opened date",
|
||||
"inventory_finishedDate": "Finished date",
|
||||
"inventory_expiryDate": "Expiry date",
|
||||
"inventory_currentWeight": "Current weight (g)",
|
||||
"inventory_lastWeighed": "Last weighed",
|
||||
"inventory_remainingLevel": "Remaining product level",
|
||||
"inventory_remainingHigh": "high",
|
||||
"inventory_remainingMedium": "medium",
|
||||
"inventory_remainingLow": "low",
|
||||
"inventory_remainingNearlyEmpty": "nearly empty",
|
||||
"inventory_notes": "Notes",
|
||||
"inventory_badgeOpen": "Open",
|
||||
"inventory_badgeSealed": "Sealed",
|
||||
|
|
@ -104,8 +107,6 @@
|
|||
"inventory_exp": "Exp:",
|
||||
"inventory_opened": "Opened:",
|
||||
"inventory_finished": "Finished:",
|
||||
"inventory_remaining": "g remaining",
|
||||
"inventory_weighed": "Weighed:",
|
||||
"inventory_confirmDelete": "Delete this package?",
|
||||
|
||||
"routines_title": "Routines",
|
||||
|
|
@ -514,7 +515,6 @@
|
|||
"productForm_isTool": "Is tool (e.g. dermaroller)",
|
||||
"productForm_needleLengthMm": "Needle length (mm, tools only)",
|
||||
"productForm_personalNotes": "Personal notes",
|
||||
"productForm_repurchaseIntent": "Repurchase intent",
|
||||
"productForm_toleranceNotes": "Tolerance notes",
|
||||
"productForm_toleranceNotesPlaceholder": "e.g. Causes mild stinging, fine after 2 weeks",
|
||||
|
||||
|
|
|
|||
|
|
@ -97,8 +97,11 @@
|
|||
"inventory_openedDate": "Data otwarcia",
|
||||
"inventory_finishedDate": "Data skończenia",
|
||||
"inventory_expiryDate": "Data ważności",
|
||||
"inventory_currentWeight": "Aktualna waga (g)",
|
||||
"inventory_lastWeighed": "Ostatnie ważenie",
|
||||
"inventory_remainingLevel": "Poziom pozostałego produktu:",
|
||||
"inventory_remainingHigh": "dużo",
|
||||
"inventory_remainingMedium": "średnio",
|
||||
"inventory_remainingLow": "mało",
|
||||
"inventory_remainingNearlyEmpty": "prawie puste",
|
||||
"inventory_notes": "Notatki",
|
||||
"inventory_badgeOpen": "Otwarte",
|
||||
"inventory_badgeSealed": "Zamknięte",
|
||||
|
|
@ -106,8 +109,6 @@
|
|||
"inventory_exp": "Wazność:",
|
||||
"inventory_opened": "Otwarto:",
|
||||
"inventory_finished": "Skończono:",
|
||||
"inventory_remaining": "g pozostało",
|
||||
"inventory_weighed": "Ważono:",
|
||||
"inventory_confirmDelete": "Usunąć to opakowanie?",
|
||||
|
||||
"routines_title": "Rutyny",
|
||||
|
|
@ -528,7 +529,6 @@
|
|||
"productForm_isTool": "To narzędzie (np. dermaroller)",
|
||||
"productForm_needleLengthMm": "Długość igły (mm, tylko narzędzia)",
|
||||
"productForm_personalNotes": "Notatki osobiste",
|
||||
"productForm_repurchaseIntent": "Zamiar ponownego zakupu",
|
||||
"productForm_toleranceNotes": "Notatki o tolerancji",
|
||||
"productForm_toleranceNotesPlaceholder": "np. Lekkie pieczenie, ustępuje po 2 tygodniach",
|
||||
|
||||
|
|
|
|||
|
|
@ -135,8 +135,6 @@ export interface ProductParseResponse {
|
|||
price_amount?: number;
|
||||
price_currency?: string;
|
||||
size_ml?: number;
|
||||
full_weight_g?: number;
|
||||
empty_weight_g?: number;
|
||||
pao_months?: number;
|
||||
inci?: string[];
|
||||
actives?: ActiveIngredient[];
|
||||
|
|
|
|||
|
|
@ -165,8 +165,6 @@
|
|||
let sku = $state(untrack(() => product?.sku ?? ''));
|
||||
let barcode = $state(untrack(() => product?.barcode ?? ''));
|
||||
let sizeMl = $state(untrack(() => (product?.size_ml != null ? String(product.size_ml) : '')));
|
||||
let fullWeightG = $state(untrack(() => (product?.full_weight_g != null ? String(product.full_weight_g) : '')));
|
||||
let emptyWeightG = $state(untrack(() => (product?.empty_weight_g != null ? String(product.empty_weight_g) : '')));
|
||||
let paoMonths = $state(untrack(() => (product?.pao_months != null ? String(product.pao_months) : '')));
|
||||
let phMin = $state(untrack(() => (product?.ph_min != null ? String(product.ph_min) : '')));
|
||||
let phMax = $state(untrack(() => (product?.ph_max != null ? String(product.ph_max) : '')));
|
||||
|
|
@ -221,8 +219,6 @@
|
|||
if (r.price_currency) priceCurrency = r.price_currency;
|
||||
if (r.leave_on != null) leaveOn = String(r.leave_on);
|
||||
if (r.size_ml != null) sizeMl = String(r.size_ml);
|
||||
if (r.full_weight_g != null) fullWeightG = String(r.full_weight_g);
|
||||
if (r.empty_weight_g != null) emptyWeightG = String(r.empty_weight_g);
|
||||
if (r.pao_months != null) paoMonths = String(r.pao_months);
|
||||
if (r.ph_min != null) phMin = String(r.ph_min);
|
||||
if (r.ph_max != null) phMax = String(r.ph_max);
|
||||
|
|
@ -281,9 +277,6 @@
|
|||
let pregnancySafe = $state(
|
||||
untrack(() => (product?.pregnancy_safe != null ? String(product.pregnancy_safe) : ''))
|
||||
);
|
||||
let personalRepurchaseIntent = $state(
|
||||
untrack(() => (product?.personal_repurchase_intent != null ? String(product.personal_repurchase_intent) : ''))
|
||||
);
|
||||
|
||||
// context rules tristate
|
||||
const cr = untrack(() => product?.context_rules);
|
||||
|
|
@ -403,8 +396,6 @@
|
|||
sku,
|
||||
barcode,
|
||||
sizeMl,
|
||||
fullWeightG,
|
||||
emptyWeightG,
|
||||
paoMonths,
|
||||
phMin,
|
||||
phMax,
|
||||
|
|
@ -428,7 +419,6 @@
|
|||
essentialOilsFree,
|
||||
alcoholDenatFree,
|
||||
pregnancySafe,
|
||||
personalRepurchaseIntent,
|
||||
ctxAfterShaving,
|
||||
ctxAfterAcids,
|
||||
ctxAfterRetinoids,
|
||||
|
|
@ -723,8 +713,6 @@
|
|||
bind:priceAmount
|
||||
bind:priceCurrency
|
||||
bind:sizeMl
|
||||
bind:fullWeightG
|
||||
bind:emptyWeightG
|
||||
bind:paoMonths
|
||||
bind:phMin
|
||||
bind:phMax
|
||||
|
|
@ -743,10 +731,7 @@
|
|||
{@const NotesSection = mod.default}
|
||||
<NotesSection
|
||||
visible={editSection === 'notes'}
|
||||
{selectClass}
|
||||
{textareaClass}
|
||||
{tristate}
|
||||
bind:personalRepurchaseIntent
|
||||
bind:personalToleranceNotes
|
||||
/>
|
||||
{/await}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@
|
|||
priceAmount = $bindable(''),
|
||||
priceCurrency = $bindable('PLN'),
|
||||
sizeMl = $bindable(''),
|
||||
fullWeightG = $bindable(''),
|
||||
emptyWeightG = $bindable(''),
|
||||
paoMonths = $bindable(''),
|
||||
phMin = $bindable(''),
|
||||
phMax = $bindable(''),
|
||||
|
|
@ -27,8 +25,6 @@
|
|||
priceAmount?: string;
|
||||
priceCurrency?: string;
|
||||
sizeMl?: string;
|
||||
fullWeightG?: string;
|
||||
emptyWeightG?: string;
|
||||
paoMonths?: string;
|
||||
phMin?: string;
|
||||
phMax?: string;
|
||||
|
|
@ -63,16 +59,6 @@
|
|||
<Input id="size_ml" name="size_ml" type="number" min="0" step="0.1" placeholder={m["productForm_sizePlaceholder"]()} bind:value={sizeMl} />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="full_weight_g">{m["productForm_fullWeightG"]()}</Label>
|
||||
<Input id="full_weight_g" name="full_weight_g" type="number" min="0" step="0.1" placeholder={m["productForm_fullWeightPlaceholder"]()} bind:value={fullWeightG} />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="empty_weight_g">{m["productForm_emptyWeightG"]()}</Label>
|
||||
<Input id="empty_weight_g" name="empty_weight_g" type="number" min="0" step="0.1" placeholder={m["productForm_emptyWeightPlaceholder"]()} bind:value={emptyWeightG} />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="pao_months">{m["productForm_paoMonths"]()}</Label>
|
||||
<Input id="pao_months" name="pao_months" type="number" min="1" max="60" placeholder={m["productForm_paoPlaceholder"]()} bind:value={paoMonths} />
|
||||
|
|
|
|||
|
|
@ -3,21 +3,13 @@
|
|||
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
|
||||
type TriOption = { value: string; label: string };
|
||||
|
||||
let {
|
||||
visible = false,
|
||||
selectClass,
|
||||
textareaClass,
|
||||
tristate,
|
||||
personalRepurchaseIntent = $bindable(''),
|
||||
personalToleranceNotes = $bindable('')
|
||||
}: {
|
||||
visible?: boolean;
|
||||
selectClass: string;
|
||||
textareaClass: string;
|
||||
tristate: TriOption[];
|
||||
personalRepurchaseIntent?: string;
|
||||
personalToleranceNotes?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
|
@ -25,15 +17,6 @@
|
|||
<Card class={visible ? '' : 'hidden'}>
|
||||
<CardHeader><CardTitle>{m["productForm_personalNotes"]()}</CardTitle></CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="repurchase_intent_select">{m["productForm_repurchaseIntent"]()}</Label>
|
||||
<select id="repurchase_intent_select" name="personal_repurchase_intent" class={selectClass} bind:value={personalRepurchaseIntent}>
|
||||
{#each tristate as opt (opt.value)}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="personal_tolerance_notes">{m["productForm_toleranceNotes"]()}</Label>
|
||||
<textarea
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export type OverallSkinState = "excellent" | "good" | "fair" | "poor";
|
|||
export type PartOfDay = "am" | "pm";
|
||||
export type PriceTier = "budget" | "mid" | "premium" | "luxury";
|
||||
export type PriceTierSource = "category" | "fallback" | "insufficient_data";
|
||||
export type RemainingLevel = "high" | "medium" | "low" | "nearly_empty";
|
||||
export type ProductCategory =
|
||||
| "cleanser"
|
||||
| "toner"
|
||||
|
|
@ -129,8 +130,7 @@ export interface ProductInventory {
|
|||
opened_at?: string;
|
||||
finished_at?: string;
|
||||
expiry_date?: string;
|
||||
current_weight_g?: number;
|
||||
last_weighed_at?: string;
|
||||
remaining_level?: RemainingLevel;
|
||||
notes?: string;
|
||||
created_at: string;
|
||||
product?: Product;
|
||||
|
|
@ -155,8 +155,6 @@ export interface Product {
|
|||
price_per_use_pln?: number;
|
||||
price_tier_source?: PriceTierSource;
|
||||
size_ml?: number;
|
||||
full_weight_g?: number;
|
||||
empty_weight_g?: number;
|
||||
pao_months?: number;
|
||||
inci: string[];
|
||||
actives?: ActiveIngredient[];
|
||||
|
|
@ -176,7 +174,6 @@ export interface Product {
|
|||
is_tool: boolean;
|
||||
needle_length_mm?: number;
|
||||
personal_tolerance_notes?: string;
|
||||
personal_repurchase_intent?: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
inventory: ProductInventory[];
|
||||
|
|
|
|||
|
|
@ -127,8 +127,6 @@ export const actions: Actions = {
|
|||
|
||||
// Optional numbers
|
||||
body.size_ml = parseOptionalFloat(form.get('size_ml') as string | null) ?? null;
|
||||
body.full_weight_g = parseOptionalFloat(form.get('full_weight_g') as string | null) ?? null;
|
||||
body.empty_weight_g = parseOptionalFloat(form.get('empty_weight_g') as string | null) ?? null;
|
||||
body.pao_months = parseOptionalInt(form.get('pao_months') as string | null) ?? null;
|
||||
body.ph_min = parseOptionalFloat(form.get('ph_min') as string | null) ?? null;
|
||||
body.ph_max = parseOptionalFloat(form.get('ph_max') as string | null) ?? null;
|
||||
|
|
@ -144,7 +142,7 @@ export const actions: Actions = {
|
|||
body.is_tool = form.get('is_tool') === 'true';
|
||||
|
||||
// Nullable booleans
|
||||
for (const field of ['fragrance_free', 'essential_oils_free', 'alcohol_denat_free', 'pregnancy_safe', 'personal_repurchase_intent']) {
|
||||
for (const field of ['fragrance_free', 'essential_oils_free', 'alcohol_denat_free', 'pregnancy_safe']) {
|
||||
body[field] = parseTristate(form.get(field) as string | null) ?? null;
|
||||
}
|
||||
|
||||
|
|
@ -192,10 +190,8 @@ export const actions: Actions = {
|
|||
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 remainingLevel = form.get('remaining_level');
|
||||
if (remainingLevel) body.remaining_level = remainingLevel;
|
||||
const notes = form.get('notes');
|
||||
if (notes) body.notes = notes;
|
||||
try {
|
||||
|
|
@ -219,10 +215,8 @@ export const actions: Actions = {
|
|||
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 remainingLevel = form.get('remaining_level');
|
||||
body.remaining_level = remainingLevel || null;
|
||||
const notes = form.get('notes');
|
||||
body.notes = notes || null;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { enhance } from '$app/forms';
|
||||
import { resolve } from '$app/paths';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import SimpleSelect from '$lib/components/forms/SimpleSelect.svelte';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
|
|
@ -15,8 +16,16 @@
|
|||
let { data, form }: { data: PageData; form: ActionData } = $props();
|
||||
let { product } = $derived(data);
|
||||
|
||||
const remainingLevels = ['high', 'medium', 'low', 'nearly_empty'] as const;
|
||||
const remainingLevelOptions = remainingLevels.map((level) => ({
|
||||
value: level,
|
||||
label: remainingLevelLabel(level)
|
||||
}));
|
||||
|
||||
let showInventoryForm = $state(false);
|
||||
let addInventoryOpened = $state(false);
|
||||
let editingInventoryId = $state<string | null>(null);
|
||||
let editingInventoryOpened = $state<Record<string, boolean>>({});
|
||||
let activeTab = $state<'inventory' | 'edit'>('edit');
|
||||
let isEditDirty = $state(false);
|
||||
let editSaveVersion = $state(0);
|
||||
|
|
@ -60,6 +69,14 @@
|
|||
if (value === 'luxury') return m['productForm_priceLuxury']();
|
||||
return value;
|
||||
}
|
||||
|
||||
function remainingLevelLabel(value?: string): string {
|
||||
if (value === 'high') return m['inventory_remainingHigh']();
|
||||
if (value === 'medium') return m['inventory_remainingMedium']();
|
||||
if (value === 'low') return m['inventory_remainingLow']();
|
||||
if (value === 'nearly_empty') return m['inventory_remainingNearlyEmpty']();
|
||||
return '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head><title>{product.name} — innercontext</title></svelte:head>
|
||||
|
|
@ -147,41 +164,43 @@
|
|||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-base">{m["inventory_addPackage"]()}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form method="POST" action="?/addInventory" use:enhance class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class="sm:col-span-2 flex items-center gap-2">
|
||||
<input type="checkbox" id="add_is_opened" name="is_opened" value="true" class="h-4 w-4" />
|
||||
<Label for="add_is_opened">{m["inventory_alreadyOpened"]()}</Label>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="add_opened_at">{m["inventory_openedDate"]()}</Label>
|
||||
<Input id="add_opened_at" name="opened_at" type="date" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="add_finished_at">{m["inventory_finishedDate"]()}</Label>
|
||||
<Input id="add_finished_at" name="finished_at" type="date" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="add_expiry_date">{m["inventory_expiryDate"]()}</Label>
|
||||
<Input id="add_expiry_date" name="expiry_date" type="date" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="add_current_weight_g">{m["inventory_currentWeight"]()}</Label>
|
||||
<Input id="add_current_weight_g" name="current_weight_g" type="number" min="0" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="add_last_weighed_at">{m["inventory_lastWeighed"]()}</Label>
|
||||
<Input id="add_last_weighed_at" name="last_weighed_at" type="date" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="add_notes">{m.inventory_notes()}</Label>
|
||||
<Input id="add_notes" name="notes" />
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<Button type="submit" size="sm">{m.common_add()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<form method="POST" action="?/addInventory" use:enhance class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class="sm:col-span-2 flex items-center gap-2">
|
||||
<input type="checkbox" id="add_is_opened" name="is_opened" value="true" class="h-4 w-4" bind:checked={addInventoryOpened} />
|
||||
<Label for="add_is_opened">{m["inventory_alreadyOpened"]()}</Label>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<Label for="add_opened_at">{m["inventory_openedDate"]()}</Label>
|
||||
<Input id="add_opened_at" name="opened_at" type="date" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="add_finished_at">{m["inventory_finishedDate"]()}</Label>
|
||||
<Input id="add_finished_at" name="finished_at" type="date" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="add_expiry_date">{m["inventory_expiryDate"]()}</Label>
|
||||
<Input id="add_expiry_date" name="expiry_date" type="date" />
|
||||
</div>
|
||||
{#if addInventoryOpened}
|
||||
<SimpleSelect
|
||||
id="add_remaining_level"
|
||||
name="remaining_level"
|
||||
label={m["inventory_remainingLevel"]()}
|
||||
options={remainingLevelOptions}
|
||||
placeholder={m.common_unknown()}
|
||||
/>
|
||||
{/if}
|
||||
<div class="space-y-1">
|
||||
<Label for="add_notes">{m.inventory_notes()}</Label>
|
||||
<Input id="add_notes" name="notes" />
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<Button type="submit" size="sm">{m.common_add()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
|
|
@ -206,11 +225,8 @@
|
|||
{#if pkg.finished_at}
|
||||
<span class="text-muted-foreground">{m.inventory_finished()} {pkg.finished_at.slice(0, 10)}</span>
|
||||
{/if}
|
||||
{#if pkg.current_weight_g}
|
||||
<span class="text-muted-foreground">{pkg.current_weight_g}g {m.inventory_remaining()}</span>
|
||||
{/if}
|
||||
{#if pkg.last_weighed_at}
|
||||
<span class="text-muted-foreground">{m.inventory_weighed()} {pkg.last_weighed_at.slice(0, 10)}</span>
|
||||
{#if pkg.is_opened && pkg.remaining_level}
|
||||
<span class="text-muted-foreground">{m["inventory_remainingLevel"]()} {remainingLevelLabel(pkg.remaining_level)}</span>
|
||||
{/if}
|
||||
{#if pkg.notes}
|
||||
<span class="text-muted-foreground">{pkg.notes}</span>
|
||||
|
|
@ -220,7 +236,14 @@
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onclick={() => (editingInventoryId = editingInventoryId === pkg.id ? null : pkg.id)}
|
||||
onclick={() => {
|
||||
if (editingInventoryId === pkg.id) {
|
||||
editingInventoryId = null;
|
||||
} else {
|
||||
editingInventoryId = pkg.id;
|
||||
editingInventoryOpened = { ...editingInventoryOpened, [pkg.id]: pkg.is_opened };
|
||||
}
|
||||
}}
|
||||
>
|
||||
{editingInventoryId === pkg.id ? m.common_cancel() : m.common_edit()}
|
||||
</Button>
|
||||
|
|
@ -256,7 +279,11 @@
|
|||
id="edit_is_opened_{pkg.id}"
|
||||
name="is_opened"
|
||||
value="true"
|
||||
checked={pkg.is_opened}
|
||||
checked={editingInventoryOpened[pkg.id] ?? pkg.is_opened}
|
||||
onchange={(e) => {
|
||||
const target = e.currentTarget as HTMLInputElement;
|
||||
editingInventoryOpened = { ...editingInventoryOpened, [pkg.id]: target.checked };
|
||||
}}
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<Label for="edit_is_opened_{pkg.id}">{m["inventory_alreadyOpened"]()}</Label>
|
||||
|
|
@ -273,14 +300,16 @@
|
|||
<Label for="edit_expiry_{pkg.id}">{m["inventory_expiryDate"]()}</Label>
|
||||
<Input id="edit_expiry_{pkg.id}" name="expiry_date" type="date" value={pkg.expiry_date?.slice(0, 10) ?? ''} />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="edit_weight_{pkg.id}">{m["inventory_currentWeight"]()}</Label>
|
||||
<Input id="edit_weight_{pkg.id}" name="current_weight_g" type="number" min="0" value={pkg.current_weight_g ?? ''} />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="edit_last_weighed_{pkg.id}">{m["inventory_lastWeighed"]()}</Label>
|
||||
<Input id="edit_last_weighed_{pkg.id}" name="last_weighed_at" type="date" value={pkg.last_weighed_at?.slice(0, 10) ?? ''} />
|
||||
</div>
|
||||
{#if editingInventoryOpened[pkg.id] ?? pkg.is_opened}
|
||||
<SimpleSelect
|
||||
id={`edit_remaining_level_${pkg.id}`}
|
||||
name="remaining_level"
|
||||
label={m["inventory_remainingLevel"]()}
|
||||
options={remainingLevelOptions}
|
||||
placeholder={m.common_unknown()}
|
||||
value={pkg.remaining_level ?? ''}
|
||||
/>
|
||||
{/if}
|
||||
<div class="space-y-1">
|
||||
<Label for="edit_notes_{pkg.id}">{m.inventory_notes()}</Label>
|
||||
<Input id="edit_notes_{pkg.id}" name="notes" value={pkg.notes ?? ''} />
|
||||
|
|
|
|||
|
|
@ -118,12 +118,6 @@ 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;
|
||||
|
||||
|
|
@ -149,7 +143,7 @@ export const actions: Actions = {
|
|||
payload.is_tool = form.get('is_tool') === 'true';
|
||||
|
||||
// Nullable booleans
|
||||
for (const field of ['fragrance_free', 'essential_oils_free', 'alcohol_denat_free', 'pregnancy_safe', 'personal_repurchase_intent']) {
|
||||
for (const field of ['fragrance_free', 'essential_oils_free', 'alcohol_denat_free', 'pregnancy_safe']) {
|
||||
const v = parseTristate(form.get(field) as string | null);
|
||||
if (v !== undefined) payload[field] = v;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue