[CLAUDE] FE-Admin+FE-User: PE InfoTab auto re-edit on pencil click + active state visual feedback
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m7s

User feedback 2026-05-07: bấm pencil cho phiếu khác KHÔNG sáng + KHÔNG vào edit
mode (do useState init mount-time only, ev.id thay đổi không re-trigger).
Cũng cần visual feedback "sáng lên" để user biết đang edit phiếu nào.

Implementation:
  ~ PeDetailTabs.tsx (× 2 app)
    + import useEffect
    ~ InfoTab: thêm useEffect watch [autoEdit, canEdit, ev.id, ev.tenGoiThau,
      ev.diaDiem, ev.moTa, ev.paymentTerms]. Khi autoEdit && canEdit → setEditing(true)
      + sync values từ ev mới (tránh stale state khi switch giữa 2 phiếu khác id).
    Note: Dự án disabled đã có sẵn (line 458 `<Input value={ev.projectName}
    disabled className="bg-slate-100" />`) — verify hỏi user, KHÔNG thay đổi.
  ~ PeListPanel.tsx (× 2 app)
    + Prop `editingRowId?: string | null` — row đang edit (URL editHeader=1)
    ~ Pencil icon: thêm `isEditingThis = editable && editingRowId === p.id` state
      → bg-brand-100 + text-brand-700 + ring-brand-300 + shadow-sm khi active
      → tooltip đổi "✎ Đang sửa phiếu này — click để toggle / xem khác"
  ~ PurchaseEvaluationWorkspacePage.tsx (× 2 app)
    + Pass `editingRowId={autoEditHeader ? selectedId : null}` xuống PeListPanel

Verify: npm run build fe-admin + fe-user pass · 0 TS error · áp rule strict
verify khi add new prop chain + useEffect.

UAT mode: skip dotnet test (FE-only), push ngay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-07 16:41:33 +07:00
parent 378c9939e6
commit e320027074
6 changed files with 60 additions and 8 deletions

View File

