From fa6654b8f45da59ff364b00296cf14c7fc6945fa Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Fri, 19 Jun 2026 14:57:10 +0700 Subject: [PATCH] [CLAUDE] FE: PE tach chon-phieu (inline 3-panel nhu cu) khoi mo-rong (overlay) + nut Xem mo rong moi dong anh (annotate 2026-06-19): bam dong phieu = xem thuong inline 3-panel NHU CU; them nut 'Xem mo rong' (Maximize2) goc moi dong -> overlay full-man. Param ?expand=1 tach khoi ?id: click row=select (wide: inline 3-panel / narrow tranh double-mount PeDetailTabs/PeWorkflowPanel phia sau overlay (intent comment co nhung code sot). Giu tree 4 tang + PeUrgentChips + overlay a11y (slide/Esc/focus-trap). FE-only, 2 app SHA256-identical. Build PASS x2. Co-Authored-By: Claude Opus 4.8 --- .../pages/pe/PurchaseEvaluationsListPage.tsx | 254 ++++++++++++------ .../pages/pe/PurchaseEvaluationsListPage.tsx | 254 ++++++++++++------ 2 files changed, 336 insertions(+), 172 deletions(-) diff --git a/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx b/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx index c6321a5..7fecabd 100644 --- a/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx +++ b/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx @@ -1,5 +1,5 @@ // List + Detail phiếu Duyệt NCC — 3-panel: List | Detail tabs | Workflow + history. -// URL params: type (filter A/B), pendingMe (1=inbox), id (selected), q (search). +// URL params: type (filter A/B), pendingMe (1=inbox), id (selected), q (search), expand (1=overlay). // Plan AG Phase 1 (S26 2026-05-21) — Tree view 2-level Project > Gói thầu > PE // UAT feedback bro Tra Sol: flat list "đám rừng" → Outlook folder structure. // FE-only group view (no schema change) — Phase 2 ProjectPackage defer sau UAT confirm. @@ -7,7 +7,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useNavigate, useSearchParams } from 'react-router-dom' import { toast } from 'sonner' -import { ArrowLeft, ClipboardCheck, Search, X } from 'lucide-react' +import { ArrowLeft, ClipboardCheck, Maximize2, Search, X } from 'lucide-react' import { Input } from '@/components/ui/Input' import { Select } from '@/components/ui/Select' import { EmptyState } from '@/components/EmptyState' @@ -38,6 +38,7 @@ export function PurchaseEvaluationsListPage() { const phase = sp.get('phase') ?? '' const approvalWorkflowId = sp.get('awId') ?? '' // Mig 23 — filter quy trình const selectedId = sp.get('id') + const isExpand = sp.get('expand') === '1' // S79 — overlay mở rộng // Mig 23 — list quy trình duyệt V2 cho dropdown filter (filter theo type screen) const approvalWorkflows = useQuery({ @@ -88,7 +89,12 @@ export function PurchaseEvaluationsListPage() { mutationFn: async (id: string) => api.delete(`/purchase-evaluations/${id}`), onSuccess: () => { toast.success('Đã xóa phiếu.') - setParam('id', null) + // Xoá phiếu → clear cả id + expand (về danh sách). + const next = new URLSearchParams(sp) + next.delete('id') + next.delete('expand') + next.delete('page') + setSp(next) qc.invalidateQueries({ queryKey: ['pe-list'] }) }, onError: e => toast.error(getErrorMessage(e)), @@ -102,12 +108,47 @@ export function PurchaseEvaluationsListPage() { setSp(next, { replace: key === 'q' }) }, [sp, setSp]) - // S78 — focus mode: chọn phiếu → mở overlay full-bleed (che menu + list). - // KHÔNG còn route /purchase-evaluations/:id riêng cho mobile — overlay phục vụ - // CẢ desktop (phiếu rộng + panel duyệt cạnh) lẫn mobile (stack dọc). 1 code path. - const selectRow = useCallback((id: string) => setParam('id', id), [setParam]) - // Đóng focus = clear ?id → quay lại danh sách. Duyệt xong cũng gọi closeFocus. - const closeFocus = useCallback(() => setParam('id', null), [setParam]) + // S79 — decouple "chọn" khỏi "mở rộng" (anh chốt 2026-06-19, annotated screenshot): + // • Bấm dòng phiếu = chọn → detail INLINE 3-panel "như cũ" (8e68ed1). Trên màn + // hẹp ( typeof window !== 'undefined' && window.innerWidth >= LG_BREAKPOINT + + // Chọn dòng: set id; clear expand. { + const next = new URLSearchParams(sp) + next.set('id', id) + if (isWideViewport()) next.delete('expand') + else next.set('expand', '1') + setSp(next) + }, [sp, setSp]) + + // "Xem mở rộng" trên 1 dòng → mở overlay cho phiếu đó (id + expand=1). + const expandRow = useCallback((id: string) => { + const next = new URLSearchParams(sp) + next.set('id', id) + next.set('expand', '1') + setSp(next) + }, [sp, setSp]) + + // Thu gọn overlay → bỏ expand, GIỮ id (quay lại inline detail, KHÔNG về list). + const collapseFocus = useCallback(() => setParam('expand', null), [setParam]) + + // Đóng inline detail (nút ← Đóng / xoá phiếu) → clear id + expand → về danh sách. + const closeDetail = useCallback(() => { + const next = new URLSearchParams(sp) + next.delete('id') + next.delete('expand') + next.delete('page') + setSp(next) + }, [sp, setSp]) + + // Duyệt xong (onApproved, không phải Trả lại/Từ chối) → phiếu rời inbox → + // đóng hẳn overlay + clear selection (về danh sách). + const onApproved = useCallback(() => closeDetail(), [closeDetail]) const allRows = list.data?.items ?? [] // Duyệt (pendingMe) → filter cứng client-side chỉ "Đã gửi duyệt" (Nháp/Trả lại/ @@ -208,13 +249,11 @@ export function PurchaseEvaluationsListPage() { ? (pendingMe ? `${PurchaseEvaluationTypeLabel[typeFilter]} — Chờ duyệt` : PurchaseEvaluationTypeLabel[typeFilter]) : pendingMe ? 'Duyệt NCC — Chờ tôi' : 'Quy trình Duyệt NCC' - // ─── S78 Focus-mode overlay (anh Kiệt FDC + anh chốt 2026-06-18) ─────────── - // Phiếu chính bị nhỏ trong panel giữa 1fr (~600px) → chọn phiếu mở overlay - // fixed inset-0 z-50 trượt từ phải, CHE hẳn menu trái + list rail. Phiếu full - // width (scroll riêng) + panel duyệt cạnh phải (desktop) / stack dưới (mobile). - // Mount-then-animate: `id` set → mount overlay → next frame bật `focusVisible` - // → translate-x-0 (trượt vào). Đóng → tắt visible → chờ transition → unmount. - const hasSelection = !!selectedId + // ─── S79 Focus-mode overlay (anh chốt 2026-06-19) ───────────────────────── + // Overlay full-bleed trượt từ phải CHE menu trái + TopBar + list + inline + // detail. Chỉ mở khi `expand=1`. Thu gọn → bỏ expand (về inline detail). + // Mount-then-animate: expand bật → mount overlay → next frame → translate-x-0. + const overlayActive = !!selectedId && isExpand const [focusMounted, setFocusMounted] = useState(false) const [focusVisible, setFocusVisible] = useState(false) const overlayRef = useRef(null) @@ -224,7 +263,7 @@ export function PurchaseEvaluationsListPage() { ) useEffect(() => { - if (hasSelection) { + if (overlayActive) { setFocusMounted(true) // 2 rAF để browser commit translate-x-full TRƯỚC khi đổi → translate-x-0 // (1 rAF đôi khi bị batch chung frame → không thấy slide). reduced-motion @@ -251,7 +290,7 @@ export function PurchaseEvaluationsListPage() { return () => clearTimeout(t) } } - }, [hasSelection]) + }, [overlayActive]) // Khoá scroll body khi overlay mở (tránh double-scroll nền). Focus nút Thu gọn // khi mở (a11y — đưa focus vào overlay). @@ -266,13 +305,13 @@ export function PurchaseEvaluationsListPage() { } }, [focusMounted]) - // Esc đóng + focus-trap (Tab vòng trong overlay) — a11y dialog floor (FD5). + // Esc thu gọn + focus-trap (Tab vòng trong overlay) — a11y dialog floor (FD5). useEffect(() => { if (!focusMounted) return const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault() - closeFocus() + collapseFocus() return } if (e.key === 'Tab' && overlayRef.current) { @@ -293,7 +332,7 @@ export function PurchaseEvaluationsListPage() { } document.addEventListener('keydown', onKey) return () => document.removeEventListener('keydown', onKey) - }, [focusMounted, closeFocus]) + }, [focusMounted, collapseFocus]) return (
@@ -307,10 +346,11 @@ export function PurchaseEvaluationsListPage() {
- {/* S79 — list state RESTORE bản gốc 8e68ed1: grid 3-panel bám trái lấp đầy - màn hình (KHÔNG canh giữa). List @@ -624,4 +707,3 @@ export function PurchaseEvaluationDetailPage() { ) } - diff --git a/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx b/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx index c6321a5..7fecabd 100644 --- a/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx +++ b/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx @@ -1,5 +1,5 @@ // List + Detail phiếu Duyệt NCC — 3-panel: List | Detail tabs | Workflow + history. -// URL params: type (filter A/B), pendingMe (1=inbox), id (selected), q (search). +// URL params: type (filter A/B), pendingMe (1=inbox), id (selected), q (search), expand (1=overlay). // Plan AG Phase 1 (S26 2026-05-21) — Tree view 2-level Project > Gói thầu > PE // UAT feedback bro Tra Sol: flat list "đám rừng" → Outlook folder structure. // FE-only group view (no schema change) — Phase 2 ProjectPackage defer sau UAT confirm. @@ -7,7 +7,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useNavigate, useSearchParams } from 'react-router-dom' import { toast } from 'sonner' -import { ArrowLeft, ClipboardCheck, Search, X } from 'lucide-react' +import { ArrowLeft, ClipboardCheck, Maximize2, Search, X } from 'lucide-react' import { Input } from '@/components/ui/Input' import { Select } from '@/components/ui/Select' import { EmptyState } from '@/components/EmptyState' @@ -38,6 +38,7 @@ export function PurchaseEvaluationsListPage() { const phase = sp.get('phase') ?? '' const approvalWorkflowId = sp.get('awId') ?? '' // Mig 23 — filter quy trình const selectedId = sp.get('id') + const isExpand = sp.get('expand') === '1' // S79 — overlay mở rộng // Mig 23 — list quy trình duyệt V2 cho dropdown filter (filter theo type screen) const approvalWorkflows = useQuery({ @@ -88,7 +89,12 @@ export function PurchaseEvaluationsListPage() { mutationFn: async (id: string) => api.delete(`/purchase-evaluations/${id}`), onSuccess: () => { toast.success('Đã xóa phiếu.') - setParam('id', null) + // Xoá phiếu → clear cả id + expand (về danh sách). + const next = new URLSearchParams(sp) + next.delete('id') + next.delete('expand') + next.delete('page') + setSp(next) qc.invalidateQueries({ queryKey: ['pe-list'] }) }, onError: e => toast.error(getErrorMessage(e)), @@ -102,12 +108,47 @@ export function PurchaseEvaluationsListPage() { setSp(next, { replace: key === 'q' }) }, [sp, setSp]) - // S78 — focus mode: chọn phiếu → mở overlay full-bleed (che menu + list). - // KHÔNG còn route /purchase-evaluations/:id riêng cho mobile — overlay phục vụ - // CẢ desktop (phiếu rộng + panel duyệt cạnh) lẫn mobile (stack dọc). 1 code path. - const selectRow = useCallback((id: string) => setParam('id', id), [setParam]) - // Đóng focus = clear ?id → quay lại danh sách. Duyệt xong cũng gọi closeFocus. - const closeFocus = useCallback(() => setParam('id', null), [setParam]) + // S79 — decouple "chọn" khỏi "mở rộng" (anh chốt 2026-06-19, annotated screenshot): + // • Bấm dòng phiếu = chọn → detail INLINE 3-panel "như cũ" (8e68ed1). Trên màn + // hẹp ( typeof window !== 'undefined' && window.innerWidth >= LG_BREAKPOINT + + // Chọn dòng: set id; clear expand. { + const next = new URLSearchParams(sp) + next.set('id', id) + if (isWideViewport()) next.delete('expand') + else next.set('expand', '1') + setSp(next) + }, [sp, setSp]) + + // "Xem mở rộng" trên 1 dòng → mở overlay cho phiếu đó (id + expand=1). + const expandRow = useCallback((id: string) => { + const next = new URLSearchParams(sp) + next.set('id', id) + next.set('expand', '1') + setSp(next) + }, [sp, setSp]) + + // Thu gọn overlay → bỏ expand, GIỮ id (quay lại inline detail, KHÔNG về list). + const collapseFocus = useCallback(() => setParam('expand', null), [setParam]) + + // Đóng inline detail (nút ← Đóng / xoá phiếu) → clear id + expand → về danh sách. + const closeDetail = useCallback(() => { + const next = new URLSearchParams(sp) + next.delete('id') + next.delete('expand') + next.delete('page') + setSp(next) + }, [sp, setSp]) + + // Duyệt xong (onApproved, không phải Trả lại/Từ chối) → phiếu rời inbox → + // đóng hẳn overlay + clear selection (về danh sách). + const onApproved = useCallback(() => closeDetail(), [closeDetail]) const allRows = list.data?.items ?? [] // Duyệt (pendingMe) → filter cứng client-side chỉ "Đã gửi duyệt" (Nháp/Trả lại/ @@ -208,13 +249,11 @@ export function PurchaseEvaluationsListPage() { ? (pendingMe ? `${PurchaseEvaluationTypeLabel[typeFilter]} — Chờ duyệt` : PurchaseEvaluationTypeLabel[typeFilter]) : pendingMe ? 'Duyệt NCC — Chờ tôi' : 'Quy trình Duyệt NCC' - // ─── S78 Focus-mode overlay (anh Kiệt FDC + anh chốt 2026-06-18) ─────────── - // Phiếu chính bị nhỏ trong panel giữa 1fr (~600px) → chọn phiếu mở overlay - // fixed inset-0 z-50 trượt từ phải, CHE hẳn menu trái + list rail. Phiếu full - // width (scroll riêng) + panel duyệt cạnh phải (desktop) / stack dưới (mobile). - // Mount-then-animate: `id` set → mount overlay → next frame bật `focusVisible` - // → translate-x-0 (trượt vào). Đóng → tắt visible → chờ transition → unmount. - const hasSelection = !!selectedId + // ─── S79 Focus-mode overlay (anh chốt 2026-06-19) ───────────────────────── + // Overlay full-bleed trượt từ phải CHE menu trái + TopBar + list + inline + // detail. Chỉ mở khi `expand=1`. Thu gọn → bỏ expand (về inline detail). + // Mount-then-animate: expand bật → mount overlay → next frame → translate-x-0. + const overlayActive = !!selectedId && isExpand const [focusMounted, setFocusMounted] = useState(false) const [focusVisible, setFocusVisible] = useState(false) const overlayRef = useRef(null) @@ -224,7 +263,7 @@ export function PurchaseEvaluationsListPage() { ) useEffect(() => { - if (hasSelection) { + if (overlayActive) { setFocusMounted(true) // 2 rAF để browser commit translate-x-full TRƯỚC khi đổi → translate-x-0 // (1 rAF đôi khi bị batch chung frame → không thấy slide). reduced-motion @@ -251,7 +290,7 @@ export function PurchaseEvaluationsListPage() { return () => clearTimeout(t) } } - }, [hasSelection]) + }, [overlayActive]) // Khoá scroll body khi overlay mở (tránh double-scroll nền). Focus nút Thu gọn // khi mở (a11y — đưa focus vào overlay). @@ -266,13 +305,13 @@ export function PurchaseEvaluationsListPage() { } }, [focusMounted]) - // Esc đóng + focus-trap (Tab vòng trong overlay) — a11y dialog floor (FD5). + // Esc thu gọn + focus-trap (Tab vòng trong overlay) — a11y dialog floor (FD5). useEffect(() => { if (!focusMounted) return const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault() - closeFocus() + collapseFocus() return } if (e.key === 'Tab' && overlayRef.current) { @@ -293,7 +332,7 @@ export function PurchaseEvaluationsListPage() { } document.addEventListener('keydown', onKey) return () => document.removeEventListener('keydown', onKey) - }, [focusMounted, closeFocus]) + }, [focusMounted, collapseFocus]) return (
@@ -307,10 +346,11 @@ export function PurchaseEvaluationsListPage() {
- {/* S79 — list state RESTORE bản gốc 8e68ed1: grid 3-panel bám trái lấp đầy - màn hình (KHÔNG canh giữa). List @@ -624,4 +707,3 @@ export function PurchaseEvaluationDetailPage() { ) } -