[CLAUDE] Contract: man KHKK Thao tac thanh host 2-panel giong Duyet NCC + REVERT cot da chen nham vao man PE
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m40s

Anh bac ban truoc: "Bo phan nay di cho duyet NCC da golive va da tot roi, y tao la dang
muon may lam giong giao dien thao tac cua Duyet NCC".

REVERT (quan trong nhat)
- `PurchaseEvaluationWorkspacePage` x2 app tro ve DUNG BYTE cua 1b3bcaa (grid 2 panel,
  0 import, 0 markup thua). Verify bang sha256 vs `git show 1b3bcaa:` + control-duong
  grep 0 hit ten panel trong pages/pe/.
- Bai hoc ghi vao dau file panel: "lam giong man X" != "sua man X". Ban dau doc anh 2/3
  (anh khoanh o man PE) thanh "chen them cot vao man PE"; anh 7 moi lo cho dat that la
  cot TRAI cua man Thao tac KHKK.

DI DOI
- `components/pe/PeKhkkSidePanel.tsx` -> `components/khkk/KhkkWorkListPanel.tsx` (git mv,
  giu lich su) — no phuc vu KHKK chu khong phai PE.

MAN KHKK — THAO TAC (`/khkk/create`, 8 leaf nhom) doi tu trang-giay-cuon sang HOST 2-PANEL
- Khuon be nguyen tu `PurchaseEvaluationWorkspacePage`: header 1 dong + grid
  `lg:grid-cols-[300px_1fr]` + Panel1 danh sach + nut dinh day + Panel2 noi dung.
- Panel 1 = 2 TANG dung anh anh khoanh:
    tang TREN "Cot danh sach Duyet NCC" — PE da duyet CHUA lap KHKK, gom theo DU AN;
                                          bam = nap thang vao form tao (tao luon KHKK).
    tang DUOI "Cot KHKK da tao"        — bam = XEM tai cho, read-only.
