Compare commits
2 Commits
b811e1125d
...
32cc8d5cc6
| Author | SHA1 | Date | |
|---|---|---|---|
| 32cc8d5cc6 | |||
| 06918622a6 |
@ -14,6 +14,9 @@ chain:
|
||||
- [x] đợt B ship `43e1bd7`: B1 panel duyệt (gỡ card-accent CLASS · emerald · glyph · Eye · badge 3-ngả · bỏ gập +GỠ `enabled` kẻo query câm) · B2 6 P0 (table-fixed 880 · orDash · Lock) · B3 (đầu cột · `hideSearch` gộp 2 ô tìm · 3 nấc · WCAG slate-500 ×7 · icon phân trang · chip baseline) · B4 (2 comment sai) — mirror 7/7 sha256, build XANH ×2
|
||||
- [x] deploy `43e1bd7` VERIFIED 2-vế ×2 app: hash ĐỔI (admin `Dlhi3eF1`→`O5Z5H2FK` · eoffice `B9PVjJnU`→`CD72NWts`) ∧ chuỗi MỚI `Ghi chú của bạn`=1/1 · `Cây toàn trình`=3/3 ∧ chuỗi CŨ `Ý kiến của bạn`=0/0; control-dương `Tải xuống`=10/10 (title= giữ được) · `Ý kiến cấp duyệt`=6/6 (CỐ Ý không đổi)
|
||||
- [ ] 🔴 CHƯA soi mắt prod 375/1366/1440 (5 lane NO-SCREENSHOT, gotcha #3) — mọi kết luận đợt B là ĐỌC MÃ
|
||||
- [x] ship `b811e11` + deploy VERIFIED 2-vế ×2 app (admin `BTWwiQTV`→`2Z9zO1QN` · eoffice `Bn8xEtmU`→`Cmm0TPlx`; 4 chuỗi mới ≥1/1, chuỗi CŨ `3. Căn cứ hồ sơ (mẫu vật liệu`=0/0, control-dương `Tải file lên`=1/1): **① thêm N hạng mục/NCC + giá + ghi chú nhập tay** (0-mig; POST/DELETE lines mới + PUT mở rộng; `PeReferenceAmount=0` chống đếm-đôi + là DẤU phân biệt dòng-gốc/thêm-tay; 409 đọc-được thay 500 unique) · **② gộp mục 3+4 → 1 khối File đính kèm khuôn PE** (gỡ bảng 5 cột khỏi MÀN, GIỮ nguyên máy căn cứ)
|
||||
- [x] `/fable-real investigator-codebase` `wf_a574cff1-6b9` — spec 3-mục ĐÃ ghi. **Engine BÁC 2 giả-thuyết của lead:** (a) "cột DỰ ÁN = di-chứng Mig 50" SAI → cột sinh `e33481e` 19-06 SAU Mig 50 6 ngày, CÙNG commit với comment ý-định ⇒ **CHỖ-CHỪA-SẴN**; (b) lead đo "≥3 ô `—`" THIẾU → thật **11 ô/app**. 🔑 (c) phát hiện: **`Project.BudgetTotal` có từ Mig 2 day-1, wired trọn 2 đầu**, chỉ **DỮ-LIỆU RỖNG 61/69 dự án NULL** — rất có thể CHÍNH LÀ "mục dự án chưa điền vào" anh nói
|
||||
- [ ] 🔴 HỎI ANH 3 câu để land cột DỰ ÁN (spec §③): ① Σ KHKK đổ vào Ô NÀO trong 11 ô · ② phiếu DaDuyet thì cột DỰ ÁN live hay đóng-băng · ③ sửa thẳng `ProposedAmount` đủ hay cần field "điều chỉnh" riêng
|
||||
- [ ] 🅿️ PARK (anh chưa giao làm — chỉ ĐO để biết trạng-thái): đường **KHKK → lũy kế NGÂN SÁCH chưa tồn tại**. `PeBudgetAccumulator.cs` (140 dòng) chỉ đọc 3 bảng PE (`PurchaseEvaluations` · `…Suppliers` · `…Quotes`); `grep -c 'ContractSigningPlan'` = **0** — control-dương: chuỗi đó có ở **7 file** khác trong Application layer ∧ `PurchaseEvaluations` = 6 hit trong chính file này ⇒ grep sống, 0-hit là THẬT. Trường "điều chỉnh tăng tiền" anh nói = `AdjustmentAmount` / `ProAdjustmentAmount` (*NS V0/hiệu chỉnh tăng giảm*, cho phép ÂM) per cặp Dự án × Hạng mục, sửa qua `PUT /purchase-evaluations/{id}/budget/{pro|ccm}`
|
||||
- [x] đợt D + C3 ship `a0ccac3` + deploy VERIFIED 2-vế ×2 app (admin `O5Z5H2FK`→`BTWwiQTV` · eoffice `CD72NWts`→`Bn8xEtmU`; 5 chuỗi mới =1/1, control-dương cây =1/1): O6 `onOpenPe` + xem PE tại chỗ (`?pe=`, loại-trừ `?id=`) · O4 gửi `dossierItemId` (rail BE có sẵn, FE chưa từng gửi) · O7 bút chì + chế-độ sửa · O8 cây 2 cấp · C3 value slate-900 · O3 **vế (2)** 2 tổng toàn phiếu
|
||||
- [x] 🔴 **ANH CHỐT @S175 — O3 vế (1) GIỮ 1-1, KHÔNG LÀM**: *"Cái này ko theo tiền, hiện cho 1-1 trước đã"* (anh nói 2 lần, mở+đóng tin nhắn). ⇒ **KHÔNG viết route POST/DELETE line**, KHÔNG đổi dialog cầu HĐ sang checkbox. Đúng khuôn **máy rộng / UI hẹp** anh chốt S164: schema để rộng sẵn (unique 3 cột Mig 71), UI giữ ca hẹp. 🔸 Cơ-chế cho ca "nhiều hơn 1" anh chỉ: **KHÔNG tách dòng mà ĐIỀU CHỈNH TĂNG TIỀN**, đi qua mục NGÂN SÁCH — *"bên mục ngân sách có mục dự án chưa điền vào; nếu tăng tiền lũy kế thì phía KHKK sẽ điền vào đấy"*
|
||||
|
||||
@ -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}` : ''}
|
||||
{/* 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 className="text-slate-300">— Chưa chọ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 có 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 là 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 có giá tham chiếu (hiện “—”) — số tham chiếu là 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 && (
|
||||
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>
|
||||
)}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
>
|
||||
{/* [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ồ sơ / 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ó căn cứ hồ sơ 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
|
||||
|
||||
@ -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 có 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
|
||||
|
||||
@ -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} | ||||