[598][@ant] feat(desktop): branch-chairman/branch-orders — сводный стол КУ

Эпик 6 / Story 6.x. Председатель КУ видит 3 ленты процессов своего
участка в одном экране с табами и счётчиками:

- Приёмки партий (Queries.Marketplace.ListAplReceptionsByBraname, Эпик 5)
- Выдачи заказов (Queries.Marketplace.ListIssuancesByBraname, Эпик 6)
- Гарантийные возвраты (Queries.Marketplace.ListReturnClaimsByBraname, Эпик 7)

Backend готов — обновление аудита §9.7 второй раз: «нужна агрегационная
query» неверно, 3 существующих query достаточно через Promise.all.

Read-only обзор; действия выполняются на /reception, /issuance, /returns.
Поле braname — ручной ввод (auto-detect через whoami — следующий шаг).

Polling 20s; mp-role-operator; роут /market-pvz/branch-orders (chairman).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ant
2026-05-21 17:13:26 +00:00
parent 69d3e79f36
commit d1bebf6f64
3 changed files with 244 additions and 0 deletions
@@ -22,6 +22,7 @@ import { OnboardingCoopAcceptCppPage } from 'src/pages/Marketplace/OnboardingCoo
import { OrdererConsolidatedPage } from 'src/pages/Marketplace/OrdererConsolidated'
import { OffererIncomingOrdersPage } from 'src/pages/Marketplace/OffererIncomingOrders'
import { OffererMyOffersPage } from 'src/pages/Marketplace/OffererMyOffers'
import { BranchChairmanBranchOrdersPage } from 'src/pages/Marketplace/BranchChairmanBranchOrders'
import type { IWorkspaceConfig } from 'src/shared/lib/types/workspace'
import { agreementsBase } from 'src/shared/lib/consts/workspaces'
import { registerMarketplaceProcessInfoHandlers } from './app/extensions'
@@ -419,6 +420,22 @@ export default async function (): Promise<IWorkspaceConfig[]> {
agreements: agreementsBase,
},
},
{
// Эпик 6 / Story 6.x: «Сводный стол КУ» — объединяет ленты
// приёмок / выдач / возвратов в один экран с табами и счётчиками
// для председателя КУ. Read-only обзор; действия — на /reception,
// /issuance, /returns.
path: 'branch-orders',
name: 'marketplace-pvz-branch-orders',
component: markRaw(BranchChairmanBranchOrdersPage),
meta: {
title: 'Сводный стол КУ',
icon: 'fa-solid fa-list-check',
roles: ['chairman'],
requiresAuth: true,
agreements: agreementsBase,
},
},
{
// Эпик 9 / Story 9.1: operator-стол «Склад моего КУ».
// Таблица marketplace_inventory отфильтрована по braname; per-row
@@ -0,0 +1 @@
export { default as BranchChairmanBranchOrdersPage } from './ui/BranchChairmanBranchOrdersPage.vue';
@@ -0,0 +1,226 @@
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { Notify } from 'quasar';
import { listAplReceptionsByBraname } from '../../OperatorReception/api';
import { listIssuancesByBraname } from '../../OperatorIssuance/api';
import { listReturnClaimsByBraname } from '../../OperatorReturnClaims/api';
import type { MarketplaceAplReceptionView } from '../../OffererPendingAplReceptions/api';
import type { MarketplaceOrderIssuanceView } from '../../OperatorIssuance/api';
import type { MarketplaceReturnClaimView } from '../../OperatorReturnClaims/api';
/**
* Эпик 6 / Story 6.x: «Сводный стол КУ» председателя кооперативного участка.
*
* Объединяет три существующих ленты по braname в один экран с табами:
* - Приёмки партий (ListAplReceptionsByBraname, Эпик 5)
* - Выдачи заказов (ListIssuancesByBraname, Эпик 6)
* - Гарантийные возвраты (ListReturnClaimsByBraname, Эпик 7)
*
* Действия (открыть акт приёмки, выдачу, решение по возврату) выполняются
* на соответствующих специализированных страницах /market-pvz/reception,
* /issuance, /returns — здесь только сводный read-обзор для председателя КУ.
*
* Поле braname — пока ручной ввод; в Story 6.x+1 подключится auto-detect
* из marketplace_whoami / marketplace_member_wallet (председатель привязан
* к одному КУ через trustee).
*/
const POLL_INTERVAL_MS = 20_000;
const braname = ref<string>('');
const activeTab = ref<'receptions' | 'issuances' | 'returns'>('receptions');
const receptions = ref<MarketplaceAplReceptionView[]>([]);
const issuances = ref<MarketplaceOrderIssuanceView[]>([]);
const returns = ref<MarketplaceReturnClaimView[]>([]);
const loading = ref(false);
let pollTimer: ReturnType<typeof setInterval> | null = null;
const totals = computed(() => ({
receptions: receptions.value.length,
issuances: issuances.value.length,
returns: returns.value.length,
}));
const hasBranch = computed(() => braname.value.trim().length > 0);
async function loadAll(): Promise<void> {
const name = braname.value.trim();
if (!name) return;
loading.value = true;
try {
const [r, i, ret] = await Promise.all([
listAplReceptionsByBraname(name),
listIssuancesByBraname(name),
listReturnClaimsByBraname({ delivery_braname: name }),
]);
receptions.value = r;
issuances.value = i;
returns.value = ret;
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
Notify.create({ type: 'negative', message });
} finally {
loading.value = false;
}
}
watch(braname, () => {
receptions.value = [];
issuances.value = [];
returns.value = [];
});
onMounted(() => {
pollTimer = setInterval(() => {
if (hasBranch.value) void loadAll();
}, POLL_INTERVAL_MS);
});
onUnmounted(() => {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
});
function formatDate(value: string | null | undefined): string {
if (!value) return '—';
const d = new Date(value);
return Number.isNaN(d.getTime()) ? String(value) : d.toLocaleString('ru-RU');
}
</script>
<template lang="pug">
q-page.mp-role-operator.mp-branch-orders(role="region", aria-label="Сводный стол КУ")
div.mp-branch-orders__header
div
div.text-h5 Сводный стол кооперативного участка
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="Введите код вашего КУ — например voskhod-kpvz",
@keyup.enter="loadAll"
)
template(#prepend)
q-icon(name="fa-solid fa-warehouse")
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"
)
q-card(v-if="hasBranch", flat, bordered)
q-tabs(
v-model="activeTab",
inline-label,
align="left",
no-caps,
dense,
indicator-color="primary",
active-color="primary"
)
q-tab(name="receptions", icon="fa-solid fa-truck-ramp-box")
| Приёмки
q-badge.q-ml-sm(color="primary", :label="String(totals.receptions)")
q-tab(name="issuances", icon="fa-solid fa-handshake")
| Выдачи
q-badge.q-ml-sm(color="primary", :label="String(totals.issuances)")
q-tab(name="returns", icon="fa-solid fa-clipboard-check")
| Возвраты
q-badge.q-ml-sm(color="primary", :label="String(totals.returns)")
q-separator
q-tab-panels(v-model="activeTab", animated)
q-tab-panel(name="receptions")
div.mp-branch-orders__empty(v-if="!loading && receptions.length === 0")
q-icon(name="fa-solid fa-box-open", size="40px", color="grey-5")
div.text-subtitle1.q-mt-sm Нет активных приёмок
div.text-caption Партии в статусе CONFIRMED появятся здесь после подтверждения поставщиком.
q-list(v-else, separator)
q-item(v-for="r in receptions", :key="r.id")
q-item-section
q-item-label {{ r.id.slice(0, 8) }} партия {{ r.shipment_id?.slice?.(0, 8) ?? '—' }}
q-item-label(caption) Создана: {{ formatDate(r.created_at) }} / Статус: {{ r.status ?? '—' }}
q-tab-panel(name="issuances")
div.mp-branch-orders__empty(v-if="!loading && issuances.length === 0")
q-icon(name="fa-solid fa-people-carry-box", size="40px", color="grey-5")
div.text-subtitle1.q-mt-sm Нет заказов на выдачу
div.text-caption Заказы в статусе ACCEPTED_TO_COOP / READY_TO_RECEIVE появятся здесь после маркировки на ПВЗ.
q-list(v-else, separator)
q-item(v-for="o in issuances", :key="o.id")
q-item-section
q-item-label {{ o.id.slice(0, 8) }} {{ o.orderer_account }}
q-item-label(caption)
| Кол-во: {{ o.quantity }} ед. / Статус: {{ o.status }} / Создан: {{ formatDate(o.created_at) }}
q-tab-panel(name="returns")
div.mp-branch-orders__empty(v-if="!loading && returns.length === 0")
q-icon(name="fa-solid fa-clipboard-check", size="40px", color="grey-5")
div.text-subtitle1.q-mt-sm Нет заявлений на возврат
div.text-caption Заявления пайщиков в гарантийный период появятся здесь.
q-list(v-else, separator)
q-item(v-for="c in returns", :key="c.id")
q-item-section
q-item-label {{ c.id.slice(0, 8) }} {{ c.orderer_account }}
q-item-label(caption) Статус: {{ 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 вашего КУ, чтобы увидеть сводку процессов.
</template>
<style scoped lang="scss">
.mp-branch-orders {
padding: var(--mp-space-lg);
display: flex;
flex-direction: column;
gap: var(--mp-space-md);
&__header {
display: flex;
align-items: flex-start;
gap: var(--mp-space-md);
}
&__subtitle {
color: var(--mp-on-surface-muted);
max-width: 720px;
}
&__empty {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: var(--mp-space-lg) 0;
color: var(--mp-on-surface-muted);
}
&__hint {
display: flex;
align-items: center;
gap: var(--mp-space-xs);
color: var(--mp-on-surface-muted);
padding: var(--mp-space-md) 0;
}
}
</style>