[CLAUDE] Users: Plan D — F2 toggle AllowDrafterSkipToFinal per-user (Mig 29 wire UI)

BE: UserDto +AllowDrafterSkipToFinal + SetUserAllowDrafterSkipToFinalCommand
+ Handler + UsersController PATCH /api/users/{id}/allow-skip-final body
{allowDrafterSkipToFinal:bool} Policy=Users.Update.

FE Admin: User type +allowDrafterSkipToFinal. UsersPage column "Skip cuối"
violet FastForward badge + action button toggle mirror bypass-review pattern.

fe-user KHÔNG mirror (UsersPage admin-only).

Verify:
- dotnet build SolutionErp.slnx — 0 err, 2 warning DocxRenderer pre-existing
- npm run build fe-admin — pass 638ms

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-13 21:03:27 +07:00
parent 3d725c42f7
commit 60efeeda63
4 changed files with 76 additions and 4 deletions

View File

@ -1,6 +1,6 @@
import { useState, type FormEvent } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Building2, KeyRound, Pencil, Plus, Shield, Unlock, Users, CheckCircle2, XCircle, ShieldCheck } from 'lucide-react'
import { Building2, KeyRound, Pencil, Plus, Shield, Unlock, Users, CheckCircle2, XCircle, ShieldCheck, FastForward } from 'lucide-react'
import { toast } from 'sonner'
import { PageHeader } from '@/components/PageHeader'
import { DataTable, Pagination, type Column } from '@/components/DataTable'
@ -175,6 +175,19 @@ export function UsersPage() {
onError: err => toast.error(getErrorMessage(err)),
})
// F2 per-Drafter (Mig 29): toggle AllowDrafterSkipToFinal. Khi true, Drafter
// được tick "Gửi thẳng Cấp cuối" trong PE Workspace để skip Bước/Cấp trung
// gian và bay thẳng tới Cấp cuối workflow.
const allowSkipMut = useMutation({
mutationFn: (u: User) =>
api.patch(`/users/${u.id}/allow-skip-final`, { allowDrafterSkipToFinal: !u.allowDrafterSkipToFinal }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['users'] })
toast.success('Đã cập nhật quyền gửi thẳng Cấp cuối')
},
onError: err => toast.error(getErrorMessage(err)),
})
function nextPositionLevel(current: number | null): number | null {
// Cycle null → 1 (NV) → 2 (PP) → 3 (TP) → null
if (current == null) return 1
@ -289,6 +302,21 @@ export function UsersPage() {
<span className="text-xs text-slate-400"></span>
),
},
{
key: 'allowDrafterSkipToFinal',
header: 'Skip cuối',
width: 'w-20',
align: 'center',
render: u =>
u.allowDrafterSkipToFinal ? (
<span title="Drafter được gửi PE thẳng Cấp cuối workflow (skip Bước/Cấp trung gian)" className="inline-flex items-center gap-1 rounded bg-violet-100 px-1.5 py-0.5 text-[10px] text-violet-700">
<FastForward className="h-3 w-3" />
skip
</span>
) : (
<span className="text-xs text-slate-400"></span>
),
},
{ key: 'createdAt', header: 'Ngày tạo', width: 'w-24', render: u => fmtDate(u.createdAt) },
{
key: 'actions',
@ -334,6 +362,14 @@ export function UsersPage() {
{u.positionLevel != null ? PositionLevelShort[u.positionLevel] : '—'}
</span>
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => allowSkipMut.mutate(u)}
title={u.allowDrafterSkipToFinal ? 'Tắt quyền skip — Drafter phải tuần tự qua mọi Bước/Cấp' : 'Bật quyền skip — Drafter được gửi PE thẳng Cấp cuối workflow'}
>
<FastForward className={`h-3.5 w-3.5 ${u.allowDrafterSkipToFinal ? 'text-violet-600' : 'text-slate-400'}`} />
</Button>
<Button size="sm" variant="ghost" onClick={() => toggleActiveMut.mutate(u)} title={u.isActive ? 'Vô hiệu hóa' : 'Kích hoạt'}>
{u.isActive ? <XCircle className="h-3.5 w-3.5 text-red-500" /> : <CheckCircle2 className="h-3.5 w-3.5 text-emerald-600" />}
</Button>

View File

@ -11,6 +11,7 @@ export type User = {
position: string | null
canBypassReview: boolean
positionLevel: number | null // Mig 18 — 1=NV, 2=PP, 3=TP, null=admin/external
allowDrafterSkipToFinal: boolean // Mig 29 — F2: Drafter được gửi thẳng Cấp cuối workflow PE
}
// Cấp chức danh trong phòng (Mig 18) — phục vụ N-stage workflow inner step.