[598-51][@ant] fix(desktop): убрать утечку прав видимости стола поставщика и показать ФИО в реестре — председатель без одобренного допуска видел рабочие страницы поставщика из-за общего гранта Offer:read вместо offerer-эксклюзивного, а реестр поставщиков показывал только username вместо ФИО/названия организации
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<IWorkspaceConfig[]> {
|
||||
// Онбординг поставщика: 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<IWorkspaceConfig[]> {
|
||||
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<IWorkspaceConfig[]> {
|
||||
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<IWorkspaceConfig[]> {
|
||||
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<IWorkspaceConfig[]> {
|
||||
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<IWorkspaceConfig[]> {
|
||||
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<IWorkspaceConfig[]> {
|
||||
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<IWorkspaceConfig[]> {
|
||||
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<IWorkspaceConfig[]> {
|
||||
meta: {
|
||||
title: 'Выплаты',
|
||||
icon: 'fa-solid fa-money-bill-transfer',
|
||||
requires: 'Offer:read',
|
||||
requires: 'Offer:create:own',
|
||||
requiresAuth: true,
|
||||
agreements: agreementsBase,
|
||||
},
|
||||
|
||||
+3
-3
@@ -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="Договор",
|
||||
|
||||
+37
-1
@@ -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<MarketplaceSupplierView[]>([]);
|
||||
const loading = ref(false);
|
||||
const acting = ref<string | null>(null);
|
||||
|
||||
// Резолв ФИО/наименования организации по username (реестр несёт только
|
||||
// member_account) — канон из NotificationJournalWidget. Кэш на компонент.
|
||||
const supplierNames = ref<Record<string, string>>({});
|
||||
|
||||
function supplierName(username: string): string {
|
||||
return supplierNames.value[username] ?? '';
|
||||
}
|
||||
|
||||
async function resolveSupplierNames(): Promise<void> {
|
||||
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<void> {
|
||||
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
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface IdentityCellProps {
|
||||
accountName: string;
|
||||
fullName?: string | null;
|
||||
copyable?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<template lang="pug">
|
||||
.identity-cell
|
||||
span.identity-cell__name(v-if='fullName') {{ fullName }}
|
||||
AccountBadge(:account-name='accountName', size='sm', :copyable='copyable')
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AccountBadge } from 'src/shared/ui/domain/AccountBadge';
|
||||
import type { IdentityCellProps } from './IdentityCell.types';
|
||||
|
||||
withDefaults(defineProps<IdentityCellProps>(), {
|
||||
copyable: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.identity-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--p-1, 4px);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.identity-cell__name {
|
||||
color: var(--p-ink);
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
line-height: 1.3;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as IdentityCell } from './IdentityCell.vue';
|
||||
export type { IdentityCellProps } from './IdentityCell.types';
|
||||
@@ -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';
|
||||
|
||||
+12
-18
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user