- 🔴 KHONG re-direct o BAT KY duong nao (anh chot "cho hien phieu do luon, chi view la dc,
  ko re-direct ve phieu goc"): panel nhan CALLBACK onPickPe/onPickPlan thay vi tu navigate.
  Van con nut phu "Mo trang day du" cho ai muon roi man — la lua chon, khong phai ep buoc.
- Section 1 cua form doi tu DANH SACH CHON thanh KHOI XAC NHAN phieu dang chon — danh sach
  da o Panel 1, de 2 cho chon cung 1 thu = 2 nguon su that tren cung mot man.
- Tao xong KHONG navigate('/khkk/:id') nua ma mo luon phieu vua tao o Panel 2 + tra form ve
  trang. Cung tinh than "khong roi man".
- Nut "Them moi" (dinh day Panel 1) = tra Panel 2 ve form trang.
- Gate `KeHoachKyKet.Read` giu nguyen trong panel (gotcha #85); thieu quyen thi NOI RA,
  va nut hanh-dong VAN hien de man khong thanh ngo cut.

Verify: build 2 app PASS; mirror SHA256 3/3 cap KHOP; PE workspace 0 du luong (byte-exact
vs 1b3bcaa) + control-duong grep 0 hit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-08-05 15:26:35 +07:00
parent e6347fd42d
commit 3f339c02c6
6 changed files with 662 additions and 560 deletions

View File

@ -1,9 +1,15 @@
// [S174 owner] Cột PHẢI của màn "Duyệt NCC — Thao tác" — 1 cột, 2 TẦNG 50/50:
// [S174 owner] PANEL 1 (danh sách) của màn "Kế hoạch ký kết HĐ — Thao tác" — 1 cột, 2 TẦNG:
// · tầng TRÊN = phiếu Duyệt NCC ĐÃ DUYỆT (có NCC trúng thầu) mà CHƯA lập kế hoạch ký kết,
// gom theo DỰ ÁN — đây là "việc còn phải làm";
// · tầng DƯỚI = kế hoạch ký kết ĐÃ TẠO — đây là "việc đã làm rồi".
// Anh chốt: "Thử ý tưởng 1 cột 2 tầng thử, 50% tầng trên là duyệt NCC, còn 50% tầng dưới
// là KHKK". File MIRROR SHA256 identical với fe-user counterpart.
// gom theo DỰ ÁN — "việc còn phải làm"; bấm = nạp thẳng vào form tạo;
// · tầng DƯỚI = kế hoạch ký kết ĐÃ TẠO — "việc đã làm rồi"; bấm = XEM tại chỗ (read-only).
// File MIRROR SHA256 identical với fe-user counterpart.
//
// 🔴 LỊCH SỬ ĐẶT SAI CHỖ — ghi để không tái phạm: bản đầu S174 gắn panel này thành cột 3 của
// màn *Duyệt NCC — Thao tác* (`PurchaseEvaluationWorkspacePage`). Anh bác: **"chỗ duyệt NCC
// đã golive và đã tốt rồi"** — ý anh từ đầu là làm màn Thao tác của KHKK **GIỐNG GIAO DIỆN**
// Thao tác của Duyệt NCC, chứ không phải chèn thêm thứ vào màn Duyệt NCC. Đã revert
// `PurchaseEvaluationWorkspacePage` về đúng byte của `1b3bcaa`. Bài: "làm giống màn X" ≠
// "sửa màn X".
//
// 🔴 0 ENDPOINT MỚI — dùng lại đúng 2 cửa đã sống:
// · `GET /contract-signing-plans/approved-pe-awaiting-plan` (chính cửa `KhkkCreatePage`
@ -18,7 +24,6 @@
// 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 { useNavigate } from 'react-router-dom'
import { FileCheck, FileSignature, Lock } from 'lucide-react'
import { usePermission } from '@/hooks/usePermission'
import { api } from '@/lib/api'
@ -68,8 +73,23 @@ function Tier({
)
}
export function PeKhkkSidePanel() {
const navigate = useNavigate()
export function KhkkWorkListPanel({
onPickPe,
onPickPlan,
selectedPeId,
selectedPlanId,
footer,
}: {
/** Bấm PE ở tầng trên → host nạp phiếu đó vào form tạo (Ở LẠI màn, không điều hướng). */
onPickPe: (peId: string) => void
/** Bm KHKK tng dưi host m phiếu đó panel 2, CH XEM. Anh cht: "click vào phiếu
* nào thì cho hiện phiếu đó luôn, chỉ view đc, ko re-direct về phiếu gốc". */
onPickPlan: (planId: string) => void
selectedPeId?: string | null
selectedPlanId?: string | null
/** Nút "+ Thêm mới" dính đáy — mirror `PeListPanel` của màn Duyệt NCC — Thao tác. */
footer?: React.ReactNode
}) {
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')
@ -109,7 +129,7 @@ export function PeKhkkSidePanel() {
const planItems = plans.data?.items ?? []
return (
<aside className="hidden min-h-0 flex-col overflow-hidden border-l border-slate-200 bg-white xl:flex">
<aside className="flex min-h-0 flex-col overflow-hidden border-r border-slate-200 bg-white">
{!canKhkk ? (
// Cột trắng đọc thành "chưa có gì" — phải nói ĐÚNG lý do là thiếu quyền.
<div className="flex flex-1 flex-col items-center justify-center gap-2 px-4 text-center">
@ -158,11 +178,14 @@ export function PeKhkkSidePanel() {
<li key={pe.id}>
<button
type="button"
// Bấm = đi thẳng tới màn lập kế hoạch. Không preset `?group=` vì
// PE này chưa dùng nhóm nào ⇒ để màn tạo giữ default của nó.
onClick={() => navigate('/khkk/create')}
// Bấm = nạp THẲNG phiếu này vào form tạo bên panel 2. Không điều hướng,
// không mở lại màn khác — đúng "click vào cột này để tạo luôn KHKK".
onClick={() => onPickPe(pe.id)}
title="Lập kế hoạch ký kết cho phiếu này"
className="block w-full px-3 py-2 text-left transition hover:bg-amber-50/60"
className={cn(
'block w-full px-3 py-2 text-left transition hover:bg-amber-50/60',
selectedPeId === pe.id && 'bg-amber-50 ring-1 ring-inset ring-amber-300',
)}
>
<div className="flex items-center gap-1.5">
<span className="font-mono text-[10px] text-brand-700">{pe.maPhieu ?? '—'}</span>
@ -212,9 +235,14 @@ export function PeKhkkSidePanel() {
<li key={k.id}>
<button
type="button"
onClick={() => navigate(`/khkk/${k.id}`)}
title="Mở kế hoạch ký kết"
className="block w-full px-3 py-2 text-left transition hover:bg-violet-50/60"
// 🔴 CHỈ XEM tại chỗ — KHÔNG `navigate('/khkk/:id')`. Anh chốt: "cho hiện
// phiếu đó luôn, chỉ view là đc, ko re-direct về phiếu gốc".
onClick={() => onPickPlan(k.id)}
title="Xem kế hoạch ký kết này (chỉ xem)"
className={cn(
'block w-full px-3 py-2 text-left transition hover:bg-violet-50/60',
selectedPlanId === k.id && 'bg-violet-50 ring-1 ring-inset ring-violet-300',
)}
>
<div className="flex items-center gap-1.5">
<span className="font-mono text-[10px] text-violet-700">{k.maKeHoach ?? '(chưa cấp mã)'}</span>
@ -247,6 +275,11 @@ export function PeKhkkSidePanel() {
</Tier>
</>
)}
{/* Nút hành-đng dính ĐÁY, ngoài vùng cun ca 2 tng mirror "+ Thêm mi" ca
`PeListPanel` (màn Duyệt NCC Thao tác). Hiện KỂ CẢ khi thiếu quyền KHKK: cột
không dựng đưc thì vẫn phải còn đưng bấm, không thì màn thành ngõ cụt. */}
{footer && <div className="shrink-0 border-t border-slate-200 p-2">{footer}</div>}
</aside>
)
}