@ -2,7 +2,7 @@
// NCC + Hạng mục + Báo giá stack vertically trong 1 màn hình. // NCC + Hạng mục + Báo giá stack vertically trong 1 màn hình.
// Duyệt history + Lịch sử thay đổi → moved to Panel 3 (xem PeWorkflowPanel // Duyệt history + Lịch sử thay đổi → moved to Panel 3 (xem PeWorkflowPanel
// → PeApprovalsSection + PeHistorySection). // → PeApprovalsSection + PeHistorySection).
import { useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { toast } from 'sonner' import { toast } from 'sonner'
@ -399,6 +399,20 @@ function InfoTab({ ev, readOnly, autoEdit }: { ev: PeDetailBundle; readOnly: boo
const [moTa, setMoTa] = useState(ev.moTa ?? '') const [moTa, setMoTa] = useState(ev.moTa ?? '')
const [paymentTerms, setPaymentTerms] = useState(ev.paymentTerms ?? '') const [paymentTerms, setPaymentTerms] = useState(ev.paymentTerms ?? '')
// User 2026-05-07: re-trigger editing mode khi click pencil ở Panel 1 cho
// PHIẾU KHÁC (ev.id thay đổi) hoặc autoEdit prop change. useState init chỉ
// chạy mount-time → cần useEffect sync khi parent re-render với props mới.
useEffect(() => {
if (autoEdit && canEdit) {
setEditing(true)
// Sync values từ ev mới (tránh stale state khi switch giữa 2 phiếu)
setTenGoiThau(ev.tenGoiThau)
setDiaDiem(ev.diaDiem ?? '')
setMoTa(ev.moTa ?? '')
setPaymentTerms(ev.paymentTerms ?? '')
}
}, [autoEdit, canEdit, ev.id, ev.tenGoiThau, ev.diaDiem, ev.moTa, ev.paymentTerms])
const dirty = tenGoiThau !== ev.tenGoiThau const dirty = tenGoiThau !== ev.tenGoiThau
|| diaDiem !== (ev.diaDiem ?? '') || diaDiem !== (ev.diaDiem ?? '')
|| moTa !== (ev.moTa ?? '') || moTa !== (ev.moTa ?? '')

View File

@ -39,6 +39,7 @@ export function PeListPanel({
onCreate, onCreate,
onEditClick, onEditClick,
editableOnly = false, editableOnly = false,
editingRowId = null,
}: { }: {
typeFilter: number | null typeFilter: number | null
pendingMe?: boolean pendingMe?: boolean
@ -55,6 +56,9 @@ export function PeListPanel({
/** Workspace mode: chỉ list phiếu editable (DangSoanThao + TraLai). Filter /** Workspace mode: chỉ list phiếu editable (DangSoanThao + TraLai). Filter
* client-side sau khi fetch — BE chưa hỗ trợ multi-phase param. */ * client-side sau khi fetch — BE chưa hỗ trợ multi-phase param. */
editableOnly?: boolean editableOnly?: boolean
/** Row đang được edit (URL editHeader=1) — pencil icon "sáng lên" active state.
* User 2026-05-07: visual feedback khi click pencil. */
editingRowId?: string | null
}) { }) {
const list = useQuery({ const list = useQuery({
queryKey: ['pe-list', { typeFilter, pendingMe, search, phase }], queryKey: ['pe-list', { typeFilter, pendingMe, search, phase }],
@ -185,20 +189,27 @@ export function PeListPanel({
{/* Edit pencil — LUÔN visible (user 2026-05-07). {/* Edit pencil — LUÔN visible (user 2026-05-07).
Bright/active khi phase editable (DangSoanThao + TraLai). Bright/active khi phase editable (DangSoanThao + TraLai).
Dim/disabled khi phase không edit được (Đã gửi duyệt / Đã duyệt Dim/disabled khi phase không edit được (Đã gửi duyệt / Đã duyệt
/ Từ chối) — click không có tác dụng. */} / Từ chối) — click không có tác dụng.
"Sáng lên" active state khi row.id === editingRowId (user
vừa click pencil + đang edit) — bg-brand-100 + ring. */}
{onEditClick && (() => { {onEditClick && (() => {
const editable = isEditablePhase(p.phase) const editable = isEditablePhase(p.phase)
const isEditingThis = editable && editingRowId === p.id
return ( return (
<button <button
onClick={() => editable && onEditClick(p.id)} onClick={() => editable && onEditClick(p.id)}
disabled={!editable} disabled={!editable}
className={cn( className={cn(
'absolute right-2 top-2 rounded p-1.5 transition', 'absolute right-2 top-2 rounded p-1.5 transition',
editable isEditingThis
? 'bg-brand-100 text-brand-700 shadow-sm ring-1 ring-brand-300 cursor-pointer'
: editable
? 'text-brand-600 hover:bg-brand-50 hover:shadow-sm cursor-pointer' ? 'text-brand-600 hover:bg-brand-50 hover:shadow-sm cursor-pointer'
: 'text-slate-300 cursor-not-allowed', : 'text-slate-300 cursor-not-allowed',
)} )}
title={editable title={isEditingThis
? '✎ Đang sửa phiếu này — click để toggle / xem khác'
: editable
? 'Sửa phiếu (header + chi tiết)' ? 'Sửa phiếu (header + chi tiết)'
: 'Phiếu đã gửi duyệt / đã duyệt / từ chối — không sửa được'} : 'Phiếu đã gửi duyệt / đã duyệt / từ chối — không sửa được'}
> >

View File

@ -93,6 +93,7 @@ export function PurchaseEvaluationWorkspacePage() {
onCreate={() => setParams({ mode: 'new', id: null, editHeader: null })} onCreate={() => setParams({ mode: 'new', id: null, editHeader: null })}
onEditClick={id => setParams({ id, mode: null, editHeader: '1' })} onEditClick={id => setParams({ id, mode: null, editHeader: '1' })}
editableOnly editableOnly
editingRowId={autoEditHeader ? selectedId : null}
/> />
{/* Panel 2: Empty | Header form | Detail tabs (workspace mode) */} {/* Panel 2: Empty | Header form | Detail tabs (workspace mode) */}

View File

@ -2,7 +2,7 @@
// NCC + Hạng mục + Báo giá stack vertically trong 1 màn hình. // NCC + Hạng mục + Báo giá stack vertically trong 1 màn hình.
// Duyệt history + Lịch sử thay đổi → moved to Panel 3 (xem PeWorkflowPanel // Duyệt history + Lịch sử thay đổi → moved to Panel 3 (xem PeWorkflowPanel
// → PeApprovalsSection + PeHistorySection). // → PeApprovalsSection + PeHistorySection).
import { useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { toast } from 'sonner' import { toast } from 'sonner'
@ -399,6 +399,20 @@ function InfoTab({ ev, readOnly, autoEdit }: { ev: PeDetailBundle; readOnly: boo
const [moTa, setMoTa] = useState(ev.moTa ?? '') const [moTa, setMoTa] = useState(ev.moTa ?? '')
const [paymentTerms, setPaymentTerms] = useState(ev.paymentTerms ?? '') const [paymentTerms, setPaymentTerms] = useState(ev.paymentTerms ?? '')
// User 2026-05-07: re-trigger editing mode khi click pencil ở Panel 1 cho
// PHIẾU KHÁC (ev.id thay đổi) hoặc autoEdit prop change. useState init chỉ
// chạy mount-time → cần useEffect sync khi parent re-render với props mới.
useEffect(() => {
if (autoEdit && canEdit) {
setEditing(true)
// Sync values từ ev mới (tránh stale state khi switch giữa 2 phiếu)
setTenGoiThau(ev.tenGoiThau)
setDiaDiem(ev.diaDiem ?? '')
setMoTa(ev.moTa ?? '')
setPaymentTerms(ev.paymentTerms ?? '')
}
}, [autoEdit, canEdit, ev.id, ev.tenGoiThau, ev.diaDiem, ev.moTa, ev.paymentTerms])
const dirty = tenGoiThau !== ev.tenGoiThau const dirty = tenGoiThau !== ev.tenGoiThau
|| diaDiem !== (ev.diaDiem ?? '') || diaDiem !== (ev.diaDiem ?? '')
|| moTa !== (ev.moTa ?? '') || moTa !== (ev.moTa ?? '')

View File

@ -39,6 +39,7 @@ export function PeListPanel({
onCreate, onCreate,
onEditClick, onEditClick,
editableOnly = false, editableOnly = false,
editingRowId = null,
}: { }: {
typeFilter: number | null typeFilter: number | null
pendingMe?: boolean pendingMe?: boolean
@ -55,6 +56,9 @@ export function PeListPanel({
/** Workspace mode: chỉ list phiếu editable (DangSoanThao + TraLai). Filter /** Workspace mode: chỉ list phiếu editable (DangSoanThao + TraLai). Filter
* client-side sau khi fetch — BE chưa hỗ trợ multi-phase param. */ * client-side sau khi fetch — BE chưa hỗ trợ multi-phase param. */
editableOnly?: boolean editableOnly?: boolean
/** Row đang được edit (URL editHeader=1) — pencil icon "sáng lên" active state.
* User 2026-05-07: visual feedback khi click pencil. */
editingRowId?: string | null
}) { }) {
const list = useQuery({ const list = useQuery({
queryKey: ['pe-list', { typeFilter, pendingMe, search, phase }], queryKey: ['pe-list', { typeFilter, pendingMe, search, phase }],
@ -185,20 +189,27 @@ export function PeListPanel({
{/* Edit pencil — LUÔN visible (user 2026-05-07). {/* Edit pencil — LUÔN visible (user 2026-05-07).
Bright/active khi phase editable (DangSoanThao + TraLai). Bright/active khi phase editable (DangSoanThao + TraLai).
Dim/disabled khi phase không edit được (Đã gửi duyệt / Đã duyệt Dim/disabled khi phase không edit được (Đã gửi duyệt / Đã duyệt
/ Từ chối) — click không có tác dụng. */} / Từ chối) — click không có tác dụng.
"Sáng lên" active state khi row.id === editingRowId (user
vừa click pencil + đang edit) — bg-brand-100 + ring. */}
{onEditClick && (() => { {onEditClick && (() => {
const editable = isEditablePhase(p.phase) const editable = isEditablePhase(p.phase)
const isEditingThis = editable && editingRowId === p.id
return ( return (
<button <button
onClick={() => editable && onEditClick(p.id)} onClick={() => editable && onEditClick(p.id)}
disabled={!editable} disabled={!editable}
className={cn( className={cn(
'absolute right-2 top-2 rounded p-1.5 transition', 'absolute right-2 top-2 rounded p-1.5 transition',
editable isEditingThis
? 'bg-brand-100 text-brand-700 shadow-sm ring-1 ring-brand-300 cursor-pointer'
: editable
? 'text-brand-600 hover:bg-brand-50 hover:shadow-sm cursor-pointer' ? 'text-brand-600 hover:bg-brand-50 hover:shadow-sm cursor-pointer'
: 'text-slate-300 cursor-not-allowed', : 'text-slate-300 cursor-not-allowed',
)} )}
title={editable title={isEditingThis
? '✎ Đang sửa phiếu này — click để toggle / xem khác'
: editable
? 'Sửa phiếu (header + chi tiết)' ? 'Sửa phiếu (header + chi tiết)'
: 'Phiếu đã gửi duyệt / đã duyệt / từ chối — không sửa được'} : 'Phiếu đã gửi duyệt / đã duyệt / từ chối — không sửa được'}
> >

View File

@ -93,6 +93,7 @@ export function PurchaseEvaluationWorkspacePage() {
onCreate={() => setParams({ mode: 'new', id: null, editHeader: null })} onCreate={() => setParams({ mode: 'new', id: null, editHeader: null })}
onEditClick={id => setParams({ id, mode: null, editHeader: '1' })} onEditClick={id => setParams({ id, mode: null, editHeader: '1' })}
editableOnly editableOnly
editingRowId={autoEditHeader ? selectedId : null}
/> />
{/* Panel 2: Empty | Header form | Detail tabs (workspace mode) */} {/* Panel 2: Empty | Header form | Detail tabs (workspace mode) */}