From 725e63dfc123774eabcfe2e481525b50e368c789 Mon Sep 17 00:00:00 2001 From: ant Date: Sat, 13 Jun 2026 13:38:45 +0000 Subject: [PATCH] =?UTF-8?q?[598][@ant]=20feat:=20=D1=84=D1=80=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=20=D1=81=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=81=D0=BA=D0=BE=D1=80=D0=BE=D0=BF=D0=BE=D1=80=D1=82=D0=B0=20?= =?UTF-8?q?=E2=80=94=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D0=B0=20=D0=BA?= =?UTF-8?q?=D0=B0=D0=BD=D0=B4=D0=B8=D0=B4=D0=B0=D1=82=D0=BE=D0=B2=20(?= =?UTF-8?q?=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD)=20+=20=D0=BF=D0=BE=D0=B4=D1=82?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=81=D1=82=D0=BE=D0=BB=D0=B5=20=D0=9F=D0=92=D0=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Стол администратора: на странице списаний — выделяемая таблица кандидатов (просроченный скоропорт на складах), «Новый черновик» активна при выделении и собирает предзаполненный черновик из выбранного. Новая страница стола ПВЗ marketplace-pvz-writeoffs: группы одобренных советом списаний по КУ оператора, просмотр позиций и протокола совета, «Подтвердить списание» — подпись Служебной записки 1111 (confirmwroff). SDK-обёртки запросов/мутации. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../desktop/extensions/market/install.ts | 16 ++ .../Marketplace/AdminWriteoffs/api/index.ts | 11 ++ .../AdminWriteoffs/ui/AdminWriteoffsPage.vue | 107 ++++++++++- .../Marketplace/PvzWriteoffs/api/index.ts | 40 ++++ .../pages/Marketplace/PvzWriteoffs/index.ts | 2 + .../PvzWriteoffs/ui/ConfirmWriteoffDialog.vue | 108 +++++++++++ .../PvzWriteoffs/ui/PvzWriteoffsPage.vue | 181 ++++++++++++++++++ .../Marketplace/PvzWriteoffs/ui/index.ts | 1 + 8 files changed, 461 insertions(+), 5 deletions(-) create mode 100644 components/desktop/src/pages/Marketplace/PvzWriteoffs/api/index.ts create mode 100644 components/desktop/src/pages/Marketplace/PvzWriteoffs/index.ts create mode 100644 components/desktop/src/pages/Marketplace/PvzWriteoffs/ui/ConfirmWriteoffDialog.vue create mode 100644 components/desktop/src/pages/Marketplace/PvzWriteoffs/ui/PvzWriteoffsPage.vue create mode 100644 components/desktop/src/pages/Marketplace/PvzWriteoffs/ui/index.ts diff --git a/components/desktop/extensions/market/install.ts b/components/desktop/extensions/market/install.ts index b763c5cf4eb..3ea710a3e80 100644 --- a/components/desktop/extensions/market/install.ts +++ b/components/desktop/extensions/market/install.ts @@ -20,6 +20,7 @@ import { OffererSupplyPreparationPage } from 'src/pages/Marketplace/OffererSuppl import { OffererShipPartyPage } from 'src/pages/Marketplace/OffererShipParty' import { OffererPaymentHistoryPage } from 'src/pages/Marketplace/OffererPaymentHistory' import { AdminWriteoffsPage } from 'src/pages/Marketplace/AdminWriteoffs' +import { PvzWriteoffsPage } from 'src/pages/Marketplace/PvzWriteoffs' import { ChairmanModerationPage } from 'src/pages/Marketplace/ChairmanModeration' import { AdminOrdersPage } from 'src/pages/Marketplace/AdminOrders' import { AdminOffersPage } from 'src/pages/Marketplace/AdminOffers' @@ -517,6 +518,21 @@ export default async function (): Promise { agreements: agreementsBase, }, }, + { + // Эпик 8: operator-стол подтверждения списания со склада. Совет + // одобрил проект списания — председатель КУ подтверждает + // фактическое выбытие имущества, подписав Служебную записку 1111. + path: 'writeoffs', + name: 'marketplace-pvz-writeoffs', + component: markRaw(PvzWriteoffsPage), + meta: { + title: 'Списание со склада', + icon: 'fa-solid fa-trash-can-arrow-up', + requires: 'Writeoff:read:own-KU', + requiresAuth: true, + agreements: agreementsBase, + }, + }, { // Эпик 6 / Story 6.6: operator-стол выдачи имущества пайщику на ПВЗ. path: 'issuance', diff --git a/components/desktop/src/pages/Marketplace/AdminWriteoffs/api/index.ts b/components/desktop/src/pages/Marketplace/AdminWriteoffs/api/index.ts index 577a58ac3dd..3b33b9bd68e 100644 --- a/components/desktop/src/pages/Marketplace/AdminWriteoffs/api/index.ts +++ b/components/desktop/src/pages/Marketplace/AdminWriteoffs/api/index.ts @@ -24,6 +24,17 @@ export type ISubmitWriteoffDraftInput = export type IWriteoffStatementSignablePayloadInput = Queries.Marketplace.WriteoffStatementSignablePayload.IInput['data']; +export type MarketplaceWriteoffCandidateView = + Queries.Marketplace.ListWriteoffCandidates.IOutput['marketplaceListWriteoffCandidates'][number]; + +export async function listWriteoffCandidates(): Promise { + const { [Queries.Marketplace.ListWriteoffCandidates.name]: result } = await client.Query( + Queries.Marketplace.ListWriteoffCandidates.query, + {}, + ); + return result; +} + export async function getOpenWriteoffDraft(): Promise { const { [Queries.Marketplace.OpenWriteoffDraft.name]: result } = await client.Query( Queries.Marketplace.OpenWriteoffDraft.query, diff --git a/components/desktop/src/pages/Marketplace/AdminWriteoffs/ui/AdminWriteoffsPage.vue b/components/desktop/src/pages/Marketplace/AdminWriteoffs/ui/AdminWriteoffsPage.vue index 668c304104c..9f9af07d019 100644 --- a/components/desktop/src/pages/Marketplace/AdminWriteoffs/ui/AdminWriteoffsPage.vue +++ b/components/desktop/src/pages/Marketplace/AdminWriteoffs/ui/AdminWriteoffsPage.vue @@ -2,15 +2,18 @@ import { computed, onMounted, ref } from 'vue'; import { debounce } from 'quasar'; import { Zeus } from '@coopenomics/sdk'; -import { FailAlert } from 'src/shared/api'; +import { FailAlert, SuccessAlert } from 'src/shared/api'; import { useMarketplaceRealtime } from 'src/shared/lib/marketplace'; import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits'; import { BaseBadge, BaseButton, BaseCard, CardListSkeleton } from 'src/shared/ui/base'; import type { BaseBadgeVariant } from 'src/shared/ui/base'; import { PageHint } from 'src/shared/ui/domain'; import { + createWriteoffDraft, getOpenWriteoffDraft, + listWriteoffCandidates, listWriteoffProposals, + type MarketplaceWriteoffCandidateView, type MarketplaceWriteoffProposalView, } from '../api'; import DraftEditorDialog from './DraftEditorDialog.vue'; @@ -39,6 +42,20 @@ const inCouncil = ref([]); const archive = ref([]); const loading = ref(false); +// Кандидаты на списание — просроченный скоропорт на складах. Председатель +// выделяет позиции и одной кнопкой собирает из них черновик проекта. +const candidates = ref([]); +const selectedCandidates = ref([]); +const creatingDraft = ref(false); + +const candidateColumns = [ + { name: 'braname', align: 'left' as const, label: 'Кооп. участок', field: 'braname' }, + { name: 'asset_title', align: 'left' as const, label: 'Наименование', field: 'asset_title' }, + { name: 'quantity', align: 'right' as const, label: 'Кол-во', field: 'quantity' }, + { name: 'expiry_date', align: 'left' as const, label: 'Срок годности', field: 'expiry_date' }, + { name: 'amount', align: 'right' as const, label: 'Сумма', field: 'amount' }, +]; + const draftEditorOpen = ref(false); const submitDialogOpen = ref(false); const detailsOpen = ref(false); @@ -47,21 +64,29 @@ const selected = ref(null); async function load(): Promise { loading.value = true; try { - const [openDraft, councilPage, archivePage] = await Promise.all([ + const [openDraft, councilPage, archivePage, candidatesList] = await Promise.all([ getOpenWriteoffDraft(), listWriteoffProposals({ statuses: [ Zeus.MarketplaceWriteoffProposalStatus.ON_AGENDA, Zeus.MarketplaceWriteoffProposalStatus.AUTHORIZED, + Zeus.MarketplaceWriteoffProposalStatus.PENDING_CONFIRMATION, Zeus.MarketplaceWriteoffProposalStatus.EXECUTING, ] }), listWriteoffProposals({ statuses: [ Zeus.MarketplaceWriteoffProposalStatus.EXECUTED, Zeus.MarketplaceWriteoffProposalStatus.REJECTED, ] }), + listWriteoffCandidates(), ]); draft.value = openDraft; inCouncil.value = councilPage.items; archive.value = archivePage.items; + candidates.value = candidatesList; + // Позиции, уже попавшие в открытый черновик, из выбора убираем. + if (selectedCandidates.value.length) { + const present = new Set(candidatesList.map((c) => c.inventory_id)); + selectedCandidates.value = selectedCandidates.value.filter((c) => present.has(c.inventory_id)); + } } catch (e) { FailAlert(e, 'Не удалось загрузить проекты списания'); } finally { @@ -69,6 +94,42 @@ async function load(): Promise { } } +function hasAmount(c: MarketplaceWriteoffCandidateView): boolean { + return Number.parseFloat(c.amount) > 0; +} + +async function createDraftFromSelection(): Promise { + const picked = selectedCandidates.value.filter(hasAmount); + if (picked.length === 0) { + FailAlert( + new Error('Нет позиций с известной стоимостью'), + 'Выделите позиции с ненулевой суммой списания', + ); + return; + } + creatingDraft.value = true; + try { + const created = await createWriteoffDraft({ + items: picked.map((c) => ({ + braname: c.braname, + asset_title: c.asset_title, + quantity: c.quantity, + amount: c.amount, + reason: c.reason, + inventory_id: c.inventory_id, + })), + }); + draft.value = created; + selectedCandidates.value = []; + SuccessAlert('Черновик списания собран из выбранных позиций'); + void load(); + } catch (e) { + FailAlert(e, 'Не удалось создать черновик списания'); + } finally { + creatingDraft.value = false; + } +} + function openDetails(proposal: MarketplaceWriteoffProposalView): void { selected.value = proposal; detailsOpen.value = true; @@ -81,6 +142,7 @@ function statusVariant(status: MarketplaceWriteoffProposalView['status']): BaseB case 'ON_AGENDA': return 'accent'; case 'AUTHORIZED': + case 'PENDING_CONFIRMATION': case 'EXECUTING': return 'info'; case 'EXECUTED': @@ -99,7 +161,9 @@ function humanStatus(status: MarketplaceWriteoffProposalView['status']): string case 'ON_AGENDA': return 'На повестке'; case 'AUTHORIZED': - return 'Утверждено'; + return 'Утверждено советом'; + case 'PENDING_CONFIRMATION': + return 'Ожидает подтверждения склада'; case 'EXECUTING': return 'Идёт списание'; case 'EXECUTED': @@ -155,11 +219,44 @@ q-page.writeoffs(role="region", aria-label="Списания скоропорт | Сначала собирается черновик списания — вручную председателем или автоматически по сроку годности. Затем председатель подписывает заявление и выносит проект на повестку совета. Совет утверждает списание протоколом, после чего имущество списывается со склада. //- Главное действие страницы — в шапку (канон: CTA в топбаре, dense/primary). + //- Активна только когда выделены позиции-кандидаты; собирает из них черновик. Teleport(to="#header-actions-host", defer) - BaseButton(v-if="!draft", variant="primary", size="sm", @click="draftEditorOpen = true") + BaseButton( + v-if="!draft", + variant="primary", + size="sm", + :disabled="selectedCandidates.length === 0", + :loading="creatingDraft", + @click="createDraftFromSelection" + ) template(#icon-left) q-icon(name="add", size="18px") - | Новый черновик + | Новый черновик{{ selectedCandidates.length ? ` (${selectedCandidates.length})` : '' }} + + //- Кандидаты на списание: выделяемая таблица просроченного скоропорта. + //- Один черновик за раз — пока есть открытый черновик, таблицу прячем. + BaseCard(v-if="!draft") + .writeoffs__candidates-head + .t-h3 Кандидаты на списание + .t-muted Просроченный скоропорт на складах кооператива. Выделите позиции и нажмите «Новый черновик» в шапке. + q-table.full-width.q-mt-sm( + flat, + :rows="candidates", + :columns="candidateColumns", + row-key="inventory_id", + selection="multiple", + v-model:selected="selectedCandidates", + :loading="loading", + :rows-per-page-options="[0]", + hide-bottom, + no-data-label="Просроченных позиций на складах не найдено" + ) + template(#body-cell-quantity="props") + q-td.text-right(:props="props") {{ props.row.quantity }} + template(#body-cell-expiry_date="props") + q-td(:props="props") {{ formatDate(props.row.expiry_date) }} + template(#body-cell-amount="props") + q-td.text-right(:props="props") {{ formatAsset2Digits(props.row.amount) }} BaseCard(v-if="draft") .writeoffs__draft diff --git a/components/desktop/src/pages/Marketplace/PvzWriteoffs/api/index.ts b/components/desktop/src/pages/Marketplace/PvzWriteoffs/api/index.ts new file mode 100644 index 00000000000..de679d475b2 --- /dev/null +++ b/components/desktop/src/pages/Marketplace/PvzWriteoffs/api/index.ts @@ -0,0 +1,40 @@ +import { Mutations, Queries, type Types } from '@coopenomics/sdk'; +import { client } from 'src/shared/api/client'; + +export type MarketplaceWriteoffConfirmationGroupView = + Queries.Marketplace.WriteoffPendingConfirmations.IOutput['marketplaceWriteoffPendingConfirmations'][number]; + +export type MarketplaceWriteoffServiceMemoDocumentView = Types.Document.IGeneratedDocument; + +export type IConfirmWriteoffInput = Mutations.Marketplace.ConfirmWriteoff.IInput['data']; + +export type IServiceMemoSignablePayloadInput = + Queries.Marketplace.WriteoffServiceMemoSignablePayload.IInput['data']; + +export async function listWriteoffPendingConfirmations(): Promise< + MarketplaceWriteoffConfirmationGroupView[] +> { + const { [Queries.Marketplace.WriteoffPendingConfirmations.name]: result } = await client.Query( + Queries.Marketplace.WriteoffPendingConfirmations.query, + {}, + ); + return result; +} + +export async function getWriteoffServiceMemoSignablePayload( + data: IServiceMemoSignablePayloadInput, +): Promise { + const { [Queries.Marketplace.WriteoffServiceMemoSignablePayload.name]: result } = + await client.Query(Queries.Marketplace.WriteoffServiceMemoSignablePayload.query, { + variables: { data }, + }); + return result; +} + +export async function confirmWriteoff(data: IConfirmWriteoffInput): Promise { + const { [Mutations.Marketplace.ConfirmWriteoff.name]: result } = await client.Mutation( + Mutations.Marketplace.ConfirmWriteoff.mutation, + { variables: { data } }, + ); + return result; +} diff --git a/components/desktop/src/pages/Marketplace/PvzWriteoffs/index.ts b/components/desktop/src/pages/Marketplace/PvzWriteoffs/index.ts new file mode 100644 index 00000000000..7a71583dc85 --- /dev/null +++ b/components/desktop/src/pages/Marketplace/PvzWriteoffs/index.ts @@ -0,0 +1,2 @@ +export { PvzWriteoffsPage } from './ui'; +export * from './api'; diff --git a/components/desktop/src/pages/Marketplace/PvzWriteoffs/ui/ConfirmWriteoffDialog.vue b/components/desktop/src/pages/Marketplace/PvzWriteoffs/ui/ConfirmWriteoffDialog.vue new file mode 100644 index 00000000000..0ef3400d0bd --- /dev/null +++ b/components/desktop/src/pages/Marketplace/PvzWriteoffs/ui/ConfirmWriteoffDialog.vue @@ -0,0 +1,108 @@ + + + diff --git a/components/desktop/src/pages/Marketplace/PvzWriteoffs/ui/PvzWriteoffsPage.vue b/components/desktop/src/pages/Marketplace/PvzWriteoffs/ui/PvzWriteoffsPage.vue new file mode 100644 index 00000000000..b1be05a8e64 --- /dev/null +++ b/components/desktop/src/pages/Marketplace/PvzWriteoffs/ui/PvzWriteoffsPage.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/components/desktop/src/pages/Marketplace/PvzWriteoffs/ui/index.ts b/components/desktop/src/pages/Marketplace/PvzWriteoffs/ui/index.ts new file mode 100644 index 00000000000..9edce57b7b1 --- /dev/null +++ b/components/desktop/src/pages/Marketplace/PvzWriteoffs/ui/index.ts @@ -0,0 +1 @@ +export { default as PvzWriteoffsPage } from './PvzWriteoffsPage.vue';