[CLAUDE] PurchaseEvaluation: cờ gấp PRO/CCM + CCM duyệt-final theo ngưỡng giá trị (Mig 53) + 14 test
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m41s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m41s
Yêu cầu anh Kiệt FDC (sau họp sếp). Mig 53 AddPeUrgentAndCeoApprovalThreshold — 3 AddColumn, no new table (Mig 52→53). Rollout an toàn: cột nullable, ngưỡng null = giữ luồng duyệt cũ 100% cho tới khi admin set.
B — CCM duyệt-final theo NGƯỠNG GIÁ TRỊ ("gói CEO phân quyền theo giá trị"):
- ApprovalWorkflow += CeoApprovalThreshold (decimal?, admin nhập trong Workflow Designer).
- ApproveV2Async: actor role CostControl (CCM) + winnerQuoteTotal (tổng giá NCC được chọn) < ngưỡng → DaDuyet luôn (bỏ CEO); ≥ ngưỡng → đẩy lên CEO như cũ. Ngưỡng null = luồng tuyến tính cũ. Q4 chốt nhận diện theo ROLE người duyệt.
- reviewer PASS 0 blocker: cascade-safe (Off/role không lan), tested load-bearing (CCM dưới ngưỡng → DaDuyet skip CEO).
A — cờ gấp per-vai (visibility-only, Q3 KHÔNG đổi luồng):
- PE += IsUrgentByPro (PRO đỏ) / IsUrgentByCcm (CCM xanh).
- Endpoint PUT /purchase-evaluations/{id}/urgent role-gated (Procurement→ByPro, CostControl→ByCcm, Admin→cả 2, khác→Forbidden) + notify CEO (Director) khi MỚI bật (best-effort).
FE ×2 app: Workflow Designer ô "Ngưỡng giá trị gói CEO" (fe-admin) + PE detail nút bật/tắt cờ gấp đỏ/xanh theo role + badge GẤP + hint "giá trị gói vs ngưỡng → CCM duyệt-final/cần CEO" + PE list badge gấp.
DTO: PE detail += isUrgentByPro/Ccm + winnerQuoteTotal + ceoApprovalThreshold; list += isUrgentByPro/Ccm; workflow V2 += ceoApprovalThreshold.
+14 test (292→306): PeCcmThresholdFinalizeTests 5 (B routing) + PeUrgentToggleAuthzTests 9 (A authz). Build slnx 0/0 · npm build ×2 0 err · dotnet test 306 PASS.
C (sau duyệt xong chuyển phiếu đến dự án) — chờ anh Kiệt làm chi tiết form, CHƯA làm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -349,7 +349,12 @@ export function PurchaseEvaluationsListPage() {
|
||||
>
|
||||
{/* Plan AG6 — compact card 3 row: title+badge / mã+time / drafter+dept+contract */}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1 truncate text-[13px] font-medium text-slate-900">{p.tenGoiThau}</div>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1 truncate text-[13px] font-medium text-slate-900">
|
||||
{/* S69 — chấm gấp: ĐỎ (PRO) / XANH-lá (CCM) cạnh tên gói. */}
|
||||
{p.isUrgentByPro && <span className="shrink-0 text-[11px]" title="GẤP — Phòng Cung ứng (PRO)">🔴</span>}
|
||||
{p.isUrgentByCcm && <span className="shrink-0 text-[11px]" title="GẤP — Phòng Kiểm soát chi phí (CCM)">🟢</span>}
|
||||
<span className="truncate">{p.tenGoiThau}</span>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium',
|
||||
|
||||
@ -70,6 +70,9 @@ type DefinitionDto = {
|
||||
description: string | null
|
||||
isActive: boolean
|
||||
isUserSelectable: boolean // Mig 25 — admin toggle cho user pick
|
||||
// S69 — ngưỡng giá trị "gói CEO". Null = không áp ngưỡng (luồng đủ quy trình).
|
||||
// Gói < ngưỡng: CCM duyệt-final không cần CEO. ≥ ngưỡng: phải lên CEO.
|
||||
ceoApprovalThreshold: number | null
|
||||
// Mig 29 (S21 t5) + Mig 30 (S22+5) + Mig 31 (S23 t1) — 7 Allow* options
|
||||
// ALL xuống per slot Level (xem LevelDto). Admin opt-in per-Approver-slot
|
||||
// pattern proven 3× cumulative: F1+F3 (5 flag Mig 29) + F4 (Mig 30) + F2
|
||||
@ -393,6 +396,15 @@ function DefinitionCard({
|
||||
Cho user chọn
|
||||
</span>
|
||||
)}
|
||||
{/* S69 — badge ngưỡng gói CEO (nếu có set) */}
|
||||
{def.ceoApprovalThreshold != null && (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 rounded-full bg-rose-100 px-2 py-0.5 text-[10px] font-medium text-rose-700"
|
||||
title="Gói ≥ ngưỡng phải lên CEO duyệt. Gói < ngưỡng: CCM duyệt là xong."
|
||||
>
|
||||
Ngưỡng CEO: {def.ceoApprovalThreshold.toLocaleString('vi-VN')}đ
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{def.description && <p className="mt-1 text-xs leading-relaxed text-slate-500">{def.description}</p>}
|
||||
|
||||
@ -498,6 +510,10 @@ function Designer({
|
||||
const [code, setCode] = useState(cloneFrom?.code ?? defaultCode)
|
||||
const [name, setName] = useState(cloneFrom ? cloneFrom.name : `Quy trình ${applicableTypeLabel}`)
|
||||
const [description, setDescription] = useState(cloneFrom?.description ?? '')
|
||||
// S69 — Ngưỡng gói CEO (nullable). String form, '' = null. Clone giữ ngưỡng cũ.
|
||||
const [ceoThreshold, setCeoThreshold] = useState(
|
||||
cloneFrom?.ceoApprovalThreshold != null ? String(cloneFrom.ceoApprovalThreshold) : '',
|
||||
)
|
||||
const [steps, setSteps] = useState<EditStep[]>(initialSteps)
|
||||
|
||||
// Mig 29 (S21 t5) + Mig 30 (S22+5) + Mig 31 (S23 t1) — 7 Allow* options
|
||||
@ -550,6 +566,8 @@ function Designer({
|
||||
code,
|
||||
name,
|
||||
description: description || null,
|
||||
// S69 — ngưỡng gói CEO: '' → null (không áp ngưỡng), else parse số.
|
||||
ceoApprovalThreshold: ceoThreshold.trim() === '' ? null : Number(ceoThreshold.replace(/[^\d]/g, '')),
|
||||
steps: steps.map((s, i) => ({
|
||||
order: i + 1,
|
||||
name: s.name,
|
||||
@ -628,6 +646,20 @@ function Designer({
|
||||
<Label>Mô tả</Label>
|
||||
<Textarea rows={2} value={description} onChange={e => setDescription(e.target.value)} />
|
||||
</div>
|
||||
{/* S69 — Ngưỡng giá trị gói CEO (nullable). Empty = không áp ngưỡng. */}
|
||||
<div className="col-span-2 space-y-1.5">
|
||||
<Label>Ngưỡng giá trị gói CEO (đ)</Label>
|
||||
<Input
|
||||
inputMode="numeric"
|
||||
value={ceoThreshold === '' ? '' : Number(ceoThreshold.replace(/[^\d]/g, '')).toLocaleString('vi-VN')}
|
||||
onChange={e => setCeoThreshold(e.target.value.replace(/[^\d]/g, ''))}
|
||||
placeholder="Để trống = luôn theo đủ quy trình"
|
||||
/>
|
||||
<div className="text-[11px] leading-relaxed text-slate-400">
|
||||
Gói < ngưỡng: CCM duyệt là xong, không cần CEO. ≥ ngưỡng: phải lên CEO.
|
||||
Để trống = luôn theo đủ quy trình.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mig 29 (S21 t5) — 5 Allow* F1+F3 per slot Approver.
|
||||
|
||||
Reference in New Issue
Block a user