244 lines
7.8 KiB
Vue
244 lines
7.8 KiB
Vue
<template>
|
|
<div class="form-item-stack">
|
|
<el-switch
|
|
:model-value="modelValue.applyToAll"
|
|
:validate-event="false"
|
|
active-text="All products"
|
|
@update:model-value="value => updateScope({ applyToAll: Boolean(value) })"
|
|
/>
|
|
|
|
<template v-if="!modelValue.applyToAll">
|
|
<div class="scope-field">
|
|
<span class="secondary-text m-0">Applies to all products in category</span>
|
|
<el-select
|
|
:model-value="modelValue.categoryIds"
|
|
:validate-event="false"
|
|
multiple
|
|
collapse-tags
|
|
:max-collapse-tags="3"
|
|
collapse-tags-tooltip
|
|
class="cms-scope-select"
|
|
placeholder="Select categories"
|
|
@update:model-value="value => updateScope({ categoryIds: value })"
|
|
>
|
|
<el-option
|
|
v-for="category in categories"
|
|
:key="category.id"
|
|
:label="category.name"
|
|
:value="category.id"
|
|
/>
|
|
</el-select>
|
|
</div>
|
|
|
|
<div class="scope-field">
|
|
<span class="secondary-text m-0">Applies to all variants of the product</span>
|
|
<el-select
|
|
:model-value="modelValue.productIds"
|
|
:validate-event="false"
|
|
multiple
|
|
filterable
|
|
remote
|
|
reserve-keyword
|
|
collapse-tags
|
|
:max-collapse-tags="3"
|
|
collapse-tags-tooltip
|
|
class="cms-scope-select"
|
|
:remote-method="loadProducts"
|
|
:loading="productSearchLoading"
|
|
placeholder="Search products"
|
|
@update:model-value="value => updateScope({ productIds: value })"
|
|
>
|
|
<el-option
|
|
v-for="product in productOptions"
|
|
:key="product.id"
|
|
:label="product.title"
|
|
:value="product.id"
|
|
/>
|
|
</el-select>
|
|
</div>
|
|
|
|
<div class="scope-field">
|
|
<span class="secondary-text m-0">Applies to selected variants of the product</span>
|
|
<el-select
|
|
:model-value="modelValue.variantIds"
|
|
:validate-event="false"
|
|
multiple
|
|
filterable
|
|
remote
|
|
reserve-keyword
|
|
collapse-tags
|
|
:max-collapse-tags="3"
|
|
collapse-tags-tooltip
|
|
class="cms-scope-select"
|
|
:remote-method="loadVariants"
|
|
:loading="variantSearchLoading"
|
|
placeholder="Search variants"
|
|
@update:model-value="value => updateScope({ variantIds: value })"
|
|
>
|
|
<el-option
|
|
v-for="variant in variantOptions"
|
|
:key="variant.id"
|
|
:label="variant.label"
|
|
:value="variant.id"
|
|
/>
|
|
</el-select>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { getProductTitle } from '@/utils/product/getProductTitle';
|
|
import { getVariantLabel } from '@/utils/product/getVariantLabel';
|
|
import { useCategoriesStore } from '@/stores/categories';
|
|
import { useProductsStore } from '@/stores/products';
|
|
import type { DiscountCode } from '@/types/discountCode/DiscountCode';
|
|
import type { DiscountScope } from '@/types/discountCode/DiscountScope';
|
|
import type { Product } from '@/types/product/Product';
|
|
import type { ProductOption } from '@/types/product/ProductOption';
|
|
import type { ProductVariant } from '@/types/product/ProductVariant';
|
|
import type { VariantOption } from '@/types/product/VariantOption';
|
|
import { ElMessage } from 'element-plus';
|
|
import { onMounted, ref, type PropType } from 'vue';
|
|
import { storeToRefs } from 'pinia';
|
|
|
|
const props = defineProps({
|
|
modelValue: {
|
|
type: Object as PropType<DiscountScope>,
|
|
required: true
|
|
},
|
|
discountCode: {
|
|
type: Object as PropType<DiscountCode | null>,
|
|
default: null
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: DiscountScope];
|
|
change: [];
|
|
}>();
|
|
|
|
const { fetchProducts, fetchProductVariants } = useProductsStore();
|
|
const { categories } = storeToRefs(useCategoriesStore());
|
|
|
|
const productOptions = ref<ProductOption[]>([]);
|
|
const variantOptions = ref<VariantOption[]>([]);
|
|
const productSearchLoading = ref(false);
|
|
const variantSearchLoading = ref(false);
|
|
|
|
onMounted(() => {
|
|
if (props.discountCode) {
|
|
mergeProductOptions(props.discountCode.products?.map(productOptionFromEntity) ?? []);
|
|
mergeVariantOptions(props.discountCode.variants?.map(variantOptionFromEntity) ?? []);
|
|
}
|
|
});
|
|
|
|
const mergeProductOptions = (products: ProductOption[]): void => {
|
|
const selectedIds = new Set(props.modelValue.productIds);
|
|
|
|
const byId = new Map(
|
|
productOptions.value.filter(product => selectedIds.has(product.id)).map(product => [product.id, product])
|
|
);
|
|
|
|
for (const product of products) {
|
|
byId.set(product.id, product);
|
|
}
|
|
|
|
productOptions.value = [...byId.values()].sort((a, b) => a.title.localeCompare(b.title));
|
|
};
|
|
|
|
const productOptionFromEntity = (product: Pick<Product, 'id' | 'title'>): ProductOption => ({
|
|
id: product.id,
|
|
title: getProductTitle(product.title)
|
|
});
|
|
|
|
const mergeVariantOptions = (variants: VariantOption[]): void => {
|
|
const selectedIds = new Set(props.modelValue.variantIds);
|
|
|
|
const byId = new Map(
|
|
variantOptions.value.filter(variant => selectedIds.has(variant.id)).map(variant => [variant.id, variant])
|
|
);
|
|
|
|
for (const variant of variants) {
|
|
byId.set(variant.id, variant);
|
|
}
|
|
|
|
variantOptions.value = [...byId.values()].sort((a, b) => a.label.localeCompare(b.label));
|
|
};
|
|
|
|
const variantOptionFromEntity = (variant: ProductVariant): VariantOption => ({
|
|
id: variant.id,
|
|
label: getVariantLabel(variant.title, variant.product?.title)
|
|
});
|
|
|
|
const loadProducts = async (search: string) => {
|
|
productSearchLoading.value = true;
|
|
|
|
try {
|
|
const { items } = await fetchProducts({ search, page: 1, limit: 20 });
|
|
|
|
mergeProductOptions(items.map(product => productOptionFromEntity(product)));
|
|
} catch {
|
|
ElMessage.error('Failed to load products');
|
|
} finally {
|
|
productSearchLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const loadVariants = async (search: string) => {
|
|
variantSearchLoading.value = true;
|
|
|
|
try {
|
|
const { items } = await fetchProductVariants({ search, page: 1, limit: 20 });
|
|
|
|
mergeVariantOptions(items.map(variantOptionFromEntity));
|
|
} catch {
|
|
ElMessage.error('Failed to load variants');
|
|
} finally {
|
|
variantSearchLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const updateScope = (patch: Partial<DiscountScope>) => {
|
|
emit('update:modelValue', { ...props.modelValue, ...patch });
|
|
emit('change');
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.form-item-stack {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
}
|
|
|
|
.scope-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 4px;
|
|
width: 100%;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.scope-field .cms-scope-select {
|
|
width: 100%;
|
|
}
|
|
|
|
.scope-field :deep(.el-select__wrapper) {
|
|
min-width: 0;
|
|
}
|
|
|
|
.scope-field :deep(.el-select__selected-item) {
|
|
max-width: 100%;
|
|
}
|
|
|
|
.scope-field :deep(.el-select__tags-text) {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
max-width: min(160px, 45vw);
|
|
}
|
|
</style>
|