[CLAUDE] Contract: K4a+K4b SP-002 sidebar 8 nhom x 6 leaf + noi ?group= end-to-end (0-mig)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m23s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m23s
K4a (gate PASS-WITH-FLAGS 4, sub-reviewer-gate-k4a.md): 49 key Khkk_G{2-8} bo-sinh
by-construction 3-phia (All/seed/grant) MenuKeys.cs + DbInitializer 49 row + labelBackfill
G1 nhan A1 + site-2 Concat + va 7/10/11/13/15 (nhan N2 co "pha do"; comment 1893 -> mo-neo
grep). All 64->113, Policies 256->452 (test T2 khoa runtime). Test +7 KhkkGroupMenuSeedTests
(T7 revoker-khong-dung-Khkk: probe vi-phan 112->0, gotcha #84).
K4b (gate PASS-WITH-FLAGS 4M/5m 0-chan, sub-reviewer-gate-k4b.md): KhkkListPage x2 derive
phase+group tu searchParams (leaf->leaf khong F5, navKey reset) + queryKey {group,phase,
search,page,deletedView} + KhkkCreatePage preset ?group= + staticMap G1 6-leaf group=1 +
regex Khkk_G([1-8])_* fe-user (fe-admin GHI RANG BUOC isAdminHidden) + FLAG-1 lead-fix:
GET /deleted +group mirror List (Controller+Query+handler SAU-IDOR) + FE noi 2-nhanh.
2 wave 1 commit CO KHAI: Layout.tsx dinh ca 2 wave khong tach hunk phi-tuong-tac; moi dong
deu qua gate rieng. STATUS canonical 5-row dong bo (71/97/452/113/620->621) + bundle :479.
No K4c: MAJOR-A test /deleted+group filter + MAJOR-C WfView doc group. Suite 621/0
(45D+576I) + filter 41/41 + npm x2 + dotnet 0E.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -14,7 +14,7 @@
|
||||
// không phân biệt được "PE không đủ điều kiện" với "PE đã dùng hết nhóm".
|
||||
import { useState, type FormEvent } from 'react'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { CheckCircle2, FileCheck, FileText, Layers, Plus, Save, Settings2, X } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { PageHeader } from '@/components/ui/PageHeader'
|
||||
@ -57,11 +57,31 @@ function firstFreeGroup(taken: number[]): number | null {
|
||||
|
||||
export function KhkkCreatePage() {
|
||||
const navigate = useNavigate()
|
||||
const [searchParams] = useSearchParams()
|
||||
|
||||
// [K4b S166 — vá-6] Vào từ leaf "Thao tác" của một nhóm (`/khkk/create?group={n}`)
|
||||
// hoặc từ nút "Lập kế hoạch" trên màn danh sách đang lọc nhóm ⇒ ô chọn nhóm mở sẵn
|
||||
// đúng nhóm đó. Không có tham số (vào thẳng /khkk/create) thì giữ nguyên default cũ.
|
||||
// Chỉ nhận 1..8, rác coi như không có — ô chọn không được phép mang giá trị ngoài dải.
|
||||
const groupParam = searchParams.get('group')
|
||||
const presetGroup = groupParam !== null && /^[1-8]$/.test(groupParam) ? Number(groupParam) : null
|
||||
|
||||
const [purchaseEvaluationId, setPurchaseEvaluationId] = useState('')
|
||||
const [approvalWorkflowId, setApprovalWorkflowId] = useState('')
|
||||
const [approvalGroup, setApprovalGroup] = useState<number>(KHKK_APPROVAL_GROUP_DEFAULT)
|
||||
const [approvalGroup, setApprovalGroup] = useState<number>(presetGroup ?? KHKK_APPROVAL_GROUP_DEFAULT)
|
||||
const [ghiChu, setGhiChu] = useState('')
|
||||
|
||||
// Cùng lý do với `KhkkListPage` (vá-5): 8 leaf "Thao tác" dùng chung route
|
||||
// `/khkk/create`, nhảy leaf→leaf không dựng lại component nên giá trị khởi tạo ở
|
||||
// trên KHÔNG chạy lại. Đồng bộ lại ngay trong render khi tham số nhóm đổi.
|
||||
// 🔴 Chỉ đổi khi CHÍNH THAM SỐ URL đổi — nếu so với `approvalGroup` thì người dùng
|
||||
// tự chọn nhóm khác sẽ bị kéo ngược về nhóm của URL, không bấm đi đâu được.
|
||||
const [lastPresetGroup, setLastPresetGroup] = useState(presetGroup)
|
||||
if (presetGroup !== lastPresetGroup) {
|
||||
setLastPresetGroup(presetGroup)
|
||||
if (presetGroup !== null) setApprovalGroup(presetGroup)
|
||||
}
|
||||
|
||||
// Phiếu PE đủ điều kiện: Phase=DaDuyet VÀ có ≥1 NCC trúng thầu (BE lọc — khớp
|
||||
// ĐÚNG 2 rào còn lại của Create nên picker không mời bấm vào thứ chắc chắn 409).
|
||||
// [K2 S164] Vị ngữ "chưa có kế hoạch sống" ĐÃ BỎ (`Features.cs:1291-1295`).
|
||||
|
||||
@ -3,13 +3,20 @@
|
||||
// KpiCard làm bộ lọc + bảng + phân trang). File MIRROR SHA256 identical với
|
||||
// fe-admin counterpart.
|
||||
//
|
||||
// URL vào từ menu (Layout.tsx staticMap — chỗ thứ 4 của Pattern 16-bis):
|
||||
// Khkk_List → /khkk/list
|
||||
// Khkk_Pending → /khkk/list?filter=ChoDuyet
|
||||
// Khkk_Approved → /khkk/list?filter=DaDuyet
|
||||
// Khkk_Deleted → /khkk/list?view=deleted
|
||||
// URL vào từ menu (Layout.tsx — staticMap cho nhóm 1, regex `Khkk_G{n}_*` cho nhóm
|
||||
// 2..8; đây là "chỗ thứ 4" của Pattern 16-bis):
|
||||
// Khkk_List / Khkk_G{n}_List → /khkk/list?group={n}
|
||||
// Khkk_Pending / Khkk_G{n}_Pending → /khkk/list?group={n}&filter=ChoDuyet
|
||||
// Khkk_Approved / Khkk_G{n}_Approved → /khkk/list?group={n}&filter=DaDuyet
|
||||
// Khkk_Deleted / Khkk_G{n}_Deleted → /khkk/list?group={n}&view=deleted
|
||||
// 🔴 Chip lọc chỉ đổi STATE, KHÔNG ghi lại URL — giữ nguyên URL của leaf đang đứng
|
||||
// nên menu không sáng nhầm (bug UAT S155 khi URL leaf trùng URL user tự lọc).
|
||||
//
|
||||
// [K4b S166] 🔴 8 nhóm dùng CHUNG một route ⇒ mọi thứ lấy từ URL phải ĐỌC LẠI MỖI
|
||||
// RENDER, không chụp một lần lúc mount: đi từ leaf nhóm 2 sang leaf nhóm 5 là đổi
|
||||
// query chứ không đổi route, React Router không dựng lại component nên `useState(...)`
|
||||
// khởi tạo một lần sẽ giữ nguyên bộ lọc cũ (trước K4b phải F5 mới đúng — vá-5).
|
||||
// Bộ lọc người dùng bấm được giữ ở lớp override và tự nhả ra khi URL đổi (xem dưới).
|
||||
import { useMemo, useState, type ReactNode } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
@ -29,6 +36,7 @@ import {
|
||||
KHKK_PHASE_BADGE,
|
||||
KHKK_PHASE_LABELS,
|
||||
KhkkPhase,
|
||||
khkkGroupMenuLabel,
|
||||
type KhkkListItemDto,
|
||||
type KhkkPhaseValue,
|
||||
type PagedResult,
|
||||
@ -58,17 +66,50 @@ function formatDate(iso: string): string {
|
||||
export function KhkkListPage() {
|
||||
const navigate = useNavigate()
|
||||
const [searchParams] = useSearchParams()
|
||||
const initialFilter = searchParams.get('filter')
|
||||
const deletedView = searchParams.get('view') === 'deleted'
|
||||
|
||||
const [phase, setPhase] = useState<KhkkPhaseValue | null>(
|
||||
initialFilter ? (FILTER_TO_PHASE[initialFilter] ?? null) : null,
|
||||
)
|
||||
// ---- [K4b S166] 3 trục DANH TÍNH đọc thẳng từ URL, KHÔNG chụp một lần lúc mount ----
|
||||
// Cả 8 nhóm dùng CHUNG một route `/khkk/list`; bấm leaf này sang leaf kia chỉ đổi
|
||||
// query, React Router KHÔNG remount ⇒ giá trị khởi tạo của `useState` không chạy lại.
|
||||
// Đó là lý do trước K4b bấm leaf→leaf phải F5 mới thấy đúng (vá-5).
|
||||
const urlFilter = searchParams.get('filter')
|
||||
const urlPhase: KhkkPhaseValue | null = urlFilter ? (FILTER_TO_PHASE[urlFilter] ?? null) : null
|
||||
const deletedView = searchParams.get('view') === 'deleted'
|
||||
// `?group=` — chỉ nhận 1..8; rác/thiếu ⇒ null = XEM TẤT CẢ NHÓM (không tự đoán nhóm 1,
|
||||
// vì "vắng tham số" và "nhóm 1" là hai ý khác nhau; leaf nhóm 1 nay mang group=1 tường minh).
|
||||
const groupParam = searchParams.get('group')
|
||||
const group = groupParam !== null && /^[1-8]$/.test(groupParam) ? Number(groupParam) : null
|
||||
|
||||
// ---- Bộ lọc do NGƯỜI DÙNG bấm (6 chip KpiCard) đè lên bộ lọc của URL ----
|
||||
// 🔴 3 trạng thái, KHÔNG gộp được: `undefined` = chưa bấm chip ⇒ nghe theo URL;
|
||||
// `null` = user chủ động chọn "Tất cả"; số = user chọn một trạng thái.
|
||||
// Gộp `undefined` với `null` thì chọn "Tất cả" sẽ bị URL kéo ngược về filter cũ.
|
||||
const [chipPhase, setChipPhase] = useState<KhkkPhaseValue | null | undefined>(undefined)
|
||||
const [search, setSearch] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
// Đổi leaf ⇒ trả bộ lọc chip về cho URL và về trang 1 (đang đứng trang 5 của nhóm 2
|
||||
// mà nhảy sang nhóm 5 thì trang 5 gần như chắc chắn rỗng). Điều chỉnh state NGAY
|
||||
// TRONG RENDER theo khuôn React "reset state khi input đổi" — dùng `useEffect` sẽ
|
||||
// vẽ một lượt bằng dữ liệu cũ rồi mới sửa (nháy). `search` CỐ Ý giữ lại: đang tìm
|
||||
// một mã rồi đổi nhóm để dò tiếp là thao tác có thật.
|
||||
const navKey = `${group ?? ''}|${urlFilter ?? ''}|${deletedView ? 'del' : ''}`
|
||||
const [lastNavKey, setLastNavKey] = useState(navKey)
|
||||
if (navKey !== lastNavKey) {
|
||||
setLastNavKey(navKey)
|
||||
setChipPhase(undefined)
|
||||
setPage(1)
|
||||
}
|
||||
const phase: KhkkPhaseValue | null = chipPhase !== undefined ? chipPhase : urlPhase
|
||||
|
||||
const list = useQuery({
|
||||
queryKey: ['khkk-list', { phase, search, page, deletedView }],
|
||||
// 🔴 [vá-3] `group` PHẢI nằm trong khoá cache. Thiếu nó thì 8 leaf "Danh sách" của
|
||||
// 8 nhóm dùng CHUNG một ô cache ⇒ mở nhóm 5 lại thấy dữ liệu nhóm 2 trong khoảnh
|
||||
// khắc trước khi refetch, và tệ hơn là không refetch nếu còn fresh (sai CÂM, không
|
||||
// có lỗi nào nổ). Trục `?filter=` đã nằm sẵn trong khoá dưới dạng ĐÃ GIẢI MÃ là
|
||||
// `phase` (`FILTER_TO_PHASE` đơn ánh, và `phase` cũng chính là thứ gửi lên server)
|
||||
// ⇒ khoá này phủ đủ cả hai trục vá-3 nhắc; thêm `filter` thô nữa chỉ tách đôi ô
|
||||
// cache cho hai request y hệt nhau.
|
||||
queryKey: ['khkk-list', { group, phase, search, page, deletedView }],
|
||||
queryFn: async () => {
|
||||
// Phiếu xóa mềm bị HasQueryFilter che ở list thường — chỉ endpoint riêng
|
||||
// `/deleted` thấy (mirror PE `/purchase-evaluations/deleted`, S155).
|
||||
@ -76,6 +117,11 @@ export function KhkkListPage() {
|
||||
return (await api.get<PagedResult<KhkkListItemDto>>(url, {
|
||||
params: {
|
||||
phase: deletedView ? undefined : (phase ?? undefined),
|
||||
// [K4b S166 — FLAG-1 ĐÃ NỐI CẢ 2 NHÁNH] `/deleted` nay nhận `group` mirror
|
||||
// List (lead vá BE cùng lượt: Controller + `ListDeletedContractSigningPlansQuery`
|
||||
// + handler lọc SAU vế IDOR). Gửi thẳng cho cả hai — lọc là việc server,
|
||||
// total/số-trang vì thế nói thật ở cả màn sống lẫn màn đã-xóa.
|
||||
group: group ?? undefined,
|
||||
search: search.trim() || undefined,
|
||||
page,
|
||||
pageSize: PAGE_SIZE,
|
||||
@ -115,17 +161,29 @@ export function KhkkListPage() {
|
||||
<div className="space-y-5">
|
||||
<PageHeader
|
||||
eyebrow="Giai đoạn 2"
|
||||
title={deletedView ? 'Kế hoạch ký kết HĐ — đã xóa' : 'Kế hoạch ký kết HĐ'}
|
||||
subtitle={
|
||||
title={
|
||||
deletedView
|
||||
? 'Phiếu đã xóa mềm — chỉ xem danh sách'
|
||||
: 'Chốt giá per-NCC trúng thầu + căn cứ hồ sơ trước khi ký hợp đồng'
|
||||
? (group !== null ? `Kế hoạch ký kết HĐ — N${group} · đã xóa` : 'Kế hoạch ký kết HĐ — đã xóa')
|
||||
: group !== null
|
||||
? `Kế hoạch ký kết HĐ — N${group}`
|
||||
: 'Kế hoạch ký kết HĐ'
|
||||
}
|
||||
subtitle={
|
||||
// [K4b S166] Màn "Đã xóa" nay ĐƯỢC in tên nhóm — `/deleted` đã lọc server-side
|
||||
// theo `group` (FLAG-1 nối cùng lượt), tên nhóm hết là lời nói dối.
|
||||
deletedView
|
||||
? (group !== null ? `${khkkGroupMenuLabel(group)} — phiếu đã xóa mềm, chỉ xem` : 'Phiếu đã xóa mềm — chỉ xem danh sách')
|
||||
: group !== null
|
||||
? khkkGroupMenuLabel(group)
|
||||
: 'Chốt giá per-NCC trúng thầu + căn cứ hồ sơ trước khi ký hợp đồng'
|
||||
}
|
||||
icon={deletedView ? <Trash2 className="h-5 w-5" /> : <FileCheck className="h-5 w-5" />}
|
||||
accent="brand"
|
||||
actions={
|
||||
deletedView ? undefined : (
|
||||
<Button onClick={() => navigate('/khkk/create')}>
|
||||
// Mang nhóm đang xem sang màn lập phiếu — không bắt người dùng chọn lại
|
||||
// đúng cái nhóm họ vừa đứng (bắt tay với vá-6 bên `KhkkCreatePage`).
|
||||
<Button onClick={() => navigate(group !== null ? `/khkk/create?group=${group}` : '/khkk/create')}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Lập kế hoạch
|
||||
</Button>
|
||||
@ -151,7 +209,9 @@ export function KhkkListPage() {
|
||||
accent={c.accent}
|
||||
active={phase === c.value}
|
||||
onClick={() => {
|
||||
setPhase(c.value)
|
||||
// Ghi vào lớp override, KHÔNG ghi vào URL — giữ URL của leaf đang đứng
|
||||
// để sidebar không sáng nhầm mục khác (bài S155, xem ghi chú đầu file).
|
||||
setChipPhase(c.value)
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user