diff --git a/components/desktop/extensions/market/install.ts b/components/desktop/extensions/market/install.ts index dd67a9081c6..48b2d03c912 100644 --- a/components/desktop/extensions/market/install.ts +++ b/components/desktop/extensions/market/install.ts @@ -73,7 +73,13 @@ import { registerMarketplaceProcessInfoHandlers } from './app/extensions' * на резолверах бэкенда; здесь — UI-видимость + навигационный гард. * * Соответствие столов правам (как воспроизведена прежняя видимость по ролям): - * - market / market-supplier → `Offer:read` (любой онбординутый пайщик) + * - market → `Order:create` (orderer-эксклюзивный грант, + * НЕ выдаётся admin/board — иначе председатель видел бы рабочие страницы + * заказчика до подписи своей персональной оферты, см. ниже) + * - market-supplier → `Offer:create:own` (offerer-эксклюзивный грант; + * НЕ `Offer:read` — это право есть и у orderer, и у admin (каталог/модерация), + * поэтому им нельзя гейтить рабочие страницы поставщика — председатель без + * одобренного допуска видел бы «Мои предложения» и т.д., см. ниже) * - market-pvz → `Warehouse:read:own-KU` (оператор КУ + админ) * - market-admin → `Order:read:all` (совет + председатель), * кроме `Whitelist:manage` (только председатель) и `Extension:configure` @@ -317,7 +323,7 @@ export default async function (): Promise { // Онбординг поставщика: backend выдаёт `Onboarding:offerer` // пайщику без одобренного допуска в реестре — тогда виден только // этот экран (выбор модели + реквизиты договора + заявка). После - // одобрения offerer-роль даёт `Offer:read` и открывает стол. + // одобрения offerer-роль даёт `Offer:create:own` и открывает стол. // Зеркало L3-гейта заказчика (marketplace-onboarding-member-cpp). path: 'onboarding', name: 'marketplace-onboarding-supplier', @@ -342,7 +348,7 @@ export default async function (): Promise { meta: { title: 'Мои предложения', icon: 'fa-solid fa-clipboard-list', - requires: 'Offer:read', + requires: 'Offer:create:own', requiresAuth: true, agreements: agreementsBase, }, @@ -358,7 +364,7 @@ export default async function (): Promise { meta: { title: 'Создать предложение', icon: 'fa-solid fa-plus-circle', - requires: 'Offer:read', + requires: 'Offer:create:own', requiresAuth: true, agreements: agreementsBase, hidden: true, @@ -374,7 +380,7 @@ export default async function (): Promise { meta: { title: 'Редактирование предложения', icon: 'fa-solid fa-pen', - requires: 'Offer:read', + requires: 'Offer:create:own', requiresAuth: true, agreements: agreementsBase, hidden: true, @@ -390,7 +396,7 @@ export default async function (): Promise { meta: { title: 'Входящие заказы', icon: 'fa-solid fa-inbox', - requires: 'Offer:read', + requires: 'Offer:create:own', requiresAuth: true, agreements: agreementsBase, }, @@ -406,7 +412,7 @@ export default async function (): Promise { meta: { title: 'Подготовка отгрузки', icon: 'fa-solid fa-truck-ramp-box', - requires: 'Offer:read', + requires: 'Offer:create:own', requiresAuth: true, agreements: agreementsBase, }, @@ -424,7 +430,7 @@ export default async function (): Promise { meta: { title: 'Отгрузить партию', icon: 'fa-solid fa-qrcode', - requires: 'Offer:read', + requires: 'Offer:create:own', requiresAuth: true, agreements: agreementsBase, }, @@ -440,7 +446,7 @@ export default async function (): Promise { meta: { title: 'Подпись приёмки', icon: 'fa-solid fa-file-signature', - requires: 'Offer:read', + requires: 'Offer:create:own', requiresAuth: true, agreements: agreementsBase, }, @@ -456,7 +462,7 @@ export default async function (): Promise { meta: { title: 'Выплаты', icon: 'fa-solid fa-money-bill-transfer', - requires: 'Offer:read', + requires: 'Offer:create:own', requiresAuth: true, agreements: agreementsBase, }, diff --git a/components/desktop/src/pages/Marketplace/SupplierOnboarding/ui/SupplierOnboardingPage.vue b/components/desktop/src/pages/Marketplace/SupplierOnboarding/ui/SupplierOnboardingPage.vue index 2834d690128..b132568d1f9 100644 --- a/components/desktop/src/pages/Marketplace/SupplierOnboarding/ui/SupplierOnboardingPage.vue +++ b/components/desktop/src/pages/Marketplace/SupplierOnboarding/ui/SupplierOnboardingPage.vue @@ -126,9 +126,9 @@ q-page.mp-role-offerer.supplier-onboarding(role="region", aria-label="Подкл .supplier-onboarding__status-text .text-h6 Заявка на рассмотрении .text-body2.text-grey-7 - | Ваша заявка на допуск поставщика передана председателю кооператива. - | Как только её рассмотрят, стол поставщика откроется — здесь появятся - | публикация предложений и приёмка партий. + | Заявку рассмотрит председатель кооператива. После одобрения + | откроется стол поставщика — там можно будет публиковать + | предложения, принимать заказы и отгружать партии. .supplier-onboarding__contract(v-if="contractLabel") DataRow( label="Договор", diff --git a/components/desktop/src/pages/Marketplace/SupplierRegistry/ui/SupplierRegistryPage.vue b/components/desktop/src/pages/Marketplace/SupplierRegistry/ui/SupplierRegistryPage.vue index a68c216c05f..bc6aad50a93 100644 --- a/components/desktop/src/pages/Marketplace/SupplierRegistry/ui/SupplierRegistryPage.vue +++ b/components/desktop/src/pages/Marketplace/SupplierRegistry/ui/SupplierRegistryPage.vue @@ -5,6 +5,9 @@ import { useSessionStore } from 'src/entities/Session/model'; import { BaseBadge, BaseButton, BaseInput, BaseDialog, EmptyState } from 'src/shared/ui/base'; import { TableSkeleton } from 'src/shared/ui/base/TableSkeleton'; import type { TableSkeletonColumn } from 'src/shared/ui/base/TableSkeleton'; +import { IdentityCell } from 'src/shared/ui/domain'; +import { getName } from 'src/shared/lib/utils/account'; +import { api as accountApi } from 'src/entities/Account/api'; import { addSupplier, approveSupplier, @@ -32,6 +35,34 @@ const items = ref([]); const loading = ref(false); const acting = ref(null); +// Резолв ФИО/наименования организации по username (реестр несёт только +// member_account) — канон из NotificationJournalWidget. Кэш на компонент. +const supplierNames = ref>({}); + +function supplierName(username: string): string { + return supplierNames.value[username] ?? ''; +} + +async function resolveSupplierNames(): Promise { + const usernames = [...new Set(items.value.map((i) => i.member_account).filter(Boolean))]; + const missing = usernames.filter((u) => !(u in supplierNames.value)); + if (!missing.length) return; + await Promise.all( + missing.map(async (username) => { + let name = ''; + try { + const account = await accountApi.getAccount(username); + name = account + ? (getName(account) ?? '').replace(/undefined/g, '').replace(/\s+/g, ' ').trim() + : ''; + } catch { + name = ''; + } + supplierNames.value = { ...supplierNames.value, [username]: name }; + }), + ); +} + const skeletonColumns: TableSkeletonColumn[] = [ { label: 'Поставщик' }, { label: 'Модель' }, @@ -53,6 +84,7 @@ async function load(): Promise { loading.value = true; try { items.value = await fetchSuppliers(); + void resolveSupplierNames(); } catch (e) { FailAlert(e); } finally { @@ -149,7 +181,11 @@ q-page.mp-role-admin.supplier-registry(role="region", aria-label="Реестр th.col-action Действия tbody tr.data-row(v-for="row in items", :key="row.id") - td.cell-name {{ row.member_account }} + td + IdentityCell( + :account-name="row.member_account", + :full-name="supplierName(row.member_account)" + ) td {{ SUPPLIER_MODEL_LABEL[row.model] || row.model }} td {{ contractLabel(row) }} td diff --git a/components/desktop/src/shared/ui/domain/IdentityCell/IdentityCell.types.ts b/components/desktop/src/shared/ui/domain/IdentityCell/IdentityCell.types.ts new file mode 100644 index 00000000000..fb137a97bcb --- /dev/null +++ b/components/desktop/src/shared/ui/domain/IdentityCell/IdentityCell.types.ts @@ -0,0 +1,5 @@ +export interface IdentityCellProps { + accountName: string; + fullName?: string | null; + copyable?: boolean; +} diff --git a/components/desktop/src/shared/ui/domain/IdentityCell/IdentityCell.vue b/components/desktop/src/shared/ui/domain/IdentityCell/IdentityCell.vue new file mode 100644 index 00000000000..dccb35ab184 --- /dev/null +++ b/components/desktop/src/shared/ui/domain/IdentityCell/IdentityCell.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/components/desktop/src/shared/ui/domain/IdentityCell/index.ts b/components/desktop/src/shared/ui/domain/IdentityCell/index.ts new file mode 100644 index 00000000000..71d4eb9da58 --- /dev/null +++ b/components/desktop/src/shared/ui/domain/IdentityCell/index.ts @@ -0,0 +1,2 @@ +export { default as IdentityCell } from './IdentityCell.vue'; +export type { IdentityCellProps } from './IdentityCell.types'; diff --git a/components/desktop/src/shared/ui/domain/index.ts b/components/desktop/src/shared/ui/domain/index.ts index 69c21ed581a..16c978ee0d4 100644 --- a/components/desktop/src/shared/ui/domain/index.ts +++ b/components/desktop/src/shared/ui/domain/index.ts @@ -14,6 +14,7 @@ export * from './ExpenseProposalDocuments'; export * from './ExpenseProposalList'; export * from './FileUploader'; export * from './FilterBar'; +export * from './IdentityCell'; export * from './IdentityPanel'; export * from './NotificationCenter'; export * from './OtpInput'; diff --git a/components/desktop/src/widgets/Cooperative/Notifications/NotificationJournal/ui/NotificationJournalWidget.vue b/components/desktop/src/widgets/Cooperative/Notifications/NotificationJournal/ui/NotificationJournalWidget.vue index 1826d92ddbd..aec0581455d 100644 --- a/components/desktop/src/widgets/Cooperative/Notifications/NotificationJournal/ui/NotificationJournalWidget.vue +++ b/components/desktop/src/widgets/Cooperative/Notifications/NotificationJournal/ui/NotificationJournalWidget.vue @@ -33,9 +33,11 @@ tr(v-for='row in store.items', :key='row.id') td.col-date {{ formatDateToHumanDateTime(row.createdAt) }} td.cell-recipient - template(v-if='row.recipientUsername') - span.cell-recipient__name(v-if='recipientName(row.recipientUsername)') {{ recipientName(row.recipientUsername) }} - AccountBadge(:account-name='row.recipientUsername', size='sm') + IdentityCell( + v-if='row.recipientUsername', + :account-name='row.recipientUsername', + :full-name='recipientName(row.recipientUsername)' + ) span.cell-recipient__sub(v-else) {{ row.recipientSubscriberId }} td {{ workflowLabel(row.workflowId) }} td {{ channelLabels[row.channel] ?? row.channel }} @@ -64,9 +66,12 @@ .nj-card(v-for='row in store.items', :key='row.id') .nj-card__head .nj-card__recipient - span.nj-card__name(v-if='row.recipientUsername && recipientName(row.recipientUsername)') {{ recipientName(row.recipientUsername) }} - AccountBadge(v-if='row.recipientUsername', :account-name='row.recipientUsername', size='sm') - span.nj-card__sub(v-else-if='!row.recipientUsername') {{ row.recipientSubscriberId }} + IdentityCell( + v-if='row.recipientUsername', + :account-name='row.recipientUsername', + :full-name='recipientName(row.recipientUsername)' + ) + span.nj-card__sub(v-else) {{ row.recipientSubscriberId }} BaseBadge(:variant='statusVariant(row.status)') {{ statusLabels[row.status] ?? row.status }} .nj-card__grid .nj-card__cell @@ -121,7 +126,7 @@ import { BaseButton } from 'src/shared/ui/base/BaseButton'; import { EmptyState } from 'src/shared/ui/base/EmptyState'; import { TableSkeleton } from 'src/shared/ui/base/TableSkeleton'; import type { TableSkeletonColumn } from 'src/shared/ui/base/TableSkeleton'; -import { AccountBadge } from 'src/shared/ui/domain/AccountBadge'; +import { IdentityCell } from 'src/shared/ui/domain/IdentityCell'; import { FilterBar } from 'src/shared/ui/domain/FilterBar'; import type { FilterDefinition, FilterValues } from 'src/shared/ui/domain/FilterBar'; import { formatDateToHumanDateTime } from 'src/shared/lib/utils/dates/formatDateToHumanDateTime'; @@ -315,11 +320,6 @@ onMounted(() => { align-items: flex-start; gap: 4px; } -.cell-recipient__name { - color: var(--p-ink); - font-size: var(--p-fs-body-sm, 13px); - line-height: 1.3; -} .cell-recipient__sub { font-family: var(--p-mono); font-size: var(--p-fs-mono-sm, 12px); @@ -372,12 +372,6 @@ onMounted(() => { gap: 2px; min-width: 0; } -.nj-card__name { - font-weight: 500; - color: var(--p-ink); - font-size: var(--p-fs-body-sm, 13px); - overflow-wrap: break-word; -} .nj-card__sub { font-family: var(--p-mono); font-size: var(--p-fs-mono-sm, 12px);