[598-6][@ant] fix(marketplace): ревью Эпика 3 — каталог через SDK Zeus + модерация FIFO + чистка approve/reject при edit
This commit is contained in:
+3
-1
@@ -55,7 +55,9 @@ export class MarketplaceModerationResolver {
|
||||
page: input?.page ?? 1,
|
||||
limit: input?.limit ?? 50,
|
||||
sortBy: input?.sortBy ?? 'created_at',
|
||||
sortOrder: (input?.sortOrder ?? 'DESC') as 'ASC' | 'DESC',
|
||||
// FIFO: первой берётся самая старая заявка — модератор не должен
|
||||
// пропускать давно ждущие офферы из-за свежих.
|
||||
sortOrder: (input?.sortOrder ?? 'ASC') as 'ASC' | 'DESC',
|
||||
};
|
||||
const result = await this.service.listPending(config.coopname, pagination);
|
||||
return {
|
||||
|
||||
+16
-1
@@ -183,9 +183,24 @@ export class MarketplaceOfferService {
|
||||
});
|
||||
}
|
||||
|
||||
const normalizedPatch: OfferUpdateInput & { status: MarketplaceOfferStatus } = {
|
||||
const normalizedPatch: OfferUpdateInput & {
|
||||
status: MarketplaceOfferStatus;
|
||||
approved_by: string | null;
|
||||
approved_at: Date | null;
|
||||
rejected_by: string | null;
|
||||
rejected_at: Date | null;
|
||||
reject_reason: string | null;
|
||||
} = {
|
||||
...patch,
|
||||
status: 'PENDING_MODERATION',
|
||||
// edit повторно отправляет оффер на модерацию — поля прошлых решений
|
||||
// (approve/reject) не должны утечь в UI как «уже одобрен» / «отклонён
|
||||
// с прошлой причиной».
|
||||
approved_by: null,
|
||||
approved_at: null,
|
||||
rejected_by: null,
|
||||
rejected_at: null,
|
||||
reject_reason: null,
|
||||
};
|
||||
|
||||
if (patch.unlimited_flag === true) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { sendPOST } from 'src/shared/api/axios';
|
||||
import { Queries } from '@coopenomics/sdk';
|
||||
import { client } from 'src/shared/api/client';
|
||||
import type {
|
||||
CatalogSort,
|
||||
MarketplaceCategoryOfferCount,
|
||||
@@ -7,101 +8,56 @@ import type {
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* Story 3.5: raw GraphQL к marketplace-backend.
|
||||
*
|
||||
* Техдолг: после `pnpm cooptypes:gen-zeus` (регенерация Zeus в
|
||||
* `@coopenomics/sdk`) переписать на типизированные
|
||||
* `Queries.Marketplace.ListCatalog` / `CategoryOfferCounts` /
|
||||
* `ListCategories` — см. паттерн `entities/MarketplaceKUDetails` после
|
||||
* 2-го коммита refactor в PR #381.
|
||||
*
|
||||
* До тех пор используем `sendPOST('/v1/graphql', { query, variables })`,
|
||||
* без типов schema.gql на клиенте.
|
||||
* Story 3.5: типизированные запросы каталога Стола заказов через SDK Zeus.
|
||||
* Все GraphQL-запросы идут через `@coopenomics/sdk` Queries.Marketplace —
|
||||
* raw query-строки и `sendPOST('/v1/graphql', ...)` в marketplace запрещены.
|
||||
*/
|
||||
|
||||
const LIST_CATALOG_QUERY = `
|
||||
query MarketplaceListCatalog($input: MarketplaceListCatalogInput) {
|
||||
marketplaceListCatalog(input: $input) {
|
||||
total
|
||||
items {
|
||||
id
|
||||
coopname
|
||||
supplier_account
|
||||
vitrine_id
|
||||
product_name
|
||||
description
|
||||
category_id
|
||||
price_per_unit
|
||||
unit_of_measure
|
||||
quantity_available
|
||||
quantity_blocked
|
||||
quantity_consumed
|
||||
unlimited_flag
|
||||
cycle_type
|
||||
cycle_days
|
||||
target_volume
|
||||
max_wait_days
|
||||
min_threshold
|
||||
warranty_days
|
||||
status
|
||||
created_at
|
||||
updated_at
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const LIST_CATEGORIES_QUERY = `
|
||||
query MarketplaceListCategories {
|
||||
marketplaceListCategories {
|
||||
id
|
||||
display_name
|
||||
sort_order
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const CATEGORY_COUNTS_QUERY = `
|
||||
query MarketplaceCategoryOfferCounts {
|
||||
marketplaceCategoryOfferCounts {
|
||||
category_id
|
||||
count
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export interface ListCatalogVariables {
|
||||
category_id?: number | null;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
sort?: CatalogSort;
|
||||
}
|
||||
|
||||
function mapSortToBackend(sort: CatalogSort | undefined): {
|
||||
sortBy: string;
|
||||
sortOrder: 'ASC' | 'DESC';
|
||||
} {
|
||||
switch (sort) {
|
||||
case 'price_asc':
|
||||
return { sortBy: 'price_per_unit', sortOrder: 'ASC' };
|
||||
case 'price_desc':
|
||||
return { sortBy: 'price_per_unit', sortOrder: 'DESC' };
|
||||
case 'created_at_desc':
|
||||
default:
|
||||
return { sortBy: 'created_at', sortOrder: 'DESC' };
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchCatalog(
|
||||
variables: ListCatalogVariables
|
||||
): Promise<MarketplaceOfferPage> {
|
||||
const body = await sendPOST('/v1/graphql', {
|
||||
query: LIST_CATALOG_QUERY,
|
||||
variables: { input: variables },
|
||||
const { sortBy, sortOrder } = mapSortToBackend(variables.sort);
|
||||
const input = {
|
||||
category_id: variables.category_id ?? null,
|
||||
page: variables.page ?? 1,
|
||||
limit: variables.limit ?? 24,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
};
|
||||
const result = await client.Query(Queries.Marketplace.ListCatalog.query, {
|
||||
variables: { input },
|
||||
});
|
||||
if (body?.errors?.length) {
|
||||
throw new Error(body.errors[0].message);
|
||||
}
|
||||
return body.data.marketplaceListCatalog;
|
||||
return result[Queries.Marketplace.ListCatalog.name] as MarketplaceOfferPage;
|
||||
}
|
||||
|
||||
export async function fetchCategories(): Promise<MarketplaceCategoryView[]> {
|
||||
const body = await sendPOST('/v1/graphql', { query: LIST_CATEGORIES_QUERY });
|
||||
if (body?.errors?.length) {
|
||||
throw new Error(body.errors[0].message);
|
||||
}
|
||||
return body.data.marketplaceListCategories;
|
||||
const result = await client.Query(Queries.Marketplace.ListCategories.query);
|
||||
return (result[Queries.Marketplace.ListCategories.name] ?? []) as MarketplaceCategoryView[];
|
||||
}
|
||||
|
||||
export async function fetchCategoryOfferCounts(): Promise<MarketplaceCategoryOfferCount[]> {
|
||||
const body = await sendPOST('/v1/graphql', { query: CATEGORY_COUNTS_QUERY });
|
||||
if (body?.errors?.length) {
|
||||
throw new Error(body.errors[0].message);
|
||||
}
|
||||
return body.data.marketplaceCategoryOfferCounts;
|
||||
const result = await client.Query(Queries.Marketplace.CategoryOfferCounts.query);
|
||||
return (result[Queries.Marketplace.CategoryOfferCounts.name] ?? []) as MarketplaceCategoryOfferCount[];
|
||||
}
|
||||
|
||||
@@ -31,8 +31,10 @@ export interface MarketplaceOfferView {
|
||||
}
|
||||
|
||||
export interface MarketplaceOfferPage {
|
||||
total: number;
|
||||
items: MarketplaceOfferView[];
|
||||
totalCount: number;
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
}
|
||||
|
||||
export interface MarketplaceCategoryView {
|
||||
|
||||
+6
-6
@@ -37,7 +37,7 @@ const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const selectedCategoryId = ref<number>(ALL_KEY);
|
||||
const sort = ref<CatalogSort>('created_at_desc');
|
||||
const offset = ref(0);
|
||||
const currentPage = ref(1);
|
||||
|
||||
const hasMore = computed(() => items.value.length < total.value);
|
||||
|
||||
@@ -89,11 +89,11 @@ async function loadPage(append: boolean): Promise<void> {
|
||||
try {
|
||||
const page = await fetchCatalog({
|
||||
category_id: selectedCategoryId.value === ALL_KEY ? null : selectedCategoryId.value,
|
||||
page: currentPage.value,
|
||||
limit: PAGE_SIZE,
|
||||
offset: offset.value,
|
||||
sort: sort.value,
|
||||
});
|
||||
total.value = page.total;
|
||||
total.value = page.totalCount;
|
||||
items.value = append ? items.value.concat(page.items) : page.items;
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
@@ -105,19 +105,19 @@ async function loadPage(append: boolean): Promise<void> {
|
||||
|
||||
function selectCategory(id: number): void {
|
||||
selectedCategoryId.value = id;
|
||||
offset.value = 0;
|
||||
currentPage.value = 1;
|
||||
void loadPage(false);
|
||||
}
|
||||
|
||||
function changeSort(newSort: CatalogSort): void {
|
||||
sort.value = newSort;
|
||||
offset.value = 0;
|
||||
currentPage.value = 1;
|
||||
void loadPage(false);
|
||||
}
|
||||
|
||||
async function onLoadMore(): Promise<void> {
|
||||
if (!hasMore.value || loading.value) return;
|
||||
offset.value = items.value.length;
|
||||
currentPage.value += 1;
|
||||
await loadPage(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { type GraphQLTypes, type InputType, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'marketplaceCategoryOfferCounts'
|
||||
|
||||
export const query = Selector('Query')({
|
||||
[name]: {
|
||||
category_id: true,
|
||||
count: true,
|
||||
},
|
||||
})
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Query'], typeof query>
|
||||
@@ -6,6 +6,10 @@ export * as ListMyOrders from './listMyOrders'
|
||||
export * as GetOrder from './getOrder'
|
||||
/** Базовый справочник категорий товаров */
|
||||
export * as ListCategories from './listCategories'
|
||||
/** Каталог активных Offer'ов (Story 3.5) */
|
||||
export * as ListCatalog from './listCatalog'
|
||||
/** Счётчики активных Offer'ов per category для фильтр-чипов (Story 3.5) */
|
||||
export * as CategoryOfferCounts from './categoryOfferCounts'
|
||||
/** Эпик 5: партии поставки кооператива */
|
||||
export * as ListShipments from './listShipments'
|
||||
/** Эпик 5: одна партия поставки по идентификатору */
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { marketplaceOfferSelector } from '../../selectors/marketplace/offerSelector'
|
||||
import { $, type GraphQLTypes, type InputType, Selector } from '../../zeus/index'
|
||||
|
||||
export const name = 'marketplaceListCatalog'
|
||||
|
||||
export const query = Selector('Query')({
|
||||
[name]: [
|
||||
{ input: $('input', 'MarketplaceListCatalogInput') },
|
||||
{
|
||||
items: marketplaceOfferSelector,
|
||||
totalCount: true,
|
||||
totalPages: true,
|
||||
currentPage: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export type IOutput = InputType<GraphQLTypes['Query'], typeof query>
|
||||
Reference in New Issue
Block a user