[598][@ant] feat: фронт списания скоропорта — таблица кандидатов (админ) + подтверждение на столе ПВЗ
Стол администратора: на странице списаний — выделяемая таблица кандидатов (просроченный скоропорт на складах), «Новый черновик» активна при выделении и собирает предзаполненный черновик из выбранного. Новая страница стола ПВЗ marketplace-pvz-writeoffs: группы одобренных советом списаний по КУ оператора, просмотр позиций и протокола совета, «Подтвердить списание» — подпись Служебной записки 1111 (confirmwroff). SDK-обёртки запросов/мутации. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<IWorkspaceConfig[]> {
|
||||
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',
|
||||
|
||||
@@ -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<MarketplaceWriteoffCandidateView[]> {
|
||||
const { [Queries.Marketplace.ListWriteoffCandidates.name]: result } = await client.Query(
|
||||
Queries.Marketplace.ListWriteoffCandidates.query,
|
||||
{},
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getOpenWriteoffDraft(): Promise<MarketplaceWriteoffProposalView | null> {
|
||||
const { [Queries.Marketplace.OpenWriteoffDraft.name]: result } = await client.Query(
|
||||
Queries.Marketplace.OpenWriteoffDraft.query,
|
||||
|
||||
@@ -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<MarketplaceWriteoffProposalView[]>([]);
|
||||
const archive = ref<MarketplaceWriteoffProposalView[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// Кандидаты на списание — просроченный скоропорт на складах. Председатель
|
||||
// выделяет позиции и одной кнопкой собирает из них черновик проекта.
|
||||
const candidates = ref<MarketplaceWriteoffCandidateView[]>([]);
|
||||
const selectedCandidates = ref<MarketplaceWriteoffCandidateView[]>([]);
|
||||
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<MarketplaceWriteoffProposalView | null>(null);
|
||||
async function load(): Promise<void> {
|
||||
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<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function hasAmount(c: MarketplaceWriteoffCandidateView): boolean {
|
||||
return Number.parseFloat(c.amount) > 0;
|
||||
}
|
||||
|
||||
async function createDraftFromSelection(): Promise<void> {
|
||||
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
|
||||
|
||||
@@ -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<MarketplaceWriteoffServiceMemoDocumentView> {
|
||||
const { [Queries.Marketplace.WriteoffServiceMemoSignablePayload.name]: result } =
|
||||
await client.Query(Queries.Marketplace.WriteoffServiceMemoSignablePayload.query, {
|
||||
variables: { data },
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function confirmWriteoff(data: IConfirmWriteoffInput): Promise<unknown> {
|
||||
const { [Mutations.Marketplace.ConfirmWriteoff.name]: result } = await client.Mutation(
|
||||
Mutations.Marketplace.ConfirmWriteoff.mutation,
|
||||
{ variables: { data } },
|
||||
);
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { PvzWriteoffsPage } from './ui';
|
||||
export * from './api';
|
||||
@@ -0,0 +1,108 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { useSessionStore } from 'src/entities/Session';
|
||||
import { FailAlert, SuccessAlert } from 'src/shared/api';
|
||||
import { DigitalDocument } from 'src/shared/lib/document';
|
||||
import { BaseButton } from 'src/shared/ui/base';
|
||||
import {
|
||||
confirmWriteoff,
|
||||
getWriteoffServiceMemoSignablePayload,
|
||||
type MarketplaceWriteoffConfirmationGroupView,
|
||||
type MarketplaceWriteoffServiceMemoDocumentView,
|
||||
} from '../api';
|
||||
|
||||
/**
|
||||
* Эпик 8: диалог подтверждения списания председателем кооперативного участка.
|
||||
* Председатель смотрит preview Служебной записки о списании (registry 1111),
|
||||
* подписывает приватным ключом — backend выполняет confirmwroff: имущество
|
||||
* этого КУ фактически выбывает со склада.
|
||||
*/
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
group: MarketplaceWriteoffConfirmationGroupView | null;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
(e: 'confirmed'): void;
|
||||
}>();
|
||||
|
||||
const session = useSessionStore();
|
||||
const previewDoc = ref<MarketplaceWriteoffServiceMemoDocumentView | null>(null);
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (open) => {
|
||||
if (!open || !props.group) {
|
||||
previewDoc.value = null;
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
previewDoc.value = await getWriteoffServiceMemoSignablePayload({
|
||||
proposal_id: props.group.proposal_id,
|
||||
braname: props.group.braname,
|
||||
});
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось сформировать Служебную записку о списании');
|
||||
emit('update:modelValue', false);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
async function signAndConfirm(): Promise<void> {
|
||||
if (!previewDoc.value || !props.group) return;
|
||||
submitting.value = true;
|
||||
try {
|
||||
const digital = new DigitalDocument(previewDoc.value);
|
||||
const signed = await digital.sign(session.username);
|
||||
await confirmWriteoff({
|
||||
proposal_id: props.group.proposal_id,
|
||||
braname: props.group.braname,
|
||||
signed_memo: signed,
|
||||
});
|
||||
SuccessAlert('Списание подтверждено — имущество выбыло со склада');
|
||||
emit('confirmed');
|
||||
emit('update:modelValue', false);
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось подтвердить списание');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
q-dialog(
|
||||
:model-value="modelValue",
|
||||
@update:model-value="(v) => emit('update:modelValue', v)",
|
||||
full-width,
|
||||
persistent
|
||||
)
|
||||
q-card
|
||||
q-card-section.row.items-center
|
||||
.text-h6 Подтверждение списания со склада
|
||||
q-space
|
||||
q-btn(flat, round, icon="close", @click="emit('update:modelValue', false)")
|
||||
|
||||
q-card-section(v-if="loading")
|
||||
q-spinner(size="24px")
|
||||
span.q-ml-md.text-grey-7 Формируем Служебную записку…
|
||||
|
||||
q-card-section(v-else-if="previewDoc")
|
||||
.text-body2.text-grey-7.q-mb-md
|
||||
| Подписав эту Служебную записку, вы подтверждаете фактическое списание имущества со склада участка «{{ group?.branch_name }}». Имущество выбудет со склада и будет снято с учёта.
|
||||
q-card(flat, bordered).q-pa-md(style="max-height: 50vh; overflow: auto")
|
||||
div(v-html="previewDoc.html")
|
||||
|
||||
q-card-section.row.items-center.q-gutter-sm(v-if="previewDoc")
|
||||
q-space
|
||||
BaseButton(variant="secondary", @click="emit('update:modelValue', false)") Отмена
|
||||
BaseButton(variant="primary", :loading="submitting", @click="signAndConfirm")
|
||||
template(#icon-left)
|
||||
q-icon(name="task_alt", size="18px")
|
||||
| Подписать и списать
|
||||
</template>
|
||||
@@ -0,0 +1,181 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { debounce } from 'quasar';
|
||||
import { FailAlert } from 'src/shared/api';
|
||||
import { useMarketplaceRealtime } from 'src/shared/lib/marketplace';
|
||||
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits';
|
||||
import { BaseBadge, BaseButton, BaseCard, CardListSkeleton, EmptyState } from 'src/shared/ui/base';
|
||||
import { PageHint } from 'src/shared/ui/domain';
|
||||
import {
|
||||
listWriteoffPendingConfirmations,
|
||||
type MarketplaceWriteoffConfirmationGroupView,
|
||||
} from '../api';
|
||||
import ConfirmWriteoffDialog from './ConfirmWriteoffDialog.vue';
|
||||
|
||||
/**
|
||||
* Эпик 8: стол ПВЗ — подтверждение списания со склада. Совет одобрил проект
|
||||
* списания; председатель кооперативного участка видит здесь группы своих
|
||||
* участков и подтверждает фактическое выбытие имущества со склада, подписывая
|
||||
* Служебную записку о списании (registry 1111).
|
||||
*/
|
||||
|
||||
const groups = ref<MarketplaceWriteoffConfirmationGroupView[]>([]);
|
||||
const loading = ref(false);
|
||||
const confirmOpen = ref(false);
|
||||
const selectedGroup = ref<MarketplaceWriteoffConfirmationGroupView | null>(null);
|
||||
const expanded = ref<Record<string, boolean>>({});
|
||||
|
||||
const itemColumns = [
|
||||
{ name: 'asset_title', align: 'left' as const, label: 'Наименование', field: 'asset_title' },
|
||||
{ name: 'quantity', align: 'right' as const, label: 'Кол-во', field: 'quantity' },
|
||||
{ name: 'amount', align: 'right' as const, label: 'Сумма', field: 'amount' },
|
||||
{ name: 'reason', align: 'left' as const, label: 'Причина', field: 'reason' },
|
||||
];
|
||||
|
||||
function groupKey(g: MarketplaceWriteoffConfirmationGroupView): string {
|
||||
return `${g.proposal_id}:${g.braname}`;
|
||||
}
|
||||
|
||||
async function load(): Promise<void> {
|
||||
loading.value = true;
|
||||
try {
|
||||
groups.value = await listWriteoffPendingConfirmations();
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось загрузить списания на подтверждение');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openConfirm(g: MarketplaceWriteoffConfirmationGroupView): void {
|
||||
selectedGroup.value = g;
|
||||
confirmOpen.value = true;
|
||||
}
|
||||
|
||||
function toggleProtocol(g: MarketplaceWriteoffConfirmationGroupView): void {
|
||||
const k = groupKey(g);
|
||||
expanded.value[k] = !expanded.value[k];
|
||||
}
|
||||
|
||||
function protocolHtml(g: MarketplaceWriteoffConfirmationGroupView): string | null {
|
||||
const doc = g.protocol_doc as { html?: string } | null;
|
||||
return doc?.html ?? null;
|
||||
}
|
||||
|
||||
function onConfirmed(): void {
|
||||
void load();
|
||||
}
|
||||
|
||||
const reloadLive = debounce(() => {
|
||||
if (loading.value) return;
|
||||
void load();
|
||||
}, 400);
|
||||
useMarketplaceRealtime(
|
||||
{ MarketplaceWriteoffStatusChangedEvent: () => reloadLive() },
|
||||
{ onResync: () => reloadLive() },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
void load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
q-page.pvz-writeoffs(role="region", aria-label="Списание со склада")
|
||||
PageHint(storage-key="mp:pvz-writeoffs:banner-dismissed")
|
||||
| Совет одобрил списание имущества со складов. Подтвердите фактическое списание со склада своего участка — для этого подпишите Служебную записку о списании. Только после вашей подписи имущество выбывает со склада.
|
||||
|
||||
CardListSkeleton(v-if="loading && !groups.length", :count="2")
|
||||
|
||||
.pvz-writeoffs__empty(v-if="!loading && groups.length === 0")
|
||||
EmptyState(
|
||||
title="Нет списаний на подтверждение",
|
||||
body="Когда совет одобрит проект списания по вашему участку, он появится здесь."
|
||||
)
|
||||
template(#icon)
|
||||
q-icon(name="inventory_2", size="48px")
|
||||
|
||||
BaseCard(v-for="g in groups", :key="groupKey(g)")
|
||||
.pvz-writeoffs__card
|
||||
.pvz-writeoffs__head
|
||||
div
|
||||
.t-h3 {{ g.branch_name }}
|
||||
.t-muted {{ g.items.length }} позиций · {{ formatAsset2Digits(g.total_amount) }}
|
||||
BaseBadge(variant="info") Ожидает подтверждения
|
||||
|
||||
q-table.full-width.q-mt-sm(
|
||||
flat,
|
||||
:rows="g.items",
|
||||
:columns="itemColumns",
|
||||
row-key="inventory_id",
|
||||
hide-bottom,
|
||||
:rows-per-page-options="[0]"
|
||||
)
|
||||
template(#body-cell-quantity="props")
|
||||
q-td.text-right(:props="props") {{ props.row.quantity }}
|
||||
template(#body-cell-amount="props")
|
||||
q-td.text-right(:props="props") {{ formatAsset2Digits(props.row.amount) }}
|
||||
|
||||
.pvz-writeoffs__actions
|
||||
BaseButton(variant="ghost", size="sm", @click="toggleProtocol(g)")
|
||||
template(#icon-left)
|
||||
q-icon(name="gavel", size="16px")
|
||||
| {{ expanded[groupKey(g)] ? 'Скрыть протокол' : 'Протокол совета' }}
|
||||
q-space
|
||||
BaseButton(variant="primary", @click="openConfirm(g)")
|
||||
template(#icon-left)
|
||||
q-icon(name="task_alt", size="16px")
|
||||
| Подтвердить списание
|
||||
|
||||
q-card(v-if="expanded[groupKey(g)]", flat, bordered).q-mt-sm.q-pa-md(
|
||||
style="max-height: 40vh; overflow: auto"
|
||||
)
|
||||
div(v-if="protocolHtml(g)", v-html="protocolHtml(g)")
|
||||
.t-muted(v-else) Протокол совета недоступен для просмотра.
|
||||
|
||||
ConfirmWriteoffDialog(
|
||||
v-model="confirmOpen",
|
||||
:group="selectedGroup",
|
||||
@confirmed="onConfirmed"
|
||||
)
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pvz-writeoffs {
|
||||
padding: var(--p-6, 24px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-4, 16px);
|
||||
|
||||
&__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-2, 8px);
|
||||
}
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--p-3, 12px);
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--p-2, 8px);
|
||||
margin-top: var(--p-2, 8px);
|
||||
}
|
||||
|
||||
&__empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pvz-writeoffs {
|
||||
padding: var(--p-4, 16px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as PvzWriteoffsPage } from './PvzWriteoffsPage.vue';
|
||||
Reference in New Issue
Block a user