[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
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:
@ -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 ?? '')
|
||||||
|
|||||||
@ -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'}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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) */}
|
||||||
|
|||||||
@ -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 ?? '')
|
||||||
|
|||||||
@ -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'}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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) */}
|
||||||
|
|||||||
Reference in New Issue
Block a user