[CLAUDE] Contract: KHKK UAT S175 dot-E — rut gon man theo anh khoanh do (FE-only, 0-mig)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m28s

Anh review truc tiep tren prod, khoanh do tung cho. Land dung nhung gi anh chi:

=== GO (anh: "Bo di" / "Bo het di" / "Bo may dong nay di") ===
· 2 o TONG (Sigma de xuat / Sigma duyet) — chinh anh chon "hien 2 tong rieng" khi em hoi
  o vong AskUser truoc, nhin that thi thua => go. Giu 1 dong vet trong ma de lan sau khong
  ai dung lai tuong la thieu.
· 3 dong chu-thich duoi bang gia (SNAPSHOT / gan hang muc / dong them tay). Noi dung khong
  mat: rang-buoc "phai gan hang muc moi trinh duyet" van do submit-guard BE chan THAT, va o
  Hang muc o muc 1 da dem "con N dong chua gan".
· Muc 1: GO **Phong ban · Quy trinh · Han xu ly** — Quy trinh van xem duoc o PANEL 3, go o
  day la bo cho ve TRUNG chu khong mat thong tin.
· Muc 1: GO **Link ho so · Ghi chu** khoi o xem (form Sua van con 2 truong nay).
· Chip "Giai doan 2 · <ten nhom dai>" o header — ten nhom da co trong tieu de ("— N2") va
  dai 4 giai doan da chi ro dang o GD2 => chip la lan ke thu BA.
· Dong "Dang loc — danh sach lay thang tu may chu…" + dai dau cot "DANH SACH KE HOACH".
  🔸 Dai dau cot chinh la thu em THEM o dot B de chua "cot cut dau" — anh xem that roi bao
    bo. Ghi lai de khong tu dung lai.

=== THEM / DOI ===
· Muc 1 +o **Hang muc**: 1 phieu nay duoc N hang muc => liet ke DUY-NHAT theo thu tu dong,
  kem badge amber "con N dong chua gan" (dong chua gan KHONG bia ten).
· Header phieu +**ma phieu PE nguon** +**NCC trung thau** (anh: "Dua len tren phieu — NCC").
  NCC lay duy-nhat tu lines nen lien-danh N NCC liet ke het.
· Cho tai file: be DUNG khuon Duyet NCC (`PeDetailTabs.tsx:3527-3531` + `:3601-3608`) —
  dong chu mo-ta o TREN, nut vien DUT nen brand-nhat o DUOI danh sach, thay nut dac o goc
  header. Nut vien dut doc ra la "cho tha them vao"; nut dac o header doc ra la "hanh dong
  cua ca khoi" — day la cai thu nhat. +`accept` bo loc dinh dang y PE.
· Cay panel Thao tac: 2 cap Du an > Hang muc nay GAP/MO duoc (anh: "Cho hien thi theo cay").
  🔴 Dung tap-GAP (khong phai tap-MO) => mac dinh MO HET: du an/hang muc moi xuat hien sau
    nay tu mo, khong bi an im lang vi chua ai bam mo bao gio.

=== CON NO (chua land, ghi ra de khong roi) ===
🔴 "So tien CCM va PRO duyet" dua len header — CHUA lam duoc: `KhkkDetailDto` hien 0 truong
  ngan sach (do: grep -c ccm khkk.ts = 0, control-duong pro = 50) => can BE tra them.
🔴 Gom nut thao tac: go "+ Lap ke hoach" (header) · "Sua" (muc 1) · khoi "HANH DONG" (panel 3)
  · banner "Dang xem ke hoach" => THAY bang 1 THANH NUT DAY khuon PE workspace
  (Xoa phieu | Luu | Luu & Gui Duyet).
🔴 "Bo cai dam ke ben" — dai doc giua panel 1 va panel 2, chua dinh vi duoc.
🔴 Danh sach hien theo ngay thang > du an > hang muc > phieu PE + phieu KHKK tuong ung.
🔴 CHUA soi mat prod 375/1366/1440.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-08-05 21:21:08 +07:00
parent 06918622a6
commit 32cc8d5cc6
8 changed files with 296 additions and 220 deletions

