[598][@ant] feat: реестр всех предложений кооператива на столе администратора (Offer:read:all)

Новая страница «Реестр предложений» (market-admin) — все предложения всех
поставщиков любого статуса (опубликованные/снятые/отклонённые/на модерации),
отдельно от «Модерации» (там только ждущие решения). Клик по строке → readonly-
карточка предложения (marketplace-admin-offer-detail) — туда же ведёт переход
«Открыть предложение» из реестра заказов.

- backend: marketplaceListAllOffers (Offer:read:all) + offerService.listAll;
  read:all добавлен админу в access-matrix (у совета уже был);
- DRY: логика username→ФИО вынесена из реестра процессов в общий composable
  useFioCache, переиспользована в реестре предложений.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ant
2026-06-13 10:33:01 +00:00
parent 61018b5a8a
commit 0f25943842
18 changed files with 618 additions and 34 deletions
@@ -95,7 +95,9 @@ export const marketplaceAccessMatrix: Record<MarketplaceRole, Record<string, str
Economy: ['read', 'read:own-KU', 'configure:own-KU', 'use:own'],
},
admin: {
Offer: ['moderate', 'read'],
// read:all — реестр всех предложений кооператива любого статуса (наряду с
// модерацией PENDING); read:all есть и у совета (board_readonly).
Offer: ['moderate', 'read', 'read:all'],
Order: ['read:all'],
KU: ['manage'],
Whitelist: ['manage'],
@@ -18,7 +18,7 @@ import {
} from 'class-validator';
import { PaginationInputDTO } from '~/application/common/dto/pagination.dto';
import { MARKETPLACE_OFFER_MAX_IMAGES } from '../../domain/entities/marketplace-offer.types';
import { MarketplaceBarcodeStrategyEnum } from './marketplace-offer.dto';
import { MarketplaceBarcodeStrategyEnum, MarketplaceOfferStatusEnum } from './marketplace-offer.dto';
const UNITS = ['piece', 'kg', 'liter', 'pack'] as const;
@@ -249,3 +249,17 @@ export class MarketplaceRepublishOfferInputDTO {
@InputType('MarketplaceListMyOffersInput')
export class MarketplaceListMyOffersInputDTO extends PaginationInputDTO {}
@InputType('MarketplaceListAllOffersInput', {
description: 'Параметры фильтрации реестра всех предложений кооператива (стол администратора).',
})
export class MarketplaceListAllOffersInputDTO extends PaginationInputDTO {
@Field(() => [MarketplaceOfferStatusEnum], {
nullable: true,
description: 'Один или несколько статусов предложения, по которым нужно отфильтровать список.',
})
public readonly statuses?: MarketplaceOfferStatusEnum[];
@Field(() => String, { nullable: true, description: 'Фильтр по аккаунту поставщика.' })
public readonly supplier_account?: string;
}
@@ -14,6 +14,7 @@ import {
} from '../dto/marketplace-offer.dto';
import {
MarketplaceCreateOfferInputDTO,
MarketplaceListAllOffersInputDTO,
MarketplaceListMyOffersInputDTO,
MarketplaceRepublishOfferInputDTO,
MarketplaceUpdateOfferInputDTO,
@@ -35,6 +36,7 @@ import {
type AvailableCategoryDomainService,
} from '../../domain/services/available-category-domain.service';
import type { MarketplaceOfferDomainEntity } from '../../domain/entities/marketplace-offer.entity';
import type { MarketplaceOfferStatus } from '../../domain/entities/marketplace-offer.types';
const toDTO = toMarketplaceOfferDTO;
@@ -216,4 +218,37 @@ export class MarketplaceOfferResolver {
currentPage: result.currentPage,
};
}
@Query(() => MarketplaceOfferPaginationResultDTO, {
name: 'marketplaceListAllOffers',
description: 'Реестр всех предложений кооператива любого статуса (стол администратора).',
})
@UseGuards(GqlJwtAuthGuard, MarketplaceMembershipGuard, MarketplaceRoleGuard)
@RequireMarketplaceAccess('Offer', 'read:all')
async marketplaceListAllOffers(
@Args('input', { nullable: true }) input?: MarketplaceListAllOffersInputDTO
): Promise<MarketplaceOfferPaginationResultDTO> {
const pagination = {
page: input?.page ?? 1,
limit: input?.limit ?? 50,
sortBy: input?.sortBy ?? 'created_at',
sortOrder: (input?.sortOrder ?? 'DESC') as 'ASC' | 'DESC',
};
const result = await this.offerService.listAll(
config.coopname,
{
statuses: input?.statuses?.length
? (input.statuses as unknown as MarketplaceOfferStatus[])
: undefined,
supplier_account: input?.supplier_account,
},
pagination
);
return {
items: result.items.map(toDTO),
totalCount: result.totalCount,
totalPages: result.totalPages,
currentPage: result.currentPage,
};
}
}
@@ -400,6 +400,23 @@ export class MarketplaceOfferService {
return this.repo.list({ coopname, supplier_account }, pagination);
}
// Реестр всех предложений кооператива (стол администратора): любой статус
// и любой поставщик, с опциональными фильтрами по статусу и поставщику.
async listAll(
coopname: string,
filter: { statuses?: MarketplaceOfferStatus[]; supplier_account?: string },
pagination: PaginationInputDomainInterface
): Promise<PaginationResultDomainInterface<MarketplaceOfferDomainEntity>> {
return this.repo.list(
{
coopname,
...(filter.supplier_account ? { supplier_account: filter.supplier_account } : {}),
...(filter.statuses?.length ? { status: filter.statuses } : {}),
},
pagination
);
}
async getById(id: string): Promise<MarketplaceOfferDomainEntity | null> {
return this.repo.findById(id);
}
+7
View File
@@ -795,6 +795,9 @@ export const AllTypesProps: Record<string,any> = {
MarketplaceKUStatus: "enum" as const,
MarketplaceListAidsInput:{
},
MarketplaceListAllOffersInput:{
statuses:"MarketplaceOfferStatus"
},
MarketplaceListAplReceptionsByBranameInput:{
@@ -2163,6 +2166,9 @@ export const AllTypesProps: Record<string,any> = {
marketplaceListAids:{
data:"MarketplaceListAidsInput"
},
marketplaceListAllOffers:{
input:"MarketplaceListAllOffersInput"
},
marketplaceListAllOrders:{
input:"MarketplaceListOrdersInput",
options:"PaginationInput"
@@ -5850,6 +5856,7 @@ export const ReturnTypes: Record<string,any> = {
marketplaceIssueActChairmanSignablePayload:"GeneratedDocument",
marketplaceIssueActOrdererSignablePayload:"DocumentAggregate",
marketplaceListAids:"MarketplaceAid",
marketplaceListAllOffers:"MarketplaceOfferPaginationResult",
marketplaceListAllOrders:"MarketplaceOrderPaginationResult",
marketplaceListAplReceptionsAsSupplier:"MarketplaceAplReception",
marketplaceListAplReceptionsByBraname:"MarketplaceAplReception",
+67
View File
@@ -7193,6 +7193,21 @@ export type ValueTypes = {
["MarketplaceListAidsInput"]: {
/** Показать заявки только этого получателя (по умолчанию — свои). */
username?: string | undefined | null | Variable<any, string>
};
/** Параметры фильтрации реестра всех предложений кооператива (стол администратора). */
["MarketplaceListAllOffersInput"]: {
/** Количество элементов на странице */
limit: number | Variable<any, string>,
/** Номер страницы */
page: number | Variable<any, string>,
/** Ключ сортировки (например, "name") */
sortBy?: string | undefined | null | Variable<any, string>,
/** Направление сортировки ("ASC" или "DESC") */
sortOrder: string | Variable<any, string>,
/** Один или несколько статусов предложения, по которым нужно отфильтровать список. */
statuses?: Array<ValueTypes["MarketplaceOfferStatus"]> | undefined | null | Variable<any, string>,
/** Фильтр по аккаунту поставщика. */
supplier_account?: string | undefined | null | Variable<any, string>
};
["MarketplaceListAplReceptionsByBranameInput"]: {
/** Идентификатор КУ-получателя. */
@@ -10369,6 +10384,7 @@ marketplaceGetUserRequests?: [{ data?: ValueTypes["GetUserRequestsInput"] | unde
marketplaceIssueActChairmanSignablePayload?: [{ data: ValueTypes["MarketplaceIssueActPayloadInput"] | Variable<any, string>},ValueTypes["GeneratedDocument"]],
marketplaceIssueActOrdererSignablePayload?: [{ data: ValueTypes["MarketplaceIssueActPayloadInput"] | Variable<any, string>},ValueTypes["DocumentAggregate"]],
marketplaceListAids?: [{ data?: ValueTypes["MarketplaceListAidsInput"] | undefined | null | Variable<any, string>},ValueTypes["MarketplaceAid"]],
marketplaceListAllOffers?: [{ input?: ValueTypes["MarketplaceListAllOffersInput"] | undefined | null | Variable<any, string>},ValueTypes["MarketplaceOfferPaginationResult"]],
marketplaceListAllOrders?: [{ input?: ValueTypes["MarketplaceListOrdersInput"] | undefined | null | Variable<any, string>, options?: ValueTypes["PaginationInput"] | undefined | null | Variable<any, string>},ValueTypes["MarketplaceOrderPaginationResult"]],
/** Список актов приёмки, ожидающих подписи текущего поставщика. */
marketplaceListAplReceptionsAsSupplier?:ValueTypes["MarketplaceAplReception"],
@@ -17953,6 +17969,21 @@ export type ResolverInputTypes = {
["MarketplaceListAidsInput"]: {
/** Показать заявки только этого получателя (по умолчанию — свои). */
username?: string | undefined | null
};
/** Параметры фильтрации реестра всех предложений кооператива (стол администратора). */
["MarketplaceListAllOffersInput"]: {
/** Количество элементов на странице */
limit: number,
/** Номер страницы */
page: number,
/** Ключ сортировки (например, "name") */
sortBy?: string | undefined | null,
/** Направление сортировки ("ASC" или "DESC") */
sortOrder: string,
/** Один или несколько статусов предложения, по которым нужно отфильтровать список. */
statuses?: Array<ResolverInputTypes["MarketplaceOfferStatus"]> | undefined | null,
/** Фильтр по аккаунту поставщика. */
supplier_account?: string | undefined | null
};
["MarketplaceListAplReceptionsByBranameInput"]: {
/** Идентификатор КУ-получателя. */
@@ -21011,6 +21042,7 @@ marketplaceGetUserRequests?: [{ data?: ResolverInputTypes["GetUserRequestsInput"
marketplaceIssueActChairmanSignablePayload?: [{ data: ResolverInputTypes["MarketplaceIssueActPayloadInput"]},ResolverInputTypes["GeneratedDocument"]],
marketplaceIssueActOrdererSignablePayload?: [{ data: ResolverInputTypes["MarketplaceIssueActPayloadInput"]},ResolverInputTypes["DocumentAggregate"]],
marketplaceListAids?: [{ data?: ResolverInputTypes["MarketplaceListAidsInput"] | undefined | null},ResolverInputTypes["MarketplaceAid"]],
marketplaceListAllOffers?: [{ input?: ResolverInputTypes["MarketplaceListAllOffersInput"] | undefined | null},ResolverInputTypes["MarketplaceOfferPaginationResult"]],
marketplaceListAllOrders?: [{ input?: ResolverInputTypes["MarketplaceListOrdersInput"] | undefined | null, options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["MarketplaceOrderPaginationResult"]],
/** Список актов приёмки, ожидающих подписи текущего поставщика. */
marketplaceListAplReceptionsAsSupplier?:ResolverInputTypes["MarketplaceAplReception"],
@@ -28341,6 +28373,21 @@ export type ModelTypes = {
["MarketplaceListAidsInput"]: {
/** Показать заявки только этого получателя (по умолчанию — свои). */
username?: string | undefined | null
};
/** Параметры фильтрации реестра всех предложений кооператива (стол администратора). */
["MarketplaceListAllOffersInput"]: {
/** Количество элементов на странице */
limit: number,
/** Номер страницы */
page: number,
/** Ключ сортировки (например, "name") */
sortBy?: string | undefined | null,
/** Направление сортировки ("ASC" или "DESC") */
sortOrder: string,
/** Один или несколько статусов предложения, по которым нужно отфильтровать список. */
statuses?: Array<ModelTypes["MarketplaceOfferStatus"]> | undefined | null,
/** Фильтр по аккаунту поставщика. */
supplier_account?: string | undefined | null
};
["MarketplaceListAplReceptionsByBranameInput"]: {
/** Идентификатор КУ-получателя. */
@@ -32016,6 +32063,8 @@ export type ModelTypes = {
marketplaceIssueActOrdererSignablePayload: ModelTypes["DocumentAggregate"],
/** Заявки на материальную помощь: свои — для доверенного; все заявки кооператива — для администратора. */
marketplaceListAids: Array<ModelTypes["MarketplaceAid"]>,
/** Реестр всех предложений кооператива любого статуса (стол администратора). */
marketplaceListAllOffers: ModelTypes["MarketplaceOfferPaginationResult"],
/** Реестр всех заказов кооператива с их текущими статусами (стол администратора). */
marketplaceListAllOrders: ModelTypes["MarketplaceOrderPaginationResult"],
/** Список актов приёмки, ожидающих подписи текущего поставщика. */
@@ -39708,6 +39757,21 @@ export type GraphQLTypes = {
["MarketplaceListAidsInput"]: {
/** Показать заявки только этого получателя (по умолчанию — свои). */
username?: string | undefined | null
};
/** Параметры фильтрации реестра всех предложений кооператива (стол администратора). */
["MarketplaceListAllOffersInput"]: {
/** Количество элементов на странице */
limit: number,
/** Номер страницы */
page: number,
/** Ключ сортировки (например, "name") */
sortBy?: string | undefined | null,
/** Направление сортировки ("ASC" или "DESC") */
sortOrder: string,
/** Один или несколько статусов предложения, по которым нужно отфильтровать список. */
statuses?: Array<GraphQLTypes["MarketplaceOfferStatus"]> | undefined | null,
/** Фильтр по аккаунту поставщика. */
supplier_account?: string | undefined | null
};
["MarketplaceListAplReceptionsByBranameInput"]: {
/** Идентификатор КУ-получателя. */
@@ -43653,6 +43717,8 @@ export type GraphQLTypes = {
marketplaceIssueActOrdererSignablePayload: GraphQLTypes["DocumentAggregate"],
/** Заявки на материальную помощь: свои — для доверенного; все заявки кооператива — для администратора. */
marketplaceListAids: Array<GraphQLTypes["MarketplaceAid"]>,
/** Реестр всех предложений кооператива любого статуса (стол администратора). */
marketplaceListAllOffers: GraphQLTypes["MarketplaceOfferPaginationResult"],
/** Реестр всех заказов кооператива с их текущими статусами (стол администратора). */
marketplaceListAllOrders: GraphQLTypes["MarketplaceOrderPaginationResult"],
/** Список актов приёмки, ожидающих подписи текущего поставщика. */
@@ -46240,6 +46306,7 @@ type ZEUS_VARIABLES = {
["MarketplaceIssueActSignedMetaDocumentInput"]: ValueTypes["MarketplaceIssueActSignedMetaDocumentInput"];
["MarketplaceKUStatus"]: ValueTypes["MarketplaceKUStatus"];
["MarketplaceListAidsInput"]: ValueTypes["MarketplaceListAidsInput"];
["MarketplaceListAllOffersInput"]: ValueTypes["MarketplaceListAllOffersInput"];
["MarketplaceListAplReceptionsByBranameInput"]: ValueTypes["MarketplaceListAplReceptionsByBranameInput"];
["MarketplaceListCatalogInput"]: ValueTypes["MarketplaceListCatalogInput"];
["MarketplaceListConsolidatedRequestsInput"]: ValueTypes["MarketplaceListConsolidatedRequestsInput"];
@@ -22,6 +22,7 @@ import { OffererPaymentHistoryPage } from 'src/pages/Marketplace/OffererPaymentH
import { AdminWriteoffsPage } from 'src/pages/Marketplace/AdminWriteoffs'
import { ChairmanModerationPage } from 'src/pages/Marketplace/ChairmanModeration'
import { AdminOrdersPage } from 'src/pages/Marketplace/AdminOrders'
import { AdminOffersPage } from 'src/pages/Marketplace/AdminOffers'
import { AdminIssuancePointsPage } from 'src/pages/Marketplace/AdminIssuancePoints'
import { OperatorOwnWarehousePage } from 'src/pages/Marketplace/OperatorOwnWarehouse'
import { AdminWarehouseSummaryPage } from 'src/pages/Marketplace/AdminWarehouseSummary'
@@ -637,6 +638,24 @@ export default async function (): Promise<IWorkspaceConfig[]> {
},
children: [],
},
{
// Реестр всех предложений кооператива любого статуса
// (опубликованные/снятые/отклонённые/на модерации), всех
// поставщиков (`Offer:read:all` — у admin и board_readonly).
// Отдельно от «Модерации» (там только ждущие решения); на эти
// карточки ведёт переход «Открыть предложение» из реестра заказов.
path: 'offers',
name: 'marketplace-admin-offers',
component: markRaw(AdminOffersPage),
meta: {
title: 'Реестр предложений',
icon: 'storefront',
requires: 'Offer:read:all',
requiresAuth: true,
agreements: agreementsBase,
},
children: [],
},
{
// Эпик 3 / Story 3.6: admin-стол модерации offer'ов. Виден совету
// и председателю (`Order:read:all` есть у board_readonly и admin).
@@ -143,7 +143,7 @@ import { ExpandToggleButton } from 'src/shared/ui/ExpandToggleButton'
import { EntityIdBadge } from 'src/shared/ui'
import { copyToClipboard } from 'quasar'
import { useProcessStore, type IProcessSummary } from 'src/entities/Process'
import { useAccountStore } from 'src/entities/Account'
import { useFioCache } from 'src/shared/lib/account/useFioCache'
import { ProcessDetailCard } from 'src/widgets/Process/ProcessDetailCard'
import {
processChipBg,
@@ -157,12 +157,11 @@ const { isMobile } = useWindowSize()
const route = useRoute()
const router = useRouter()
const processStore = useProcessStore()
const accountStore = useAccountStore()
const { fioCache, enrichFio } = useFioCache()
const loading = ref(false)
const items = ref<IProcessSummary[]>([])
const expanded = ref(new Map<string, boolean>())
const fioCache = ref(new Map<string, string>())
const pagination = ref({ page: 1, rowsPerPage: 50, rowsNumber: 0 })
@@ -333,35 +332,6 @@ function toggleExpand(processHash: string) {
expanded.value.set(processHash, !expanded.value.get(processHash))
}
async function enrichFio(rawUsernames: (string | null | undefined)[]) {
const usernames = [
...new Set(rawUsernames.filter((u): u is string => !!u && !fioCache.value.has(u))),
]
if (!usernames.length) return
await Promise.allSettled(
usernames.map(async (username) => {
try {
const acc = await accountStore.getAccount(username)
const pd = acc?.private_account
if (!pd) return
let fio = ''
if (pd.type === 'individual' && pd.individual_data) {
const d = pd.individual_data
fio = [d.last_name, d.first_name, d.middle_name].filter(Boolean).join(' ')
} else if (pd.type === 'organization' && pd.organization_data) {
fio = (pd.organization_data as any).short_name ?? username
} else if (pd.type === 'entrepreneur' && pd.entrepreneur_data) {
const d = pd.entrepreneur_data as any
fio = [d.last_name, d.first_name, d.middle_name].filter(Boolean).join(' ')
}
if (fio) fioCache.value.set(username, fio)
} catch {
// молча — username остаётся как fallback
}
}),
)
}
onMounted(async () => {
try {
if (route.query.process_type) {
@@ -0,0 +1,40 @@
import { Queries } from '@coopenomics/sdk';
import { client } from 'src/shared/api/client';
import type { AdminOfferPage, AdminOfferStatusView } from '../types';
export interface ListAllOffersVariables {
statuses?: AdminOfferStatusView[];
supplier_account?: string;
page?: number;
limit?: number;
sortBy?: string;
sortOrder?: 'ASC' | 'DESC';
}
/**
* Реестр всех предложений кооператива любого статуса (стол администратора,
* Offer:read:all). Фильтры (статус/поставщик) и пагинация — серверные,
* передаются единым input'ом.
*/
export async function fetchAllOffers(
variables: ListAllOffersVariables = {},
): Promise<AdminOfferPage> {
const { page, limit, sortBy, sortOrder, statuses, supplier_account } = variables;
const { [Queries.Marketplace.ListAllOffers.name]: result } = await client.Query(
Queries.Marketplace.ListAllOffers.query,
{
variables: {
input: {
statuses,
supplier_account,
page: page ?? 1,
limit: limit ?? 50,
sortBy: sortBy ?? 'created_at',
sortOrder: sortOrder ?? 'DESC',
},
},
},
);
// Zeus отдаёт DateTime как unknown; сужаем скалярную дату до строки во view-типе.
return result as AdminOfferPage;
}
@@ -0,0 +1,2 @@
export * from './ui';
export * from './types';
@@ -0,0 +1,26 @@
/**
* Реестр всех предложений кооператива (стол администратора).
* Источник истины — Zeus-вывод операции marketplaceListAllOffers из @coopenomics/sdk.
*/
import { Queries } from '@coopenomics/sdk';
type _RawOfferPage =
Queries.Marketplace.ListAllOffers.IOutput['marketplaceListAllOffers'];
type _RawOffer = _RawOfferPage['items'][number];
/**
* Zeus маппит DateTime в `unknown`. Структуру/enum'ы оставляем из Zeus, но
* скалярную дату создания переопределяем на строку для форматирования в UI.
*/
export type AdminOfferView = Omit<_RawOffer, 'created_at'> & {
created_at: string;
};
/** Страница предложений: обёртка из Zeus, но items со строгой датой создания. */
export type AdminOfferPage = Omit<_RawOfferPage, 'items'> & {
items: AdminOfferView[];
};
/** Доменный статус предложения как строковый литерал (совпадает с Zeus-enum'ом). */
export type AdminOfferStatusView = `${AdminOfferView['status']}`;
@@ -0,0 +1,254 @@
<script lang="ts" setup>
/**
* Реестр всех предложений кооператива (стол администратора).
* Backend: marketplaceListAllOffers (Offer:read:all) — все предложения любого
* статуса (опубликованные/снятые/отклонённые/на модерации), всех поставщиков.
* Отдельно от «Модерации» (там только то, что прямо сейчас ждёт решения).
* Клик по строке открывает readonly-карточку предложения; на эти же карточки
* ведёт переход «Открыть предложение» из реестра заказов.
*/
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { FailAlert } from 'src/shared/api';
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits';
import { marketplaceUnitShort } from 'src/shared/lib/consts/marketplace-units';
import { useSystemStore } from 'src/entities/System/model';
import { useFioCache } from 'src/shared/lib/account/useFioCache';
import { BaseBadge, EmptyState } from 'src/shared/ui/base';
import type { BaseBadgeVariant } from 'src/shared/ui/base';
import { EntityIdBadge } from 'src/shared/ui';
import { PageHint } from 'src/shared/ui/domain';
import { fetchAllOffers } from '../api';
import type { AdminOfferView, AdminOfferStatusView } from '../types';
const { info } = useSystemStore();
const router = useRouter();
const { fioCache, enrichFio } = useFioCache();
const items = ref<AdminOfferView[]>([]);
const loading = ref(false);
const statusFilter = ref<AdminOfferStatusView[]>([]);
const pagination = ref({ page: 1, rowsPerPage: 50, rowsNumber: 0 });
const OFFER_STATUS: Record<string, { label: string; variant: BaseBadgeVariant }> = {
PENDING_MODERATION: { label: 'На модерации', variant: 'warn' },
ACTIVE: { label: 'Опубликовано', variant: 'pos' },
REJECTED: { label: 'Отклонено', variant: 'neg' },
WITHDRAWN: { label: 'Снято с публикации', variant: 'neutral' },
};
const ALL_STATUSES = Object.keys(OFFER_STATUS) as AdminOfferStatusView[];
function statusLabel(s: string): string {
return OFFER_STATUS[s]?.label ?? s;
}
function statusVariant(s: string): BaseBadgeVariant {
return OFFER_STATUS[s]?.variant ?? 'neutral';
}
const columns = [
{ name: 'status', align: 'left' as const, label: 'Статус', field: 'status' },
{ name: 'offer', align: 'left' as const, label: 'Предложение', field: 'id' },
{ name: 'product', align: 'left' as const, label: 'Товар', field: 'product_name' },
{ name: 'supplier', align: 'left' as const, label: 'Поставщик', field: 'supplier_account' },
{ name: 'price', align: 'right' as const, label: 'Цена', field: 'price_per_unit' },
{ name: 'available', align: 'right' as const, label: 'Доступно', field: 'quantity_available' },
{ name: 'created', align: 'left' as const, label: 'Создано', field: 'created_at' },
];
function isStatusActive(s: AdminOfferStatusView): boolean {
return statusFilter.value.includes(s);
}
function toggleStatus(s: AdminOfferStatusView): void {
statusFilter.value = isStatusActive(s)
? statusFilter.value.filter((x) => x !== s)
: [...statusFilter.value, s];
void reload();
}
function resetFilters(): void {
if (!statusFilter.value.length) return;
statusFilter.value = [];
void reload();
}
function shortId(id: string | null | undefined): string {
return id ? id.slice(0, 8) : '—';
}
function unitLabel(o: AdminOfferView): string {
return marketplaceUnitShort(o.unit_of_measure);
}
function formatPrice(v: string | null | undefined): string {
return v ? formatAsset2Digits(String(v)) : '—';
}
function supplierTitle(o: AdminOfferView): string {
return fioCache.value.get(o.supplier_account) || o.supplier_account || '—';
}
function formatDate(d: unknown): string {
if (d === null || d === undefined) return '—';
const parsed = new Date(String(d));
return Number.isNaN(parsed.getTime())
? String(d)
: parsed.toLocaleString('ru-RU', {
day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit',
});
}
function goToOffer(o: AdminOfferView): void {
if (!o.id) return;
void router.push({
name: 'marketplace-admin-offer-detail',
params: { coopname: info.coopname, offerId: o.id },
});
}
let lastRequestId = 0;
async function reload(): Promise<void> {
pagination.value.page = 1;
await load();
}
async function load(): Promise<void> {
const myId = ++lastRequestId;
loading.value = true;
try {
const resp = await fetchAllOffers({
statuses: statusFilter.value.length ? statusFilter.value : undefined,
page: pagination.value.page,
limit: pagination.value.rowsPerPage,
sortOrder: 'DESC',
});
if (myId !== lastRequestId) return;
items.value = resp.items ?? [];
pagination.value.rowsNumber = resp.totalCount ?? 0;
void enrichFio(items.value.map((o) => o.supplier_account));
} catch (e) {
if (myId === lastRequestId) FailAlert(e, 'Не удалось загрузить реестр предложений');
} finally {
if (myId === lastRequestId) loading.value = false;
}
}
function onRequest(props: { pagination: { page: number; rowsPerPage: number; rowsNumber?: number } }): void {
pagination.value = {
page: props.pagination.page,
rowsPerPage: props.pagination.rowsPerPage,
rowsNumber: props.pagination.rowsNumber ?? pagination.value.rowsNumber,
};
void load();
}
onMounted(() => {
void load();
});
</script>
<template lang="pug">
q-page.admin-offers(role="region", aria-label="Реестр предложений кооператива")
PageHint(storage-key="mp:admin-offers:banner-dismissed")
| Все предложения поставщиков кооператива любого статуса опубликованные, снятые, отклонённые и ждущие модерации. Нажмите на предложение, чтобы открыть его карточку. Модерация ждущих решения на отдельной странице.
.admin-offers__chips(role="group", aria-label="Фильтр по статусу")
.chip(
v-for="s in ALL_STATUSES",
:key="s",
:class="isStatusActive(s) ? 'chip--accent' : 'chip--neutral'",
role="button",
tabindex="0",
@click="toggleStatus(s)",
@keydown.enter="toggleStatus(s)"
) {{ statusLabel(s) }}
.chip.chip--reset(
v-if="statusFilter.length",
role="button",
tabindex="0",
@click="resetFilters",
@keydown.enter="resetFilters"
)
q-icon(name="close", size="14px")
| Сбросить
q-card.q-mt-md(flat)
q-table.full-height(
flat,
:rows="items",
:columns="columns",
row-key="id",
:loading="loading",
:pagination="pagination",
:rows-per-page-options="[25, 50, 100, 200]",
no-data-label="Предложения не найдены",
@request="onRequest",
@row-click="(_evt, row) => goToOffer(row)"
)
template(#body-cell-status="props")
q-td(:props="props")
BaseBadge(:variant="statusVariant(props.row.status)") {{ statusLabel(props.row.status) }}
template(#body-cell-offer="props")
q-td(:props="props")
EntityIdBadge(:rawId="shortId(props.row.id)", copy-on-click)
template(#body-cell-product="props")
q-td(:props="props") {{ props.row.product_name || 'Товар по предложению' }}
template(#body-cell-supplier="props")
q-td(:props="props") {{ supplierTitle(props.row) }}
template(#body-cell-price="props")
q-td.text-right.font-monospace(:props="props") {{ formatPrice(props.row.price_per_unit) }}
template(#body-cell-available="props")
q-td.text-right(:props="props") {{ props.row.unlimited_flag ? '∞' : props.row.quantity_available }} {{ unitLabel(props.row) }}
template(#body-cell-created="props")
q-td(:props="props") {{ formatDate(props.row.created_at) }}
template(#no-data)
.admin-offers__nodata
EmptyState(
title="Предложений нет",
body="Предложений по выбранным фильтрам не найдено."
)
template(#icon)
q-icon(name="storefront", size="48px")
</template>
<style scoped lang="scss">
.admin-offers {
padding: var(--p-6, 24px);
display: flex;
flex-direction: column;
gap: var(--p-4, 16px);
&__nodata {
width: 100%;
display: flex;
justify-content: center;
}
&__chips {
display: flex;
flex-wrap: wrap;
gap: var(--p-2, 8px);
.chip {
cursor: pointer;
user-select: none;
height: 28px;
padding: 0 12px;
display: inline-flex;
align-items: center;
gap: 4px;
}
}
// Строки кликабельны (ведут на карточку) — курсор-указатель как аффорданс.
:deep(.q-table tbody tr) {
cursor: pointer;
}
}
.font-monospace {
font-family: 'JetBrains Mono', 'Courier New', monospace;
letter-spacing: 0.03em;
}
@media (max-width: 768px) {
.admin-offers {
padding: var(--p-4, 16px);
}
}
</style>
@@ -0,0 +1 @@
export { default as AdminOffersPage } from './AdminOffersPage.vue';
@@ -0,0 +1,44 @@
import { ref } from 'vue'
import { useAccountStore } from 'src/entities/Account'
/**
* Кэш ФИО пайщиков по username — общий для реестров (процессы/заказы/предложения),
* где из цепочки приходит служебный аккаунт, а показывать нужно человеческое имя
* (НЕ braname). enrichFio догружает недостающие имена через accountStore и
* кладёт в кэш; в UI используется `fioCache.get(username) || username`.
*/
export function useFioCache() {
const accountStore = useAccountStore()
const fioCache = ref(new Map<string, string>())
async function enrichFio(rawUsernames: (string | null | undefined)[]): Promise<void> {
const usernames = [
...new Set(rawUsernames.filter((u): u is string => !!u && !fioCache.value.has(u))),
]
if (!usernames.length) return
await Promise.allSettled(
usernames.map(async (username) => {
try {
const acc = await accountStore.getAccount(username)
const pd = acc?.private_account
if (!pd) return
let fio = ''
if (pd.type === 'individual' && pd.individual_data) {
const d = pd.individual_data
fio = [d.last_name, d.first_name, d.middle_name].filter(Boolean).join(' ')
} else if (pd.type === 'organization' && pd.organization_data) {
fio = (pd.organization_data as any).short_name ?? username
} else if (pd.type === 'entrepreneur' && pd.entrepreneur_data) {
const d = pd.entrepreneur_data as any
fio = [d.last_name, d.first_name, d.middle_name].filter(Boolean).join(' ')
}
if (fio) fioCache.value.set(username, fio)
} catch {
// молча — username остаётся как fallback
}
}),
)
}
return { fioCache, enrichFio }
}
@@ -72,6 +72,8 @@ export * as MarketplaceCppStatus from './marketplaceCppStatus'
export * as ListSupplierOrders from './listSupplierOrders'
/** Эпик 3 / Story 3.4: собственные Offer'ы поставщика (стол поставщика, все статусы) */
export * as ListMyOffers from './listMyOffers'
/** Реестр всех предложений кооператива любого статуса (стол администратора, Offer:read:all) */
export * as ListAllOffers from './listAllOffers'
/** Доступные категории и типы товаров для кооператива (admin-настройка whitelist'а) */
export * as GetAvailableCategories from './getAvailableCategories'
/** Эпик 16: полный список категорий кооператива — общие baseline + собственные */
@@ -0,0 +1,10 @@
import { marketplaceOfferPaginationSelector } from '../../selectors/marketplace/marketplaceOfferPaginationSelector'
import { $, type GraphQLTypes, type InputType, Selector } from '../../zeus/index'
export const name = 'marketplaceListAllOffers'
export const query = Selector('Query')({
[name]: [{ input: $('input', 'MarketplaceListAllOffersInput') }, marketplaceOfferPaginationSelector],
})
export type IOutput = InputType<GraphQLTypes['Query'], typeof query>
+7
View File
@@ -795,6 +795,9 @@ export const AllTypesProps: Record<string,any> = {
MarketplaceKUStatus: "enum" as const,
MarketplaceListAidsInput:{
},
MarketplaceListAllOffersInput:{
statuses:"MarketplaceOfferStatus"
},
MarketplaceListAplReceptionsByBranameInput:{
@@ -2163,6 +2166,9 @@ export const AllTypesProps: Record<string,any> = {
marketplaceListAids:{
data:"MarketplaceListAidsInput"
},
marketplaceListAllOffers:{
input:"MarketplaceListAllOffersInput"
},
marketplaceListAllOrders:{
input:"MarketplaceListOrdersInput",
options:"PaginationInput"
@@ -5850,6 +5856,7 @@ export const ReturnTypes: Record<string,any> = {
marketplaceIssueActChairmanSignablePayload:"GeneratedDocument",
marketplaceIssueActOrdererSignablePayload:"DocumentAggregate",
marketplaceListAids:"MarketplaceAid",
marketplaceListAllOffers:"MarketplaceOfferPaginationResult",
marketplaceListAllOrders:"MarketplaceOrderPaginationResult",
marketplaceListAplReceptionsAsSupplier:"MarketplaceAplReception",
marketplaceListAplReceptionsByBraname:"MarketplaceAplReception",
+67
View File
@@ -7193,6 +7193,21 @@ export type ValueTypes = {
["MarketplaceListAidsInput"]: {
/** Показать заявки только этого получателя (по умолчанию — свои). */
username?: string | undefined | null | Variable<any, string>
};
/** Параметры фильтрации реестра всех предложений кооператива (стол администратора). */
["MarketplaceListAllOffersInput"]: {
/** Количество элементов на странице */
limit: number | Variable<any, string>,
/** Номер страницы */
page: number | Variable<any, string>,
/** Ключ сортировки (например, "name") */
sortBy?: string | undefined | null | Variable<any, string>,
/** Направление сортировки ("ASC" или "DESC") */
sortOrder: string | Variable<any, string>,
/** Один или несколько статусов предложения, по которым нужно отфильтровать список. */
statuses?: Array<ValueTypes["MarketplaceOfferStatus"]> | undefined | null | Variable<any, string>,
/** Фильтр по аккаунту поставщика. */
supplier_account?: string | undefined | null | Variable<any, string>
};
["MarketplaceListAplReceptionsByBranameInput"]: {
/** Идентификатор КУ-получателя. */
@@ -10369,6 +10384,7 @@ marketplaceGetUserRequests?: [{ data?: ValueTypes["GetUserRequestsInput"] | unde
marketplaceIssueActChairmanSignablePayload?: [{ data: ValueTypes["MarketplaceIssueActPayloadInput"] | Variable<any, string>},ValueTypes["GeneratedDocument"]],
marketplaceIssueActOrdererSignablePayload?: [{ data: ValueTypes["MarketplaceIssueActPayloadInput"] | Variable<any, string>},ValueTypes["DocumentAggregate"]],
marketplaceListAids?: [{ data?: ValueTypes["MarketplaceListAidsInput"] | undefined | null | Variable<any, string>},ValueTypes["MarketplaceAid"]],
marketplaceListAllOffers?: [{ input?: ValueTypes["MarketplaceListAllOffersInput"] | undefined | null | Variable<any, string>},ValueTypes["MarketplaceOfferPaginationResult"]],
marketplaceListAllOrders?: [{ input?: ValueTypes["MarketplaceListOrdersInput"] | undefined | null | Variable<any, string>, options?: ValueTypes["PaginationInput"] | undefined | null | Variable<any, string>},ValueTypes["MarketplaceOrderPaginationResult"]],
/** Список актов приёмки, ожидающих подписи текущего поставщика. */
marketplaceListAplReceptionsAsSupplier?:ValueTypes["MarketplaceAplReception"],
@@ -17953,6 +17969,21 @@ export type ResolverInputTypes = {
["MarketplaceListAidsInput"]: {
/** Показать заявки только этого получателя (по умолчанию — свои). */
username?: string | undefined | null
};
/** Параметры фильтрации реестра всех предложений кооператива (стол администратора). */
["MarketplaceListAllOffersInput"]: {
/** Количество элементов на странице */
limit: number,
/** Номер страницы */
page: number,
/** Ключ сортировки (например, "name") */
sortBy?: string | undefined | null,
/** Направление сортировки ("ASC" или "DESC") */
sortOrder: string,
/** Один или несколько статусов предложения, по которым нужно отфильтровать список. */
statuses?: Array<ResolverInputTypes["MarketplaceOfferStatus"]> | undefined | null,
/** Фильтр по аккаунту поставщика. */
supplier_account?: string | undefined | null
};
["MarketplaceListAplReceptionsByBranameInput"]: {
/** Идентификатор КУ-получателя. */
@@ -21011,6 +21042,7 @@ marketplaceGetUserRequests?: [{ data?: ResolverInputTypes["GetUserRequestsInput"
marketplaceIssueActChairmanSignablePayload?: [{ data: ResolverInputTypes["MarketplaceIssueActPayloadInput"]},ResolverInputTypes["GeneratedDocument"]],
marketplaceIssueActOrdererSignablePayload?: [{ data: ResolverInputTypes["MarketplaceIssueActPayloadInput"]},ResolverInputTypes["DocumentAggregate"]],
marketplaceListAids?: [{ data?: ResolverInputTypes["MarketplaceListAidsInput"] | undefined | null},ResolverInputTypes["MarketplaceAid"]],
marketplaceListAllOffers?: [{ input?: ResolverInputTypes["MarketplaceListAllOffersInput"] | undefined | null},ResolverInputTypes["MarketplaceOfferPaginationResult"]],
marketplaceListAllOrders?: [{ input?: ResolverInputTypes["MarketplaceListOrdersInput"] | undefined | null, options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["MarketplaceOrderPaginationResult"]],
/** Список актов приёмки, ожидающих подписи текущего поставщика. */
marketplaceListAplReceptionsAsSupplier?:ResolverInputTypes["MarketplaceAplReception"],
@@ -28341,6 +28373,21 @@ export type ModelTypes = {
["MarketplaceListAidsInput"]: {
/** Показать заявки только этого получателя (по умолчанию — свои). */
username?: string | undefined | null
};
/** Параметры фильтрации реестра всех предложений кооператива (стол администратора). */
["MarketplaceListAllOffersInput"]: {
/** Количество элементов на странице */
limit: number,
/** Номер страницы */
page: number,
/** Ключ сортировки (например, "name") */
sortBy?: string | undefined | null,
/** Направление сортировки ("ASC" или "DESC") */
sortOrder: string,
/** Один или несколько статусов предложения, по которым нужно отфильтровать список. */
statuses?: Array<ModelTypes["MarketplaceOfferStatus"]> | undefined | null,
/** Фильтр по аккаунту поставщика. */
supplier_account?: string | undefined | null
};
["MarketplaceListAplReceptionsByBranameInput"]: {
/** Идентификатор КУ-получателя. */
@@ -32016,6 +32063,8 @@ export type ModelTypes = {
marketplaceIssueActOrdererSignablePayload: ModelTypes["DocumentAggregate"],
/** Заявки на материальную помощь: свои — для доверенного; все заявки кооператива — для администратора. */
marketplaceListAids: Array<ModelTypes["MarketplaceAid"]>,
/** Реестр всех предложений кооператива любого статуса (стол администратора). */
marketplaceListAllOffers: ModelTypes["MarketplaceOfferPaginationResult"],
/** Реестр всех заказов кооператива с их текущими статусами (стол администратора). */
marketplaceListAllOrders: ModelTypes["MarketplaceOrderPaginationResult"],
/** Список актов приёмки, ожидающих подписи текущего поставщика. */
@@ -39708,6 +39757,21 @@ export type GraphQLTypes = {
["MarketplaceListAidsInput"]: {
/** Показать заявки только этого получателя (по умолчанию — свои). */
username?: string | undefined | null
};
/** Параметры фильтрации реестра всех предложений кооператива (стол администратора). */
["MarketplaceListAllOffersInput"]: {
/** Количество элементов на странице */
limit: number,
/** Номер страницы */
page: number,
/** Ключ сортировки (например, "name") */
sortBy?: string | undefined | null,
/** Направление сортировки ("ASC" или "DESC") */
sortOrder: string,
/** Один или несколько статусов предложения, по которым нужно отфильтровать список. */
statuses?: Array<GraphQLTypes["MarketplaceOfferStatus"]> | undefined | null,
/** Фильтр по аккаунту поставщика. */
supplier_account?: string | undefined | null
};
["MarketplaceListAplReceptionsByBranameInput"]: {
/** Идентификатор КУ-получателя. */
@@ -43653,6 +43717,8 @@ export type GraphQLTypes = {
marketplaceIssueActOrdererSignablePayload: GraphQLTypes["DocumentAggregate"],
/** Заявки на материальную помощь: свои — для доверенного; все заявки кооператива — для администратора. */
marketplaceListAids: Array<GraphQLTypes["MarketplaceAid"]>,
/** Реестр всех предложений кооператива любого статуса (стол администратора). */
marketplaceListAllOffers: GraphQLTypes["MarketplaceOfferPaginationResult"],
/** Реестр всех заказов кооператива с их текущими статусами (стол администратора). */
marketplaceListAllOrders: GraphQLTypes["MarketplaceOrderPaginationResult"],
/** Список актов приёмки, ожидающих подписи текущего поставщика. */
@@ -46240,6 +46306,7 @@ type ZEUS_VARIABLES = {
["MarketplaceIssueActSignedMetaDocumentInput"]: ValueTypes["MarketplaceIssueActSignedMetaDocumentInput"];
["MarketplaceKUStatus"]: ValueTypes["MarketplaceKUStatus"];
["MarketplaceListAidsInput"]: ValueTypes["MarketplaceListAidsInput"];
["MarketplaceListAllOffersInput"]: ValueTypes["MarketplaceListAllOffersInput"];
["MarketplaceListAplReceptionsByBranameInput"]: ValueTypes["MarketplaceListAplReceptionsByBranameInput"];
["MarketplaceListCatalogInput"]: ValueTypes["MarketplaceListCatalogInput"];
["MarketplaceListConsolidatedRequestsInput"]: ValueTypes["MarketplaceListConsolidatedRequestsInput"];