I can see the product with different options
This commit is contained in:
@@ -212,6 +212,7 @@ sale-price = Sale price
|
|||||||
variants-options = Variants / options
|
variants-options = Variants / options
|
||||||
add-option = Add option
|
add-option = Add option
|
||||||
option-label = Option label
|
option-label = Option label
|
||||||
|
optional = optional
|
||||||
choose-option = Choose an option
|
choose-option = Choose an option
|
||||||
from-price = from { $price }
|
from-price = from { $price }
|
||||||
admin-discounts = Discounts
|
admin-discounts = Discounts
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ sale-price = Zľavnená cena
|
|||||||
variants-options = Varianty / možnosti
|
variants-options = Varianty / možnosti
|
||||||
add-option = Pridať možnosť
|
add-option = Pridať možnosť
|
||||||
option-label = Označenie možnosti
|
option-label = Označenie možnosti
|
||||||
|
optional = voliteľné
|
||||||
choose-option = Vyberte možnosť
|
choose-option = Vyberte možnosť
|
||||||
from-price = od { $price }
|
from-price = od { $price }
|
||||||
admin-discounts = Zľavy
|
admin-discounts = Zľavy
|
||||||
|
|||||||
@@ -37,9 +37,11 @@
|
|||||||
|
|
||||||
{# --- Variants / options editor ------------------------------------------- #}
|
{# --- Variants / options editor ------------------------------------------- #}
|
||||||
{# Each product is sold as one or more variants (a free-text label such as #}
|
{# Each product is sold as one or more variants (a free-text label such as #}
|
||||||
{# "10cm x 13cm" or "5ml" plus its own price/stock/sku, and optional public & #}
|
{# "10cm x 13cm" or "5ml" plus its own price/stock, optional sku & business #}
|
||||||
{# business sale prices). Rows are managed client-side; names are indexed #}
|
{# price). Price and stock are required; the browser blocks save if a row is #}
|
||||||
{# (variants[i][...]) and read back by the controller. #}
|
{# incomplete. Rows are managed client-side; names are indexed (variants[i][…]) #}
|
||||||
|
{# and read back by the controller. #}
|
||||||
|
{% set opt = " (" ~ t(key="optional", lang=lang | default(value='sk')) ~ ")" %}
|
||||||
<script id="variants-data" type="application/json">{{ variants | json_encode() | safe }}</script>
|
<script id="variants-data" type="application/json">{{ variants | json_encode() | safe }}</script>
|
||||||
<div class="space-y-3" x-data="variantEditor(JSON.parse(document.getElementById('variants-data').textContent))">
|
<div class="space-y-3" x-data="variantEditor(JSON.parse(document.getElementById('variants-data').textContent))">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
@@ -55,32 +57,28 @@
|
|||||||
<input type="hidden" :name="`variants[${i}][id]`" :value="row.id">
|
<input type="hidden" :name="`variants[${i}][id]`" :value="row.id">
|
||||||
|
|
||||||
<div class="space-y-1 sm:col-span-5">
|
<div class="space-y-1 sm:col-span-5">
|
||||||
<label class="{{ sublabel }}">{{ t(key="option-label", lang=lang | default(value='sk')) }}</label>
|
<label class="{{ sublabel }}">{{ t(key="option-label", lang=lang | default(value='sk')) }}{{ opt }}</label>
|
||||||
<input :name="`variants[${i}][label]`" x-model="row.label" class="{{ inp }}" placeholder="napr. 10cm x 13cm">
|
<input :name="`variants[${i}][label]`" x-model="row.label" class="{{ inp }}" placeholder="napr. 10cm x 13cm">
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1 sm:col-span-3">
|
<div class="space-y-1 sm:col-span-3">
|
||||||
<label class="{{ sublabel }}">{{ t(key="sku", lang=lang | default(value='sk')) }}</label>
|
<label class="{{ sublabel }}">{{ t(key="sku", lang=lang | default(value='sk')) }}{{ opt }}</label>
|
||||||
<input :name="`variants[${i}][sku]`" x-model="row.sku" class="{{ inp }}">
|
<input :name="`variants[${i}][sku]`" x-model="row.sku" class="{{ inp }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1 sm:col-span-2">
|
<div class="space-y-1 sm:col-span-2">
|
||||||
<label class="{{ sublabel }}">{{ t(key="stock", lang=lang | default(value='sk')) }}</label>
|
<label class="{{ sublabel }}">{{ t(key="stock", lang=lang | default(value='sk')) }}</label>
|
||||||
<input type="number" min="0" :name="`variants[${i}][stock]`" x-model="row.stock" class="{{ inp }}">
|
<input type="number" min="0" required :name="`variants[${i}][stock]`" x-model="row.stock" class="{{ inp }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-end sm:col-span-2">
|
<div class="flex items-end sm:col-span-2">
|
||||||
<button type="button" @click="remove(i)"
|
<button type="button" @click="remove(i)"
|
||||||
class="ml-auto rounded-radius px-2 py-2 text-sm text-danger hover:bg-danger/10" title="{{ t(key='delete', lang=lang | default(value='sk')) }}">✕</button>
|
class="ml-auto rounded-radius px-2 py-2 text-sm text-danger hover:bg-danger/10" title="{{ t(key='delete', lang=lang | default(value='sk')) }}">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-1 sm:col-span-4">
|
<div class="space-y-1 sm:col-span-6">
|
||||||
<label class="{{ sublabel }}">{{ t(key="price", lang=lang | default(value='sk')) }}</label>
|
<label class="{{ sublabel }}">{{ t(key="price", lang=lang | default(value='sk')) }}</label>
|
||||||
<input :name="`variants[${i}][price]`" x-model="row.price" inputmode="decimal" class="{{ inp }}" placeholder="0.00">
|
<input :name="`variants[${i}][price]`" x-model="row.price" inputmode="decimal" required class="{{ inp }}" placeholder="0.00">
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1 sm:col-span-4">
|
<div class="space-y-1 sm:col-span-6">
|
||||||
<label class="{{ sublabel }}">{{ t(key="sale-price", lang=lang | default(value='sk')) }}</label>
|
<label class="{{ sublabel }}">{{ t(key="business-price", lang=lang | default(value='sk')) }}{{ opt }}</label>
|
||||||
<input :name="`variants[${i}][sale]`" x-model="row.sale" inputmode="decimal" class="{{ inp }}" placeholder="—">
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1 sm:col-span-4">
|
|
||||||
<label class="{{ sublabel }}">{{ t(key="business-price", lang=lang | default(value='sk')) }}</label>
|
|
||||||
<input :name="`variants[${i}][business_sale]`" x-model="row.business_sale" inputmode="decimal" class="{{ inp }}" placeholder="—">
|
<input :name="`variants[${i}][business_sale]`" x-model="row.business_sale" inputmode="decimal" class="{{ inp }}" placeholder="—">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,15 +87,14 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
function variantEditor(initial) {
|
function variantEditor(initial) {
|
||||||
const blank = () => ({ id: '', label: '', sku: '', stock: 0, price: '', sale: '', business_sale: '' });
|
const blank = () => ({ id: '', label: '', sku: '', stock: '', price: '', business_sale: '' });
|
||||||
return {
|
return {
|
||||||
rows: (initial || []).map(r => ({
|
rows: (initial || []).map(r => ({
|
||||||
id: r.id || '',
|
id: r.id || '',
|
||||||
label: r.label || '',
|
label: r.label || '',
|
||||||
sku: r.sku || '',
|
sku: r.sku || '',
|
||||||
stock: (r.stock === null || r.stock === undefined) ? 0 : r.stock,
|
stock: (r.stock === null || r.stock === undefined) ? '' : r.stock,
|
||||||
price: r.price || '',
|
price: r.price || '',
|
||||||
sale: r.sale || '',
|
|
||||||
business_sale: r.business_sale || '',
|
business_sale: r.business_sale || '',
|
||||||
})),
|
})),
|
||||||
init() { if (this.rows.length === 0) this.add(); },
|
init() { if (this.rows.length === 0) this.add(); },
|
||||||
|
|||||||
@@ -60,6 +60,19 @@
|
|||||||
|
|
||||||
<template x-if="current">
|
<template x-if="current">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
<!-- option picker (only when there's a real choice); first option is
|
||||||
|
selected by default and switching it updates the price + buy form -->
|
||||||
|
<template x-if="variants.length > 1">
|
||||||
|
<div class="max-w-sm space-y-1.5">
|
||||||
|
<label for="variant-select" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="choose-option", lang=lang | default(value='sk')) }}</label>
|
||||||
|
<select id="variant-select" x-model.number="sel" class="{{ fld }}">
|
||||||
|
<template x-for="(v, i) in variants" :key="v.id">
|
||||||
|
<option :value="i" x-text="(v.label || '—') + ' · ' + v.price + ' {{ product.currency }}' + (v.in_stock ? '' : ' ({{ t(key='out-of-stock', lang=lang | default(value='sk')) }})')"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="flex items-baseline gap-3">
|
<div class="flex items-baseline gap-3">
|
||||||
<p class="text-2xl font-semibold" :class="current.on_sale ? 'text-danger' : 'text-primary dark:text-primary-dark'">
|
<p class="text-2xl font-semibold" :class="current.on_sale ? 'text-danger' : 'text-primary dark:text-primary-dark'">
|
||||||
<span x-text="current.price"></span> {{ product.currency }}
|
<span x-text="current.price"></span> {{ product.currency }}
|
||||||
@@ -73,18 +86,6 @@
|
|||||||
<div class="whitespace-pre-line leading-relaxed text-on-surface/80 dark:text-on-surface-dark/80">{{ product.description }}</div>
|
<div class="whitespace-pre-line leading-relaxed text-on-surface/80 dark:text-on-surface-dark/80">{{ product.description }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- variant picker (only when there's a real choice) -->
|
|
||||||
<template x-if="variants.length > 1">
|
|
||||||
<div class="max-w-sm space-y-1.5">
|
|
||||||
<label for="variant-select" class="text-sm font-medium text-on-surface-strong dark:text-on-surface-dark-strong">{{ t(key="choose-option", lang=lang | default(value='sk')) }}</label>
|
|
||||||
<select id="variant-select" x-model.number="sel" class="{{ fld }}">
|
|
||||||
<template x-for="(v, i) in variants" :key="v.id">
|
|
||||||
<option :value="i" x-text="(v.label || '—') + ' · ' + v.price + ' {{ product.currency }}' + (v.in_stock ? '' : ' ({{ t(key='out-of-stock', lang=lang | default(value='sk')) }})')"></option>
|
|
||||||
</template>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-if="current.in_stock">
|
<template x-if="current.in_stock">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<form method="post" action="/cart/add" hx-post="/cart/add" hx-swap="none" class="flex flex-wrap items-end gap-3"
|
<form method="post" action="/cart/add" hx-post="/cart/add" hx-swap="none" class="flex flex-wrap items-end gap-3"
|
||||||
|
|||||||
@@ -104,13 +104,12 @@ struct VariantInput {
|
|||||||
sku: Option<String>,
|
sku: Option<String>,
|
||||||
stock: i32,
|
stock: i32,
|
||||||
price_cents: i64,
|
price_cents: i64,
|
||||||
sale_cents: Option<i64>,
|
|
||||||
business_sale_cents: Option<i64>,
|
business_sale_cents: Option<i64>,
|
||||||
position: i32,
|
position: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An optional price field on a variant row (sale / business sale): blank means
|
/// The optional business-sale price field on a variant row: blank means "no
|
||||||
/// "no quick-sale", a value must parse and be below the regular price.
|
/// business quick-sale", a value must parse and be below the regular price.
|
||||||
fn parse_optional_sale(
|
fn parse_optional_sale(
|
||||||
form: &MultipartForm,
|
form: &MultipartForm,
|
||||||
i: usize,
|
i: usize,
|
||||||
@@ -161,8 +160,7 @@ fn parse_variants(form: &MultipartForm) -> Result<Vec<VariantInput>> {
|
|||||||
.text(&format!("variants[{i}][stock]"))
|
.text(&format!("variants[{i}][stock]"))
|
||||||
.and_then(|s| s.parse::<i32>().ok())
|
.and_then(|s| s.parse::<i32>().ok())
|
||||||
.filter(|n| *n >= 0)
|
.filter(|n| *n >= 0)
|
||||||
.unwrap_or(0);
|
.ok_or_else(|| Error::BadRequest("each option needs a stock quantity".to_string()))?;
|
||||||
let sale_cents = parse_optional_sale(form, i, "sale", price_cents)?;
|
|
||||||
let business_sale_cents = parse_optional_sale(form, i, "business_sale", price_cents)?;
|
let business_sale_cents = parse_optional_sale(form, i, "business_sale", price_cents)?;
|
||||||
let id = form
|
let id = form
|
||||||
.text(&format!("variants[{i}][id]"))
|
.text(&format!("variants[{i}][id]"))
|
||||||
@@ -174,7 +172,6 @@ fn parse_variants(form: &MultipartForm) -> Result<Vec<VariantInput>> {
|
|||||||
sku,
|
sku,
|
||||||
stock,
|
stock,
|
||||||
price_cents,
|
price_cents,
|
||||||
sale_cents,
|
|
||||||
business_sale_cents,
|
business_sale_cents,
|
||||||
position: out.len() as i32,
|
position: out.len() as i32,
|
||||||
});
|
});
|
||||||
@@ -193,7 +190,8 @@ fn apply_variant(active: &mut product_variants::ActiveModel, input: &VariantInpu
|
|||||||
active.sku = Set(input.sku.clone());
|
active.sku = Set(input.sku.clone());
|
||||||
active.stock = Set(input.stock);
|
active.stock = Set(input.stock);
|
||||||
active.price_cents = Set(input.price_cents);
|
active.price_cents = Set(input.price_cents);
|
||||||
active.sale_price_cents = Set(input.sale_cents);
|
// The per-variant public sale price was removed from the UI; keep it cleared.
|
||||||
|
active.sale_price_cents = Set(None);
|
||||||
active.business_sale_price_cents = Set(input.business_sale_cents);
|
active.business_sale_price_cents = Set(input.business_sale_cents);
|
||||||
active.position = Set(input.position);
|
active.position = Set(input.position);
|
||||||
}
|
}
|
||||||
@@ -252,7 +250,6 @@ fn variant_form_json(variant: &product_variants::Model) -> serde_json::Value {
|
|||||||
"sku": variant.sku,
|
"sku": variant.sku,
|
||||||
"stock": variant.stock,
|
"stock": variant.stock,
|
||||||
"price": format_price(variant.price_cents),
|
"price": format_price(variant.price_cents),
|
||||||
"sale": variant.sale_price_cents.map(format_price),
|
|
||||||
"business_sale": variant.business_sale_price_cents.map(format_price),
|
"business_sale": variant.business_sale_price_cents.map(format_price),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user