View File

@ -361,6 +361,28 @@ export function KhkkDetailContent({
<div className="flex flex-wrap items-center gap-2 border-b border-slate-200 px-5 py-3">
<span className="font-mono text-sm font-semibold text-brand-800">{plan.maKeHoach ?? '(chưa cấp mã)'}</span>
<span className="min-w-0 flex-1 truncate text-sm text-slate-700">{plan.peTenGoiThau ?? '—'}</span>
{/* [S175 — anh: "Đưa lên trên phiếu — NCC"] Mã phiếu PE nguồn + NCC trúng thầu dời từ
mục 1 lên ĐÂY. Đây là 2 thứ người xem cần biết ngay khi mở phiếu, không phải cuộn
xuống bảng mới thấy. NCC lấy DUY-NHẤT từ `lines` (liên danh N NCC ⇒ liệt kê hết).
🔴 CÒN NỢ: "số tiền CCM và PRO duyệt" anh yêu cầu CHƯA đưa lên được — `KhkkDetailDto`
hiện 0 trường ngân sách (đo: `grep -c ccm khkk.ts` = 0); cần BE trả thêm. */}
{plan.peMaPhieu && (
<span
className="inline-flex items-center rounded border border-slate-300 bg-white px-1.5 py-0.5 font-mono text-[11px] text-slate-600"
title="Phiếu Duyệt NCC nguồn"
>
{plan.peMaPhieu}
</span>
)}
{[...new Set(plan.lines.map(l => l.supplierName).filter(Boolean))].map(n => (
<span
key={n as string}
className="inline-flex max-w-[16rem] items-center truncate rounded bg-emerald-50 px-1.5 py-0.5 text-[11px] font-medium text-emerald-700"
title={`NCC trúng thầu: ${n}`}
>
{n}
</span>
))}
{plan.approvalGroup != null && (
<span
className="inline-flex items-center rounded-md border border-violet-300 bg-violet-100 px-2 py-0.5 text-[11px] font-semibold text-violet-800"
@ -391,27 +413,42 @@ export function KhkkDetailContent({
) : undefined
}
>
{/* [S175 — anh chốt trên ảnh khoanh đỏ] Mục 1 RÚT GỌN còn 4 ô:
· "Bỏ hết đi" → GỠ **Phòng ban · Quy trình · Hạn xử lý**
🔸 Quy trình vẫn xem được ở PANEL 3 (`KhkkWorkflowPanel` đầu khối) — gỡ ở đây
là bỏ chỗ vẽ TRÙNG, không mất thông tin.
· "Bỏ đi" → GỠ **Link hồ sơ · Ghi chú** khỏi ô xem (form Sửa vẫn còn, xem dưới)
· "Thêm hạng mục vào đây" → +ô **Hạng mục** ngay cạnh Dự án
· "Đưa lên trên phiếu" → **NCC** + mã phiếu PE dời lên dải header (xem `:361`) */}
<div className="grid grid-cols-1 gap-x-6 gap-y-4 md:grid-cols-2">
<Field label="Mã kế hoạch" value={plan.maKeHoach ?? '—'} mono />
<Field label="Phiếu Duyệt NCC nguồn">
<span className="font-mono text-sm">{plan.peMaPhieu ?? '—'}</span>
</Field>
<Field label="Dự án" value={plan.projectName ?? '—'} />
<Field label="Phòng ban" value={plan.departmentName ?? '—'} />
<Field label="Người soạn" value={plan.drafterFullName ?? '—'} />
<Field label="Quy trình">
{plan.workflowCode ? (
<span>
<span className="font-mono">{plan.workflowCode}</span>
{plan.workflowVersion != null && <span className="font-mono"> v{plan.workflowVersion}</span>}
{plan.workflowName ? ` · ${plan.workflowName}` : ''}
</span>
) : (
<span className="text-slate-300"> Chưa chọn </span>
)}
{/* 1 phiếu nay được N hạng mục (S175) ⇒ liệt kê DUY-NHẤT, giữ thứ tự dòng.
Dòng chưa gán KHÔNG bịa tên — đếm riêng để người soạn biết còn nợ mấy dòng,
vì submit-guard sẽ chặn trình duyệt khi còn dòng chưa gán. */}
<Field label="Hạng mục" full>
{(() => {
const names = [...new Set(plan.lines.map(l => l.tenHangMuc).filter(Boolean))] as string[]
const unassigned = plan.lines.filter(l => !l.catalogEntryId).length
if (names.length === 0 && unassigned === 0) return DASH
return (
<span className="flex flex-wrap items-center gap-1">
{names.map(n => (
<span key={n} className="inline-flex items-center rounded bg-brand-50 px-1.5 py-0.5 text-[11px] font-medium text-brand-700">
{n}
</span>
))}
{unassigned > 0 && (
<span className="inline-flex items-center rounded bg-amber-100 px-1.5 py-0.5 text-[11px] font-medium text-amber-800">
còn {unassigned} dòng chưa gán
</span>
)}
</span>
)
})()}
</Field>
<Field label="Người soạn" value={plan.drafterFullName ?? '—'} />
<Field label="Ngày tạo" value={formatDateTime(plan.createdAt)} />
<Field label="Hạn xử lý (tham khảo)" value={plan.slaDeadline ? formatDateTime(plan.slaDeadline) : '—'} />
</div>
{editHeader ? (
@ -468,49 +505,9 @@ export function KhkkDetailContent({
<div className="text-sm italic text-slate-400">Phiếu nguồn chưa NCC trúng thầu.</div>
) : (
<>
{/* [S175 · O3 — ANH CHỐT] TỔNG đặt PHÍA TRÊN bảng, đúng 2 con số:
① trục = CHỈ TỔNG TOÀN PHIẾU (anh bác phương án tổng-mỗi-NCC);
② hiện 2 TỔNG RIÊNG cạnh nhau — Σ giá ĐỀ XUẤT ⟂ Σ giá DUYỆT.
🔴 KHÔNG cộng gộp 2 cột thành một số "hiện hành": đề-xuất và duyệt là hai
con số có ý nghĩa pháp-lý khác nhau (một cái chưa ai chốt, một cái đã qua
choke-point `ApplyApprovedValuesOnFinalize`); trộn lại là bịa ra một số
không tồn tại trong hồ-sơ nào.
🔸 Dòng chưa có giá duyệt ⇒ `null`, `moneyOrDash` in em-dash mờ; tổng duyệt
chỉ cộng phần ĐÃ có, và nói rõ còn bao nhiêu dòng chưa chốt. */}
{(() => {
const sumProposed = plan.lines.reduce((s, l) => s + (l.proposedAmount ?? 0), 0)
const approvedLines = plan.lines.filter(l => l.approvedAmount != null)
const sumApproved = approvedLines.reduce((s, l) => s + (l.approvedAmount ?? 0), 0)
const pending = plan.lines.length - approvedLines.length
return (
<div className="mb-3 flex flex-wrap items-stretch gap-2">
<div className="min-w-[13rem] flex-1 rounded-md border border-slate-200 bg-white px-3 py-2">
<div className="label-eyebrow">Tổng giá đ xuất</div>
<div className="mt-0.5 text-base font-semibold tabular-nums text-brand-800">
{formatVnd(sumProposed)}
</div>
<div className="text-[10px] text-slate-500">{plan.lines.length} dòng</div>
</div>
<div className={cn(
'min-w-[13rem] flex-1 rounded-md border px-3 py-2',
pending === 0 ? 'border-emerald-300 bg-emerald-50' : 'border-slate-200 bg-white',
)}>
<div className="label-eyebrow">Tổng giá duyệt</div>
<div className={cn(
'mt-0.5 text-base font-semibold tabular-nums',
pending === 0 ? 'text-emerald-700' : 'text-slate-400',
)}>
{approvedLines.length === 0 ? '—' : formatVnd(sumApproved)}
</div>
<div className="text-[10px] text-slate-500">
{pending === 0
? 'đã chốt đủ mọi dòng'
: `còn ${pending}/${plan.lines.length} dòng chưa chốt giá`}
</div>
</div>
</div>
)
})()}
{/* 🧊 [S175] 2 ô TỔNG (Σ đề xuất ⟂ Σ duyệt) đã GỠ — anh xem trên màn rồi chốt "Bỏ đi".
Trước đó chính anh chọn "hiện 2 tổng riêng" khi em hỏi; nhìn thật thì thừa. Giữ 1 dòng
vết để lần sau khỏi ai dựng lại tưởng là thiếu. */}
{/* [S175 · B2 · P0] `table-fixed` + `<colgroup>` + `min-w-[880px]`: bảng co giãn
tự-do trước đây để cột tiền nhảy bề rộng theo độ dài tên NCC ⇒ 3 cột số không
thẳng hàng giữa các phiếu. Chốt bề rộng rồi thì hẹp màn hình sẽ CUỘN NGANG
@ -729,11 +726,9 @@ export function KhkkDetailContent({
)}
</>
)}
<div className="mt-3 text-xs text-muted-foreground">
Giá tham chiếu SNAPSHOT tổng báo giá đưc chọn của NCC trên phiếu Duyệt NCC tại thời điểm lập kế hoạch.
{' '}Cột Hạng mục gán ngay tại bảng khi phiếu Nháp/Trả lại mọi dòng phải gán xong mới trình duyệt đưc.
{' '}🔴 Dòng THÊM TAY không giá tham chiếu (hiện &ldquo;&rdquo;) số tham chiếu của cả NCC, đã nằm dòng gốc.
</div>
{/* 🧊 [S175] 3 dòng chú-thích dưới bảng ĐÃ GỠ — anh: "Bỏ mấy dòng này đi".
Nội dung không mất: ràng-buộc "phải gán hạng mục mới trình duyệt được" vẫn do
submit-guard BE chặn thật, và ô Hạng mục ở mục 1 đã đếm "còn N dòng chưa gán". */}
</Section>
{/* ─────── 3 — GỘP "Căn cứ hồ sơ" + "File đính kèm" (anh chốt S175) ─────── */}
@ -752,22 +747,22 @@ export function KhkkDetailContent({
chipFg="var(--color-teal-700)"
count={mergedRows.length}
actions={
readOnly ? undefined : (
<div className="flex items-center gap-2">
{isDraft && (
<Button variant="outline" size="sm" onClick={() => setItemForm({ ...EMPTY_ITEM })}>
<Plus className="mr-2 h-3.5 w-3.5" /> Thêm căn cứ
</Button>
)}
<label className="inline-flex cursor-pointer items-center rounded-md border border-slate-300 px-2.5 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-50">
<Upload className="mr-2 h-3.5 w-3.5" />
{upload.isPending ? 'Đang tải...' : 'Tải file lên'}
<input type="file" multiple className="hidden" onChange={(e) => onPickFiles(e)} />
</label>
</div>
readOnly || !isDraft ? undefined : (
<Button variant="outline" size="sm" onClick={() => setItemForm({ ...EMPTY_ITEM })}>
<Plus className="mr-2 h-3.5 w-3.5" /> Thêm căn cứ
</Button>
)
}
>
{/* [S175 — anh chốt] Chỗ tải file bê ĐÚNG khuôn Duyệt NCC (`PeDetailTabs.tsx:3527-3531`
+ `:3601-3608`): dòng chữ mô-tả ở TRÊN, nút viền ĐỨT nền brand-nhạt ở DƯỚI danh
sách — thay cho nút đặc nằm ở góc header. Nút viền đứt đọc ra là "chỗ thả thêm
vào", nút đặc ở header đọc ra là "hành động của cả khối"; đây là cái thứ nhất. */}
{!readOnly && (
<p className="mb-2 text-[12px] text-slate-500">
File căn cứ hồ / shopdrawing của kế hoạch. Gắn vào 1 căn cứ cụ thể bằng nút dòng căn cứ đó.
</p>
)}
{mergedRows.length === 0 ? (
<div className="text-sm italic text-slate-400">Chưa căn cứ hồ hay file đính kèm nào.</div>
) : (
@ -854,6 +849,27 @@ export function KhkkDetailContent({
))}
</div>
)}
{/* Nút tải lên — viền ĐỨT, đặt DƯỚI danh sách, y khuôn PE `:3601-3608`.
`accept` bê nguyên danh-sách của PE: chặn ngay ở hộp chọn file đỡ được vòng
upload-rồi-mới-biết-sai-định-dạng. */}
{!readOnly && (
<label className={cn(
'mt-2 inline-flex cursor-pointer items-center gap-1.5 rounded border border-dashed border-brand-300',
'bg-brand-50/50 px-3 py-2 text-xs font-medium text-brand-700',
'hover:border-brand-500 hover:bg-brand-50',
upload.isPending && 'pointer-events-none opacity-50',
)}>
<Upload className="h-3.5 w-3.5" />
{upload.isPending ? 'Đang tải…' : '+ Tải lên hồ sơ'}
<input
type="file"
multiple
accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.webp"
className="hidden"
onChange={(e) => onPickFiles(e)}
/>
</label>
)}
{previewAtt && (
<AttachmentPreviewDialog
open

View File

@ -23,7 +23,7 @@
// (bật chế-độ danh-sách phẳng), luôn có đường tới phiếu cũ; và dòng cảnh-báo
// "Cây tải 200 phiếu mới nhất…" của cây KHÔNG bị nuốt.
import { useQuery } from '@tanstack/react-query'
import { ChevronLeft, ChevronRight, FileCheck, Inbox, ListTree, Search } from 'lucide-react'
import { ChevronLeft, ChevronRight, FileCheck, Inbox, Search } from 'lucide-react'
import { Input } from '@/components/ui/Input'
import { Select } from '@/components/ui/Select'
import { PipelineTreePanel } from '@/components/pipeline/PipelineTreePanel'
@ -281,11 +281,9 @@ export function KhkkListPanel({
<option key={p} value={p}>{KHKK_PHASE_LABELS[Number(p) as KhkkPhaseValue]}</option>
))}
</Select>
{flatMode && !deletedView && canPe && (
<p className="text-[10px] italic text-slate-500">
Đang lọc danh sách lấy thẳng từ máy chủ (không giới hạn 200 phiếu của cây).
</p>
)}
{/* 🧊 [S175] Dòng "Đang lọc — danh sách lấy thẳng từ máy chủ…" ĐÃ GỠ (anh: "Bỏ mấy dòng
này đi"). Đó là chú-thích về CƠ-CHẾ bên trong, người dùng không cần biết cây tải 200
phiếu hay lấy thẳng máy chủ — họ chỉ cần thấy đúng phiếu. */}
{!canPe && (
<p className="text-[10px] italic text-slate-500">
Bạn chưa quyền xem phiếu Duyệt NCC nên cây toàn trình không dựng đưc danh
@ -298,20 +296,8 @@ export function KhkkListPanel({
không số đếm ⇒ cột "cụt đầu": lệch cả với màn Thao tác (`KhkkWorkListPanel` có
`Tier` header) LẪN với chính chế-độ phẳng (có dòng đếm ở chân). Mirror đúng khuôn
`Tier` để 2 màn KHKK cùng một hình. */}
<div className="flex shrink-0 items-center gap-2 border-b border-slate-200 bg-slate-50/80 px-3 py-2">
<span className="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded bg-brand-100 text-brand-700" aria-hidden>
{flatMode ? <FileCheck className="h-3 w-3" /> : <ListTree className="h-3 w-3" />}
</span>
<span className="min-w-0 flex-1 truncate text-[11px] font-bold uppercase tracking-wide text-slate-600">
{deletedView ? 'Phiếu đã xóa' : flatMode ? 'Danh sách kế hoạch' : 'Cây toàn trình'}
</span>
{flatMode && !list.isLoading && !list.isError && (
<span className="shrink-0 rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-medium tabular-nums text-slate-600">
{total}
</span>
)}
</div>
{/* 🧊 [S175] Dải "ĐẦU CỘT" (thêm ở đợt B để chữa "cột cụt đầu") ĐÃ GỠ — anh khoanh đỏ
cùng dòng "Đang lọc" và bảo bỏ. Tiêu-đề cột đã có ở header trang ngay trên. */}
<div className="flex-1 overflow-y-auto">
{!flatMode && (
<PipelineTreePanel

View File

@ -24,7 +24,8 @@
// con). Người chỉ có quyền PE mà không có KHKK sẽ ăn 403 rải khắp cột. ⇒ gate bằng ĐÚNG key
// của endpoint, và khi thiếu quyền thì NÓI RA thay vì để cột trắng trông như "chưa có gì".
import { useQuery } from '@tanstack/react-query'
import { FileCheck, FileSignature, FolderTree, Lock, Pencil } from 'lucide-react'
import { ChevronRight, FileCheck, FileSignature, FolderTree, Lock, Pencil } from 'lucide-react'
import { useState } from 'react'
import { usePermission } from '@/hooks/usePermission'
import { api } from '@/lib/api'
import { cn } from '@/lib/cn'
@ -92,6 +93,10 @@ export function KhkkWorkListPanel({
* không vẽ bút chì (cột giữ đúng hình cũ). */
onEditPlan?: (planId: string) => void
}) {
// [S175 — anh: "Cho hiển thị theo cây nằm dự án hạng mục"] Tập KHOÁ đang GẬP.
// Dùng tập-gập (không phải tập-mở) để mặc-định là MỞ HẾT: dự án/hạng mục mới xuất hiện sau
// này tự mở, không bị ẩn im lặng vì chưa ai bấm mở bao giờ.
const [collapsed, setCollapsed] = useState<Set<string>>(new Set())
const { can } = usePermission()
// 🔴 ĐÚNG key policy của endpoint — KHÔNG dùng `Khkk_*` (đó là key MENU, tầng hiển thị).
const canKhkk = can('KeHoachKyKet', 'Read')
@ -193,23 +198,52 @@ export function KhkkWorkListPanel({
{/* [S175 · O8] CẤP 1 = Dự án (sticky, `z-20`) · CẤP 2 = Hạng mục (sticky dưới nó,
`z-10` + thụt lề + vạch trái). 🔴 Hai `z` khác nhau là BẮT BUỘC: cùng `z` thì
header hạng mục trượt ĐÈ lên header dự án khi cuộn, mất mất cấp cha. */}
{[...byProject.entries()].map(([projectName, byWorkItem]) => (
{[...byProject.entries()].map(([projectName, byWorkItem]) => {
const pCollapsed = collapsed.has(projectName)
return (
<div key={projectName}>
<div className="sticky top-0 z-20 flex items-center gap-1.5 border-b border-slate-200 bg-slate-50/95 px-3 py-1 backdrop-blur">
{/* [S175 — anh: "Cho hiển thị theo cây nằm dự án hạng mục"] Header CẤP 1 nay là
NÚT gập/mở. Mặc-định MỞ: gập sẵn thì mở panel ra thấy toàn thư-mục đóng,
người dùng tưởng chưa có việc nào. */}
<button
type="button"
onClick={() => setCollapsed(prev => {
const next = new Set(prev)
if (next.has(projectName)) next.delete(projectName); else next.add(projectName)
return next
})}
aria-expanded={!pCollapsed}
className="sticky top-0 z-20 flex w-full items-center gap-1.5 border-b border-slate-200 bg-slate-50/95 px-3 py-1 text-left backdrop-blur transition hover:bg-slate-100"
>
<ChevronRight className={cn('h-3 w-3 shrink-0 text-slate-400 transition-transform', !pCollapsed && 'rotate-90')} aria-hidden />
<FolderTree className="h-3 w-3 shrink-0 text-slate-400" aria-hidden />
<span className="min-w-0 flex-1 truncate text-[10px] font-semibold uppercase tracking-wide text-slate-600">
{projectName}
</span>
<span className="shrink-0 text-[10px] tabular-nums text-slate-500">{countProject(byWorkItem)}</span>
</div>
{[...byWorkItem.entries()].map(([workItemName, items]) => (
</button>
{!pCollapsed && [...byWorkItem.entries()].map(([workItemName, items]) => {
const wKey = `${projectName}${workItemName}`
const wCollapsed = collapsed.has(wKey)
return (
<div key={workItemName}>
<div className="sticky top-[1.375rem] z-10 flex items-center gap-1.5 border-b border-slate-100 bg-white/95 py-1 pl-6 pr-3 backdrop-blur">
<button
type="button"
onClick={() => setCollapsed(prev => {
const next = new Set(prev)
if (next.has(wKey)) next.delete(wKey); else next.add(wKey)
return next
})}
aria-expanded={!wCollapsed}
className="sticky top-[1.375rem] z-10 flex w-full items-center gap-1.5 border-b border-slate-100 bg-white/95 py-1 pl-5 pr-3 text-left backdrop-blur transition hover:bg-slate-50"
>
<ChevronRight className={cn('h-3 w-3 shrink-0 text-slate-300 transition-transform', !wCollapsed && 'rotate-90')} aria-hidden />
<span className="min-w-0 flex-1 truncate text-[10px] font-medium text-slate-500">
{workItemName}
</span>
<span className="shrink-0 text-[10px] tabular-nums text-slate-400">{items.length}</span>
</div>
</button>
{!wCollapsed && (
<ul className="divide-y divide-slate-100 border-l-2 border-slate-100 pl-1">
{items.map((pe) => (
<li key={pe.id}>
@ -239,10 +273,13 @@ export function KhkkWorkListPanel({
</li>
))}
</ul>
)}
</div>
))}
)
})}
</div>
))}
)
})}
</Tier>
<div className="shrink-0 border-t-2 border-slate-200" />

View File

@ -41,7 +41,6 @@ import { useAuth } from '@/contexts/AuthContext'
import { api } from '@/lib/api'
import {
KhkkPhase,
khkkGroupMenuLabel,
type KhkkDetailDto,
type KhkkPhaseValue,
} from '@/types/khkk'
@ -159,9 +158,9 @@ export function KhkkListPage() {
<div className="flex min-w-0 items-center gap-2">
<FileCheck className="h-5 w-5 text-slate-500" />
<h1 className="truncate text-base font-semibold tracking-tight text-slate-900">{headerTitle}</h1>
<span className="ml-2 truncate rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-medium text-slate-600">
Giai đoạn 2{group !== null ? ` · ${khkkGroupMenuLabel(group)}` : ''}
</span>
{/* 🧊 [S175] Chip "Giai đoạn 2 · <tên nhóm dài>" ĐÃ GỠ — anh: "Bỏ mấy dòng này đi".
Tên nhóm đã có trong `headerTitle` ngay bên trái (dạng ngắn "— N2"), và dải 4 giai
đoạn ở panel giữa đã chỉ rõ đang đứng ở GĐ2 ⇒ chip này là lần kể thứ ba. */}
</div>
{!deletedView && (
<Button onClick={() => navigate(group !== null ? `/khkk/create?group=${group}` : '/khkk/create')}>