[CLAUDE] FE-User: YC-029 W2 — hoán đổi cây toàn trình sang trái + Select loại + list phải (mirror cặp pipeline ×2 app)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -108,7 +108,10 @@ export function PipelineTreePanel({
|
||||
// NGUYÊN hành vi cũ (8 ngăn nhóm + navigate) ở trang Duyệt NCC / GĐ3 / GĐ4.
|
||||
// [S175 · O6] +`onOpenPe`: leaf GĐ1 (phiếu Duyệt NCC) cũng cho host bắt tại chỗ, đối
|
||||
// xứng với `onOpenKhkk` của leaf GĐ2. Cả 2 đều OPT-IN nên 4 host không truyền giữ nguyên.
|
||||
stage2Options?: Pick<BuildStagesOptions, 'stage2Content' | 'stage2Count' | 'onOpenKhkk' | 'onOpenPe'>
|
||||
// [S190 · W2 YC-029] +`onOpenContract` (leaf GĐ3 — khép kín bộ ba GĐ1/GĐ2/GĐ3) và
|
||||
// +`stage3TypeFilter` (lọc leaf GĐ3 theo loại HĐ, nguồn = Select loại ở host `my-contracts`).
|
||||
// GIỮ TÊN prop `stage2Options` dù nay mang cả tuỳ-chọn GĐ3: đổi tên = chạm 7 host, ngoài wave.
|
||||
stage2Options?: Pick<BuildStagesOptions, 'stage2Content' | 'stage2Count' | 'onOpenKhkk' | 'onOpenPe' | 'onOpenContract' | 'stage3TypeFilter'>
|
||||
// [S168 GĐ3-D gate F-1] Host 3-panel (Panel-3 TỰ CUỘN) truyền class trần riêng cho THÂN
|
||||
// cây (vd `lg:max-h-none` — panel giữ đúng 1 thanh cuộn; max-h mặc định 100vh−16rem là
|
||||
// hiệu-chỉnh cho trang giấy-cuộn, vượt chỗ Panel-3 /inbox ~26px ⇒ 2 cuộn lồng nhau).
|
||||
|
||||
@ -190,6 +190,22 @@ export type BuildStagesOptions = {
|
||||
* `navigate('/purchase-evaluations?id=…&expand=1')` ⇒ 5 host còn lại 0 đổi hành vi.
|
||||
*/
|
||||
onOpenPe?: (id: string) => void
|
||||
/**
|
||||
* [S190 · W2 YC-029] Bộ ba khép kín với `onOpenKhkk` (GĐ2) / `onOpenPe` (GĐ1): leaf GĐ3 (HĐ)
|
||||
* cho HOST bắt tại chỗ. Màn `my-contracts` sau hoán-đổi đặt cây ở panel-1 TRÁI ⇒ bấm leaf phải
|
||||
* chọn HĐ vào panel-2 (URL ở lại `/my-contracts`), không rời trang.
|
||||
* không truyền ⇒ giữ nguyên `navigate('/contracts/:id')` — 6 host còn lại (kể cả fe-admin
|
||||
* `ContractsListPage.tsx:265`) 0 đổi hành vi.
|
||||
*/
|
||||
onOpenContract?: (id: string) => void
|
||||
/**
|
||||
* [S190 · W2 YC-029] Lọc leaf GĐ3 theo `ContractType` (1..7) — nguồn là thanh `Select` loại HĐ
|
||||
* ở đầu panel cây `my-contracts` (Q2 nhánh-đã-chọn).
|
||||
* không truyền ⇒ giữ nguyên cây đủ MỌI loại HĐ — 6 host còn lại 0 đổi hành vi.
|
||||
* 🔴 HĐ nằm NGOÀI cửa sổ index 200 KHÔNG suy được loại ⇒ vẫn hiện dù filter bật; lý-do ghi tại
|
||||
* chỗ dùng (`passStage3Type`).
|
||||
*/
|
||||
stage3TypeFilter?: number
|
||||
}
|
||||
|
||||
/**
|
||||
@ -365,7 +381,13 @@ export function usePipelineStages() {
|
||||
peItems.find(p => p.contractId === contractId)?.tenGoiThau
|
||||
?? khkkItems.find(k => khkkPlanContractIds(k).includes(contractId))?.peTenGoiThau
|
||||
?? null
|
||||
if (linkage.canContract) {
|
||||
// [S190 W2] Vế lọc loại HĐ — CHỈ chạy khi host bật `stage3TypeFilter` (opt-in).
|
||||
// 🔴 QUYẾT ĐỊNH "THÀ THỪA CÒN HƠN THIẾU": `c` vắng = HĐ ngoài cửa sổ index 200 (xem
|
||||
// `primary` ngay dưới) ⇒ KHÔNG có `c.type` để phân loại. Ẩn nó đi khi filter bật là làm
|
||||
// HĐ biến mất IM LẶNG — đúng cái bệnh F-3 S162 đã vá bằng nhãn "(ngoài chỉ mục 200)".
|
||||
// Vậy nên `!c` được cho QUA: người dùng thấy thừa 1 leaf còn hơn mất trắng 1 HĐ.
|
||||
const passStage3Type = !opts.stage3TypeFilter || !c || c.type === opts.stage3TypeFilter
|
||||
if (linkage.canContract && passStage3Type) {
|
||||
stage3Leaves.push({
|
||||
id: contractId,
|
||||
// [F-3 S162] c vắng = HĐ NGOÀI cửa sổ index 200 — đừng dán nhãn
|
||||
@ -374,7 +396,11 @@ export function usePipelineStages() {
|
||||
secondary: c?.tenHopDong ?? fallbackName,
|
||||
badgeLabel: c ? ContractPhaseLabel[c.phase] : null,
|
||||
badgeClass: c ? ContractPhaseColor[c.phase] : null,
|
||||
onOpen: () => navigate(`/contracts/${contractId}`),
|
||||
// [S190 W2] Host truyền callback ⇒ chọn tại chỗ; không truyền ⇒ navigate như cũ.
|
||||
// Ternary y khuôn leaf GĐ1 `:228-230` / GĐ2 `:244` — 3 giai-đoạn cùng một luật.
|
||||
onOpen: opts.onOpenContract
|
||||
? () => opts.onOpenContract!(contractId)
|
||||
: () => navigate(`/contracts/${contractId}`),
|
||||
})
|
||||
}
|
||||
if (linkage.canHardCopy && c?.hasSealedCopy) {
|
||||
|
||||
@ -108,7 +108,10 @@ export function PipelineTreePanel({
|
||||
// NGUYÊN hành vi cũ (8 ngăn nhóm + navigate) ở trang Duyệt NCC / GĐ3 / GĐ4.
|
||||
// [S175 · O6] +`onOpenPe`: leaf GĐ1 (phiếu Duyệt NCC) cũng cho host bắt tại chỗ, đối
|
||||
// xứng với `onOpenKhkk` của leaf GĐ2. Cả 2 đều OPT-IN nên 4 host không truyền giữ nguyên.
|
||||
stage2Options?: Pick<BuildStagesOptions, 'stage2Content' | 'stage2Count' | 'onOpenKhkk' | 'onOpenPe'>
|
||||
// [S190 · W2 YC-029] +`onOpenContract` (leaf GĐ3 — khép kín bộ ba GĐ1/GĐ2/GĐ3) và
|
||||
// +`stage3TypeFilter` (lọc leaf GĐ3 theo loại HĐ, nguồn = Select loại ở host `my-contracts`).
|
||||
// GIỮ TÊN prop `stage2Options` dù nay mang cả tuỳ-chọn GĐ3: đổi tên = chạm 7 host, ngoài wave.
|
||||
stage2Options?: Pick<BuildStagesOptions, 'stage2Content' | 'stage2Count' | 'onOpenKhkk' | 'onOpenPe' | 'onOpenContract' | 'stage3TypeFilter'>
|
||||
// [S168 GĐ3-D gate F-1] Host 3-panel (Panel-3 TỰ CUỘN) truyền class trần riêng cho THÂN
|
||||
// cây (vd `lg:max-h-none` — panel giữ đúng 1 thanh cuộn; max-h mặc định 100vh−16rem là
|
||||
// hiệu-chỉnh cho trang giấy-cuộn, vượt chỗ Panel-3 /inbox ~26px ⇒ 2 cuộn lồng nhau).
|
||||
|
||||
@ -190,6 +190,22 @@ export type BuildStagesOptions = {
|
||||
* `navigate('/purchase-evaluations?id=…&expand=1')` ⇒ 5 host còn lại 0 đổi hành vi.
|
||||
*/
|
||||
onOpenPe?: (id: string) => void
|
||||
/**
|
||||
* [S190 · W2 YC-029] Bộ ba khép kín với `onOpenKhkk` (GĐ2) / `onOpenPe` (GĐ1): leaf GĐ3 (HĐ)
|
||||
* cho HOST bắt tại chỗ. Màn `my-contracts` sau hoán-đổi đặt cây ở panel-1 TRÁI ⇒ bấm leaf phải
|
||||
* chọn HĐ vào panel-2 (URL ở lại `/my-contracts`), không rời trang.
|
||||
* không truyền ⇒ giữ nguyên `navigate('/contracts/:id')` — 6 host còn lại (kể cả fe-admin
|
||||
* `ContractsListPage.tsx:265`) 0 đổi hành vi.
|
||||
*/
|
||||
onOpenContract?: (id: string) => void
|
||||
/**
|
||||
* [S190 · W2 YC-029] Lọc leaf GĐ3 theo `ContractType` (1..7) — nguồn là thanh `Select` loại HĐ
|
||||
* ở đầu panel cây `my-contracts` (Q2 nhánh-đã-chọn).
|
||||
* không truyền ⇒ giữ nguyên cây đủ MỌI loại HĐ — 6 host còn lại 0 đổi hành vi.
|
||||
* 🔴 HĐ nằm NGOÀI cửa sổ index 200 KHÔNG suy được loại ⇒ vẫn hiện dù filter bật; lý-do ghi tại
|
||||
* chỗ dùng (`passStage3Type`).
|
||||
*/
|
||||
stage3TypeFilter?: number
|
||||
}
|
||||
|
||||
/**
|
||||
@ -365,7 +381,13 @@ export function usePipelineStages() {
|
||||
peItems.find(p => p.contractId === contractId)?.tenGoiThau
|
||||
?? khkkItems.find(k => khkkPlanContractIds(k).includes(contractId))?.peTenGoiThau
|
||||
?? null
|
||||
if (linkage.canContract) {
|
||||
// [S190 W2] Vế lọc loại HĐ — CHỈ chạy khi host bật `stage3TypeFilter` (opt-in).
|
||||
// 🔴 QUYẾT ĐỊNH "THÀ THỪA CÒN HƠN THIẾU": `c` vắng = HĐ ngoài cửa sổ index 200 (xem
|
||||
// `primary` ngay dưới) ⇒ KHÔNG có `c.type` để phân loại. Ẩn nó đi khi filter bật là làm
|
||||
// HĐ biến mất IM LẶNG — đúng cái bệnh F-3 S162 đã vá bằng nhãn "(ngoài chỉ mục 200)".
|
||||
// Vậy nên `!c` được cho QUA: người dùng thấy thừa 1 leaf còn hơn mất trắng 1 HĐ.
|
||||
const passStage3Type = !opts.stage3TypeFilter || !c || c.type === opts.stage3TypeFilter
|
||||
if (linkage.canContract && passStage3Type) {
|
||||
stage3Leaves.push({
|
||||
id: contractId,
|
||||
// [F-3 S162] c vắng = HĐ NGOÀI cửa sổ index 200 — đừng dán nhãn
|
||||
@ -374,7 +396,11 @@ export function usePipelineStages() {
|
||||
secondary: c?.tenHopDong ?? fallbackName,
|
||||
badgeLabel: c ? ContractPhaseLabel[c.phase] : null,
|
||||
badgeClass: c ? ContractPhaseColor[c.phase] : null,
|
||||
onOpen: () => navigate(`/contracts/${contractId}`),
|
||||
// [S190 W2] Host truyền callback ⇒ chọn tại chỗ; không truyền ⇒ navigate như cũ.
|
||||
// Ternary y khuôn leaf GĐ1 `:228-230` / GĐ2 `:244` — 3 giai-đoạn cùng một luật.
|
||||
onOpen: opts.onOpenContract
|
||||
? () => opts.onOpenContract!(contractId)
|
||||
: () => navigate(`/contracts/${contractId}`),
|
||||
})
|
||||
}
|
||||
if (linkage.canHardCopy && c?.hasSealedCopy) {
|
||||
|
||||
@ -1,10 +1,21 @@
|
||||
// 3-panel "Danh sách HĐ" — Panel 1 (list compact, click chọn) | Panel 2
|
||||
// (detail content) | Panel 3 (workflow + lịch sử duyệt). Selected HĐ giữ qua
|
||||
// URL `?id=` để bookmarkable + back/forward navigation work.
|
||||
// 3-panel "Danh sách HĐ" — [S190 · W2 YC-029 Đ1] HOÁN ĐỔI 2 khung theo ảnh anh gửi
|
||||
// ("Bỏ phần này sang bên này nhé" — mũi tên từ cây PHẢI chỉ sang danh-sách TRÁI):
|
||||
// Panel 1 TRÁI 400px = CÂY TOÀN TRÌNH (GĐ3) + Select loại HĐ ← trước ở Panel 3
|
||||
// Panel 2 GIỮA = detail content + quy trình/lịch sử duyệt của HĐ đang chọn
|
||||
// Panel 3 PHẢI 360px = DANH SÁCH HĐ (search + empty-state) ← trước ở Panel 1
|
||||
// Selected HĐ vẫn giữ qua URL `?id=` để bookmarkable + back/forward navigation work.
|
||||
//
|
||||
// Mobile fallback (< lg): hiển thị Panel 1 list, click row → fullpage
|
||||
// /contracts/:id (giữ flow cũ, không cố nhồi 3 panel vào màn hình hẹp).
|
||||
import { useMemo } from 'react'
|
||||
// 🔴 CHỈ ĐỔI CHỖ, KHÔNG XOÁ KHỐI NÀO (Q1):
|
||||
// • Danh-sách phải ở lại: cây chỉ dựng từ phiếu Duyệt NCC ⇒ HĐ tạo TRỰC-TIẾP ngoài gói thầu
|
||||
// KHÔNG bao giờ hiện trên cây, danh-sách là đường DUY NHẤT tới nó.
|
||||
// • `WorkflowHistoryPanel` trước ở Panel 3 (chỗ nay là danh-sách) ⇒ dời xuống NGAY DƯỚI detail
|
||||
// ở Panel 2 chứ không bỏ. Delta này plan W2 KHÔNG nêu (plan chỉ nói "panel-2 giữ detail") —
|
||||
// khai ở đây để người sau khỏi tưởng ai đó lỡ tay đánh rơi.
|
||||
//
|
||||
// Mobile (< lg): GIỮ NGUYÊN 100% hiện trạng — chỉ danh-sách hiện, click row → fullpage
|
||||
// /contracts/:id. Cây là khối DESKTOP-ONLY y như TRƯỚC hoán-đổi (nó vốn nằm trong panel
|
||||
// `hidden lg:block`), nên đợt này không đổi một pixel nào ở < lg.
|
||||
import { useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { FileText, Plus, Search } from 'lucide-react'
|
||||
@ -16,6 +27,7 @@ import { SlaTimer } from '@/components/SlaTimer'
|
||||
import { EmptyState } from '@/components/EmptyState'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { Select } from '@/components/ui/Select'
|
||||
import { api } from '@/lib/api'
|
||||
import { cn } from '@/lib/cn'
|
||||
import type { Paged } from '@/types/master'
|
||||
@ -41,10 +53,27 @@ export function MyContractsPage() {
|
||||
const phaseFilter = searchParams.get('phase') ? Number(searchParams.get('phase')) : null
|
||||
const showDeleted = searchParams.get('deleted') === '1'
|
||||
|
||||
// [S190 W2 · Q2 nhánh-đã-chọn] Select loại HĐ ở đầu panel CÂY. Mặc định = `?type=N` của URL
|
||||
// (menu loại HĐ đang đứng). 🔴 Đổi Select CHỈ đổi bộ lọc CÂY — KHÔNG navigate, KHÔNG đụng
|
||||
// danh-sách panel-3 (danh-sách vẫn bám `?type=` của menu, đúng danh-tính leaf W1).
|
||||
//
|
||||
// 🔴 SYNC BẰNG PHÉP DẪN XUẤT, KHÔNG BẰNG `useEffect`: override được ĐÓNG DẤU loại nó thuộc về
|
||||
// (`forType`). URL đổi loại (bấm sang "02. HĐ Giao khoán") ⇒ dấu không khớp nữa ⇒ tự rơi về
|
||||
// mặc định MỚI, không cần đồng bộ tay. Bản đầu viết `useEffect(() => setTreeType(typeFilter))`
|
||||
// và ăn ĐÚNG 1 lỗi `react-hooks/set-state-in-effect` (baseline eslint 47 → 48) — dẫn xuất vừa
|
||||
// hết lỗi vừa hết cả một nhịp render thừa.
|
||||
const [treeTypeOverride, setTreeTypeOverride] =
|
||||
useState<{ forType: number | null; value: number | null } | null>(null)
|
||||
const treeType = treeTypeOverride !== null && treeTypeOverride.forType === typeFilter
|
||||
? treeTypeOverride.value
|
||||
: typeFilter
|
||||
|
||||
const list = useQuery({
|
||||
// `typeFilter` ĐÃ nằm trong key từ trước ⇒ đổi sang lọc server KHÔNG cần thêm trục mới;
|
||||
// cache cũ (payload chưa lọc) tự hết hiệu lực vì mỗi `type` là 1 ô key riêng sẵn rồi.
|
||||
queryKey: ['my-contracts', typeFilter, phaseFilter, showDeleted],
|
||||
// [S190 W2] +trục `search`: ô tìm nay đi SERVER-SIDE (xem `params.search`) ⇒ mỗi từ khoá
|
||||
// là một payload khác nhau, thiếu trục này là ăn cache của từ khoá trước.
|
||||
queryKey: ['my-contracts', typeFilter, phaseFilter, showDeleted, search],
|
||||
queryFn: async () =>
|
||||
(
|
||||
await api.get<Paged<ContractListItem>>(showDeleted ? '/contracts/deleted' : '/contracts', {
|
||||
@ -58,6 +87,13 @@ export function MyContractsPage() {
|
||||
// BE lọc TRƯỚC phân trang (`ContractFeatures.cs:325` trước `Skip/Take:340`).
|
||||
// `?? undefined` ⇒ axios bỏ hẳn key, giữ nguyên hành vi "không chọn loại".
|
||||
type: typeFilter ?? undefined,
|
||||
// [S190 W2] 🔴 ĐỔI HÀNH-VI CÓ CHỦ ĐÍCH: ô tìm chuyển client-side → SERVER-SIDE.
|
||||
// Param `search` sẵn ở CẢ 2 endpoint (`ContractsController.cs:19` và `:36`).
|
||||
// Vì sao đổi: lọc client chỉ soi trong 100 dòng đã tải ⇒ user nhiều HĐ gõ đúng mã
|
||||
// vẫn ra rỗng (cùng bệnh `type` đã vá @W7 S187).
|
||||
// Diện tìm RỘNG HƠN bản cũ (`ContractFeatures.cs:358-364`): mã · tên HĐ · tên NCC
|
||||
// · +tên DỰ ÁN (vế client cũ không có) — rộng hơn nên không mất kết quả nào.
|
||||
search: search.trim() || undefined,
|
||||
...(!showDeleted && phaseFilter != null ? { phase: phaseFilter } : {}),
|
||||
},
|
||||
})
|
||||
@ -70,30 +106,19 @@ export function MyContractsPage() {
|
||||
enabled: !!selectedId,
|
||||
})
|
||||
|
||||
const rows = useMemo(() => {
|
||||
// [W7 S187] Vế `items.filter(c => c.type === typeFilter)` ĐÃ GỠ — BE lọc rồi. Giữ lại
|
||||
// = lọc 2 lần, và che mất lỗi nếu server trả sai loại (sai sẽ THẦM LẶNG). `search` vẫn
|
||||
// client-side CÓ CHỦ ĐÍCH: ô tìm không đổi request (payload 100 dòng đã tải trọn).
|
||||
let items = list.data?.items ?? []
|
||||
if (search.trim()) {
|
||||
const q = search.toLowerCase()
|
||||
items = items.filter(c =>
|
||||
(c.maHopDong ?? '').toLowerCase().includes(q) ||
|
||||
(c.tenHopDong ?? '').toLowerCase().includes(q) ||
|
||||
(c.supplierName ?? '').toLowerCase().includes(q),
|
||||
)
|
||||
}
|
||||
return items
|
||||
// `typeFilter` gỡ khỏi deps CÙNG LÚC với vế lọc (không còn đọc trong thân hàm).
|
||||
}, [list.data, search])
|
||||
// [W7 S187] Vế `items.filter(c => c.type === typeFilter)` ĐÃ GỠ — BE lọc rồi.
|
||||
// [S190 W2] Vế lọc `search` client-side cũng GỠ NỐT theo lý-do y hệt: giữ lại = lọc 2 lần và
|
||||
// che mất lỗi nếu server trả sai (sai sẽ THẦM LẶNG). Không còn phép biến đổi nào ⇒ bỏ luôn
|
||||
// `useMemo` (memo hoá một phép gán là chi phí thừa).
|
||||
const rows = list.data?.items ?? []
|
||||
|
||||
function selectContract(id: string) {
|
||||
// [S159-đợt5 FLAG-2 reviewer] Màn "Đã xóa" = CHỈ XEM DANH SÁCH (khuôn PE S155
|
||||
// `disabled={deletedView}`): detail endpoint KHÔNG IgnoreQueryFilters ⇒ HĐ đã xóa
|
||||
// 404 ⇒ desktop = 2 panel trắng, mobile = navigate sang trang 404. Chặn tại gốc.
|
||||
if (showDeleted) return
|
||||
// Desktop ≥ lg: cập nhật URL để render Panel 2/3 cạnh List.
|
||||
// Mobile: Panel 2/3 hidden → navigate fullpage /contracts/:id (UX khả dụng).
|
||||
// Desktop ≥ lg: cập nhật URL để render Panel 2 cạnh cây/danh-sách.
|
||||
// Mobile: Panel 2 hidden → navigate fullpage /contracts/:id (UX khả dụng).
|
||||
if (typeof window !== 'undefined' && window.matchMedia('(min-width: 1024px)').matches) {
|
||||
const next = new URLSearchParams(searchParams)
|
||||
next.set('id', id)
|
||||
@ -149,10 +174,74 @@ export function MyContractsPage() {
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* 3-panel grid — flex-1 để fill phần còn lại của viewport */}
|
||||
<div className="grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[320px_1fr_360px]">
|
||||
{/* Panel 1 — List */}
|
||||
<aside className="flex flex-col overflow-hidden border-r border-slate-200 bg-white">
|
||||
{/* 3-panel grid — flex-1 để fill phần còn lại của viewport.
|
||||
[S190 W2] 320px → 400px cho cột cây (khuôn host KHKK `KhkkListPage.tsx:171`). */}
|
||||
<div className="grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[400px_1fr_360px]">
|
||||
{/* Panel 1 — CÂY TOÀN TRÌNH (đến từ Panel 3 cũ) + Select loại HĐ.
|
||||
`hidden … lg:flex` = GIỮ NGUYÊN diện hiển thị cũ của cây (nó vốn ở panel
|
||||
`hidden lg:block`) ⇒ < lg không đổi gì, và bẫy "`hideHeader` nuốt nút Hiện/Ẩn cây
|
||||
ở mobile" KHÔNG chạm host này vì nút đó chỉ sống ở < lg. */}
|
||||
<aside className="hidden flex-col overflow-hidden border-r border-slate-200 bg-white lg:flex">
|
||||
<div className="border-b border-slate-200 p-3">
|
||||
<Select
|
||||
value={treeType ?? ''}
|
||||
aria-label="Lọc loại HĐ trên cây toàn trình"
|
||||
onChange={e => setTreeTypeOverride({
|
||||
forType: typeFilter,
|
||||
value: e.target.value === '' ? null : Number(e.target.value),
|
||||
})}
|
||||
>
|
||||
{/* Ca URL trần (`/my-contracts` không `?type=`) phải có lựa chọn hợp lệ. */}
|
||||
<option value="">Tất cả loại HĐ</option>
|
||||
{Object.entries(ContractTypeLabel).map(([value, label]) => (
|
||||
<option key={value} value={value}>{label}</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
{/* [X-3 / SF-26 — bài S168 F-1] Cột này TỰ CUỘN ⇒ thân cây PHẢI bỏ trần `max-h` mặc
|
||||
định của panel, nếu không là 2 thanh cuộn lồng nhau. */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<PipelineTreePanel
|
||||
currentStage={3}
|
||||
hideHeader
|
||||
hideSearch
|
||||
bodyMaxHClass="lg:max-h-none"
|
||||
stage2Options={{
|
||||
// Bấm leaf HĐ ⇒ chọn vào Panel 2 qua `?id=`; URL Ở LẠI `/my-contracts`.
|
||||
onOpenContract: selectContract,
|
||||
// `null` ⇒ `undefined` ⇒ hook coi như KHÔNG lọc (đủ mọi loại).
|
||||
stage3TypeFilter: treeType ?? undefined,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Panel 2 — Detail content + quy trình/lịch sử duyệt */}
|
||||
<main className="hidden overflow-y-auto bg-slate-50 p-6 lg:block">
|
||||
{!selectedId && (
|
||||
<EmptyState
|
||||
icon={FileText}
|
||||
title="Chọn 1 HĐ ở cây bên trái hoặc danh sách bên phải"
|
||||
description="Chi tiết HĐ + thông tin / góp ý / file đính kèm sẽ hiển thị ở đây."
|
||||
/>
|
||||
)}
|
||||
{selectedId && detail.isLoading && (
|
||||
<div className="text-sm text-slate-500">Đang tải HĐ…</div>
|
||||
)}
|
||||
{selectedId && detail.data && (
|
||||
<div className="space-y-6">
|
||||
<ContractDetailContent contract={detail.data} onBack={clearSelection} />
|
||||
{/* [S190 W2] Khối "workflow + lịch sử duyệt" TRƯỚC ở Panel 3; Panel 3 nay là
|
||||
danh-sách nên nó về đứng ngay dưới detail của chính HĐ đó — KHÔNG xoá. */}
|
||||
<WorkflowHistoryPanel contract={detail.data} />
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* Panel 3 — DANH SÁCH HĐ (đến từ Panel 1 cũ, nguyên chức năng: search + empty-state +
|
||||
các lọc `?type=` / `?phase=9` / `?deleted=1` hiện có). Hiện ở MỌI bề rộng y như
|
||||
trước hoán-đổi ⇒ mobile không mất gì. */}
|
||||
<aside className="flex flex-col overflow-hidden border-l border-slate-200 bg-white">
|
||||
<div className="border-b border-slate-200 p-3">
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-2.5 top-2.5 h-4 w-4 text-slate-400" />
|
||||
@ -214,34 +303,6 @@ export function MyContractsPage() {
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Panel 2 — Detail content */}
|
||||
<main className="hidden overflow-y-auto bg-slate-50 p-6 lg:block">
|
||||
{!selectedId && (
|
||||
<EmptyState
|
||||
icon={FileText}
|
||||
title="Chọn 1 HĐ ở danh sách bên trái"
|
||||
description="Chi tiết HĐ + thông tin / góp ý / file đính kèm sẽ hiển thị ở đây."
|
||||
/>
|
||||
)}
|
||||
{selectedId && detail.isLoading && (
|
||||
<div className="text-sm text-slate-500">Đang tải HĐ…</div>
|
||||
)}
|
||||
{selectedId && detail.data && (
|
||||
<ContractDetailContent contract={detail.data} onBack={clearSelection} />
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* Panel 3 — Workflow + history */}
|
||||
<aside className="hidden overflow-y-auto border-l border-slate-200 bg-white p-4 lg:block">
|
||||
{/* [S168 GĐ3-D — owner slot (63) chốt D] Chưa chọn HĐ ⇒ cây toàn trình (GĐ3)
|
||||
thế chỗ ô trống; chọn HĐ thì nhường chỗ cho quy trình duyệt. Trade-off D
|
||||
(cây rời panel khi chọn HĐ) — sub-implfe-gd3.md §1. */}
|
||||
{/* [gate F-1/F-2] Panel-3 TỰ CUỘN ⇒ thân cây `lg:max-h-none` giữ đúng 1 thanh cuộn;
|
||||
sticky/self-start của khuôn vô hiệu ở host display:block này — by-design. */}
|
||||
{!selectedId && <PipelineTreePanel currentStage={3} bodyMaxHClass="max-h-[28rem] lg:max-h-none" />}
|
||||
{selectedId && detail.data && <WorkflowHistoryPanel contract={detail.data} />}
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user