Add catalog settings management
This commit is contained in:
@@ -83,6 +83,14 @@ const managerPageTabs = computed(() => {
|
|||||||
|
|
||||||
if (route.path.startsWith('/admin/settings')) {
|
if (route.path.startsWith('/admin/settings')) {
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
key: 'catalog',
|
||||||
|
label: 'Каталог',
|
||||||
|
active: route.path === '/admin/settings/catalog',
|
||||||
|
to: {
|
||||||
|
path: '/admin/settings/catalog',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'messages',
|
key: 'messages',
|
||||||
label: 'Сообщения',
|
label: 'Сообщения',
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useQuery } from '@vue/apollo-composable';
|
import { useQuery } from '@vue/apollo-composable';
|
||||||
import { ClientProductsDocument, type ClientProductsQuery } from '~/composables/graphql/generated';
|
import {
|
||||||
|
CatalogProductTypeSettingsDocument,
|
||||||
|
ClientProductsDocument,
|
||||||
|
type CatalogProductTypeSettingsQuery,
|
||||||
|
type ClientProductsQuery,
|
||||||
|
} from '~/composables/graphql/generated';
|
||||||
import { useClientCart } from '~/composables/useClientCart';
|
import { useClientCart } from '~/composables/useClientCart';
|
||||||
|
|
||||||
type ProductNode = ClientProductsQuery['clientProducts'][number];
|
type ProductNode = ClientProductsQuery['clientProducts'][number];
|
||||||
|
type CatalogProductTypeSettingNode = CatalogProductTypeSettingsQuery['catalogProductTypeSettings'][number];
|
||||||
type ParamFieldKey = 'widthMm' | 'lengthM' | 'thicknessMicron' | 'sleeveBrand' | 'quantityPerBox' | 'colorTag' | 'labelTag';
|
type ParamFieldKey = 'widthMm' | 'lengthM' | 'thicknessMicron' | 'sleeveBrand' | 'quantityPerBox' | 'colorTag' | 'labelTag';
|
||||||
type ParamValue = number | string;
|
type ParamValue = number | string;
|
||||||
|
|
||||||
@@ -35,6 +41,16 @@ type GroupState = {
|
|||||||
const COLOR_TAGS = ['прозрачный', 'коричневый', 'белый', 'черный', 'желтый', 'зеленый', 'красный', 'синий', 'оранжевый', 'красно-белый'];
|
const COLOR_TAGS = ['прозрачный', 'коричневый', 'белый', 'черный', 'желтый', 'зеленый', 'красный', 'синий', 'оранжевый', 'красно-белый'];
|
||||||
const LABEL_TAGS = ['хрупкое', 'подарок', 'акция'];
|
const LABEL_TAGS = ['хрупкое', 'подарок', 'акция'];
|
||||||
const PARAM_KEYS: ParamFieldKey[] = ['widthMm', 'lengthM', 'thicknessMicron', 'sleeveBrand', 'quantityPerBox', 'colorTag', 'labelTag'];
|
const PARAM_KEYS: ParamFieldKey[] = ['widthMm', 'lengthM', 'thicknessMicron', 'sleeveBrand', 'quantityPerBox', 'colorTag', 'labelTag'];
|
||||||
|
const DEFAULT_CATALOG_PRODUCT_TYPE_SETTING: CatalogProductTypeSettingNode = {
|
||||||
|
productType: '',
|
||||||
|
showQuantityPerBox: false,
|
||||||
|
allowCustomLength: false,
|
||||||
|
customLengthMinM: null,
|
||||||
|
customLengthMaxM: null,
|
||||||
|
customLengthStepM: null,
|
||||||
|
allowCustomSleeveBrand: false,
|
||||||
|
allowCustomLabel: false,
|
||||||
|
};
|
||||||
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: 'Длина' },
|
||||||
@@ -51,11 +67,22 @@ const coverPresets = [
|
|||||||
['#e8f5ec', '#b2e0c6', '#7dd0a9'],
|
['#e8f5ec', '#b2e0c6', '#7dd0a9'],
|
||||||
];
|
];
|
||||||
|
|
||||||
const { result, loading, error } = useQuery(ClientProductsDocument);
|
const productsQuery = useQuery(ClientProductsDocument);
|
||||||
|
const catalogSettingsQuery = useQuery(CatalogProductTypeSettingsDocument);
|
||||||
const search = ref('');
|
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();
|
||||||
|
|
||||||
|
const loading = computed(() => productsQuery.loading.value || catalogSettingsQuery.loading.value);
|
||||||
|
const error = computed(() => productsQuery.error.value || catalogSettingsQuery.error.value);
|
||||||
|
|
||||||
|
const catalogSettingsByType = computed<Record<string, CatalogProductTypeSettingNode>>(() => (
|
||||||
|
Object.fromEntries(
|
||||||
|
(catalogSettingsQuery.result.value?.catalogProductTypeSettings ?? [])
|
||||||
|
.map((setting) => [setting.productType, setting]),
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
function normalizeText(value: string | null | undefined) {
|
function normalizeText(value: string | null | undefined) {
|
||||||
return String(value ?? '').replaceAll(/\s+/g, ' ').trim();
|
return String(value ?? '').replaceAll(/\s+/g, ' ').trim();
|
||||||
}
|
}
|
||||||
@@ -135,7 +162,7 @@ function compareProducts(a: ParsedProduct, b: ParsedProduct) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parsedProducts = computed<ParsedProduct[]>(() => {
|
const parsedProducts = computed<ParsedProduct[]>(() => {
|
||||||
const list = result.value?.clientProducts ?? [];
|
const list = productsQuery.result.value?.clientProducts ?? [];
|
||||||
const query = search.value.trim().toLowerCase();
|
const query = search.value.trim().toLowerCase();
|
||||||
|
|
||||||
return list
|
return list
|
||||||
@@ -228,8 +255,23 @@ function getAllFieldOptions(group: ProductGroup, field: ParamFieldKey) {
|
|||||||
return sortParamValues([...values]);
|
return sortParamValues([...values]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function groupCatalogSetting(group: ProductGroup) {
|
||||||
|
return catalogSettingsByType.value[group.typeLabel] ?? {
|
||||||
|
...DEFAULT_CATALOG_PRODUCT_TYPE_SETTING,
|
||||||
|
productType: group.typeLabel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function visibleFields(group: ProductGroup) {
|
function visibleFields(group: ProductGroup) {
|
||||||
return parameterFields.filter((field) => getAllFieldOptions(group, field.key).length > 1);
|
const catalogSetting = groupCatalogSetting(group);
|
||||||
|
|
||||||
|
return parameterFields.filter((field) => {
|
||||||
|
if (field.key === 'quantityPerBox' && !catalogSetting.showQuantityPerBox) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getAllFieldOptions(group, field.key).length > 1;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function requiredKeys(group: ProductGroup) {
|
function requiredKeys(group: ProductGroup) {
|
||||||
@@ -301,7 +343,7 @@ function selectedProduct(group: ProductGroup) {
|
|||||||
const state = getGroupState(group);
|
const state = getGroupState(group);
|
||||||
|
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
return group.products.length === 1 ? group.products[0] : null;
|
return group.products[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keys.some((key) => state[key] === null)) {
|
if (keys.some((key) => state[key] === null)) {
|
||||||
@@ -309,7 +351,7 @@ function selectedProduct(group: ProductGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const matches = group.products.filter((product) => matchesProductState(product, state, keys));
|
const matches = group.products.filter((product) => matchesProductState(product, state, keys));
|
||||||
return matches.length === 1 ? matches[0] : null;
|
return matches[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatOptionLabel(field: ParamFieldKey, value: ParamValue) {
|
function formatOptionLabel(field: ParamFieldKey, value: ParamValue) {
|
||||||
@@ -441,6 +483,25 @@ function variantCountLabel(count: number) {
|
|||||||
return `${count} вариантов`;
|
return `${count} вариантов`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function customizationNotes(group: ProductGroup) {
|
||||||
|
const setting = groupCatalogSetting(group);
|
||||||
|
const notes: string[] = [];
|
||||||
|
|
||||||
|
if (setting.allowCustomLength && setting.customLengthMinM && setting.customLengthMaxM && setting.customLengthStepM) {
|
||||||
|
notes.push(`Своя длина ${setting.customLengthMinM}-${setting.customLengthMaxM} м, шаг ${setting.customLengthStepM} м`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setting.allowCustomSleeveBrand) {
|
||||||
|
notes.push('Своя втулка');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setting.allowCustomLabel) {
|
||||||
|
notes.push('Своя надпись');
|
||||||
|
}
|
||||||
|
|
||||||
|
return notes;
|
||||||
|
}
|
||||||
|
|
||||||
function toggleExpanded(group: ProductGroup) {
|
function toggleExpanded(group: ProductGroup) {
|
||||||
getGroupState(group).isExpanded = !getGroupState(group).isExpanded;
|
getGroupState(group).isExpanded = !getGroupState(group).isExpanded;
|
||||||
}
|
}
|
||||||
@@ -513,6 +574,15 @@ function decrementSelected(group: ProductGroup) {
|
|||||||
<div class="p-4 md:p-5 xl:col-span-4">
|
<div class="p-4 md:p-5 xl:col-span-4">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h2 class="text-2xl font-bold text-[#163624]">{{ group.typeLabel }}</h2>
|
<h2 class="text-2xl font-bold text-[#163624]">{{ group.typeLabel }}</h2>
|
||||||
|
<div v-if="customizationNotes(group).length" class="mt-3 flex flex-wrap gap-2">
|
||||||
|
<span
|
||||||
|
v-for="note in customizationNotes(group)"
|
||||||
|
:key="`${group.key}-${note}`"
|
||||||
|
class="rounded-full bg-[#eef8f1] px-3 py-1 text-xs font-semibold text-[#15613d]"
|
||||||
|
>
|
||||||
|
{{ note }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid content-start gap-4 md:grid-cols-2 2xl:grid-cols-3">
|
<div class="grid content-start gap-4 md:grid-cols-2 2xl:grid-cols-3">
|
||||||
@@ -599,7 +669,7 @@ function decrementSelected(group: ProductGroup) {
|
|||||||
<th class="border-b border-base-300">Длина</th>
|
<th class="border-b border-base-300">Длина</th>
|
||||||
<th class="border-b border-base-300">Толщина</th>
|
<th class="border-b border-base-300">Толщина</th>
|
||||||
<th class="border-b border-base-300">Втулка</th>
|
<th class="border-b border-base-300">Втулка</th>
|
||||||
<th class="border-b border-base-300">Короб</th>
|
<th v-if="groupCatalogSetting(group).showQuantityPerBox" class="border-b border-base-300">Короб</th>
|
||||||
<th class="border-b border-base-300 text-right">Действие</th>
|
<th class="border-b border-base-300 text-right">Действие</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -610,7 +680,7 @@ function decrementSelected(group: ProductGroup) {
|
|||||||
<td class="border-b border-base-200">{{ product.lengthM ?? '—' }}</td>
|
<td class="border-b border-base-200">{{ product.lengthM ?? '—' }}</td>
|
||||||
<td class="border-b border-base-200">{{ product.thicknessMicron ?? '—' }}</td>
|
<td class="border-b border-base-200">{{ product.thicknessMicron ?? '—' }}</td>
|
||||||
<td class="border-b border-base-200">{{ product.sleeveBrand ?? '—' }}</td>
|
<td class="border-b border-base-200">{{ product.sleeveBrand ?? '—' }}</td>
|
||||||
<td class="border-b border-base-200">{{ product.quantityPerBox ?? '—' }}</td>
|
<td v-if="groupCatalogSetting(group).showQuantityPerBox" class="border-b border-base-200">{{ product.quantityPerBox ?? '—' }}</td>
|
||||||
<td class="border-b border-base-200 text-right">
|
<td class="border-b border-base-200 text-right">
|
||||||
<button
|
<button
|
||||||
v-if="getQuantity(product.id) === 0"
|
v-if="getQuantity(product.id) === 0"
|
||||||
|
|||||||
@@ -89,6 +89,18 @@ export type CartItem = {
|
|||||||
updatedAt: Scalars['DateTime']['output'];
|
updatedAt: Scalars['DateTime']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CatalogProductTypeSetting = {
|
||||||
|
__typename?: 'CatalogProductTypeSetting';
|
||||||
|
allowCustomLabel: Scalars['Boolean']['output'];
|
||||||
|
allowCustomLength: Scalars['Boolean']['output'];
|
||||||
|
allowCustomSleeveBrand: Scalars['Boolean']['output'];
|
||||||
|
customLengthMaxM?: Maybe<Scalars['Int']['output']>;
|
||||||
|
customLengthMinM?: Maybe<Scalars['Int']['output']>;
|
||||||
|
customLengthStepM?: Maybe<Scalars['Int']['output']>;
|
||||||
|
productType: Scalars['String']['output'];
|
||||||
|
showQuantityPerBox: Scalars['Boolean']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
export type Company = {
|
export type Company = {
|
||||||
__typename?: 'Company';
|
__typename?: 'Company';
|
||||||
id: Scalars['ID']['output'];
|
id: Scalars['ID']['output'];
|
||||||
@@ -328,6 +340,7 @@ export type Mutation = {
|
|||||||
submitCalculationOrder: Order;
|
submitCalculationOrder: Order;
|
||||||
submitReadyOrder: Order;
|
submitReadyOrder: Order;
|
||||||
updateCartItemQuantity: Cart;
|
updateCartItemQuantity: Cart;
|
||||||
|
upsertCatalogProductTypeSetting: CatalogProductTypeSetting;
|
||||||
upsertMyCounterpartyProfile: CounterpartyProfile;
|
upsertMyCounterpartyProfile: CounterpartyProfile;
|
||||||
verifyLoginCode: AuthSession;
|
verifyLoginCode: AuthSession;
|
||||||
};
|
};
|
||||||
@@ -467,6 +480,11 @@ export type MutationUpdateCartItemQuantityArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationUpsertCatalogProductTypeSettingArgs = {
|
||||||
|
input: UpsertCatalogProductTypeSettingInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationUpsertMyCounterpartyProfileArgs = {
|
export type MutationUpsertMyCounterpartyProfileArgs = {
|
||||||
input: UpsertMyCounterpartyProfileInput;
|
input: UpsertMyCounterpartyProfileInput;
|
||||||
};
|
};
|
||||||
@@ -592,6 +610,7 @@ export type ProductWarehouseBalance = {
|
|||||||
|
|
||||||
export type Query = {
|
export type Query = {
|
||||||
__typename?: 'Query';
|
__typename?: 'Query';
|
||||||
|
catalogProductTypeSettings: Array<CatalogProductTypeSetting>;
|
||||||
clientProducts: Array<Product>;
|
clientProducts: Array<Product>;
|
||||||
healthcheck: Scalars['String']['output'];
|
healthcheck: Scalars['String']['output'];
|
||||||
integrationSyncDashboard: IntegrationSyncDashboard;
|
integrationSyncDashboard: IntegrationSyncDashboard;
|
||||||
@@ -763,6 +782,17 @@ export type UpdateCartItemQuantityInput = {
|
|||||||
quantity: Scalars['Float']['input'];
|
quantity: Scalars['Float']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpsertCatalogProductTypeSettingInput = {
|
||||||
|
allowCustomLabel: Scalars['Boolean']['input'];
|
||||||
|
allowCustomLength: Scalars['Boolean']['input'];
|
||||||
|
allowCustomSleeveBrand: Scalars['Boolean']['input'];
|
||||||
|
customLengthMaxM?: InputMaybe<Scalars['Int']['input']>;
|
||||||
|
customLengthMinM?: InputMaybe<Scalars['Int']['input']>;
|
||||||
|
customLengthStepM?: InputMaybe<Scalars['Int']['input']>;
|
||||||
|
productType: Scalars['String']['input'];
|
||||||
|
showQuantityPerBox: Scalars['Boolean']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
export type UpsertMyCounterpartyProfileInput = {
|
export type UpsertMyCounterpartyProfileInput = {
|
||||||
bankName: Scalars['String']['input'];
|
bankName: Scalars['String']['input'];
|
||||||
bik: Scalars['String']['input'];
|
bik: Scalars['String']['input'];
|
||||||
@@ -1125,11 +1155,23 @@ export type UpsertMyCounterpartyProfileMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type UpsertMyCounterpartyProfileMutation = { __typename?: 'Mutation', upsertMyCounterpartyProfile: { __typename?: 'CounterpartyProfile', id: string, companyName: string, companyFullName: string, inn: string, kpp?: string | null, ogrn?: string | null, legalAddress: string, bankName: string, bik: string, correspondentAccount: string, checkingAccount: string, signerFullName: string, signerPosition: string, signerBasis: string, isComplete: boolean, updatedAt: any } };
|
export type UpsertMyCounterpartyProfileMutation = { __typename?: 'Mutation', upsertMyCounterpartyProfile: { __typename?: 'CounterpartyProfile', id: string, companyName: string, companyFullName: string, inn: string, kpp?: string | null, ogrn?: string | null, legalAddress: string, bankName: string, bik: string, correspondentAccount: string, checkingAccount: string, signerFullName: string, signerPosition: string, signerBasis: string, isComplete: boolean, updatedAt: any } };
|
||||||
|
|
||||||
|
export type CatalogProductTypeSettingsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
|
export type CatalogProductTypeSettingsQuery = { __typename?: 'Query', catalogProductTypeSettings: Array<{ __typename?: 'CatalogProductTypeSetting', productType: string, showQuantityPerBox: boolean, allowCustomLength: boolean, customLengthMinM?: number | null, customLengthMaxM?: number | null, customLengthStepM?: number | null, allowCustomSleeveBrand: boolean, allowCustomLabel: boolean }> };
|
||||||
|
|
||||||
export type IntegrationSyncDashboardQueryVariables = Exact<{ [key: string]: never; }>;
|
export type IntegrationSyncDashboardQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type IntegrationSyncDashboardQuery = { __typename?: 'Query', integrationSyncDashboard: { __typename?: 'IntegrationSyncDashboard', generatedAt: any, lastActivityAt?: any | null, totalOrders: number, totalProducts: number, totalClients: number, items: Array<{ __typename?: 'IntegrationSyncItem', id: string, title: string, description: string, source: string, syncedCount: number, lastSyncedAt?: any | null, status: string, note: string }> } };
|
export type IntegrationSyncDashboardQuery = { __typename?: 'Query', integrationSyncDashboard: { __typename?: 'IntegrationSyncDashboard', generatedAt: any, lastActivityAt?: any | null, totalOrders: number, totalProducts: number, totalClients: number, items: Array<{ __typename?: 'IntegrationSyncItem', id: string, title: string, description: string, source: string, syncedCount: number, lastSyncedAt?: any | null, status: string, note: string }> } };
|
||||||
|
|
||||||
|
export type UpsertCatalogProductTypeSettingMutationVariables = Exact<{
|
||||||
|
input: UpsertCatalogProductTypeSettingInput;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type UpsertCatalogProductTypeSettingMutation = { __typename?: 'Mutation', upsertCatalogProductTypeSetting: { __typename?: 'CatalogProductTypeSetting', productType: string, showQuantityPerBox: boolean, allowCustomLength: boolean, customLengthMinM?: number | null, customLengthMaxM?: number | null, customLengthStepM?: number | null, allowCustomSleeveBrand: boolean, allowCustomLabel: boolean } };
|
||||||
|
|
||||||
|
|
||||||
export const ConsumeLoginTokenDocument = gql`
|
export const ConsumeLoginTokenDocument = gql`
|
||||||
mutation ConsumeLoginToken($token: String!) {
|
mutation ConsumeLoginToken($token: String!) {
|
||||||
@@ -2927,6 +2969,40 @@ export function useUpsertMyCounterpartyProfileMutation(options: VueApolloComposa
|
|||||||
return VueApolloComposable.useMutation<UpsertMyCounterpartyProfileMutation, UpsertMyCounterpartyProfileMutationVariables>(UpsertMyCounterpartyProfileDocument, options);
|
return VueApolloComposable.useMutation<UpsertMyCounterpartyProfileMutation, UpsertMyCounterpartyProfileMutationVariables>(UpsertMyCounterpartyProfileDocument, options);
|
||||||
}
|
}
|
||||||
export type UpsertMyCounterpartyProfileMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<UpsertMyCounterpartyProfileMutation, UpsertMyCounterpartyProfileMutationVariables>;
|
export type UpsertMyCounterpartyProfileMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<UpsertMyCounterpartyProfileMutation, UpsertMyCounterpartyProfileMutationVariables>;
|
||||||
|
export const CatalogProductTypeSettingsDocument = gql`
|
||||||
|
query CatalogProductTypeSettings {
|
||||||
|
catalogProductTypeSettings {
|
||||||
|
productType
|
||||||
|
showQuantityPerBox
|
||||||
|
allowCustomLength
|
||||||
|
customLengthMinM
|
||||||
|
customLengthMaxM
|
||||||
|
customLengthStepM
|
||||||
|
allowCustomSleeveBrand
|
||||||
|
allowCustomLabel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useCatalogProductTypeSettingsQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a Vue component, call `useCatalogProductTypeSettingsQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useCatalogProductTypeSettingsQuery` returns an object from Apollo Client that contains result, loading and error properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param options that will be passed into the query, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/query.html#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { result, loading, error } = useCatalogProductTypeSettingsQuery();
|
||||||
|
*/
|
||||||
|
export function useCatalogProductTypeSettingsQuery(options: VueApolloComposable.UseQueryOptions<CatalogProductTypeSettingsQuery, CatalogProductTypeSettingsQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<CatalogProductTypeSettingsQuery, CatalogProductTypeSettingsQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<CatalogProductTypeSettingsQuery, CatalogProductTypeSettingsQueryVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useQuery<CatalogProductTypeSettingsQuery, CatalogProductTypeSettingsQueryVariables>(CatalogProductTypeSettingsDocument, {}, options);
|
||||||
|
}
|
||||||
|
export function useCatalogProductTypeSettingsLazyQuery(options: VueApolloComposable.UseQueryOptions<CatalogProductTypeSettingsQuery, CatalogProductTypeSettingsQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<CatalogProductTypeSettingsQuery, CatalogProductTypeSettingsQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<CatalogProductTypeSettingsQuery, CatalogProductTypeSettingsQueryVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useLazyQuery<CatalogProductTypeSettingsQuery, CatalogProductTypeSettingsQueryVariables>(CatalogProductTypeSettingsDocument, {}, options);
|
||||||
|
}
|
||||||
|
export type CatalogProductTypeSettingsQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<CatalogProductTypeSettingsQuery, CatalogProductTypeSettingsQueryVariables>;
|
||||||
export const IntegrationSyncDashboardDocument = gql`
|
export const IntegrationSyncDashboardDocument = gql`
|
||||||
query IntegrationSyncDashboard {
|
query IntegrationSyncDashboard {
|
||||||
integrationSyncDashboard {
|
integrationSyncDashboard {
|
||||||
@@ -2967,4 +3043,40 @@ export function useIntegrationSyncDashboardQuery(options: VueApolloComposable.Us
|
|||||||
export function useIntegrationSyncDashboardLazyQuery(options: VueApolloComposable.UseQueryOptions<IntegrationSyncDashboardQuery, IntegrationSyncDashboardQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<IntegrationSyncDashboardQuery, IntegrationSyncDashboardQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<IntegrationSyncDashboardQuery, IntegrationSyncDashboardQueryVariables>> = {}) {
|
export function useIntegrationSyncDashboardLazyQuery(options: VueApolloComposable.UseQueryOptions<IntegrationSyncDashboardQuery, IntegrationSyncDashboardQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<IntegrationSyncDashboardQuery, IntegrationSyncDashboardQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<IntegrationSyncDashboardQuery, IntegrationSyncDashboardQueryVariables>> = {}) {
|
||||||
return VueApolloComposable.useLazyQuery<IntegrationSyncDashboardQuery, IntegrationSyncDashboardQueryVariables>(IntegrationSyncDashboardDocument, {}, options);
|
return VueApolloComposable.useLazyQuery<IntegrationSyncDashboardQuery, IntegrationSyncDashboardQueryVariables>(IntegrationSyncDashboardDocument, {}, options);
|
||||||
}
|
}
|
||||||
export type IntegrationSyncDashboardQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<IntegrationSyncDashboardQuery, IntegrationSyncDashboardQueryVariables>;
|
export type IntegrationSyncDashboardQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<IntegrationSyncDashboardQuery, IntegrationSyncDashboardQueryVariables>;
|
||||||
|
export const UpsertCatalogProductTypeSettingDocument = gql`
|
||||||
|
mutation UpsertCatalogProductTypeSetting($input: UpsertCatalogProductTypeSettingInput!) {
|
||||||
|
upsertCatalogProductTypeSetting(input: $input) {
|
||||||
|
productType
|
||||||
|
showQuantityPerBox
|
||||||
|
allowCustomLength
|
||||||
|
customLengthMinM
|
||||||
|
customLengthMaxM
|
||||||
|
customLengthStepM
|
||||||
|
allowCustomSleeveBrand
|
||||||
|
allowCustomLabel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useUpsertCatalogProductTypeSettingMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useUpsertCatalogProductTypeSettingMutation` within a Vue component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useUpsertCatalogProductTypeSettingMutation` returns an object that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - Several other properties: https://v4.apollo.vuejs.org/api/use-mutation.html#return
|
||||||
|
*
|
||||||
|
* @param options that will be passed into the mutation, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/mutation.html#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { mutate, loading, error, onDone } = useUpsertCatalogProductTypeSettingMutation({
|
||||||
|
* variables: {
|
||||||
|
* input: // value for 'input'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useUpsertCatalogProductTypeSettingMutation(options: VueApolloComposable.UseMutationOptions<UpsertCatalogProductTypeSettingMutation, UpsertCatalogProductTypeSettingMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<UpsertCatalogProductTypeSettingMutation, UpsertCatalogProductTypeSettingMutationVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useMutation<UpsertCatalogProductTypeSettingMutation, UpsertCatalogProductTypeSettingMutationVariables>(UpsertCatalogProductTypeSettingDocument, options);
|
||||||
|
}
|
||||||
|
export type UpsertCatalogProductTypeSettingMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<UpsertCatalogProductTypeSettingMutation, UpsertCatalogProductTypeSettingMutationVariables>;
|
||||||
257
app/pages/catalog-settings.vue
Normal file
257
app/pages/catalog-settings.vue
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useMutation, useQuery } from '@vue/apollo-composable';
|
||||||
|
import {
|
||||||
|
CatalogProductTypeSettingsDocument,
|
||||||
|
UpsertCatalogProductTypeSettingDocument,
|
||||||
|
type CatalogProductTypeSettingsQuery,
|
||||||
|
} from '~/composables/graphql/generated';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ['manager-only'],
|
||||||
|
path: '/admin/settings/catalog',
|
||||||
|
});
|
||||||
|
|
||||||
|
type CatalogSettingItem = CatalogProductTypeSettingsQuery['catalogProductTypeSettings'][number];
|
||||||
|
type CatalogSettingForm = {
|
||||||
|
productType: string;
|
||||||
|
showQuantityPerBox: boolean;
|
||||||
|
allowCustomLength: boolean;
|
||||||
|
customLengthMinM: string;
|
||||||
|
customLengthMaxM: string;
|
||||||
|
customLengthStepM: string;
|
||||||
|
allowCustomSleeveBrand: boolean;
|
||||||
|
allowCustomLabel: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const settingsQuery = useQuery(CatalogProductTypeSettingsDocument);
|
||||||
|
const saveSettingMutation = useMutation(UpsertCatalogProductTypeSettingDocument, { throws: 'never' });
|
||||||
|
|
||||||
|
const forms = reactive<Record<string, CatalogSettingForm>>({});
|
||||||
|
const savingState = reactive<Record<string, boolean>>({});
|
||||||
|
const successMessage = reactive<Record<string, string>>({});
|
||||||
|
const errorMessage = reactive<Record<string, string>>({});
|
||||||
|
|
||||||
|
const settings = computed<CatalogSettingItem[]>(() => settingsQuery.result.value?.catalogProductTypeSettings ?? []);
|
||||||
|
|
||||||
|
function toInputValue(value: number | null | undefined) {
|
||||||
|
return value == null ? '' : String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createForm(item: CatalogSettingItem): CatalogSettingForm {
|
||||||
|
return {
|
||||||
|
productType: item.productType,
|
||||||
|
showQuantityPerBox: item.showQuantityPerBox,
|
||||||
|
allowCustomLength: item.allowCustomLength,
|
||||||
|
customLengthMinM: toInputValue(item.customLengthMinM),
|
||||||
|
customLengthMaxM: toInputValue(item.customLengthMaxM),
|
||||||
|
customLengthStepM: toInputValue(item.customLengthStepM),
|
||||||
|
allowCustomSleeveBrand: item.allowCustomSleeveBrand,
|
||||||
|
allowCustomLabel: item.allowCustomLabel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOptionalInteger(value: string) {
|
||||||
|
const normalized = value.trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
settings,
|
||||||
|
(items) => {
|
||||||
|
const activeTypes = new Set(items.map((item) => item.productType));
|
||||||
|
|
||||||
|
for (const productType of Object.keys(forms)) {
|
||||||
|
if (!activeTypes.has(productType)) {
|
||||||
|
Reflect.deleteProperty(forms, productType);
|
||||||
|
Reflect.deleteProperty(savingState, productType);
|
||||||
|
Reflect.deleteProperty(successMessage, productType);
|
||||||
|
Reflect.deleteProperty(errorMessage, productType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
forms[item.productType] = createForm(item);
|
||||||
|
savingState[item.productType] ??= false;
|
||||||
|
successMessage[item.productType] ??= '';
|
||||||
|
errorMessage[item.productType] ??= '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
async function saveProductTypeSetting(productType: string) {
|
||||||
|
const form = forms[productType];
|
||||||
|
if (!form) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
successMessage[productType] = '';
|
||||||
|
errorMessage[productType] = '';
|
||||||
|
savingState[productType] = true;
|
||||||
|
|
||||||
|
const result = await saveSettingMutation.mutate({
|
||||||
|
input: {
|
||||||
|
productType: form.productType,
|
||||||
|
showQuantityPerBox: form.showQuantityPerBox,
|
||||||
|
allowCustomLength: form.allowCustomLength,
|
||||||
|
customLengthMinM: form.allowCustomLength ? parseOptionalInteger(form.customLengthMinM) : null,
|
||||||
|
customLengthMaxM: form.allowCustomLength ? parseOptionalInteger(form.customLengthMaxM) : null,
|
||||||
|
customLengthStepM: form.allowCustomLength ? parseOptionalInteger(form.customLengthStepM) : null,
|
||||||
|
allowCustomSleeveBrand: form.allowCustomSleeveBrand,
|
||||||
|
allowCustomLabel: form.allowCustomLabel,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
savingState[productType] = false;
|
||||||
|
|
||||||
|
if (!result?.data?.upsertCatalogProductTypeSetting) {
|
||||||
|
errorMessage[productType] = saveSettingMutation.error.value?.message || 'Не удалось сохранить настройки.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
forms[productType] = createForm(result.data.upsertCatalogProductTypeSetting);
|
||||||
|
successMessage[productType] = 'Настройки сохранены.';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="space-y-6">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h1 class="text-3xl font-extrabold text-[#0f2f20]">Каталог</h1>
|
||||||
|
<p class="text-sm leading-6 text-[#557562]">
|
||||||
|
Правила конструктора по каждому типу товара: длина, своя втулка, своя надпись и видимость короба.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="settingsQuery.loading.value" class="manager-empty-state">
|
||||||
|
Загружаем настройки каталога...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="settings.length === 0" class="manager-empty-state">
|
||||||
|
Типы товаров пока не появились в каталоге.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="grid gap-4 xl:grid-cols-2">
|
||||||
|
<article
|
||||||
|
v-for="item in settings"
|
||||||
|
:key="item.productType"
|
||||||
|
class="rounded-[28px] bg-white p-5 shadow-[0_18px_38px_rgba(18,56,36,0.08)]"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h2 class="text-xl font-bold text-[#123824]">{{ item.productType }}</h2>
|
||||||
|
<p class="text-sm text-[#557562]">
|
||||||
|
Определяет, какие параметры клиент сможет менять на витрине для этого типа товара.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-3 md:grid-cols-2">
|
||||||
|
<label class="surface-card flex items-start gap-3 rounded-[22px] p-4">
|
||||||
|
<input v-model="forms[item.productType].allowCustomLength" type="checkbox" class="toggle toggle-success mt-1">
|
||||||
|
<span>
|
||||||
|
<span class="block font-semibold text-[#123824]">Своя длина</span>
|
||||||
|
<span class="mt-1 block text-sm text-[#557562]">Разрешить длину вне стандартного списка.</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="surface-card flex items-start gap-3 rounded-[22px] p-4">
|
||||||
|
<input v-model="forms[item.productType].allowCustomSleeveBrand" type="checkbox" class="toggle toggle-success mt-1">
|
||||||
|
<span>
|
||||||
|
<span class="block font-semibold text-[#123824]">Своя втулка</span>
|
||||||
|
<span class="mt-1 block text-sm text-[#557562]">Клиент сможет запросить втулку со своим вариантом.</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="surface-card flex items-start gap-3 rounded-[22px] p-4">
|
||||||
|
<input v-model="forms[item.productType].allowCustomLabel" type="checkbox" class="toggle toggle-success mt-1">
|
||||||
|
<span>
|
||||||
|
<span class="block font-semibold text-[#123824]">Своя надпись</span>
|
||||||
|
<span class="mt-1 block text-sm text-[#557562]">Разрешить индивидуальную надпись или маркировку.</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="surface-card flex items-start gap-3 rounded-[22px] p-4">
|
||||||
|
<input v-model="forms[item.productType].showQuantityPerBox" type="checkbox" class="toggle toggle-success mt-1">
|
||||||
|
<span>
|
||||||
|
<span class="block font-semibold text-[#123824]">Показывать короб</span>
|
||||||
|
<span class="mt-1 block text-sm text-[#557562]">Если выключено, параметр короба скрывается из клиентского каталога.</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-[24px] bg-[#f7fbf8] p-4">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-bold uppercase tracking-[0.12em] text-[#355947]">Диапазон длины</h3>
|
||||||
|
<p class="mt-1 text-sm text-[#557562]">Минимум, максимум и шаг для пользовательской длины.</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="rounded-full px-3 py-1 text-xs font-bold uppercase tracking-[0.12em]"
|
||||||
|
:class="forms[item.productType].allowCustomLength ? 'bg-[#e8f5ec] text-[#1c6b45]' : 'bg-[#eef2ef] text-[#6b7f71]'"
|
||||||
|
>
|
||||||
|
{{ forms[item.productType].allowCustomLength ? 'Включено' : 'Выключено' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 grid gap-3 md:grid-cols-3">
|
||||||
|
<label class="space-y-2">
|
||||||
|
<span class="text-sm font-semibold text-[#123824]">Мин. длина, м</span>
|
||||||
|
<input
|
||||||
|
v-model="forms[item.productType].customLengthMinM"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
class="input manager-field w-full"
|
||||||
|
:disabled="!forms[item.productType].allowCustomLength"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="space-y-2">
|
||||||
|
<span class="text-sm font-semibold text-[#123824]">Макс. длина, м</span>
|
||||||
|
<input
|
||||||
|
v-model="forms[item.productType].customLengthMaxM"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
class="input manager-field w-full"
|
||||||
|
:disabled="!forms[item.productType].allowCustomLength"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="space-y-2">
|
||||||
|
<span class="text-sm font-semibold text-[#123824]">Шаг, м</span>
|
||||||
|
<input
|
||||||
|
v-model="forms[item.productType].customLengthStepM"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
class="input manager-field w-full"
|
||||||
|
:disabled="!forms[item.productType].allowCustomLength"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div class="space-y-1 text-sm">
|
||||||
|
<p v-if="successMessage[item.productType]" class="font-semibold text-[#1c6b45]">{{ successMessage[item.productType] }}</p>
|
||||||
|
<p v-if="errorMessage[item.productType]" class="font-semibold text-[#c4472d]">{{ errorMessage[item.productType] }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn h-11 rounded-full border-0 bg-[#139957] px-5 text-sm font-semibold text-white hover:bg-[#0d854a]"
|
||||||
|
:disabled="savingState[item.productType]"
|
||||||
|
@click="saveProductTypeSetting(item.productType)"
|
||||||
|
>
|
||||||
|
{{ savingState[item.productType] ? 'Сохраняем…' : 'Сохранить' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
query CatalogProductTypeSettings {
|
||||||
|
catalogProductTypeSettings {
|
||||||
|
productType
|
||||||
|
showQuantityPerBox
|
||||||
|
allowCustomLength
|
||||||
|
customLengthMinM
|
||||||
|
customLengthMaxM
|
||||||
|
customLengthStepM
|
||||||
|
allowCustomSleeveBrand
|
||||||
|
allowCustomLabel
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
mutation UpsertCatalogProductTypeSetting($input: UpsertCatalogProductTypeSettingInput!) {
|
||||||
|
upsertCatalogProductTypeSetting(input: $input) {
|
||||||
|
productType
|
||||||
|
showQuantityPerBox
|
||||||
|
allowCustomLength
|
||||||
|
customLengthMinM
|
||||||
|
customLengthMaxM
|
||||||
|
customLengthStepM
|
||||||
|
allowCustomSleeveBrand
|
||||||
|
allowCustomLabel
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -248,6 +248,17 @@ type Product {
|
|||||||
availableInWarehouses: [ProductWarehouseBalance!]!
|
availableInWarehouses: [ProductWarehouseBalance!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CatalogProductTypeSetting {
|
||||||
|
productType: String!
|
||||||
|
showQuantityPerBox: Boolean!
|
||||||
|
allowCustomLength: Boolean!
|
||||||
|
customLengthMinM: Int
|
||||||
|
customLengthMaxM: Int
|
||||||
|
customLengthStepM: Int
|
||||||
|
allowCustomSleeveBrand: Boolean!
|
||||||
|
allowCustomLabel: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
type CartItem {
|
type CartItem {
|
||||||
id: ID!
|
id: ID!
|
||||||
productId: ID!
|
productId: ID!
|
||||||
@@ -411,6 +422,7 @@ type Query {
|
|||||||
integrationSyncDashboard: IntegrationSyncDashboard!
|
integrationSyncDashboard: IntegrationSyncDashboard!
|
||||||
managerNotificationHistory(userId: ID!, channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
|
managerNotificationHistory(userId: ID!, channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
|
||||||
clientProducts: [Product!]!
|
clientProducts: [Product!]!
|
||||||
|
catalogProductTypeSettings: [CatalogProductTypeSetting!]!
|
||||||
order(id: ID!): Order
|
order(id: ID!): Order
|
||||||
myOrders: [Order!]!
|
myOrders: [Order!]!
|
||||||
myCurrentOrders: [Order!]!
|
myCurrentOrders: [Order!]!
|
||||||
@@ -491,6 +503,17 @@ input UpdateCartItemQuantityInput {
|
|||||||
quantity: Float!
|
quantity: Float!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input UpsertCatalogProductTypeSettingInput {
|
||||||
|
productType: String!
|
||||||
|
showQuantityPerBox: Boolean!
|
||||||
|
allowCustomLength: Boolean!
|
||||||
|
customLengthMinM: Int
|
||||||
|
customLengthMaxM: Int
|
||||||
|
customLengthStepM: Int
|
||||||
|
allowCustomSleeveBrand: Boolean!
|
||||||
|
allowCustomLabel: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
input ReadyOrderItemInput {
|
input ReadyOrderItemInput {
|
||||||
productId: ID!
|
productId: ID!
|
||||||
quantity: Float!
|
quantity: Float!
|
||||||
@@ -554,6 +577,7 @@ type Mutation {
|
|||||||
connectMessenger(input: ConnectMessengerInput!): MessengerConnection!
|
connectMessenger(input: ConnectMessengerInput!): MessengerConnection!
|
||||||
deleteMyMessengerConnection(connectionId: ID!): Boolean!
|
deleteMyMessengerConnection(connectionId: ID!): Boolean!
|
||||||
upsertMyCounterpartyProfile(input: UpsertMyCounterpartyProfileInput!): CounterpartyProfile!
|
upsertMyCounterpartyProfile(input: UpsertMyCounterpartyProfileInput!): CounterpartyProfile!
|
||||||
|
upsertCatalogProductTypeSetting(input: UpsertCatalogProductTypeSettingInput!): CatalogProductTypeSetting!
|
||||||
addProductToCart(productId: ID!): Cart!
|
addProductToCart(productId: ID!): Cart!
|
||||||
updateCartItemQuantity(input: UpdateCartItemQuantityInput!): Cart!
|
updateCartItemQuantity(input: UpdateCartItemQuantityInput!): Cart!
|
||||||
removeCartItem(productId: ID!): Cart!
|
removeCartItem(productId: ID!): Cart!
|
||||||
|
|||||||
Reference in New Issue
Block a user