[CLAUDE] Workflow: LeaveBalance business logic — trừ phép khi duyệt + số dư (Phase 11 P11-B)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m8s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m8s
Số dư phép theo (User × LeaveType × Year) + trừ tự động khi đơn nghỉ duyệt cuối. Policy: cho phép vượt số dư (âm) + cảnh báo (anh main chốt), tích hợp vào trang đơn nghỉ. Schema (Mig 42 AddLeaveBalances — pure additive, 1 bảng): - LeaveBalance: UserId + LeaveTypeId + Year + EntitledDays + UsedDays + AdjustmentDays. UNIQUE (UserId,LeaveTypeId,Year), FK LeaveType Restrict, decimal(5,2). Remaining = Entitled + Adjustment − Used (computed, không store). Deduction hook (ApproveLeaveRequestHandler nhánh terminal DaDuyet — exactly-once): - Upsert LeaveBalance(RequesterUserId, LeaveTypeId, StartDate.Year), auto-create từ LeaveType.DaysPerYear, UsedDays += NumDays. Guard Status!=DaGuiDuyet chặn re-approve. FK invariant guard (em main thêm sau test reveal FK risk): - Create + UpdateDraft validate LeaveTypeId tồn tại (AnyAsync) → ConflictException. Đóng cửa vào — bogus type không thể tới deduction FK insert (tránh 500 kẹt đơn). CQRS LeaveBalanceFeatures.cs: GetMy (self, lazy merge active LeaveType) + GetUser (admin) + AdjustLeaveBalance (admin upsert carry-over). Controller [Authorize] + admin Roles=Admin. Embed: GetLeaveRequestByIdHandler trả balance NGƯỜI TẠO (approver xem thấy đúng). FE: WorkflowAppDetailPage ×2 — block "Số dư phép" + cảnh báo vượt khi kind=leave (SHA256 identical). Tests (+11, 130→154 PASS): deduction single/multi-level/accumulate/negative-allowed/ reject-return-no-deduct + lazy-merge + adjust upsert + Create guard bogus→Conflict. Cũng repair 2 test S42 terminal FK-fail (template BuildLeave +seed LeaveType). Verify: build 0 error · 154 test · FE ×2 · reviewer Max PASS (deduction exactly-once + FK invariant fully closed, 2 minor concurrency/comment defer). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -345,6 +345,33 @@ export function WorkflowAppDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Số dư phép (chỉ kind=leave) — Wave 2 hiển thị balance đã embed trong detail */}
|
||||
{kind === 'leave' && d.leaveBalanceRemaining != null && (() => {
|
||||
const remaining = d.leaveBalanceRemaining
|
||||
const numDays = d.numDays ?? 0
|
||||
const year = d.startDate ? new Date(d.startDate).getFullYear() : new Date().getFullYear()
|
||||
const isApproved = d.status === WorkflowAppStatus.DaDuyet
|
||||
const overBudget = remaining < 0 || (!isApproved && remaining < numDays)
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-6 space-y-3">
|
||||
<h3 className="font-semibold text-base">Số dư phép</h3>
|
||||
<div className="text-sm">
|
||||
Số dư phép năm <span className="font-semibold">{year}</span>:{' '}
|
||||
Được hưởng <span className="font-medium">{d.leaveBalanceEntitled ?? '—'}</span> ·{' '}
|
||||
Đã dùng <span className="font-medium">{d.leaveBalanceUsed ?? '—'}</span> ·{' '}
|
||||
<span className="font-semibold">Còn {remaining}</span> ngày
|
||||
</div>
|
||||
{overBudget && (
|
||||
<div className="rounded-lg border border-red-300 bg-amber-50/50 p-3 text-sm font-medium text-amber-900">
|
||||
{remaining < 0
|
||||
? '⚠️ Đã âm số dư phép'
|
||||
: `⚠️ Đơn ${numDays} ngày vượt số dư còn lại (${remaining} ngày)`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Section 2: Quy trình duyệt */}
|
||||
<div className="rounded-lg border bg-card p-6 space-y-3">
|
||||
<h3 className="font-semibold text-base">2. Quy trình duyệt</h3>
|
||||
|
||||
@ -78,6 +78,10 @@ export interface WorkflowAppDetail {
|
||||
endDate?: string
|
||||
numDays?: number
|
||||
reason?: string
|
||||
// leave balance (P11-B Wave 1: embed số dư phép NGƯỜI TẠO cho loại phép + năm đơn; null nếu loại phép không tồn tại)
|
||||
leaveBalanceEntitled?: number | null
|
||||
leaveBalanceUsed?: number | null
|
||||
leaveBalanceRemaining?: number | null
|
||||
// ot
|
||||
otDate?: string
|
||||
startTime?: string
|
||||
|
||||
Reference in New Issue
Block a user