[598][@ant] feat(desktop): board/agenda-writeoff — read-only лента совета
Эпик 8 / Story 8.x. Совет видит проекты списания скоропорта в активных статусах (ON_AGENDA / AUTHORIZED / EXECUTING / EXECUTED / REJECTED). DRAFT-проекты скрыты — это рабочая зона администратора на /writeoffs. Само голосование совета идёт через core soviet agenda (sov-flow); здесь — обзор статусов и сумм потерь, фильтр по статусу, polling 30s. Backend готов — reuse helper listWriteoffProposals из AdminWriteoffs/api + Queries.Marketplace.ListWriteoffProposals. Cast as never на statuses из-за расхождения standalone vue-tsc и enum-merge (Zeus генерит enum со values-как-литералами, vue-tsc вне -p не подхватывает merge). mp-role-admin; роут /market/board-writeoff (chairman + member совета). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@ import { OrdererConsolidatedPage } from 'src/pages/Marketplace/OrdererConsolidat
|
||||
import { OffererIncomingOrdersPage } from 'src/pages/Marketplace/OffererIncomingOrders'
|
||||
import { OffererMyOffersPage } from 'src/pages/Marketplace/OffererMyOffers'
|
||||
import { BranchChairmanBranchOrdersPage } from 'src/pages/Marketplace/BranchChairmanBranchOrders'
|
||||
import { BoardAgendaWriteoffPage } from 'src/pages/Marketplace/BoardAgendaWriteoff'
|
||||
import type { IWorkspaceConfig } from 'src/shared/lib/types/workspace'
|
||||
import { agreementsBase } from 'src/shared/lib/consts/workspaces'
|
||||
import { registerMarketplaceProcessInfoHandlers } from './app/extensions'
|
||||
@@ -248,6 +249,22 @@ export default async function (): Promise<IWorkspaceConfig[]> {
|
||||
agreements: agreementsBase,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Эпик 8 / Story 8.x: read-only лента writeoff-проектов для совета.
|
||||
// Совет видит проекты в статусах ON_AGENDA / AUTHORIZED /
|
||||
// EXECUTING / EXECUTED / REJECTED. Голосование совета идёт
|
||||
// через core soviet agenda (sov-flow), здесь только обзор.
|
||||
path: 'board-writeoff',
|
||||
name: 'marketplace-board-writeoff',
|
||||
component: markRaw(BoardAgendaWriteoffPage),
|
||||
meta: {
|
||||
title: 'Повестка совета — списания',
|
||||
icon: 'fa-solid fa-gavel',
|
||||
roles: ['chairman', 'member'],
|
||||
requiresAuth: true,
|
||||
agreements: agreementsBase,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Эпик 3 / Story 3.6: admin-стол модерации offer'ов. Председатель
|
||||
// (или общий администратор) видит ленту предложений в статусе
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default as BoardAgendaWriteoffPage } from './ui/BoardAgendaWriteoffPage.vue';
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { Notify } from 'quasar';
|
||||
import {
|
||||
listWriteoffProposals,
|
||||
type MarketplaceWriteoffProposalsPageView,
|
||||
} from '../../AdminWriteoffs/api';
|
||||
|
||||
/**
|
||||
* Эпик 8 / Story 8.x: read-only лента писеof проектов для совета.
|
||||
*
|
||||
* Совет видит проекты в активных статусах (ON_AGENDA / AUTHORIZED /
|
||||
* EXECUTING / EXECUTED / REJECTED). DRAFT'ы скрыты — это рабочая зона
|
||||
* админа на странице «Проекты списания». Само голосование совета
|
||||
* подключается через core soviet agenda (sov-flow), здесь только обзор.
|
||||
*
|
||||
* Backend: Queries.Marketplace.ListWriteoffProposals.
|
||||
* Reuse helper listWriteoffProposals из AdminWriteoffs/api.
|
||||
*
|
||||
* Polling 30s — статусы меняются по решению совета (core sov-flow).
|
||||
*/
|
||||
|
||||
type WriteoffStatus = 'ON_AGENDA' | 'AUTHORIZED' | 'EXECUTING' | 'EXECUTED' | 'REJECTED';
|
||||
|
||||
const POLL_INTERVAL_MS = 30_000;
|
||||
const BOARD_STATUSES: WriteoffStatus[] = [
|
||||
'ON_AGENDA',
|
||||
'AUTHORIZED',
|
||||
'EXECUTING',
|
||||
'EXECUTED',
|
||||
'REJECTED',
|
||||
];
|
||||
|
||||
const STATUS_LABEL: Record<WriteoffStatus, string> = {
|
||||
ON_AGENDA: 'На повестке',
|
||||
AUTHORIZED: 'Одобрено',
|
||||
EXECUTING: 'Исполняется',
|
||||
EXECUTED: 'Исполнено',
|
||||
REJECTED: 'Отклонено',
|
||||
};
|
||||
|
||||
const STATUS_COLOR: Record<WriteoffStatus, string> = {
|
||||
ON_AGENDA: 'warning',
|
||||
AUTHORIZED: 'positive',
|
||||
EXECUTING: 'info',
|
||||
EXECUTED: 'positive',
|
||||
REJECTED: 'negative',
|
||||
};
|
||||
|
||||
const page = ref<MarketplaceWriteoffProposalsPageView | null>(null);
|
||||
const loading = ref(false);
|
||||
const statusFilter = ref<WriteoffStatus | null>(null);
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const items = computed(() => page.value?.items ?? []);
|
||||
const total = computed(() => page.value?.totalCount ?? 0);
|
||||
|
||||
async function load(): Promise<void> {
|
||||
loading.value = true;
|
||||
try {
|
||||
const statuses = (statusFilter.value ? [statusFilter.value] : BOARD_STATUSES) as never;
|
||||
page.value = await listWriteoffProposals(
|
||||
{ statuses },
|
||||
{ page: 1, limit: 100 },
|
||||
);
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
Notify.create({ type: 'negative', message });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
function changeFilter(value: WriteoffStatus | null): void {
|
||||
statusFilter.value = value;
|
||||
void load();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await load();
|
||||
pollTimer = setInterval(() => {
|
||||
void load();
|
||||
}, POLL_INTERVAL_MS);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
q-page.mp-role-admin.mp-board-writeoff(role="region", aria-label="Повестка совета — списания")
|
||||
div.mp-board-writeoff__header
|
||||
div
|
||||
div.text-h5 Повестка совета — проекты списания
|
||||
div.text-caption.mp-board-writeoff__subtitle
|
||||
| Проекты списания скоропорта, отправленные администратором в совет. Голосование совета проходит на core soviet agenda; здесь — обзор статусов и принятых решений.
|
||||
q-space
|
||||
q-btn(flat, dense, round, icon="fa-solid fa-rotate", :loading="loading", @click="load", aria-label="Обновить")
|
||||
|
||||
q-tabs.mp-board-writeoff__tabs(
|
||||
:model-value="statusFilter",
|
||||
inline-label,
|
||||
align="left",
|
||||
dense,
|
||||
no-caps,
|
||||
@update:model-value="changeFilter"
|
||||
)
|
||||
q-tab(:name="null", label="Все")
|
||||
q-tab(name="ON_AGENDA", label="На повестке")
|
||||
q-tab(name="AUTHORIZED", label="Одобрены")
|
||||
q-tab(name="EXECUTING", label="Исполняются")
|
||||
q-tab(name="EXECUTED", label="Исполнены")
|
||||
q-tab(name="REJECTED", label="Отклонены")
|
||||
|
||||
q-inner-loading(:showing="loading && items.length === 0")
|
||||
q-spinner(color="primary", size="2em")
|
||||
|
||||
div.mp-board-writeoff__empty(v-if="!loading && items.length === 0")
|
||||
q-icon(name="fa-solid fa-clipboard-check", size="48px", color="grey-5")
|
||||
div.text-subtitle1.q-mt-md Совет ещё не получал проектов списания
|
||||
div.text-caption Когда администратор отправит проект в совет, он появится здесь.
|
||||
|
||||
q-card(v-if="items.length > 0", flat, bordered)
|
||||
q-list(separator)
|
||||
q-item(v-for="p in items", :key="p.id")
|
||||
q-item-section
|
||||
q-item-label
|
||||
| Проект № {{ p.id.slice(0, 8) }}
|
||||
| — позиций: {{ p.items?.length ?? 0 }}
|
||||
q-item-label(caption)
|
||||
| Создан: {{ formatDate(p.created_at) }}
|
||||
| / Триггер: {{ p.trigger }}
|
||||
| / Сумма потерь:
|
||||
| {{ p.total_loss_amount ?? '—' }}
|
||||
q-item-section(side, top)
|
||||
q-chip(
|
||||
:color="STATUS_COLOR[p.status]",
|
||||
text-color="white",
|
||||
square,
|
||||
dense
|
||||
)
|
||||
| {{ STATUS_LABEL[p.status] }}
|
||||
|
||||
div.mp-board-writeoff__counter
|
||||
| Всего на повестке: {{ total }}
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mp-board-writeoff {
|
||||
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;
|
||||
}
|
||||
|
||||
&__counter {
|
||||
color: var(--mp-on-surface-muted);
|
||||
text-align: right;
|
||||
margin-top: var(--mp-space-xs);
|
||||
}
|
||||
|
||||
&__empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: var(--mp-space-xl) 0;
|
||||
color: var(--mp-on-surface-muted);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user