Make catalog filters adjust to valid combinations

This commit is contained in:
Ruslan Bakiev
2026-04-03 18:38:42 +07:00
parent f941ba7192
commit b131f33fbe

View File

@@ -239,7 +239,7 @@ watch(
for (const key of Object.keys(groupStates)) { for (const key of Object.keys(groupStates)) {
if (!keys.has(key)) { if (!keys.has(key)) {
delete groupStates[key]; Reflect.deleteProperty(groupStates, key);
} }
} }
@@ -269,11 +269,6 @@ function matchesProductState(product: ParsedProduct, state: GroupState, keys: Pa
}); });
} }
function matchingProducts(group: ProductGroup) {
const state = getGroupState(group);
return group.products.filter((product) => matchesProductState(product, state, requiredKeys(group)));
}
function selectedProduct(group: ProductGroup) { function selectedProduct(group: ProductGroup) {
const keys = requiredKeys(group); const keys = requiredKeys(group);
const state = getGroupState(group); const state = getGroupState(group);
@@ -303,6 +298,14 @@ function formatOptionLabel(field: ParamFieldKey, value: ParamValue) {
return String(value); return String(value);
} }
function productHasOption(product: ParsedProduct, field: ParamFieldKey, option: ParamValue) {
if (field === 'quantityPerBox') {
return product.quantityPerBoxOptions.includes(String(option));
}
return product[field] === option;
}
function isOptionAvailable(group: ProductGroup, field: ParamFieldKey, option: ParamValue) { function isOptionAvailable(group: ProductGroup, field: ParamFieldKey, option: ParamValue) {
const state = getGroupState(group); const state = getGroupState(group);
const scopedState = { const scopedState = {
@@ -313,25 +316,73 @@ function isOptionAvailable(group: ProductGroup, field: ParamFieldKey, option: Pa
return group.products.some((product) => matchesProductState(product, scopedState, requiredKeys(group))); return group.products.some((product) => matchesProductState(product, scopedState, requiredKeys(group)));
} }
function updateField(group: ProductGroup, field: ParamFieldKey, value: ParamValue) { function pickBestProductForOption(group: ProductGroup, field: ParamFieldKey, option: ParamValue) {
const state = getGroupState(group); const state = getGroupState(group);
state[field] = value as GroupState[typeof field]; const candidates = group.products.filter((product) => productHasOption(product, field, option));
const resolved = selectedProduct(group); if (candidates.length === 0) {
if (resolved) { return null;
}
return [...candidates].sort((a, b) => {
let aScore = 0;
let bScore = 0;
for (const key of PARAM_KEYS) {
if (key === field) {
continue;
}
const selectedValue = state[key];
if (selectedValue === null) {
continue;
}
if (productHasOption(a, key, selectedValue)) {
aScore += 1;
}
if (productHasOption(b, key, selectedValue)) {
bScore += 1;
}
}
if (aScore !== bScore) {
return bScore - aScore;
}
return compareProducts(a, b);
})[0];
}
function applyProductToState(state: GroupState, product: ParsedProduct, preferredBoxOption: ParamValue | null = null) {
state.widthMm = product.widthMm ?? null;
state.lengthM = product.lengthM ?? null;
state.thicknessMicron = product.thicknessMicron ?? null;
state.sleeveBrand = product.sleeveBrand ?? null;
if (preferredBoxOption !== null && product.quantityPerBoxOptions.includes(String(preferredBoxOption))) {
state.quantityPerBox = String(preferredBoxOption);
return; return;
} }
const fallback = group.products.find((product) => product[field] === value) ?? group.products[0]; state.quantityPerBox = product.quantityPerBoxOptions[0] ?? null;
}
function updateField(group: ProductGroup, field: ParamFieldKey, value: ParamValue) {
const state = getGroupState(group);
const fallback = pickBestProductForOption(group, field, value);
if (!fallback) { if (!fallback) {
return; return;
} }
state.widthMm = fallback.widthMm ?? null; applyProductToState(state, fallback, field === 'quantityPerBox' ? value : state.quantityPerBox);
state.lengthM = fallback.lengthM ?? null;
state.thicknessMicron = fallback.thicknessMicron ?? null; if (field === 'quantityPerBox') {
state.sleeveBrand = fallback.sleeveBrand ?? null; state.quantityPerBox = String(value);
state.quantityPerBox = fallback.quantityPerBoxOptions[0] ?? null; return;
}
state[field] = value as GroupState[typeof field];
} }
function articleLabel(group: ProductGroup) { function articleLabel(group: ProductGroup) {
@@ -451,14 +502,14 @@ function decrementSelected(group: ProductGroup) {
<label <label
v-for="option in getAllFieldOptions(group, field.key)" v-for="option in getAllFieldOptions(group, field.key)"
:key="`${group.key}-${field.key}-${option}`" :key="`${group.key}-${field.key}-${option}`"
class="btn btn-sm rounded-full border text-sm normal-case shadow-none transition" class="btn btn-sm rounded-full text-sm normal-case shadow-none transition"
:class="[ :class="[
getGroupState(group)[field.key] === option getGroupState(group)[field.key] === option
? 'border-neutral bg-neutral text-neutral-content' ? 'bg-neutral text-neutral-content'
: 'border-base-300 bg-base-100 text-base-content hover:border-neutral/30 hover:bg-base-200', : isOptionAvailable(group, field.key, option)
!isOptionAvailable(group, field.key, option) ? 'bg-base-100 text-base-content hover:bg-base-200'
? 'pointer-events-none opacity-35' : 'bg-[#eef1f4] text-[#7b8591] hover:bg-[#e3e7eb]',
: 'cursor-pointer', 'cursor-pointer',
]" ]"
> >
<input <input
@@ -466,7 +517,6 @@ function decrementSelected(group: ProductGroup) {
class="sr-only" class="sr-only"
:name="`${group.key}-${field.key}`" :name="`${group.key}-${field.key}`"
:checked="getGroupState(group)[field.key] === option" :checked="getGroupState(group)[field.key] === option"
:disabled="!isOptionAvailable(group, field.key, option)"
@change="updateField(group, field.key, option)" @change="updateField(group, field.key, option)"
> >
<span>{{ formatOptionLabel(field.key, option) }}</span> <span>{{ formatOptionLabel(field.key, option) }}</span>
@@ -487,14 +537,14 @@ function decrementSelected(group: ProductGroup) {
<label <label
v-for="option in getAllFieldOptions(group, field.key)" v-for="option in getAllFieldOptions(group, field.key)"
:key="`${group.key}-${field.key}-${option}`" :key="`${group.key}-${field.key}-${option}`"
class="btn btn-sm rounded-full border text-sm normal-case shadow-none transition" class="btn btn-sm rounded-full text-sm normal-case shadow-none transition"
:class="[ :class="[
getGroupState(group)[field.key] === option getGroupState(group)[field.key] === option
? 'border-neutral bg-neutral text-neutral-content' ? 'bg-neutral text-neutral-content'
: 'border-base-300 bg-base-100 text-base-content hover:border-neutral/30 hover:bg-base-200', : isOptionAvailable(group, field.key, option)
!isOptionAvailable(group, field.key, option) ? 'bg-base-100 text-base-content hover:bg-base-200'
? 'pointer-events-none opacity-35' : 'bg-[#eef1f4] text-[#7b8591] hover:bg-[#e3e7eb]',
: 'cursor-pointer', 'cursor-pointer',
]" ]"
> >
<input <input
@@ -502,7 +552,6 @@ function decrementSelected(group: ProductGroup) {
class="sr-only" class="sr-only"
:name="`${group.key}-${field.key}`" :name="`${group.key}-${field.key}`"
:checked="getGroupState(group)[field.key] === option" :checked="getGroupState(group)[field.key] === option"
:disabled="!isOptionAvailable(group, field.key, option)"
@change="updateField(group, field.key, option)" @change="updateField(group, field.key, option)"
> >
<span>{{ formatOptionLabel(field.key, option) }}</span> <span>{{ formatOptionLabel(field.key, option) }}</span>