feat(marketplace): Стол ПВЗ — рабочее место оператора конкретного КУ

Стол ПВЗ переориентирован с «списка всех ПВЗ кооператива» на рабочее место
оператора ЕГО кооперативного участка (председатель КУ или доверенное лицо).

- entities/OperatorBranch: стор активного КУ оператора на marketplaceWhoAmI
  (+ детали ПВЗ + best-effort core getBranches) и шапка OperatorBranchBar
  (название/адрес/статус ПВЗ, без кодов; селектор при нескольких КУ).
- Операторские страницы (поставки/приёмка/маркировка/выдача/возвраты/склад/
  сводный) переведены на активный КУ из стора — убраны ручные поля «Код КУ»
  и чипы-коды, автозагрузка по активному участку.
- Новая страница «Доверенные лица»: председатель КУ добавляет/снимает
  доверенных (features/Branch/ManageTrusted → addTrustedAccount/
  deleteTrustedAccount); правки гейтятся chairman.
- Страница «ПВЗ кооператива» (PvzList) удалена со Стола ПВЗ — управление
  сетью ПВЗ живёт на столе администратора.
- Админ-страница ПВЗ: кнопка «Карта» (OSM, без ключа) для точки с координатами.

Бэкенд-фоллоуапы (вне scope): обогащение whoAmI именем/доверенными для
оператора-только-доверенного; auth addtrusted для trustee КУ (сейчас chairman
кооператива); грант-нюанс — admin видит Стол ПВЗ через :all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ant
2026-05-29 19:54:58 +00:00
parent 0980c11db5
commit 0f2f288ede
26 changed files with 797 additions and 324 deletions
+27 -19
View File
@@ -2,7 +2,7 @@ import { markRaw } from 'vue'
import { MarketplaceCatalogPage } from 'src/pages/Marketplace/MarketplaceCatalog'
import { CreateMarketplaceOfferPage } from 'src/pages/Marketplace/CreateMarketplaceOffer'
import { MyOrdersPage } from 'src/pages/Marketplace/MyOrders'
import { PvzListPage } from 'src/pages/Marketplace/PvzList'
import { OperatorTrustedPersonsPage } from 'src/pages/Marketplace/OperatorTrustedPersons'
import { OperatorIssuancePage } from 'src/pages/Marketplace/OperatorIssuance'
import { OrdererReadyToReceivePage } from 'src/pages/Marketplace/OrdererReadyToReceive'
import { OrdererReturnClaimsPage } from 'src/pages/Marketplace/OrdererReturnClaims'
@@ -337,14 +337,17 @@ export default async function (): Promise<IWorkspaceConfig[]> {
extension_name: 'market',
title: 'Стол ПВЗ',
icon: 'fa-solid fa-map-location-dot',
defaultRoute: 'marketplace-pvz',
defaultRoute: 'marketplace-pvz-incoming-shipments',
routes: [
{
// Стол ПВЗ открыт оператору/председателю КУ (marketplace-роль
// `operator` → `Warehouse:read:own-KU`) и председателю кооператива
// (роль `admin` → `Warehouse:read:all`, через разворот покрывает
// own-KU). Гейтинг — через grants, отдельный per-route guard больше
// не нужен.
// Стол ПВЗ — рабочее место оператора КОНКРЕТНОГО кооперативного
// участка (marketplace-роль `operator` → `Warehouse:read:own-KU`):
// председателя КУ (trustee) либо его доверенного лица (trusted).
// Активный КУ определяется через `marketplaceWhoAmI.branches`
// (entities/OperatorBranch), коды участков в UI не показываются.
// Управление сетью ПВЗ кооператива (создание/геокодинг/статусы) —
// на столе администратора (AdminIssuancePoints), не здесь.
// Гейтинг — через grants, отдельный per-route guard не нужен.
meta: {
title: 'Стол ПВЗ',
icon: 'fa-solid fa-map-location-dot',
@@ -352,18 +355,6 @@ export default async function (): Promise<IWorkspaceConfig[]> {
path: '/:coopname/market-pvz',
name: 'market-pvz',
children: [
{
path: 'list',
name: 'marketplace-pvz',
component: markRaw(PvzListPage),
meta: {
title: 'ПВЗ кооператива',
icon: 'fa-solid fa-map-location-dot',
requires: 'Warehouse:read:own-KU',
requiresAuth: true,
agreements: agreementsBase,
},
},
{
// Поток IV шаг 1: operator-стол «Ожидаемые поставки». Лента партий,
// направленных на КУ оператора (own-KU scoping через
@@ -458,6 +449,23 @@ export default async function (): Promise<IWorkspaceConfig[]> {
agreements: agreementsBase,
},
},
{
// Управление доверенными лицами КУ: председатель участка (trustee)
// добавляет/снимает доверенных (core addTrustedAccount/
// deleteTrustedAccount) — они получают те же операционные права
// по Столу ПВЗ. Видна всем операторам КУ; правки — только
// председателю кооператива (auth мутаций = chairman).
path: 'trusted-persons',
name: 'marketplace-pvz-trusted-persons',
component: markRaw(OperatorTrustedPersonsPage),
meta: {
title: 'Доверенные лица',
icon: 'group',
requires: 'Warehouse:read:own-KU',
requiresAuth: true,
agreements: agreementsBase,
},
},
],
},
],
@@ -0,0 +1,20 @@
import { Queries } from '@coopenomics/sdk'
import { client } from 'src/shared/api/client'
export type IMarketplaceWhoAmI =
Queries.Marketplace.WhoAmI.IOutput[typeof Queries.Marketplace.WhoAmI.name]
/**
* Контекст текущего пайщика в Столе заказов: core/marketplace-роли + список
* браней КУ, на которых он оператор (trustee ИЛИ trusted). Сервер сам берёт
* пайщика из JWT — никаких идентификаторов с фронта не передаём.
*/
export async function fetchWhoAmI(): Promise<IMarketplaceWhoAmI> {
const { [Queries.Marketplace.WhoAmI.name]: result } = await client.Query(
Queries.Marketplace.WhoAmI.query,
{ variables: {} },
)
return result
}
export const api = { fetchWhoAmI }
@@ -0,0 +1,3 @@
export * from './model'
export * from './api'
export * from './ui'
@@ -0,0 +1,2 @@
export * from './store'
export * from './types'
@@ -0,0 +1,116 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { useBranchStore } from 'src/entities/Branch/model'
import { useMarketplaceKUDetailsStore } from 'src/entities/MarketplaceKUDetails'
import { fetchWhoAmI, type IMarketplaceWhoAmI } from '../api'
import type { IOperatorBranch } from './types'
const namespace = 'operatorBranchStore'
/**
* Контекст оператора Стола ПВЗ: на каких КУ текущий пайщик работает оператором
* и какой из них активен. Источник «моих КУ» — `marketplaceWhoAmI.branches`
* (trustee ИЛИ trusted, см. backend `MarketplaceKuChairmanService`); он работает
* для любой роли, включая доверенное лицо без admin/board.
*
* Обогащение активного КУ:
* - `marketplaceListKUDetails` — детализация ПВЗ (адрес/статус/координаты/часы),
* читают все роли;
* - core `getBranches` — short_name + ФИО trustee/trusted, доступно только
* председателю/совету. Если недоступно (доверенное-только) — мягкая
* деградация: имя = адрес ПВЗ, без падения.
*
* Активный КУ: один — выбирается автоматически; несколько — селектор в шапке.
* Никаких кодов КУ в UI: стол спроецирован на участок, а не на их список.
*/
export const useOperatorBranchStore = defineStore(namespace, () => {
const branchStore = useBranchStore()
const kuStore = useMarketplaceKUDetailsStore()
const who = ref<IMarketplaceWhoAmI | null>(null)
const loading = ref(false)
const loaded = ref(false)
const activeBraname = ref<string | null>(null)
const operatorBranames = computed<string[]>(() => who.value?.branches ?? [])
const isOperator = computed(() => operatorBranames.value.length > 0)
const hasMultiple = computed(() => operatorBranames.value.length > 1)
const branches = computed<IOperatorBranch[]>(() => {
const me = who.value?.username ?? ''
const detailsByBra = new Map(kuStore.details.map((d) => [d.coreBraname, d]))
const branchByBra = new Map(branchStore.branches.map((b) => [b.braname, b]))
return operatorBranames.value.map((braname) => {
const details = detailsByBra.get(braname) ?? null
const branch = branchByBra.get(braname) ?? null
return {
braname,
name: branch?.short_name || branch?.full_name || '',
address: details?.addressFull || branch?.fact_address || branch?.full_address || '',
isTrustee: branch?.trustee?.username === me,
details,
branch,
}
})
})
const activeBranch = computed<IOperatorBranch | null>(() =>
activeBraname.value
? branches.value.find((b) => b.braname === activeBraname.value) ?? null
: null,
)
async function load(coopname: string): Promise<void> {
loading.value = true
try {
// whoAmI — единственный источник «моих КУ». Если упал (гость/не-пайщик),
// стол деградирует в EmptyState «вы не оператор», а не в ошибку.
try {
who.value = await fetchWhoAmI()
} catch {
who.value = null
}
const list = who.value?.branches ?? []
if (list.length) {
// Детали ПВЗ — читают все роли (chairman/member/user).
await kuStore.load({ coopname, onlyActive: false })
// Обогащение core-веткой — best-effort: доверенному-только ветки
// закрыты по ролям, тогда остаёмся на braname/адресе ПВЗ.
try {
await branchStore.loadBranches({ coopname })
} catch {
/* нет прав читать ветки — деградируем мягко */
}
}
if (!activeBraname.value || !list.includes(activeBraname.value)) {
activeBraname.value = list[0] ?? null
}
loaded.value = true
} finally {
loading.value = false
}
}
async function ensureLoaded(coopname: string): Promise<void> {
if (!loaded.value && !loading.value) await load(coopname)
}
function setActive(braname: string): void {
if (operatorBranames.value.includes(braname)) activeBraname.value = braname
}
return {
who,
loading,
loaded,
activeBraname,
operatorBranames,
isOperator,
hasMultiple,
branches,
activeBranch,
load,
ensureLoaded,
setActive,
}
})
@@ -0,0 +1,24 @@
import type { IBranch } from 'src/entities/Branch/model'
import type { IMarketplaceKUDetails } from 'src/entities/MarketplaceKUDetails'
/**
* Кооперативный участок, на котором текущий пайщик работает оператором
* (председатель КУ `trustee` либо его доверенное лицо `trusted` — права
* по Столу ПВЗ идентичны). Самодостаточная проекция для стола оператора:
* имя/адрес для шапки, детализация ПВЗ и (если доступна по ролям) полная
* core-карточка КУ.
*/
export interface IOperatorBranch {
/** Идентификатор КУ в core (он же `braname`). Служебный — в UI не показываем. */
braname: string
/** Человекочитаемое имя КУ (short_name из core); пусто, если ветка недоступна по ролям. */
name: string
/** Фактический адрес ПВЗ (из детализации) либо адрес КУ из core. */
address: string
/** Текущий пайщик — председатель этого КУ (trustee), а не просто доверенное лицо. */
isTrustee: boolean
/** Детализация ПВЗ, если КУ подключён как пункт выдачи (иначе null). */
details: IMarketplaceKUDetails | null
/** Полная карточка КУ из core, если доступна по ролям пайщика (иначе null). */
branch: IBranch | null
}
@@ -0,0 +1,87 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { BaseBadge, BaseSelect } from 'src/shared/ui/base'
import type { BaseBadgeVariant, BaseSelectOption } from 'src/shared/ui/base'
import { useOperatorBranchStore } from '../model'
/**
* Шапка-контекст Стола ПВЗ: показывает активный кооперативный участок
* оператора (название + фактический адрес + статус ПВЗ) без служебных кодов.
* Если оператор работает на нескольких КУ — селектор переключения активного.
*/
const store = useOperatorBranchStore()
const active = computed(() => store.activeBranch)
const title = computed(
() => active.value?.name || active.value?.address || 'Пункт выдачи заказов',
)
const subtitle = computed(() => (active.value?.name ? active.value.address : ''))
const status = computed<{ label: string; variant: BaseBadgeVariant }>(() => {
const d = active.value?.details
if (!d) return { label: 'КУ не подключён как ПВЗ', variant: 'warn' }
return d.status === 'ACTIVE'
? { label: 'ПВЗ активен', variant: 'pos' }
: { label: 'ПВЗ деактивирован', variant: 'neutral' }
})
const options = computed<BaseSelectOption[]>(() =>
store.branches.map((b) => ({ value: b.braname, label: b.name || b.address || 'Участок' })),
)
</script>
<template lang="pug">
.op-branch-bar(v-if='store.isOperator')
q-icon.op-branch-bar__icon(name='storefront', size='24px')
.op-branch-bar__info
.op-branch-bar__title {{ title }}
.op-branch-bar__sub(v-if='subtitle') {{ subtitle }}
BaseSelect.op-branch-bar__select(
v-if='store.hasMultiple',
:model-value='store.activeBraname',
:options='options',
label='Участок',
@update:model-value='store.setActive(String($event))'
)
BaseBadge(:variant='status.variant') {{ status.label }}
</template>
<style scoped lang="scss">
.op-branch-bar {
display: flex;
align-items: center;
gap: var(--p-3, 12px);
padding: var(--p-3, 12px) var(--p-4, 16px);
border: 1px solid var(--p-line);
border-radius: var(--p-r-md, 8px);
background: var(--p-surface-2);
margin-bottom: var(--p-4, 16px);
&__icon {
color: var(--p-ink-3);
flex-shrink: 0;
}
&__info {
flex: 1 1 auto;
min-width: 0;
}
&__title {
font-weight: 600;
font-size: 1.0625rem;
line-height: 1.3;
}
&__sub {
color: var(--p-ink-3);
font-size: 0.875rem;
}
&__select {
min-width: 220px;
flex-shrink: 0;
}
}
</style>
@@ -0,0 +1 @@
export { default as OperatorBranchBar } from './OperatorBranchBar.vue'
@@ -0,0 +1,25 @@
import type { IBranch } from 'src/entities/Branch/model'
import type { IAddTrustedInput, IDeleteTrustedInput } from '../model'
import { client } from 'src/shared/api/client'
import { Mutations } from '@coopenomics/sdk'
async function addTrusted(data: IAddTrustedInput): Promise<IBranch> {
const { [Mutations.Branches.AddTrustedAccount.name]: result } = await client.Mutation(
Mutations.Branches.AddTrustedAccount.mutation,
{ variables: { data } },
)
return result
}
async function deleteTrusted(data: IDeleteTrustedInput): Promise<IBranch> {
const { [Mutations.Branches.DeleteTrustedAccount.name]: result } = await client.Mutation(
Mutations.Branches.DeleteTrustedAccount.mutation,
{ variables: { data } },
)
return result
}
export const api = {
addTrusted,
deleteTrusted,
}
@@ -0,0 +1 @@
export * from './model'
@@ -0,0 +1,24 @@
import type { Mutations } from '@coopenomics/sdk'
import type { IBranch } from 'src/entities/Branch/model'
import { api } from '../api'
export type IAddTrustedInput = Mutations.Branches.AddTrustedAccount.IInput['data']
export type IDeleteTrustedInput = Mutations.Branches.DeleteTrustedAccount.IInput['data']
/**
* Управление доверенными лицами кооперативного участка. Доверенное лицо
* получает те же операционные права по Столу ПВЗ, что и председатель КУ
* (приёмка, выдача, маркировка, склад). On-chain действия — `addtrusted`/
* `deltrusted` контракта branch; auth мутаций — председатель кооператива.
*/
export function useManageTrusted() {
async function addTrusted(data: IAddTrustedInput): Promise<IBranch> {
return api.addTrusted(data)
}
async function deleteTrusted(data: IDeleteTrustedInput): Promise<IBranch> {
return api.deleteTrusted(data)
}
return { addTrusted, deleteTrusted }
}
@@ -12,8 +12,11 @@ import type {
IMarketplaceKUDetails,
KuDetailsStatus,
} from 'src/entities/MarketplaceKUDetails'
import { BaseBadge, BaseButton, EmptyState, TableSkeleton } from 'src/shared/ui/base'
import { BaseBadge, BaseButton, BaseDialog, EmptyState, TableSkeleton } from 'src/shared/ui/base'
import type { BaseBadgeVariant, TableSkeletonColumn } from 'src/shared/ui/base'
// Map экспортируется как `Map` — импортируем под алиасом, чтобы не затенять
// глобальный `Map` (используется в `rows`).
import { Map as MapView } from 'src/shared/ui/Map'
import { MarketplaceDetailKUDialog } from 'src/features/MarketplaceDetailKU'
/**
@@ -84,6 +87,25 @@ function addressOf(row: IssuancePointRow): string {
return row.branch.fact_address || row.branch.full_address || '—'
}
// Карта ПВЗ: открываем точку по координатам геокодера (OSM, без API-ключа).
const mapOpen = ref(false)
const mapRow = ref<IssuancePointRow | null>(null)
const mapTitle = computed(() =>
mapRow.value
? `Карта — ${mapRow.value.branch.short_name || mapRow.value.branch.full_name || mapRow.value.branch.braname}`
: 'Карта',
)
function hasCoords(row: IssuancePointRow): boolean {
const d = row.details
return !!d && d.geocodeStatus === 'OK' && d.lat != null && d.lng != null
}
function openMap(row: IssuancePointRow): void {
mapRow.value = row
mapOpen.value = true
}
const skeletonColumns: TableSkeletonColumn[] = [
{ label: 'Участок', cell: 'text' },
{ label: 'Город', cell: 'text', cellWidth: '120px' },
@@ -206,14 +228,22 @@ q-page.admin-pvz
td.col-status
BaseBadge(:variant='statusOf(row).variant') {{ statusOf(row).label }}
td.col-geo
BaseBadge(
v-if='row.details',
:variant='GEOCODE_LABEL[row.details.geocodeStatus].variant'
)
| {{ GEOCODE_LABEL[row.details.geocodeStatus].label }}
q-tooltip(
v-if='row.details.geocodeStatus === "FAILED" && row.details.geocodeErrorMessage'
) {{ row.details.geocodeErrorMessage }}
.admin-pvz__geo(v-if='row.details')
BaseBadge(:variant='GEOCODE_LABEL[row.details.geocodeStatus].variant')
| {{ GEOCODE_LABEL[row.details.geocodeStatus].label }}
q-tooltip(
v-if='row.details.geocodeStatus === "FAILED" && row.details.geocodeErrorMessage'
) {{ row.details.geocodeErrorMessage }}
BaseButton(
v-if='hasCoords(row)',
variant='ghost',
icon-only,
size='sm',
aria-label='Открыть карту',
@click='openMap(row)'
)
template(#icon-left)
q-icon(name='map', size='18px')
span.admin-pvz__dash(v-else)
td.col-action
template(v-if='isChairman')
@@ -285,6 +315,13 @@ q-page.admin-pvz
:branch='dialogBranch',
@saved='onSaved'
)
BaseDialog(v-model='mapOpen', :title='mapTitle', size='lg')
.admin-pvz__map(
v-if='mapRow && mapRow.details && mapRow.details.lat != null && mapRow.details.lng != null'
)
.admin-pvz__map-addr {{ mapRow.details.addressFull }}
MapView(:long='Number(mapRow.details.lng)', :lat='Number(mapRow.details.lat)')
</template>
<style scoped lang="scss">
@@ -334,6 +371,17 @@ q-page.admin-pvz
&__dash {
color: var(--p-ink-3);
}
&__geo {
display: flex;
align-items: center;
gap: var(--p-1, 4px);
}
&__map-addr {
color: var(--p-ink-2);
margin-bottom: var(--p-3, 12px);
}
}
.table-scroll {
@@ -1,13 +0,0 @@
import { Queries } from '@coopenomics/sdk';
import { client } from 'src/shared/api/client';
export type MarketplaceCurrentMemberView =
Queries.Marketplace.WhoAmI.IOutput['marketplaceWhoAmI'];
export async function fetchWhoAmI(): Promise<MarketplaceCurrentMemberView> {
const { [Queries.Marketplace.WhoAmI.name]: result } = await client.Query(
Queries.Marketplace.WhoAmI.query,
{ variables: {} },
);
return result;
}
@@ -1,10 +1,11 @@
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { Notify } from 'quasar';
import { OperatorBranchBar, useOperatorBranchStore } from 'src/entities/OperatorBranch';
import { listAplReceptionsByBraname } from '../../OperatorReception/api';
import { listIssuancesByBraname } from '../../OperatorIssuance/api';
import { listReturnClaimsByBraname } from '../../OperatorReturnClaims/api';
import { fetchWhoAmI } from '../api';
import type { MarketplaceAplReceptionView } from '../../OffererPendingAplReceptions/api';
import type { MarketplaceOrderIssuanceView } from '../../OperatorIssuance/api';
import type { MarketplaceReturnClaimView } from '../../OperatorReturnClaims/api';
@@ -30,8 +31,11 @@ import type { MarketplaceReturnClaimView } from '../../OperatorReturnClaims/api'
const POLL_INTERVAL_MS = 20_000;
const braname = ref<string>('');
const ownBranches = ref<string[]>([]);
// Активный КУ оператора — из общего контекста стола (без ввода кода вручную).
const route = useRoute();
const store = useOperatorBranchStore();
const coopname = computed(() => String(route.params.coopname ?? ''));
const braname = computed(() => store.activeBraname ?? '');
const activeTab = ref<'receptions' | 'issuances' | 'returns'>('receptions');
const receptions = ref<MarketplaceAplReceptionView[]>([]);
@@ -101,23 +105,12 @@ watch(braname, () => {
receptions.value = [];
issuances.value = [];
returns.value = [];
void loadAll();
});
async function autoDetectBranch(): Promise<void> {
try {
const me = await fetchWhoAmI();
ownBranches.value = me.branches ?? [];
if (!braname.value.trim() && ownBranches.value.length > 0) {
braname.value = ownBranches.value[0];
await loadAll();
}
} catch {
// whoAmI недоступен (не залогинен / не член кооператива) — оставляем ручной ввод
}
}
onMounted(() => {
void autoDetectBranch();
onMounted(async () => {
await store.ensureLoaded(coopname.value);
void loadAll();
pollTimer = setInterval(() => {
if (hasBranch.value) void loadAll();
}, POLL_INTERVAL_MS);
@@ -145,44 +138,17 @@ q-page.mp-role-operator.mp-branch-orders(role="region", aria-label="Сводны
div.text-caption.mp-branch-orders__subtitle
| Все процессы вашего КУ в одном месте: приёмки партий, выдачи пайщикам, гарантийные возвраты. Для действий переходите на специализированные столы Стола ПВЗ.
q-card(flat, bordered)
q-card-section
div.row.q-col-gutter-md.items-end
div.col-12.col-md-8
q-input(
v-model="braname",
outlined,
dense,
clearable,
label="ID кооперативного участка (braname)",
hint="Подставлен автоматически по вашему КУ. Измените вручную при необходимости.",
@keyup.enter="loadAll"
)
template(#prepend)
q-icon(name="fa-solid fa-warehouse")
div.q-mt-sm(v-if="ownBranches.length > 1")
span.text-caption.q-mr-sm Ваши участки:
q-chip(
v-for="b in ownBranches",
:key="b",
clickable,
dense,
:color="b === braname ? 'primary' : 'grey-3'",
:text-color="b === braname ? 'white' : 'dark'",
:label="b",
@click="braname = b; loadAll()"
)
div.col-12.col-md-4
q-btn(
unelevated,
no-caps,
color="primary",
icon="fa-solid fa-magnifying-glass",
label="Загрузить",
:loading="loading",
:disable="!hasBranch",
@click="loadAll"
)
OperatorBranchBar
.row.justify-end(v-if="hasBranch")
q-btn(
flat,
no-caps,
icon="refresh",
label="Обновить",
:loading="loading",
@click="loadAll"
)
q-card(v-if="hasBranch", flat, bordered)
q-tabs(
@@ -242,8 +208,8 @@ q-page.mp-role-operator.mp-branch-orders(role="region", aria-label="Сводны
q-item-label(caption) Статус: {{ statusLabel(c.status) }} / Создано: {{ formatDate(c.created_at) }}
div.mp-branch-orders__hint(v-if="!hasBranch")
q-icon(name="fa-solid fa-circle-info", color="primary")
| Введите ID вашего КУ, чтобы увидеть сводку процессов.
q-icon(name="info", color="primary")
| Кооперативный участок не определён вы не оператор КУ.
</template>
<style scoped lang="scss">
@@ -16,14 +16,3 @@ export async function listShipmentsByBraname(
);
return result;
}
export type MarketplaceOperatorBranchesView =
Queries.Marketplace.WhoAmI.IOutput['marketplaceWhoAmI'];
export async function fetchOperatorBranches(): Promise<MarketplaceOperatorBranchesView> {
const { [Queries.Marketplace.WhoAmI.name]: result } = await client.Query(
Queries.Marketplace.WhoAmI.query,
{ variables: {} },
);
return result;
}
@@ -4,19 +4,12 @@ q-page.mp-role-operator.q-pa-md(role="region", aria-label="Ожидаемые п
.text-caption.text-grey-7.q-mb-md(style="max-width: 720px")
| Партии поставщиков, направленные на ваш кооперативный участок. Дождитесь статуса «Готова к приёмке» и открывайте акт приёмки на столе «Приёмка партии».
OperatorBranchBar
q-card.mp-card.q-mb-md
q-card-section
.row.q-col-gutter-sm.items-end
q-input.col-12.col-sm-4(
v-model="braname",
label="Код кооперативного участка",
dense,
outlined,
clearable,
hint="Подставлен автоматически по вашему КУ. Измените вручную при необходимости.",
@keyup.enter="load"
)
q-select.col-12.col-sm-4(
q-select.col-12.col-sm-6(
v-model="statusFilter",
:options="statusOptions",
label="Состояние партии",
@@ -33,18 +26,6 @@ q-page.mp-role-operator.q-pa-md(role="region", aria-label="Ожидаемые п
:disable="!braname",
@click="load"
)
.q-mt-sm(v-if="ownBranches.length > 1")
span.text-caption.q-mr-sm Ваши участки:
q-chip(
v-for="b in ownBranches",
:key="b",
clickable,
dense,
:color="b === braname ? 'primary' : 'grey-3'",
:text-color="b === braname ? 'white' : 'dark'",
:label="b",
@click="braname = b; load()"
)
.row.q-col-gutter-md.q-mb-md(v-if="items.length")
q-card.mp-card.col-12.col-sm-3
@@ -83,25 +64,26 @@ q-page.mp-role-operator.q-pa-md(role="region", aria-label="Ожидаемые п
template(#no-data)
.full-width.text-center.q-pa-md.text-grey-7
| {{ braname ? 'Партий по этому КУ нет — проверьте код или фильтр по состояниям.' : 'Введите код КУ и нажмите «Обновить».' }}
| {{ braname ? 'Ожидаемых поставок на участок нет или они скрыты фильтром.' : 'Кооперативный участок не определён — вы не оператор КУ.' }}
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import type { QTableProps } from 'quasar'
import { FailAlert } from 'src/shared/api'
import { Zeus } from '@coopenomics/sdk'
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits'
import {
fetchOperatorBranches,
listShipmentsByBraname,
type MarketplaceShipmentView,
} from '../api'
import { OperatorBranchBar, useOperatorBranchStore } from 'src/entities/OperatorBranch'
import { listShipmentsByBraname, type MarketplaceShipmentView } from '../api'
type ShipmentStatus = MarketplaceShipmentView['status']
const braname = ref<string>('')
const ownBranches = ref<string[]>([])
// Активный КУ оператора — из общего контекста стола (без ввода кода вручную).
const route = useRoute()
const store = useOperatorBranchStore()
const coopname = computed(() => String(route.params.coopname ?? ''))
const braname = computed(() => store.activeBraname ?? '')
const statusFilter = ref<ShipmentStatus[]>([])
const items = ref<MarketplaceShipmentView[]>([])
const loading = ref(false)
@@ -165,21 +147,11 @@ async function load(): Promise<void> {
}
}
async function autoDetectBranch(): Promise<void> {
try {
const me = await fetchOperatorBranches()
ownBranches.value = me.branches ?? []
if (!braname.value?.trim() && ownBranches.value.length > 0) {
braname.value = ownBranches.value[0]
}
} catch {
// whoAmI недоступен — оставляем ручной ввод braname
}
}
watch(braname, () => void load())
onMounted(async () => {
await autoDetectBranch()
await load()
await store.ensureLoaded(coopname.value)
void load()
})
function humanStatus(status: string): string {
@@ -1,7 +1,9 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { Loading } from 'quasar'
import { SuccessAlert, FailAlert } from 'src/shared/api'
import { OperatorBranchBar, useOperatorBranchStore } from 'src/entities/OperatorBranch'
import { BarcodeDisplay } from 'src/widgets/Marketplace/BarcodeDisplay'
import { Zeus } from '@coopenomics/sdk'
import {
@@ -17,7 +19,11 @@ type LabelingMode = 'PER_ORDER' | 'BATCH'
type BarcodeStrategy = Zeus.MarketplaceBarcodeStrategy
type BarcodeFormat = Zeus.MarketplaceBarcodeFormat
const braname = ref<string>('')
// Активный КУ оператора — из общего контекста стола (без ввода кода вручную).
const route = useRoute()
const store = useOperatorBranchStore()
const coopname = computed(() => String(route.params.coopname ?? ''))
const braname = computed(() => store.activeBraname ?? '')
const items = ref<MarketplaceInventoryItemView[]>([])
const loading = ref<boolean>(false)
@@ -111,9 +117,6 @@ async function generateLabelsForShipment(): Promise<void> {
SuccessAlert(
`Партия промаркирована. Заказов в работе: ${result.labeled_order_ids.length}.${skipNote} Готовы к печати ${result.inventory.length} этикеток.`,
)
if (!braname.value && result.inventory[0]?.braname) {
braname.value = result.inventory[0].braname
}
await loadInventory()
} catch (e) {
FailAlert(e, 'Не удалось промаркировать партию')
@@ -126,22 +129,23 @@ function openPrintWindow(): void {
window.print()
}
watch(braname, () => {
void loadInventory()
void loadShipments()
})
onMounted(async () => {
await store.ensureLoaded(coopname.value)
await loadShipments()
await loadInventory()
})
</script>
<template lang="pug">
q-page.mp-role-operator.mp-inventory-labeling.q-pa-md
OperatorBranchBar
.row.q-mb-md.q-gutter-md.no-print.items-center
q-input.col-3(
v-model="braname"
dense
outlined
label="ID кооперативного участка"
@keyup.enter="loadInventory"
)
q-btn(no-caps color="primary" :loading="loading" label="Загрузить инвентарь" @click="loadInventory")
q-btn(flat no-caps icon="refresh" label="Обновить список партий" @click="loadShipments")
q-space
q-btn(flat no-caps icon="print" label="Печать партии" :disable="!items.length" @click="openPrintWindow")
@@ -219,7 +223,7 @@ q-page.mp-role-operator.mp-inventory-labeling.q-pa-md
| Стратегия маркировки берётся из карточки товара поставщика (per-Offer).
.text-grey-7.text-center.q-pa-xl.no-print(v-if="!items.length")
| Инвентарь пуст. Загрузите КУ, промаркируйте партию или отдельный заказ этикетки появятся справа.
| Инвентарь участка пуст. Промаркируйте партию или отдельный заказ этикетки появятся ниже.
.mp-inventory-labeling__grid(v-else)
.mp-inventory-labeling__label(v-for="item in items" :key="item.id")
@@ -1,7 +1,9 @@
<script lang="ts" setup>
import type { QTableProps } from 'quasar';
import { onMounted, ref } from 'vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { FailAlert } from 'src/shared/api';
import { OperatorBranchBar, useOperatorBranchStore } from 'src/entities/OperatorBranch';
import {
listIssuancesByBraname,
type MarketplaceOrderIssuanceView,
@@ -25,7 +27,11 @@ import IssueActFinalizeDialog from './IssueActFinalizeDialog.vue';
* по FR23-FR25.
*/
const braname = ref<string>('');
// Активный КУ оператора — из общего контекста стола (без ввода кода вручную).
const route = useRoute();
const store = useOperatorBranchStore();
const coopname = computed(() => String(route.params.coopname ?? ''));
const braname = computed(() => store.activeBraname ?? '');
const items = ref<MarketplaceOrderIssuanceView[]>([]);
const loading = ref(false);
@@ -86,16 +92,17 @@ function onFinalized(): void {
void load();
}
onMounted(() => {
// braname придёт из current member operator-роли при подключении к роутингу.
watch(braname, () => void load());
onMounted(async () => {
await store.ensureLoaded(coopname.value);
void load();
});
</script>
<template lang="pug">
q-page.mp-role-operator.mp-issuance.q-pa-md
.row.q-mb-md.q-gutter-md
q-input.col-3(v-model="braname" dense outlined label="ID кооперативного участка выдачи")
q-btn(no-caps color="primary" :loading="loading" label="Загрузить ленту выдач" @click="load")
OperatorBranchBar
q-table(
:rows="items"
@@ -2,17 +2,12 @@
q-page.mp-role-operator.q-pa-md
.text-h5.q-mb-md Склад моего КУ
OperatorBranchBar
q-card.mp-card.q-mb-md
q-card-section.q-gutter-sm
.row.q-col-gutter-sm.items-end
q-input.col-12.col-sm-4(
v-model='braname'
label='Код кооперативного участка'
dense
outlined
@keyup.enter='load'
)
q-select.col-12.col-sm-3(
q-select.col-12.col-sm-4(
v-model='statusFilter'
:options='statusOptions'
label='Состояние'
@@ -22,7 +17,7 @@ q-page.mp-role-operator.q-pa-md
emit-value
map-options
)
q-input.col-12.col-sm-3(
q-input.col-12.col-sm-4(
v-model='ordererFilter'
label='Заказчик'
dense
@@ -72,17 +67,24 @@ q-page.mp-role-operator.q-pa-md
template(#no-data)
.full-width.text-center.q-pa-md.text-grey-7
| {{ braname ? 'Наклеек по этому КУ нет — проверьте код или фильтры по состояниям.' : 'Введите код КУ и нажмите «Обновить».' }}
| {{ braname ? 'На складе участка пока нет наклеек или они скрыты фильтрами.' : 'Кооперативный участок не определён — вы не оператор КУ.' }}
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import type { QTableProps } from 'quasar'
import { FailAlert } from 'src/shared/api'
import { Zeus } from '@coopenomics/sdk'
import { OperatorBranchBar, useOperatorBranchStore } from 'src/entities/OperatorBranch'
import { listInventory, type MarketplaceInventoryItemView } from '../api'
const braname = ref<string>('')
// Активный КУ оператора — из общего контекста стола (без ввода кода вручную).
const route = useRoute()
const store = useOperatorBranchStore()
const coopname = computed(() => String(route.params.coopname ?? ''))
const braname = computed(() => store.activeBraname ?? '')
const statusFilter = ref<MarketplaceInventoryItemView['status'][]>([])
const ordererFilter = ref<string>('')
const items = ref<MarketplaceInventoryItemView[]>([])
@@ -145,7 +147,10 @@ async function load(): Promise<void> {
}
}
onMounted(() => {
watch(braname, () => void load())
onMounted(async () => {
await store.ensureLoaded(coopname.value)
void load()
})
@@ -1,8 +1,10 @@
<script lang="ts" setup>
import type { QTableProps } from 'quasar';
import { onMounted, ref } from 'vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { Loading } from 'quasar';
import { SuccessAlert, FailAlert } from 'src/shared/api';
import { OperatorBranchBar, useOperatorBranchStore } from 'src/entities/OperatorBranch';
import {
createAplReception,
listAplReceptionsByBraname,
@@ -18,7 +20,11 @@ import SignAplReceptionChairmanDialog from './SignAplReceptionChairmanDialog.vue
* расхождением) включается следующим UI PR.
*/
const braname = ref<string>('');
// Активный КУ оператора — из общего контекста стола (без ввода кода вручную).
const route = useRoute();
const store = useOperatorBranchStore();
const coopname = computed(() => String(route.params.coopname ?? ''));
const braname = computed(() => store.activeBraname ?? '');
const items = ref<MarketplaceAplReceptionView[]>([]);
const loading = ref(false);
@@ -101,16 +107,17 @@ async function onChairmanSigned(): Promise<void> {
await load();
}
onMounted(() => {
// braname придёт из current member operator-роли при подключении к роутингу.
watch(braname, () => void load());
onMounted(async () => {
await store.ensureLoaded(coopname.value);
void load();
});
</script>
<template lang="pug">
q-page.mp-role-operator.mp-reception.q-pa-md
.row.q-mb-md.q-gutter-md
q-input.col-3(v-model="braname" dense outlined label="ID кооперативного участка")
q-btn(no-caps color="primary" :loading="loading" label="Загрузить АПП" @click="load")
OperatorBranchBar
.row.q-mb-md.q-gutter-md
q-input.col-4(v-model="shipmentIdInput" dense outlined label="ID партии (shipment_id)")
@@ -1,7 +1,9 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { Zeus } from '@coopenomics/sdk';
import { FailAlert } from 'src/shared/api';
import { OperatorBranchBar, useOperatorBranchStore } from 'src/entities/OperatorBranch';
import {
listReturnClaimsByBraname,
defectCategoryLabel,
@@ -21,7 +23,11 @@ import OnSiteDecisionDialog from './OnSiteDecisionDialog.vue';
* compensating forward `o.mkt.return + o.mkt.return2`).
*/
const braname = ref<string>('');
// Активный КУ оператора — из общего контекста стола (без ввода кода вручную).
const route = useRoute();
const store = useOperatorBranchStore();
const coopname = computed(() => String(route.params.coopname ?? ''));
const braname = computed(() => store.activeBraname ?? '');
const items = ref<MarketplaceReturnClaimView[]>([]);
const loading = ref(false);
@@ -100,18 +106,18 @@ function humanStatus(status: MarketplaceReturnClaimView['status']): string {
}
}
onMounted(() => {
// braname придёт из current member operator-роли при подключении к
// роутингу (на текущей фазе оператор задаёт КУ вручную в поле ввода).
watch(braname, () => void load());
onMounted(async () => {
await store.ensureLoaded(coopname.value);
void load();
});
</script>
<template lang="pug">
q-page.mp-role-operator.mp-return-operator.q-pa-md
.text-h6.q-mb-sm Гарантийный возврат рассмотрение заявлений
.row.q-mb-md.q-gutter-md
q-input.col-3(v-model="braname" dense outlined label="ID кооперативного участка")
q-btn(no-caps color="primary" :loading="loading" label="Загрузить заявления" @click="load")
OperatorBranchBar
.text-subtitle1.q-mb-sm.text-primary Ждут удалённого рассмотрения ({{ pendingClaims.length }})
q-list(v-if="pendingClaims.length > 0" bordered separator).q-mb-md
@@ -0,0 +1,291 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { FailAlert, SuccessAlert } from 'src/shared/api'
import { useSessionStore } from 'src/entities/Session'
import { OperatorBranchBar, useOperatorBranchStore } from 'src/entities/OperatorBranch'
import { useManageTrusted } from 'src/features/Branch/ManageTrusted'
import { BaseBadge, BaseButton, BaseDialog, BaseInput, EmptyState } from 'src/shared/ui/base'
import type { BaseBadgeVariant } from 'src/shared/ui/base'
/**
* Стол ПВЗ → «Доверенные лица». Председатель кооперативного участка (trustee)
* добавляет и снимает доверенных лиц своего КУ — они получают те же
* операционные права по Столу ПВЗ (приёмка, выдача, маркировка, склад).
*
* Активный КУ берётся из общего контекста оператора (entities/OperatorBranch),
* коды участков в UI не показываются. Список доверенных и ФИО приходят из
* core-карточки КУ (доступна председателю/совету); правки гейтятся
* `isChairman` — это совпадает с auth core-мутаций addtrusted/deltrusted.
*/
const route = useRoute()
const session = useSessionStore()
const store = useOperatorBranchStore()
const { addTrusted, deleteTrusted } = useManageTrusted()
const coopname = computed(() => String(route.params.coopname ?? ''))
const canManage = computed(() => session.isChairman ?? false)
const active = computed(() => store.activeBranch)
const branch = computed(() => active.value?.branch ?? null)
interface PersonRow {
username: string
name: string
isTrustee: boolean
}
function personName(p: { first_name?: string; last_name?: string; middle_name?: string; username: string }): string {
const full = [p.last_name, p.first_name, p.middle_name].filter(Boolean).join(' ').trim()
return full || p.username
}
const rows = computed<PersonRow[]>(() => {
const b = branch.value
if (!b) return []
const out: PersonRow[] = [{ username: b.trustee.username, name: personName(b.trustee), isTrustee: true }]
for (const t of b.trusted) {
out.push({ username: t.username, name: personName(t), isTrustee: false })
}
return out
})
const trustedCount = computed(() => rows.value.filter((r) => !r.isTrustee).length)
function roleBadge(row: PersonRow): { label: string; variant: BaseBadgeVariant } {
return row.isTrustee
? { label: 'Председатель КУ', variant: 'pos' }
: { label: 'Доверенное лицо', variant: 'neutral' }
}
// ─── Добавление ───
const newUsername = ref('')
const adding = ref(false)
const canAdd = computed(() => !!newUsername.value.trim() && !!store.activeBraname && canManage.value)
async function onAdd(): Promise<void> {
if (!canAdd.value || !store.activeBraname) return
adding.value = true
try {
await addTrusted({
coopname: coopname.value,
braname: store.activeBraname,
trusted: newUsername.value.trim(),
})
SuccessAlert('Доверенное лицо добавлено')
newUsername.value = ''
await store.load(coopname.value)
} catch (e) {
FailAlert(e, 'Не удалось добавить доверенное лицо')
} finally {
adding.value = false
}
}
// ─── Удаление ───
const removeTarget = ref<PersonRow | null>(null)
const removeOpen = ref(false)
const removing = ref(false)
function askRemove(row: PersonRow): void {
removeTarget.value = row
removeOpen.value = true
}
async function onRemove(): Promise<void> {
const target = removeTarget.value
if (!target || !store.activeBraname) return
removing.value = true
try {
await deleteTrusted({
coopname: coopname.value,
braname: store.activeBraname,
trusted: target.username,
})
SuccessAlert('Доверенное лицо снято')
removeOpen.value = false
removeTarget.value = null
await store.load(coopname.value)
} catch (e) {
FailAlert(e, 'Не удалось снять доверенное лицо')
} finally {
removing.value = false
}
}
onMounted(() => {
void store.ensureLoaded(coopname.value)
})
</script>
<template lang="pug">
q-page.trusted
OperatorBranchBar
EmptyState(
v-if='!store.loading && !store.isOperator',
title='Вы не оператор кооперативного участка',
body='Стол ПВЗ доступен председателю участка и его доверенным лицам. Управление доверенными лицами появится, когда вы станете оператором КУ.'
)
template(#icon)
q-icon(name='group_off', size='48px')
template(v-else)
.banner.banner--info
q-icon.banner__icon(name='info', size='18px')
.banner__body
| Доверенные лица получают те же права на этом пункте выдачи, что и
| председатель участка: приёмка партий, маркировка, выдача заказов,
| гарантийные возвраты и склад. Добавляйте только тех пайщиков, кому
| доверяете операции от имени участка.
.trusted__add(v-if='canManage')
BaseInput.trusted__add-input(
v-model='newUsername',
label='Аккаунт пайщика',
placeholder='например, ivanov',
mono
)
BaseButton(
variant='primary',
:loading='adding',
:disabled='!canAdd',
@click='onAdd'
)
template(#icon-left)
q-icon(name='person_add', size='16px')
| Добавить
.banner.banner--info(v-else-if='!branch')
q-icon.banner__icon(name='info', size='18px')
.banner__body
| Список доверенных лиц участка доступен председателю кооператива.
.trusted__counter(v-if='rows.length')
| Доверенных лиц: {{ trustedCount }}
.table-wrap(v-if='rows.length')
.table-scroll
table.table
thead
tr
th Лицо
th.col-acc Аккаунт
th.col-role Роль
th.col-action(v-if='canManage') Действия
tbody
tr(v-for='row in rows', :key='row.username')
td.trusted__name {{ row.name }}
td.col-acc
span.trusted__acc {{ row.username }}
td.col-role
BaseBadge(:variant='roleBadge(row).variant') {{ roleBadge(row).label }}
td.col-action(v-if='canManage')
BaseButton(
v-if='!row.isTrustee',
variant='ghost',
icon-only,
size='sm',
aria-label='Снять доверенное лицо',
@click='askRemove(row)'
)
template(#icon-left)
q-icon(name='person_remove', size='18px')
span.trusted__dash(v-else)
EmptyState(
v-else-if='!store.loading && branch',
title='Доверенных лиц пока нет',
body='Добавьте доверенных лиц, которым доверяете операции на этом пункте выдачи.'
)
template(#icon)
q-icon(name='group', size='48px')
BaseDialog(
v-model='removeOpen',
title='Снять доверенное лицо',
size='sm'
)
.trusted__confirm(v-if='removeTarget')
| Снять доверенное лицо «{{ removeTarget.name }}» с этого пункта выдачи?
| Пайщик потеряет операционные права по участку.
template(#footer)
BaseButton(variant='ghost', :disabled='removing', @click='removeOpen = false') Отмена
BaseButton(variant='primary', :loading='removing', @click='onRemove')
template(#icon-left)
q-icon(name='person_remove', size='16px')
| Снять
</template>
<style scoped lang="scss">
.trusted {
padding: var(--p-6, 24px);
display: flex;
flex-direction: column;
gap: var(--p-4, 16px);
&__add {
display: flex;
align-items: flex-end;
gap: var(--p-3, 12px);
}
&__add-input {
max-width: 320px;
width: 100%;
}
&__counter {
color: var(--p-ink-3);
font-size: 0.875rem;
}
&__name {
font-weight: 600;
}
&__acc {
font-family: var(--font-mono);
font-size: 0.8125rem;
color: var(--p-ink-3);
}
&__dash {
color: var(--p-ink-3);
}
&__confirm {
color: var(--p-ink-2);
line-height: 1.5;
}
}
.table {
table-layout: fixed;
min-width: 720px;
}
.table-scroll {
overflow-x: auto;
}
.col-acc {
width: 200px;
}
.col-role {
width: 180px;
}
.col-action {
width: 120px;
text-align: right;
}
@media (max-width: 768px) {
.trusted {
padding: var(--p-4, 16px);
}
.trusted__add {
flex-direction: column;
align-items: stretch;
}
}
</style>
@@ -0,0 +1 @@
export { default as OperatorTrustedPersonsPage } from './OperatorTrustedPersonsPage.vue'
@@ -1,120 +0,0 @@
<template lang="pug">
q-page.q-pa-md(padding)
.row.items-center.q-mb-md
.text-h5 ПВЗ Стола заказов
q-space
q-btn(
v-if="isAdmin"
icon="refresh"
label="Обновить"
flat
dense
@click="reload"
)
KUMapWithList(
:items="store.details"
:loading="store.isLoading"
:selected-braname="selectedBraname"
aria-label="Список и карта ПВЗ кооператива"
@select="onSelect"
)
template(#cardAction="{ pvz }")
.column.items-end.q-gutter-xs(v-if="isAdmin")
q-badge(:color="pvz.status === 'ACTIVE' ? 'positive' : 'grey'") {{ pvz.status === 'ACTIVE' ? 'Активен' : 'Деактивирован' }}
q-btn(size="sm" flat dense icon="edit" label="Изменить" @click.stop="editPvz(pvz)")
q-btn(
v-if="pvz.geocodeStatus !== 'OK'"
size="sm"
flat
dense
color="warning"
icon="my_location"
label="Геокодинг"
@click.stop="retryGeocode(pvz)"
)
q-btn(
v-if="pvz.status === 'ACTIVE'"
size="sm"
flat
dense
color="negative"
icon="block"
label="Деактивировать"
@click.stop="setStatus(pvz, 'INACTIVE')"
)
q-btn(
v-else
size="sm"
flat
dense
color="positive"
icon="check_circle"
label="Активировать"
@click.stop="setStatus(pvz, 'ACTIVE')"
)
MarketplaceDetailKUDialog(
v-if="editing"
v-model="dialogOpen"
:coopname="coopname"
:core-braname="editing.coreBraname"
:existing="editing"
@saved="onSaved"
)
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useRoute } from 'vue-router'
import { useSessionStore } from 'src/entities/Session'
import { useMarketplaceKUDetailsStore } from 'src/entities/MarketplaceKUDetails'
import type { IMarketplaceKUDetails } from 'src/entities/MarketplaceKUDetails'
import { KUMapWithList } from 'src/widgets/KUMapWithList'
import { MarketplaceDetailKUDialog } from 'src/features/MarketplaceDetailKU'
const route = useRoute()
const session = useSessionStore()
const store = useMarketplaceKUDetailsStore()
const { details } = storeToRefs(store)
const coopname = computed(() => String(route.params.coopname ?? ''))
const isAdmin = computed(() => session.isChairman ?? false)
const selectedBraname = ref<string | null>(null)
const editing = ref<IMarketplaceKUDetails | null>(null)
const dialogOpen = ref(false)
async function reload() {
await store.load({ coopname: coopname.value, onlyActive: !isAdmin.value })
}
function onSelect(pvz: IMarketplaceKUDetails) {
selectedBraname.value = pvz.coreBraname
}
function editPvz(pvz: IMarketplaceKUDetails) {
editing.value = pvz
dialogOpen.value = true
}
function onSaved(_pvz: IMarketplaceKUDetails) {
void _pvz
void reload()
}
async function setStatus(pvz: IMarketplaceKUDetails, status: 'ACTIVE' | 'INACTIVE') {
await store.setStatus({ coopname: coopname.value, coreBraname: pvz.coreBraname, status })
}
async function retryGeocode(pvz: IMarketplaceKUDetails) {
await store.retryGeocode(coopname.value, pvz.coreBraname)
}
onMounted(() => {
void reload()
})
// Используется для подавления "unused" на details — фактический driver UI.
void details
</script>
@@ -1 +0,0 @@
export { default as PvzListPage } from './PvzListPage.vue'