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:
@@ -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 }
|
||||
}
|
||||
+57
-9
@@ -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;
|
||||
}
|
||||
+24
-58
@@ -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;
|
||||
}
|
||||
|
||||
+16
-44
@@ -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 {
|
||||
|
||||
+18
-14
@@ -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")
|
||||
|
||||
+14
-7
@@ -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"
|
||||
|
||||
+18
-13
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
+14
-7
@@ -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)")
|
||||
|
||||
+14
-8
@@ -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
|
||||
|
||||
+291
@@ -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'
|
||||
Reference in New Issue
Block a user