refactor(catalog): use structured product toggles

This commit is contained in:
Ruslan Bakiev
2026-04-03 14:52:30 +07:00
parent 5bb0e3928f
commit 9c889e200a
4 changed files with 280 additions and 165 deletions

View File

@@ -9,11 +9,6 @@ type ParamValue = number | string;
type ParsedProduct = ProductNode & { type ParsedProduct = ProductNode & {
productTypeLabel: string; productTypeLabel: string;
widthMm: number | null;
lengthM: number | null;
thicknessMicron: number | null;
sleeveBrand: string | null;
quantityPerBox: string | null;
quantityPerBoxOptions: string[]; quantityPerBoxOptions: string[];
}; };
@@ -33,10 +28,10 @@ type GroupState = {
const PARAM_KEYS: ParamFieldKey[] = ['widthMm', 'lengthM', 'thicknessMicron', 'sleeveBrand']; const PARAM_KEYS: ParamFieldKey[] = ['widthMm', 'lengthM', 'thicknessMicron', 'sleeveBrand'];
const parameterFields: Array<{ key: ParamFieldKey; label: string }> = [ const parameterFields: Array<{ key: ParamFieldKey; label: string }> = [
{ key: 'widthMm', label: 'Ширина, мм' }, { key: 'widthMm', label: 'Ширина' },
{ key: 'lengthM', label: 'Длина, м' }, { key: 'lengthM', label: 'Длина' },
{ key: 'thicknessMicron', label: 'Толщина, мкм' }, { key: 'thicknessMicron', label: 'Толщина' },
{ key: 'sleeveBrand', label: 'Бренд втулки' }, { key: 'sleeveBrand', label: 'Втулка' },
]; ];
const coverPresets = [ const coverPresets = [
@@ -50,8 +45,15 @@ const search = ref('');
const groupStates = reactive<Record<string, GroupState>>({}); const groupStates = reactive<Record<string, GroupState>>({});
const { addProduct, getQuantity, incrementQuantity, decrementQuantity } = useClientCart(); const { addProduct, getQuantity, incrementQuantity, decrementQuantity } = useClientCart();
function normalizeText(value: string) { function normalizeText(value: string | null | undefined) {
return value.replaceAll(/\s+/g, ' ').trim(); return String(value ?? '').replaceAll(/\s+/g, ' ').trim();
}
function splitBoxValues(value: string | null | undefined) {
return normalizeText(value)
.split('/')
.map((item) => item.trim())
.filter(Boolean);
} }
function createProductCover(name: string, sku: string) { function createProductCover(name: string, sku: string) {
@@ -80,31 +82,15 @@ function createProductCover(name: string, sku: string) {
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`; return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
} }
function parseProductMeta(product: ProductNode) { function hydrateProduct(product: ProductNode): ParsedProduct {
const normalizedName = normalizeText(product.name);
const [typeLabelRaw] = normalizedName.split(',');
const typeLabel = typeLabelRaw?.trim() || 'Без типа';
const sizeMatch = normalizedName.match(/(\d+)\s*[xх*]\s*(\d+)\s*м/i);
const thicknessMatch = normalizedName.match(/(\d+)\s*мкм/i);
const sleeveMatch = normalizedName.match(/втулка\s+([^,]+)/i);
const quantityMatch = normalizedName.match(/короб\s+([^,]+)/i);
const quantityPerBox = quantityMatch?.[1]?.trim() ?? null;
return { return {
productTypeLabel: typeLabel, ...product,
widthMm: sizeMatch ? Number.parseInt(sizeMatch[1] ?? '', 10) : null, productTypeLabel: normalizeText(product.productType) || 'Без типа',
lengthM: sizeMatch ? Number.parseInt(sizeMatch[2] ?? '', 10) : null, quantityPerBoxOptions: splitBoxValues(product.quantityPerBox),
thicknessMicron: thicknessMatch ? Number.parseInt(thicknessMatch[1] ?? '', 10) : null,
sleeveBrand: sleeveMatch?.[1]?.trim() ?? null,
quantityPerBox,
quantityPerBoxOptions: quantityPerBox
? quantityPerBox.split('/').map((value) => value.trim()).filter(Boolean)
: [],
}; };
} }
function productSortValue(value: number | null) { function productSortValue(value: number | null | undefined) {
return value ?? Number.MAX_SAFE_INTEGER; return value ?? Number.MAX_SAFE_INTEGER;
} }
@@ -124,7 +110,7 @@ function compareProducts(a: ParsedProduct, b: ParsedProduct) {
return byThickness; return byThickness;
} }
const bySleeve = (a.sleeveBrand ?? '').localeCompare(b.sleeveBrand ?? '', 'ru'); const bySleeve = normalizeText(a.sleeveBrand).localeCompare(normalizeText(b.sleeveBrand), 'ru');
if (bySleeve !== 0) { if (bySleeve !== 0) {
return bySleeve; return bySleeve;
} }
@@ -137,13 +123,22 @@ const parsedProducts = computed<ParsedProduct[]>(() => {
const query = search.value.trim().toLowerCase(); const query = search.value.trim().toLowerCase();
return list return list
.map((product) => ({ ...product, ...parseProductMeta(product) })) .map(hydrateProduct)
.filter((product) => { .filter((product) => {
if (!query) { if (!query) {
return true; return true;
} }
return [product.name, product.sku, product.productTypeLabel, product.sleeveBrand ?? '']
.some((part) => part.toLowerCase().includes(query)); return [
product.name,
product.sku,
product.productTypeLabel,
String(product.widthMm ?? ''),
String(product.lengthM ?? ''),
String(product.thicknessMicron ?? ''),
normalizeText(product.sleeveBrand),
normalizeText(product.quantityPerBox),
].some((part) => part.toLowerCase().includes(query));
}) })
.sort(compareProducts); .sort(compareProducts);
}); });
@@ -152,12 +147,11 @@ const productGroups = computed<ProductGroup[]>(() => {
const map = new Map<string, ParsedProduct[]>(); const map = new Map<string, ParsedProduct[]>();
for (const product of parsedProducts.value) { for (const product of parsedProducts.value) {
const key = product.productTypeLabel; const existing = map.get(product.productTypeLabel);
const existing = map.get(key);
if (existing) { if (existing) {
existing.push(product); existing.push(product);
} else { } else {
map.set(key, [product]); map.set(product.productTypeLabel, [product]);
} }
} }
@@ -170,25 +164,6 @@ const productGroups = computed<ProductGroup[]>(() => {
})); }));
}); });
function matchesState(product: ParsedProduct, state: GroupState, skipField?: ParamFieldKey) {
for (const key of PARAM_KEYS) {
if (key === skipField) {
continue;
}
const selectedValue = state[key];
if (selectedValue === null) {
continue;
}
if (product[key] !== selectedValue) {
return false;
}
}
return true;
}
function sortParamValues(values: ParamValue[]) { function sortParamValues(values: ParamValue[]) {
return [...values].sort((a, b) => { return [...values].sort((a, b) => {
if (typeof a === 'number' && typeof b === 'number') { if (typeof a === 'number' && typeof b === 'number') {
@@ -203,7 +178,7 @@ function getAllFieldOptions(group: ProductGroup, field: ParamFieldKey) {
for (const product of group.products) { for (const product of group.products) {
const value = product[field]; const value = product[field];
if (value !== null) { if (value !== null && value !== undefined) {
values.add(value); values.add(value);
} }
} }
@@ -211,17 +186,22 @@ function getAllFieldOptions(group: ProductGroup, field: ParamFieldKey) {
return sortParamValues([...values]); return sortParamValues([...values]);
} }
function createGroupState(group: ProductGroup): GroupState { function visibleFields(group: ProductGroup) {
void group; return parameterFields.filter((field) => getAllFieldOptions(group, field.key).length > 1);
const state: GroupState = { }
function requiredKeys(group: ProductGroup) {
return visibleFields(group).map((field) => field.key);
}
function createGroupState(): GroupState {
return {
widthMm: null, widthMm: null,
lengthM: null, lengthM: null,
thicknessMicron: null, thicknessMicron: null,
sleeveBrand: null, sleeveBrand: null,
isExpanded: false, isExpanded: false,
}; };
return state;
} }
watch( watch(
@@ -236,70 +216,97 @@ watch(
} }
for (const group of groups) { for (const group of groups) {
if (!groupStates[group.key]) { groupStates[group.key] ??= createGroupState();
groupStates[group.key] = createGroupState(group);
}
} }
}, },
{ immediate: true }, { immediate: true },
); );
function getGroupState(group: ProductGroup): GroupState { function getGroupState(group: ProductGroup) {
const existing = groupStates[group.key]; groupStates[group.key] ??= createGroupState();
if (existing) { return groupStates[group.key];
return existing;
}
const created = createGroupState(group);
groupStates[group.key] = created;
return created;
} }
function updateField(group: ProductGroup, field: ParamFieldKey, value: ParamValue) { function matchesProductState(product: ParsedProduct, state: GroupState, keys: ParamFieldKey[]) {
return keys.every((key) => state[key] === null || product[key] === state[key]);
}
function matchingProducts(group: ProductGroup) {
const state = getGroupState(group); const state = getGroupState(group);
state[field] = value as GroupState[typeof field]; return group.products.filter((product) => matchesProductState(product, state, requiredKeys(group)));
} }
function selectedProduct(group: ProductGroup) { function selectedProduct(group: ProductGroup) {
const keys = requiredKeys(group);
const state = getGroupState(group); const state = getGroupState(group);
if (PARAM_KEYS.some((key) => state[key] === null)) {
if (keys.length === 0) {
return group.products.length === 1 ? group.products[0] : null;
}
if (keys.some((key) => state[key] === null)) {
return null; return null;
} }
return group.products.find((product) => matchesState(product, state)) ?? null;
const matches = group.products.filter((product) => matchesProductState(product, state, keys));
return matches.length === 1 ? matches[0] : null;
} }
function matchingCount(group: ProductGroup) { function remainingSelectionCount(group: ProductGroup) {
const state = getGroupState(group); const state = getGroupState(group);
return group.products.filter((product) => matchesState(product, state)).length; return requiredKeys(group).filter((key) => state[key] === null).length;
} }
function toggleExpanded(group: ProductGroup) { function pluralize(value: number, one: string, few: string, many: string) {
const state = getGroupState(group); const mod10 = value % 10;
state.isExpanded = !state.isExpanded; const mod100 = value % 100;
if (mod10 === 1 && mod100 !== 11) {
return one;
}
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) {
return few;
}
return many;
} }
function formatOptionLabel(field: ParamFieldKey, value: ParamValue) { function selectionHeadline(group: ProductGroup) {
if (field === 'widthMm' || field === 'lengthM') { const product = selectedProduct(group);
return `${value} мм`; if (product) {
return `SKU ${product.sku}`;
} }
if (field === 'thicknessMicron') {
return `${value} мкм`; const remaining = remainingSelectionCount(group);
if (remaining > 0) {
return `Выберите еще ${remaining} ${pluralize(remaining, 'параметр', 'параметра', 'параметров')}`;
} }
return String(value);
return 'Комбинация не найдена';
} }
function selectedFieldValue(group: ProductGroup, field: ParamFieldKey) { function selectionDescription(group: ProductGroup) {
const value = getGroupState(group)[field]; const product = selectedProduct(group);
if (value === null) { if (product) {
return 'не выбрано'; return [
product.widthMm ? `${product.widthMm} мм` : null,
product.lengthM ? `${product.lengthM} м` : null,
product.thicknessMicron ? `${product.thicknessMicron} мкм` : null,
normalizeText(product.sleeveBrand) || null,
].filter(Boolean).join(' • ');
} }
return formatOptionLabel(field, value);
const remaining = remainingSelectionCount(group);
if (remaining > 0) {
return 'Переключатели ниже собирают точную модификацию товара.';
}
return 'Разверните весь список, если нужна ручная проверка вариантов.';
} }
function boxQuantityLabel(group: ProductGroup) { function boxQuantityLabel(group: ProductGroup) {
const product = selectedProduct(group); const product = selectedProduct(group);
if (product?.quantityPerBox) { if (product?.quantityPerBoxOptions.length) {
return product.quantityPerBox; return product.quantityPerBoxOptions.join(' / ');
} }
const values = new Set<string>(); const values = new Set<string>();
@@ -312,6 +319,49 @@ function boxQuantityLabel(group: ProductGroup) {
return sortParamValues([...values]).join(' / ') || '—'; return sortParamValues([...values]).join(' / ') || '—';
} }
function formatOptionLabel(field: ParamFieldKey, value: ParamValue) {
if (field === 'widthMm') {
return `${value} мм`;
}
if (field === 'lengthM') {
return `${value} м`;
}
if (field === 'thicknessMicron') {
return `${value} мкм`;
}
return String(value);
}
function isOptionAvailable(group: ProductGroup, field: ParamFieldKey, option: ParamValue) {
const state = getGroupState(group);
const scopedState = {
...state,
[field]: option,
} satisfies GroupState;
return group.products.some((product) => matchesProductState(product, scopedState, requiredKeys(group)));
}
function updateField(group: ProductGroup, field: ParamFieldKey, value: ParamValue) {
const state = getGroupState(group);
state[field] = value as GroupState[typeof field];
}
function clearField(group: ProductGroup, field: ParamFieldKey) {
getGroupState(group)[field] = null;
}
function clearSelection(group: ProductGroup) {
const state = getGroupState(group);
for (const key of PARAM_KEYS) {
state[key] = null;
}
}
function toggleExpanded(group: ProductGroup) {
getGroupState(group).isExpanded = !getGroupState(group).isExpanded;
}
function incrementProduct(product: ProductNode) { function incrementProduct(product: ProductNode) {
if (getQuantity(product.id) === 0) { if (getQuantity(product.id) === 0) {
addProduct({ addProduct({
@@ -332,26 +382,21 @@ function decrementProduct(productId: string) {
function selectedQty(group: ProductGroup) { function selectedQty(group: ProductGroup) {
const product = selectedProduct(group); const product = selectedProduct(group);
if (!product) { return product ? getQuantity(product.id) : 0;
return 0;
}
return getQuantity(product.id);
} }
function incrementSelected(group: ProductGroup) { function incrementSelected(group: ProductGroup) {
const product = selectedProduct(group); const product = selectedProduct(group);
if (!product) { if (product) {
return; incrementProduct(product);
} }
incrementProduct(product);
} }
function decrementSelected(group: ProductGroup) { function decrementSelected(group: ProductGroup) {
const product = selectedProduct(group); const product = selectedProduct(group);
if (!product) { if (product) {
return; decrementProduct(product.id);
} }
decrementProduct(product.id);
} }
</script> </script>
@@ -380,76 +425,110 @@ function decrementSelected(group: ProductGroup) {
:key="group.key" :key="group.key"
class="surface-card rounded-3xl p-4 md:p-5" class="surface-card rounded-3xl p-4 md:p-5"
> >
<div class="flex flex-wrap items-center gap-3"> <div class="grid gap-4 xl:grid-cols-6 xl:items-stretch">
<h2 class="text-xl font-bold text-[#133826]">{{ group.typeLabel }}</h2> <div class="rounded-[28px] bg-base-100 p-3 xl:col-span-1">
<span class="badge badge-outline">{{ group.products.length }} вариантов</span>
</div>
<div class="mt-4 grid gap-4 xl:grid-cols-[360px_1fr]">
<aside class="rounded-2xl bg-base-100 p-3">
<img <img
:src="createProductCover(group.typeLabel, group.key)" :src="createProductCover(group.typeLabel, group.key)"
:alt="`Превью группы ${group.typeLabel}`" :alt="`Превью группы ${group.typeLabel}`"
class="h-56 w-full rounded-2xl object-cover" class="h-full min-h-[220px] w-full rounded-[24px] object-cover"
loading="lazy" loading="lazy"
> >
</div>
<div class="mt-3 flex flex-wrap gap-2"> <div class="rounded-[28px] bg-base-100 p-4 md:p-5 xl:col-span-4">
<span class="badge badge-neutral">SKU: {{ selectedProduct(group)?.sku ?? '—' }}</span> <div class="grid h-full gap-4 md:grid-cols-2">
<span class="badge badge-outline">Совпадений: {{ matchingCount(group) }}</span> <fieldset
<span class="badge badge-outline">Короб: {{ boxQuantityLabel(group) }}</span> v-for="field in visibleFields(group)"
</div>
<div class="mt-3 flex items-center justify-between rounded-2xl border border-base-300 bg-base-100 px-2 py-1">
<button
class="btn btn-square btn-sm"
:disabled="selectedQty(group) === 0"
@click="decrementSelected(group)"
>
-
</button>
<span class="min-w-10 text-center font-semibold">{{ selectedQty(group) }}</span>
<button class="btn btn-square btn-sm" @click="incrementSelected(group)">+</button>
</div>
<div class="mt-3 flex flex-wrap gap-2">
<span
v-for="field in parameterFields"
:key="`selected-${group.key}-${field.key}`"
class="badge badge-outline"
>
{{ field.label }}: {{ selectedFieldValue(group, field.key) }}
</span>
</div>
</aside>
<div class="space-y-4">
<div class="grid gap-3 md:grid-cols-2 2xl:grid-cols-3">
<div
v-for="field in parameterFields"
:key="`${group.key}-${field.key}`" :key="`${group.key}-${field.key}`"
class="rounded-2xl bg-base-100 p-3" class="rounded-[24px] bg-base-200/50 p-4"
> >
<p class="text-xs font-semibold uppercase tracking-wide text-base-content/60">{{ field.label }}</p> <div class="flex items-center justify-between gap-3">
<div class="mt-2 flex flex-wrap gap-2"> <legend class="text-sm font-semibold text-[#163624]">{{ field.label }}</legend>
<button <button
v-for="option in getAllFieldOptions(group, field.key)" v-if="getGroupState(group)[field.key] !== null"
:key="`${group.key}-${field.key}-${option}`" class="btn btn-ghost btn-xs rounded-full px-2"
class="btn btn-sm" @click="clearField(group, field.key)"
:class="getGroupState(group)[field.key] === option ? 'btn-neutral' : 'btn-outline'"
@click="updateField(group, field.key, option)"
> >
{{ formatOptionLabel(field.key, option) }} Сбросить
</button> </button>
</div> </div>
<div class="mt-3 flex flex-wrap gap-2">
<input
v-for="option in getAllFieldOptions(group, field.key)"
:key="`${group.key}-${field.key}-${option}`"
class="btn btn-sm rounded-full border-base-300 bg-base-100 text-sm normal-case checked:btn-neutral"
type="radio"
:name="`${group.key}-${field.key}`"
:aria-label="formatOptionLabel(field.key, option)"
:checked="getGroupState(group)[field.key] === option"
:disabled="!isOptionAvailable(group, field.key, option)"
@change="updateField(group, field.key, option)"
>
</div>
</fieldset>
</div>
</div>
<aside class="rounded-[28px] bg-base-100 p-4 md:p-5 xl:col-span-1">
<div class="flex h-full flex-col justify-between gap-4">
<div class="space-y-3">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-base-content/45">Тип товара</p>
<h2 class="mt-2 text-xl font-bold text-[#163624]">{{ group.typeLabel }}</h2>
</div>
<div class="flex flex-wrap gap-2">
<span class="badge badge-outline">{{ group.products.length }} вариантов</span>
<span class="badge badge-outline">Короб: {{ boxQuantityLabel(group) }}</span>
</div>
<div class="rounded-[22px] bg-base-200/70 p-3">
<p class="text-sm font-semibold text-[#163624]">{{ selectionHeadline(group) }}</p>
<p class="mt-1 text-xs leading-5 text-base-content/65">{{ selectionDescription(group) }}</p>
</div>
</div>
<div class="space-y-3">
<div class="rounded-[22px] border border-base-300 bg-base-100 px-2 py-1">
<div class="flex items-center justify-between gap-2">
<button
class="btn btn-square btn-sm"
:disabled="selectedQty(group) === 0"
@click="decrementSelected(group)"
>
-
</button>
<div class="text-center">
<div class="text-[11px] uppercase tracking-[0.16em] text-base-content/45">В корзине</div>
<div class="text-lg font-semibold text-[#163624]">{{ selectedQty(group) }}</div>
</div>
<button
class="btn btn-square btn-sm"
:disabled="!selectedProduct(group)"
@click="incrementSelected(group)"
>
+
</button>
</div>
</div>
<button
class="btn btn-ghost btn-sm w-full rounded-full"
:disabled="requiredKeys(group).every((key) => getGroupState(group)[key] === null)"
@click="clearSelection(group)"
>
Сбросить выбор
</button>
</div> </div>
</div> </div>
</aside>
<p class="text-sm text-base-content/70">Совпадающих вариантов: {{ matchingCount(group) }}</p>
</div>
</div> </div>
<div v-if="getGroupState(group).isExpanded" class="mt-4 overflow-x-auto rounded-2xl bg-base-100 p-2"> <div
v-if="getGroupState(group).isExpanded"
class="mt-4 overflow-x-auto rounded-[28px] bg-base-100 p-2"
>
<table class="table table-zebra"> <table class="table table-zebra">
<thead> <thead>
<tr> <tr>
@@ -481,7 +560,13 @@ function decrementSelected(group: ProductGroup) {
<td>{{ product.quantityPerBox ?? '—' }}</td> <td>{{ product.quantityPerBox ?? '—' }}</td>
<td class="text-right"> <td class="text-right">
<div class="ml-auto flex w-28 items-center justify-between rounded-xl border border-base-300 px-1 py-1"> <div class="ml-auto flex w-28 items-center justify-between rounded-xl border border-base-300 px-1 py-1">
<button class="btn btn-xs btn-square" :disabled="getQuantity(product.id) === 0" @click="decrementProduct(product.id)">-</button> <button
class="btn btn-xs btn-square"
:disabled="getQuantity(product.id) === 0"
@click="decrementProduct(product.id)"
>
-
</button>
<span class="text-sm font-semibold">{{ getQuantity(product.id) }}</span> <span class="text-sm font-semibold">{{ getQuantity(product.id) }}</span>
<button class="btn btn-xs btn-square" @click="incrementProduct(product)">+</button> <button class="btn btn-xs btn-square" @click="incrementProduct(product)">+</button>
</div> </div>
@@ -502,7 +587,13 @@ function decrementSelected(group: ProductGroup) {
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path d="M5 7.5L10 12.5L15 7.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" /> <path
d="M5 7.5L10 12.5L15 7.5"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg> </svg>
<span>{{ getGroupState(group).isExpanded ? 'Свернуть все варианты' : 'Развернуть все варианты' }}</span> <span>{{ getGroupState(group).isExpanded ? 'Свернуть все варианты' : 'Развернуть все варианты' }}</span>
</button> </button>

View File

@@ -404,8 +404,14 @@ export type Product = {
id: Scalars['ID']['output']; id: Scalars['ID']['output'];
isActive: Scalars['Boolean']['output']; isActive: Scalars['Boolean']['output'];
isCustomizable: Scalars['Boolean']['output']; isCustomizable: Scalars['Boolean']['output'];
lengthM?: Maybe<Scalars['Int']['output']>;
name: Scalars['String']['output']; name: Scalars['String']['output'];
productType?: Maybe<Scalars['String']['output']>;
quantityPerBox?: Maybe<Scalars['String']['output']>;
sku: Scalars['String']['output']; sku: Scalars['String']['output'];
sleeveBrand?: Maybe<Scalars['String']['output']>;
thicknessMicron?: Maybe<Scalars['Int']['output']>;
widthMm?: Maybe<Scalars['Int']['output']>;
}; };
export type ProductWarehouseBalance = { export type ProductWarehouseBalance = {
@@ -639,7 +645,7 @@ export type VerifyLoginCodeMutation = { __typename?: 'Mutation', verifyLoginCode
export type ClientProductsQueryVariables = Exact<{ [key: string]: never; }>; export type ClientProductsQueryVariables = Exact<{ [key: string]: never; }>;
export type ClientProductsQuery = { __typename?: 'Query', clientProducts: Array<{ __typename?: 'Product', id: string, sku: string, name: string, description?: string | null, isCustomizable: boolean, availableInWarehouses: Array<{ __typename?: 'ProductWarehouseBalance', availableQty: number, warehouse: { __typename?: 'Warehouse', id: string, code: string, name: string } }> }> }; export type ClientProductsQuery = { __typename?: 'Query', clientProducts: Array<{ __typename?: 'Product', id: string, sku: string, name: string, description?: string | null, productType?: string | null, widthMm?: number | null, lengthM?: number | null, thicknessMicron?: number | null, sleeveBrand?: string | null, quantityPerBox?: string | null, isCustomizable: boolean, availableInWarehouses: Array<{ __typename?: 'ProductWarehouseBalance', availableQty: number, warehouse: { __typename?: 'Warehouse', id: string, code: string, name: string } }> }> };
export type MyMessengerConnectionsQueryVariables = Exact<{ [key: string]: never; }>; export type MyMessengerConnectionsQueryVariables = Exact<{ [key: string]: never; }>;
@@ -920,6 +926,12 @@ export const ClientProductsDocument = gql`
sku sku
name name
description description
productType
widthMm
lengthM
thicknessMicron
sleeveBrand
quantityPerBox
isCustomizable isCustomizable
availableInWarehouses { availableInWarehouses {
availableQty availableQty

View File

@@ -4,6 +4,12 @@ query ClientProducts {
sku sku
name name
description description
productType
widthMm
lengthM
thicknessMicron
sleeveBrand
quantityPerBox
isCustomizable isCustomizable
availableInWarehouses { availableInWarehouses {
availableQty availableQty

View File

@@ -177,6 +177,12 @@ type Product {
sku: String! sku: String!
name: String! name: String!
description: String description: String
productType: String
widthMm: Int
lengthM: Int
thicknessMicron: Int
sleeveBrand: String
quantityPerBox: String
isCustomizable: Boolean! isCustomizable: Boolean!
isActive: Boolean! isActive: Boolean!
availableInWarehouses: [ProductWarehouseBalance!]! availableInWarehouses: [ProductWarehouseBalance!]!