[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:
@ -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>
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -84,9 +84,22 @@ public class UsersController(IMediator mediator) : ControllerBase
|
||||
await mediator.Send(new SetUserPositionLevelCommand(id, body.PositionLevel), ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// Mig 29 F2 per-Drafter: admin toggle AllowDrafterSkipToFinal cho user. Khi
|
||||
// true, Drafter có thể tick "Gửi thẳng Cấp cuối" trong PE Workspace để bay
|
||||
// thẳng tới Cấp cuối workflow. Mặc định false.
|
||||
[HttpPatch("{id:guid}/allow-skip-final")]
|
||||
[Authorize(Policy = "Users.Update")]
|
||||
public async Task<IActionResult> SetAllowDrafterSkipToFinal(
|
||||
Guid id, [FromBody] SetAllowDrafterSkipToFinalBody body, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new SetUserAllowDrafterSkipToFinalCommand(id, body.AllowDrafterSkipToFinal), ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
public record AssignRolesBody(List<string> Roles);
|
||||
public record ResetPasswordBody(string NewPassword);
|
||||
public record SetBypassReviewBody(bool CanBypassReview);
|
||||
public record SetPositionLevelBody(int? PositionLevel);
|
||||
public record SetAllowDrafterSkipToFinalBody(bool AllowDrafterSkipToFinal);
|
||||
|
||||
@ -22,7 +22,8 @@ public record UserDto(
|
||||
string? DepartmentName,
|
||||
string? Position,
|
||||
bool CanBypassReview,
|
||||
int? PositionLevel); // Mig 18 — 1=NV, 2=PP, 3=TP. Null cho admin/system/external user.
|
||||
int? PositionLevel, // Mig 18 — 1=NV, 2=PP, 3=TP. Null cho admin/system/external user.
|
||||
bool AllowDrafterSkipToFinal); // Mig 29 — F2 per-Drafter: cho phép Drafter gửi thẳng Cấp cuối khi tạo PE.
|
||||
|
||||
// ========== LIST ==========
|
||||
public record ListUsersQuery : PagedRequest, IRequest<PagedResult<UserDto>>;
|
||||
@ -61,7 +62,7 @@ public class ListUsersQueryHandler(UserManager<User> userManager, IApplicationDb
|
||||
var roles = await userManager.GetRolesAsync(u);
|
||||
var isLocked = u.LockoutEnd.HasValue && u.LockoutEnd.Value.UtcDateTime > now;
|
||||
string? deptName = u.DepartmentId is { } did && deptNames.TryGetValue(did, out var dn) ? dn : null;
|
||||
items.Add(new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview, (int?)u.PositionLevel));
|
||||
items.Add(new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview, (int?)u.PositionLevel, u.AllowDrafterSkipToFinal));
|
||||
}
|
||||
|
||||
return new PagedResult<UserDto>(items, total, request.Page, request.PageSize);
|
||||
@ -83,7 +84,7 @@ public class GetUserQueryHandler(UserManager<User> userManager, IApplicationDbCo
|
||||
string? deptName = null;
|
||||
if (u.DepartmentId is { } did)
|
||||
deptName = await db.Departments.AsNoTracking().Where(d => d.Id == did).Select(d => d.Name).FirstOrDefaultAsync(ct);
|
||||
return new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview, (int?)u.PositionLevel);
|
||||
return new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview, (int?)u.PositionLevel, u.AllowDrafterSkipToFinal);
|
||||
}
|
||||
}
|
||||
|
||||
@ -322,3 +323,24 @@ public class SetUserPositionLevelCommandHandler(UserManager<User> userManager)
|
||||
throw new ConflictException(string.Join("; ", result.Errors.Select(e => e.Description)));
|
||||
}
|
||||
}
|
||||
|
||||
// ========== SET ALLOW DRAFTER SKIP TO FINAL (Mig 29 — F2 per-Drafter) ==========
|
||||
// Admin toggle AllowDrafterSkipToFinal cho 1 user. Khi true, user (Drafter) được
|
||||
// dùng checkbox "Gửi thẳng Cấp cuối" trong PE Workspace để skip toàn bộ Bước/Cấp
|
||||
// trung gian và bay thẳng tới Cấp cuối. Mặc định false (an toàn — Drafter phải
|
||||
// tuần tự qua mọi Bước/Cấp theo workflow).
|
||||
public record SetUserAllowDrafterSkipToFinalCommand(Guid Id, bool AllowDrafterSkipToFinal) : IRequest;
|
||||
|
||||
public class SetUserAllowDrafterSkipToFinalCommandHandler(UserManager<User> userManager)
|
||||
: IRequestHandler<SetUserAllowDrafterSkipToFinalCommand>
|
||||
{
|
||||
public async Task Handle(SetUserAllowDrafterSkipToFinalCommand request, CancellationToken ct)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(request.Id.ToString())
|
||||
?? throw new NotFoundException("User", request.Id);
|
||||
user.AllowDrafterSkipToFinal = request.AllowDrafterSkipToFinal;
|
||||
var result = await userManager.UpdateAsync(user);
|
||||
if (!result.Succeeded)
|
||||
throw new ConflictException(string.Join("; ", result.Errors.Select(e => e.Description)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user