Files
mdm-ordinis/ordinis-admin-ui/src/routes/my-drafts.tsx
T
2026-05-14 14:06:30 +00:00

281 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react'
import { Link, createFileRoute } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import {
Alert,
Badge,
Button,
EmptyState,
LoadingBlock,
PageHeader,
Panel,
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
} from '@/ui'
import { useMyDrafts, useMySchemaDrafts } from '@/api/queries'
import { useWithdrawDraft } from '@/api/mutations'
import type { DraftOperation, DraftStatus, SchemaDraft } from '@/api/client'
import { UserCell } from '@/lib/useUserDisplay'
export const Route = createFileRoute('/my-drafts')({
component: MyDraftsPage,
})
const operationVariant = (op: DraftOperation): 'info' | 'success' | 'warning' => {
if (op === 'CREATE') return 'success'
if (op === 'CLOSE') return 'warning'
return 'info'
}
const statusVariant = (
s: DraftStatus,
): 'warning' | 'success' | 'error' | 'neutral' => {
if (s === 'PENDING') return 'warning'
if (s === 'APPROVED') return 'success'
if (s === 'REJECTED') return 'error'
return 'neutral'
}
/** Mapping schema draft статусов на UI badge variant. */
const schemaStatusVariant = (
s: string,
): 'warning' | 'success' | 'error' | 'info' | 'neutral' => {
if (s === 'review_pending') return 'warning'
if (s === 'approved' || s === 'published') return 'success'
if (s === 'rejected') return 'error'
if (s === 'changes_requested') return 'info'
return 'neutral'
}
/**
* Approval Workflow v2 Phase 4: maker self-tracking. Maker видит свои
* submissions всех статусов (PENDING/APPROVED/REJECTED/WITHDRAWN), sorted
* submitted_at DESC. Polled каждые 30s — maker увидит когда reviewer
* approve/reject. Withdraw кнопка для PENDING (только свои + maker-only
* gate в backend).
*/
function MyDraftsPage() {
const { t } = useTranslation()
const drafts = useMyDrafts(0, 50)
const schemaDrafts = useMySchemaDrafts(0, 50)
const withdrawMut = useWithdrawDraft()
const [busyId, setBusyId] = useState<string | undefined>(undefined)
const handleWithdraw = (id: string) => {
if (!confirm(t('myDrafts.confirmWithdraw'))) return
setBusyId(id)
withdrawMut.mutate(id, {
onSettled: () => setBusyId(undefined),
})
}
const recordTotal = drafts.data?.totalElements ?? 0
const schemaTotal = schemaDrafts.data?.totalElements ?? 0
const bothEmpty =
!drafts.isLoading &&
!schemaDrafts.isLoading &&
recordTotal === 0 &&
schemaTotal === 0
return (
<div className="space-y-4">
<PageHeader
title={t('myDrafts.title')}
description={t('myDrafts.description')}
/>
{(drafts.isLoading || schemaDrafts.isLoading) && (
<LoadingBlock size="md" label={t('loading')} />
)}
{drafts.error && (
<Alert variant="error" title={t('error.failed')}>
{String(drafts.error)}
</Alert>
)}
{schemaDrafts.error && (
<Alert variant="error" title={t('error.failed')}>
{String(schemaDrafts.error)}
</Alert>
)}
{bothEmpty && <EmptyState title={t('myDrafts.empty')} />}
{schemaTotal > 0 && (
<MySchemaDraftsPanel items={(schemaDrafts.data?.items ?? []) as SchemaDraft[]} total={schemaTotal} />
)}
{drafts.data && drafts.data.totalElements > 0 && (
<Panel>
<div className="flex items-center justify-between mb-3">
<div>
<h2 className="text-title-md font-semibold">
{t('myDrafts.records.title', { defaultValue: 'Записи' })}
</h2>
<p className="text-cell text-mute mt-0.5">
{t('myDrafts.total', { count: drafts.data.totalElements })}
</p>
</div>
</div>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>{t('myDrafts.col.bk')}</TableHeaderCell>
<TableHeaderCell>{t('myDrafts.col.op')}</TableHeaderCell>
<TableHeaderCell>{t('myDrafts.col.status')}</TableHeaderCell>
<TableHeaderCell>{t('myDrafts.col.submitted')}</TableHeaderCell>
<TableHeaderCell>{t('myDrafts.col.reviewed')}</TableHeaderCell>
<TableHeaderCell>{t('myDrafts.col.reviewer')}</TableHeaderCell>
<TableHeaderCell>{t('myDrafts.col.comment')}</TableHeaderCell>
<TableHeaderCell>{t('myDrafts.col.actions')}</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{drafts.data.items.map((d) => (
<TableRow key={d.id}>
<TableCell className="text-mono">{d.businessKey}</TableCell>
<TableCell>
<Badge variant={operationVariant(d.operation)}>{d.operation}</Badge>
</TableCell>
<TableCell>
<Badge variant={statusVariant(d.status)}>{d.status}</Badge>
</TableCell>
<TableCell className="text-cell text-ink-2">
{new Date(d.submittedAt).toLocaleString()}
</TableCell>
<TableCell className="text-cell text-ink-2">
{d.reviewedAt
? new Date(d.reviewedAt).toLocaleString()
: '—'}
</TableCell>
<TableCell>
{d.reviewerId ? (
<UserCell uuid={d.reviewerId} />
) : (
<span className="text-mute"></span>
)}
</TableCell>
<TableCell className="text-cell text-ink max-w-md">
{d.reviewComment ?? d.makerComment ?? '—'}
</TableCell>
<TableCell>
{d.status === 'PENDING' ? (
<Button
variant="secondary"
size="sm"
disabled={busyId === d.id}
onClick={() => handleWithdraw(d.id)}
>
{t('myDrafts.action.withdraw')}
</Button>
) : (
<span className="text-cell text-mute/70"></span>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Panel>
)}
</div>
)
}
/**
* Schema drafts maker self-tracking panel. Параллельно с record drafts
* на той же странице — пользователь не должен помнить разные типы.
*
* <p>Status badge показывает schema state machine (draft / review_pending /
* approved / rejected / changes_requested / withdrawn / published).
* "Открыть" линкует на dictionary page где есть drawer с details + actions
* (withdraw / re-submit для maker).
*/
function MySchemaDraftsPanel({ items, total }: { items: SchemaDraft[]; total: number }) {
const { t } = useTranslation()
return (
<Panel>
<div className="flex items-center justify-between mb-3">
<div>
<h2 className="text-title-md font-semibold">
{t('myDrafts.schema.title', { defaultValue: 'Черновики схем' })}
</h2>
<p className="text-cell text-mute mt-0.5">
{t('myDrafts.schema.total', {
count: total,
defaultValue: `${total} schema draft(s)`,
})}
</p>
</div>
<Badge variant="info">
{t('myDrafts.schema.label', { defaultValue: 'Schema workflow' })}
</Badge>
</div>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>
{t('myDrafts.schema.col.dictionary', { defaultValue: 'Словарь' })}
</TableHeaderCell>
<TableHeaderCell>
{t('myDrafts.schema.col.fromVersion', { defaultValue: 'От версии' })}
</TableHeaderCell>
<TableHeaderCell>
{t('myDrafts.schema.col.status', { defaultValue: 'Статус' })}
</TableHeaderCell>
<TableHeaderCell>
{t('myDrafts.schema.col.created', { defaultValue: 'Создан' })}
</TableHeaderCell>
<TableHeaderCell>
{t('myDrafts.schema.col.submitted', { defaultValue: 'Отправлен' })}
</TableHeaderCell>
<TableHeaderCell align="right">
{t('myDrafts.schema.col.action', { defaultValue: 'Действие' })}
</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{items.map((d) => (
<TableRow key={d.draftId}>
<TableCell>
<Link
to="/dictionaries/$name"
params={{ name: d.dictionaryName ?? '' }}
className="text-accent hover:underline font-mono"
>
{d.dictionaryName ?? '—'}
</Link>
</TableCell>
<TableCell className="font-mono tabular-nums">
v{d.branchedFromVersion}
</TableCell>
<TableCell>
<Badge variant={schemaStatusVariant(d.status)}>{d.status}</Badge>
</TableCell>
<TableCell className="tabular-nums">
{new Date(d.createdAt).toLocaleString()}
</TableCell>
<TableCell className="tabular-nums">
{d.submittedAt ? new Date(d.submittedAt).toLocaleString() : '—'}
</TableCell>
<TableCell align="right">
<Link
to="/dictionaries/$name"
params={{ name: d.dictionaryName ?? '' }}
className="text-accent hover:underline"
>
{t('myDrafts.schema.open', { defaultValue: 'Открыть' })}
</Link>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Panel>
)
}