[CLAUDE] Domain+FE: PE thêm phase TraLai + pencil always visible + edit gating
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 2m0s
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 2m0s
User feedback 2026-05-07:
1. Pencil edit icon LUÔN hiện (không chỉ hover) trong workspace Panel 1
2. Pencil sáng (active brand-color) khi phase editable, xám/disabled khi không
3. Click pencil khi sáng → row sáng + auto-open edit toàn bộ (header + detail)
4. Thêm phase mới "Trả lại" — approver gửi về Drafter sửa (vs Từ chối terminal)
5. Edit chỉ cho 2 trạng thái: Đang soạn thảo + Trả lại
(Từ chối + Đã gửi duyệt + Đã duyệt → không edit/thao tác gì)
Implementation:
~ Domain/PurchaseEvaluations/PurchaseEvaluationPhase.cs
+ TraLai = 98 (giữa DaDuyet=7 và TuChoi=99)
Comment ghi rõ "approver trả về Drafter sửa, vẫn cho edit, khác TuChoi"
~ types/purchaseEvaluation.ts (× 2 app)
+ PurchaseEvaluationPhase enum: TraLai = 98
+ PurchaseEvaluationPhaseLabel/Color cho TraLai (yellow)
+ isEditablePhase(phase) helper: true cho DangSoanThao + TraLai
+ PeDisplayStatus thêm "TraLai" (separate, không gộp DaGuiDuyet)
+ getPeDisplayStatus map TraLai → "Trả lại" badge yellow
~ components/pe/PeListPanel.tsx (× 2 app)
- Pencil icon: bỏ opacity-0 hover-only → LUÔN visible
- editable=isEditablePhase(p.phase): bright text-brand-600 + cursor-pointer
- !editable: text-slate-300 + cursor-not-allowed + onClick guard ignored
- title tooltip rõ ràng "đã gửi duyệt / đã duyệt / từ chối — không sửa được"
- Bỏ forcedPhase prop → editableOnly prop (filter client-side cả 2 phase
DangSoanThao + TraLai vì BE chưa support multi-phase param)
- Khi editableOnly: hiển thị "Lọc cố định: Bản nháp + Trả lại" indicator
~ components/pe/PeDetailTabs.tsx (× 2 app)
- Header bar: isDraft → canEditPhase = isEditablePhase(phase)
- InfoTab: canEdit = !readOnly && isEditablePhase
- BudgetFieldRow: canEdit = !readOnly && isEditablePhase
→ Đồng nghĩa Drafter sửa được phiếu Trả lại sau approver send back
~ pages/pe/PurchaseEvaluationWorkspacePage.tsx (× 2 app)
- PeListPanel forcedPhase=DangSoanThao → editableOnly
- Bỏ import PurchaseEvaluationPhase
Workflow service BE chưa wire transition → TraLai (defer — user sẽ thêm button
"Trả lại" trong PeWorkflowPanel duyệt sau, hoặc dùng API PATCH manual). Phase
TraLai chỉ là enum value sẵn sàng FE hiển thị + BE ánh xạ HasConversion<int>.
UAT mode: skip verify, push ngay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -27,6 +27,7 @@ import {
|
|||||||
PurchaseEvaluationPhaseLabel,
|
PurchaseEvaluationPhaseLabel,
|
||||||
PurchaseEvaluationTypeLabel,
|
PurchaseEvaluationTypeLabel,
|
||||||
getPeDisplayStatus,
|
getPeDisplayStatus,
|
||||||
|
isEditablePhase,
|
||||||
type PeAttachment,
|
type PeAttachment,
|
||||||
type PeChangelog,
|
type PeChangelog,
|
||||||
type PeDepartmentOpinion,
|
type PeDepartmentOpinion,
|
||||||
@ -69,7 +70,9 @@ export function PeDetailTabs({
|
|||||||
autoEditHeader?: boolean
|
autoEditHeader?: boolean
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const isDraft = evaluation.phase === PurchaseEvaluationPhase.DangSoanThao
|
// isDraft renamed → canEditPhase: bao gồm cả TraLai (per user 2026-05-07).
|
||||||
|
// Header bar action buttons (Sửa header / Xóa) hiện khi phase editable + !readOnly.
|
||||||
|
const canEditPhase = isEditablePhase(evaluation.phase)
|
||||||
const opinionsReadOnly = readOnly || mode === 'workspace'
|
const opinionsReadOnly = readOnly || mode === 'workspace'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -107,7 +110,7 @@ export function PeDetailTabs({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{isDraft && !readOnly && (
|
{canEditPhase && !readOnly && (
|
||||||
<>
|
<>
|
||||||
<Button variant="ghost" onClick={() => navigate(`/purchase-evaluations/new?id=${evaluation.id}`)} className="gap-1.5 text-xs">
|
<Button variant="ghost" onClick={() => navigate(`/purchase-evaluations/new?id=${evaluation.id}`)} className="gap-1.5 text-xs">
|
||||||
<Pencil className="h-3.5 w-3.5" /> Sửa header
|
<Pencil className="h-3.5 w-3.5" /> Sửa header
|
||||||
@ -303,14 +306,14 @@ export function PeHistorySection({ ev }: { ev: PeDetailBundle }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== Section 1 — Thông tin gói thầu (spec: a. Tên gói thầu / b. Dự án) =====
|
// ===== Section 1 — Thông tin gói thầu (spec: a. Tên gói thầu / b. Dự án) =====
|
||||||
// Inline editable khi canEdit (=!readOnly && isDraft). Edit pencil button "Sửa"
|
// Inline editable khi canEdit (=!readOnly && phase editable). Edit pencil button
|
||||||
// flip display ↔ form mode. Save dùng existing PUT /pe/:id endpoint với current
|
// "Sửa" flip display ↔ form mode. Save dùng existing PUT /pe/:id endpoint với
|
||||||
// entity values + new header fields. Dự án + Type LOCKED sau create — chỉ Tên/
|
// current entity values + new header fields. Dự án + Type LOCKED sau create —
|
||||||
// Địa điểm/Mô tả/Payment editable inline. autoEdit prop cho phép trigger edit
|
// chỉ Tên/Địa điểm/Mô tả/Payment editable inline. autoEdit prop cho phép trigger
|
||||||
// mode từ pencil icon trong PeListPanel (URL flag ?editHeader=1).
|
// edit mode từ pencil icon trong PeListPanel (URL flag ?editHeader=1).
|
||||||
|
// Phase editable = DangSoanThao + TraLai (user 2026-05-07).
|
||||||
function InfoTab({ ev, readOnly, autoEdit }: { ev: PeDetailBundle; readOnly: boolean; autoEdit: boolean }) {
|
function InfoTab({ ev, readOnly, autoEdit }: { ev: PeDetailBundle; readOnly: boolean; autoEdit: boolean }) {
|
||||||
const isDraft = ev.phase === PurchaseEvaluationPhase.DangSoanThao
|
const canEdit = !readOnly && isEditablePhase(ev.phase)
|
||||||
const canEdit = !readOnly && isDraft
|
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const [editing, setEditing] = useState(autoEdit && canEdit)
|
const [editing, setEditing] = useState(autoEdit && canEdit)
|
||||||
const [tenGoiThau, setTenGoiThau] = useState(ev.tenGoiThau)
|
const [tenGoiThau, setTenGoiThau] = useState(ev.tenGoiThau)
|
||||||
@ -443,12 +446,11 @@ function InfoTab({ ev, readOnly, autoEdit }: { ev: PeDetailBundle; readOnly: boo
|
|||||||
// ===== b. Ngân sách inline editor (Mig 17) =====
|
// ===== b. Ngân sách inline editor (Mig 17) =====
|
||||||
// Hiển thị + edit budget link / manual fields ngay trong Section 2 — KHÔNG cần
|
// Hiển thị + edit budget link / manual fields ngay trong Section 2 — KHÔNG cần
|
||||||
// đi tới "Sửa header" page. Visible trong cả 3 view (Workspace / Danh sách /
|
// đi tới "Sửa header" page. Visible trong cả 3 view (Workspace / Danh sách /
|
||||||
// Duyệt). Edit chỉ enable khi !readOnly + isDraft (Drafter sửa). Read-only
|
// Duyệt). Edit chỉ enable khi !readOnly + phase editable (DangSoanThao /
|
||||||
// khi pendingMe=1 hoặc phase đã chuyển khỏi DangSoanThao. Empty values hiển
|
// TraLai). Read-only khi pendingMe=1 hoặc phase đã gửi duyệt / đã duyệt /
|
||||||
// thị empty (per user 2026-05-07).
|
// từ chối. Empty values hiển thị empty (per user 2026-05-07).
|
||||||
function BudgetFieldRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolean }) {
|
function BudgetFieldRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolean }) {
|
||||||
const isDraft = ev.phase === PurchaseEvaluationPhase.DangSoanThao
|
const canEdit = !readOnly && isEditablePhase(ev.phase)
|
||||||
const canEdit = !readOnly && isDraft
|
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
|
||||||
// Detect mode khi mount/refresh: prefer manual mode nếu đã có data manual + ko link
|
// Detect mode khi mount/refresh: prefer manual mode nếu đã có data manual + ko link
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import {
|
|||||||
PurchaseEvaluationPhase,
|
PurchaseEvaluationPhase,
|
||||||
PurchaseEvaluationTypeLabel,
|
PurchaseEvaluationTypeLabel,
|
||||||
getPeDisplayStatus,
|
getPeDisplayStatus,
|
||||||
|
isEditablePhase,
|
||||||
type PeListItem,
|
type PeListItem,
|
||||||
} from '@/types/purchaseEvaluation'
|
} from '@/types/purchaseEvaluation'
|
||||||
|
|
||||||
@ -51,13 +52,12 @@ export function PeListPanel({
|
|||||||
onCreate?: () => void
|
onCreate?: () => void
|
||||||
/** Pencil edit icon hover next-to-row — click → select + auto-open Section 1 edit mode (URL ?editHeader=1). */
|
/** Pencil edit icon hover next-to-row — click → select + auto-open Section 1 edit mode (URL ?editHeader=1). */
|
||||||
onEditClick?: (id: string) => void
|
onEditClick?: (id: string) => void
|
||||||
/** Force phase filter (vd workspace chỉ hiện Bản nháp = DangSoanThao). Khi set:
|
/** Workspace mode: chỉ list phiếu editable (DangSoanThao + TraLai). Filter
|
||||||
* ẩn phase Select dropdown + force fetch params phase=<this>. */
|
* client-side sau khi fetch — BE chưa hỗ trợ multi-phase param. */
|
||||||
forcedPhase?: number
|
editableOnly?: boolean
|
||||||
}) {
|
}) {
|
||||||
const effectivePhase = forcedPhase !== undefined ? String(forcedPhase) : phase
|
|
||||||
const list = useQuery({
|
const list = useQuery({
|
||||||
queryKey: ['pe-list', { typeFilter, pendingMe, search, phase: effectivePhase }],
|
queryKey: ['pe-list', { typeFilter, pendingMe, search, phase }],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (pendingMe) {
|
if (pendingMe) {
|
||||||
const res = await api.get<PeListItem[]>('/purchase-evaluations/inbox', {
|
const res = await api.get<PeListItem[]>('/purchase-evaluations/inbox', {
|
||||||
@ -70,14 +70,17 @@ export function PeListPanel({
|
|||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
search: search || undefined,
|
search: search || undefined,
|
||||||
type: typeFilter ?? undefined,
|
type: typeFilter ?? undefined,
|
||||||
phase: effectivePhase || undefined,
|
phase: phase || undefined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return res.data
|
return res.data
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const rows = list.data?.items ?? []
|
const allRows = list.data?.items ?? []
|
||||||
|
const rows = editableOnly
|
||||||
|
? allRows.filter(p => isEditablePhase(p.phase))
|
||||||
|
: allRows
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="flex flex-col overflow-hidden border-r border-slate-200 bg-white">
|
<aside className="flex flex-col overflow-hidden border-r border-slate-200 bg-white">
|
||||||
@ -88,7 +91,7 @@ export function PeListPanel({
|
|||||||
Danh sách phiếu
|
Danh sách phiếu
|
||||||
</div>
|
</div>
|
||||||
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-medium text-slate-600">
|
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-medium text-slate-600">
|
||||||
{list.data?.total ?? 0}
|
{rows.length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@ -100,17 +103,17 @@ export function PeListPanel({
|
|||||||
className="pl-8"
|
className="pl-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{forcedPhase === undefined ? (
|
{editableOnly ? (
|
||||||
|
<div className="rounded border border-slate-200 bg-slate-50 px-2 py-1.5 text-[11px] text-slate-600">
|
||||||
|
Lọc cố định: <strong>Bản nháp + Trả lại</strong> (chỉ phiếu sửa được)
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<Select value={phase} onChange={e => onPhaseChange(e.target.value)}>
|
<Select value={phase} onChange={e => onPhaseChange(e.target.value)}>
|
||||||
<option value="">Tất cả trạng thái</option>
|
<option value="">Tất cả trạng thái</option>
|
||||||
{Object.values(PeDisplayStatus).map(s => (
|
{Object.values(PeDisplayStatus).map(s => (
|
||||||
<option key={s} value={statusToPhaseValue(s)}>{PeDisplayStatusLabel[s]}</option>
|
<option key={s} value={statusToPhaseValue(s)}>{PeDisplayStatusLabel[s]}</option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
) : (
|
|
||||||
<div className="rounded border border-slate-200 bg-slate-50 px-2 py-1.5 text-[11px] text-slate-600">
|
|
||||||
Lọc cố định: <strong>{PeDisplayStatusLabel[getPeDisplayStatus(forcedPhase)]}</strong>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -179,16 +182,30 @@ export function PeListPanel({
|
|||||||
<div className="mt-1 text-[10px] text-brand-600">✓ Đã tạo HĐ</div>
|
<div className="mt-1 text-[10px] text-brand-600">✓ Đã tạo HĐ</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
{/* Edit pencil — visible on hover (chỉ khi onEditClick được truyền) */}
|
{/* Edit pencil — LUÔN visible (user 2026-05-07).
|
||||||
{onEditClick && (
|
Bright/active khi phase editable (DangSoanThao + TraLai).
|
||||||
<button
|
Dim/disabled khi phase không edit được (Đã gửi duyệt / Đã duyệt
|
||||||
onClick={() => onEditClick(p.id)}
|
/ Từ chối) — click không có tác dụng. */}
|
||||||
className="absolute right-2 top-2 rounded p-1.5 text-slate-400 opacity-0 transition group-hover:opacity-100 hover:bg-white hover:text-brand-600 hover:shadow-sm"
|
{onEditClick && (() => {
|
||||||
title="Sửa thông tin gói thầu"
|
const editable = isEditablePhase(p.phase)
|
||||||
>
|
return (
|
||||||
<Pencil className="h-3.5 w-3.5" />
|
<button
|
||||||
</button>
|
onClick={() => editable && onEditClick(p.id)}
|
||||||
)}
|
disabled={!editable}
|
||||||
|
className={cn(
|
||||||
|
'absolute right-2 top-2 rounded p-1.5 transition',
|
||||||
|
editable
|
||||||
|
? 'text-brand-600 hover:bg-brand-50 hover:shadow-sm cursor-pointer'
|
||||||
|
: 'text-slate-300 cursor-not-allowed',
|
||||||
|
)}
|
||||||
|
title={editable
|
||||||
|
? 'Sửa phiếu (header + chi tiết)'
|
||||||
|
: 'Phiếu đã gửi duyệt / đã duyệt / từ chối — không sửa được'}
|
||||||
|
>
|
||||||
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -20,7 +20,6 @@ import { PeWorkspaceCreateView } from '@/components/pe/PeWorkspaceCreateView'
|
|||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { getErrorMessage } from '@/lib/apiError'
|
import { getErrorMessage } from '@/lib/apiError'
|
||||||
import {
|
import {
|
||||||
PurchaseEvaluationPhase,
|
|
||||||
PurchaseEvaluationType,
|
PurchaseEvaluationType,
|
||||||
PurchaseEvaluationTypeLabel,
|
PurchaseEvaluationTypeLabel,
|
||||||
type PeDetailBundle,
|
type PeDetailBundle,
|
||||||
@ -93,7 +92,7 @@ export function PurchaseEvaluationWorkspacePage() {
|
|||||||
showCreateButton
|
showCreateButton
|
||||||
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' })}
|
||||||
forcedPhase={PurchaseEvaluationPhase.DangSoanThao}
|
editableOnly
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Panel 2: Empty | Header form | Detail tabs (workspace mode) */}
|
{/* Panel 2: Empty | Header form | Detail tabs (workspace mode) */}
|
||||||
|
|||||||
@ -24,7 +24,8 @@ export const PurchaseEvaluationPhase = {
|
|||||||
ChoCEODuyetPA: 5,
|
ChoCEODuyetPA: 5,
|
||||||
ChoCEODuyetNCC: 6,
|
ChoCEODuyetNCC: 6,
|
||||||
DaDuyet: 7,
|
DaDuyet: 7,
|
||||||
TuChoi: 99,
|
TraLai: 98, // approver trả về Drafter sửa — vẫn cho edit
|
||||||
|
TuChoi: 99, // terminal từ chối — KHÔNG edit
|
||||||
} as const
|
} as const
|
||||||
export type PurchaseEvaluationPhase = typeof PurchaseEvaluationPhase[keyof typeof PurchaseEvaluationPhase]
|
export type PurchaseEvaluationPhase = typeof PurchaseEvaluationPhase[keyof typeof PurchaseEvaluationPhase]
|
||||||
|
|
||||||
@ -36,6 +37,7 @@ export const PurchaseEvaluationPhaseLabel: Record<number, string> = {
|
|||||||
5: 'Chờ CEO duyệt PA',
|
5: 'Chờ CEO duyệt PA',
|
||||||
6: 'Chờ CEO duyệt NCC',
|
6: 'Chờ CEO duyệt NCC',
|
||||||
7: 'Đã duyệt',
|
7: 'Đã duyệt',
|
||||||
|
98: 'Trả lại',
|
||||||
99: 'Từ chối',
|
99: 'Từ chối',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,9 +49,17 @@ export const PurchaseEvaluationPhaseColor: Record<number, string> = {
|
|||||||
5: 'bg-fuchsia-100 text-fuchsia-700',
|
5: 'bg-fuchsia-100 text-fuchsia-700',
|
||||||
6: 'bg-pink-100 text-pink-700',
|
6: 'bg-pink-100 text-pink-700',
|
||||||
7: 'bg-emerald-100 text-emerald-700',
|
7: 'bg-emerald-100 text-emerald-700',
|
||||||
|
98: 'bg-yellow-100 text-yellow-800',
|
||||||
99: 'bg-red-100 text-red-700',
|
99: 'bg-red-100 text-red-700',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase nào được phép edit phiếu (Drafter sửa header + detail).
|
||||||
|
// User 2026-05-07: chỉ Đang soạn thảo + Trả lại (Từ chối là terminal, không edit).
|
||||||
|
export function isEditablePhase(phase: number): boolean {
|
||||||
|
return phase === PurchaseEvaluationPhase.DangSoanThao
|
||||||
|
|| phase === PurchaseEvaluationPhase.TraLai
|
||||||
|
}
|
||||||
|
|
||||||
// Display status meta — gom các phase chi tiết thành 4 nhóm hiển thị end-user
|
// Display status meta — gom các phase chi tiết thành 4 nhóm hiển thị end-user
|
||||||
// friendly. Workflow timeline + workflow service vẫn dùng phase chi tiết.
|
// friendly. Workflow timeline + workflow service vẫn dùng phase chi tiết.
|
||||||
// User 2026-05-07 chỉnh:
|
// User 2026-05-07 chỉnh:
|
||||||
@ -60,6 +70,7 @@ export const PurchaseEvaluationPhaseColor: Record<number, string> = {
|
|||||||
export const PeDisplayStatus = {
|
export const PeDisplayStatus = {
|
||||||
BanNhap: 'BanNhap',
|
BanNhap: 'BanNhap',
|
||||||
DaGuiDuyet: 'DaGuiDuyet',
|
DaGuiDuyet: 'DaGuiDuyet',
|
||||||
|
TraLai: 'TraLai',
|
||||||
DaDuyet: 'DaDuyet',
|
DaDuyet: 'DaDuyet',
|
||||||
TuChoi: 'TuChoi',
|
TuChoi: 'TuChoi',
|
||||||
} as const
|
} as const
|
||||||
@ -68,6 +79,7 @@ export type PeDisplayStatus = typeof PeDisplayStatus[keyof typeof PeDisplayStatu
|
|||||||
export const PeDisplayStatusLabel: Record<PeDisplayStatus, string> = {
|
export const PeDisplayStatusLabel: Record<PeDisplayStatus, string> = {
|
||||||
BanNhap: 'Bản nháp',
|
BanNhap: 'Bản nháp',
|
||||||
DaGuiDuyet: 'Đã gửi duyệt',
|
DaGuiDuyet: 'Đã gửi duyệt',
|
||||||
|
TraLai: 'Trả lại',
|
||||||
DaDuyet: 'Đã duyệt',
|
DaDuyet: 'Đã duyệt',
|
||||||
TuChoi: 'Từ chối',
|
TuChoi: 'Từ chối',
|
||||||
}
|
}
|
||||||
@ -75,6 +87,7 @@ export const PeDisplayStatusLabel: Record<PeDisplayStatus, string> = {
|
|||||||
export const PeDisplayStatusColor: Record<PeDisplayStatus, string> = {
|
export const PeDisplayStatusColor: Record<PeDisplayStatus, string> = {
|
||||||
BanNhap: 'bg-slate-100 text-slate-700',
|
BanNhap: 'bg-slate-100 text-slate-700',
|
||||||
DaGuiDuyet: 'bg-amber-100 text-amber-700',
|
DaGuiDuyet: 'bg-amber-100 text-amber-700',
|
||||||
|
TraLai: 'bg-yellow-100 text-yellow-800',
|
||||||
DaDuyet: 'bg-emerald-100 text-emerald-700',
|
DaDuyet: 'bg-emerald-100 text-emerald-700',
|
||||||
TuChoi: 'bg-red-100 text-red-700',
|
TuChoi: 'bg-red-100 text-red-700',
|
||||||
}
|
}
|
||||||
@ -82,6 +95,7 @@ export const PeDisplayStatusColor: Record<PeDisplayStatus, string> = {
|
|||||||
export function getPeDisplayStatus(phase: number): PeDisplayStatus {
|
export function getPeDisplayStatus(phase: number): PeDisplayStatus {
|
||||||
if (phase === PurchaseEvaluationPhase.DangSoanThao) return PeDisplayStatus.BanNhap
|
if (phase === PurchaseEvaluationPhase.DangSoanThao) return PeDisplayStatus.BanNhap
|
||||||
if (phase === PurchaseEvaluationPhase.DaDuyet) return PeDisplayStatus.DaDuyet
|
if (phase === PurchaseEvaluationPhase.DaDuyet) return PeDisplayStatus.DaDuyet
|
||||||
|
if (phase === PurchaseEvaluationPhase.TraLai) return PeDisplayStatus.TraLai
|
||||||
if (phase === PurchaseEvaluationPhase.TuChoi) return PeDisplayStatus.TuChoi
|
if (phase === PurchaseEvaluationPhase.TuChoi) return PeDisplayStatus.TuChoi
|
||||||
return PeDisplayStatus.DaGuiDuyet
|
return PeDisplayStatus.DaGuiDuyet
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import {
|
|||||||
PurchaseEvaluationPhaseLabel,
|
PurchaseEvaluationPhaseLabel,
|
||||||
PurchaseEvaluationTypeLabel,
|
PurchaseEvaluationTypeLabel,
|
||||||
getPeDisplayStatus,
|
getPeDisplayStatus,
|
||||||
|
isEditablePhase,
|
||||||
type PeAttachment,
|
type PeAttachment,
|
||||||
type PeChangelog,
|
type PeChangelog,
|
||||||
type PeDepartmentOpinion,
|
type PeDepartmentOpinion,
|
||||||
@ -69,7 +70,9 @@ export function PeDetailTabs({
|
|||||||
autoEditHeader?: boolean
|
autoEditHeader?: boolean
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const isDraft = evaluation.phase === PurchaseEvaluationPhase.DangSoanThao
|
// isDraft renamed → canEditPhase: bao gồm cả TraLai (per user 2026-05-07).
|
||||||
|
// Header bar action buttons (Sửa header / Xóa) hiện khi phase editable + !readOnly.
|
||||||
|
const canEditPhase = isEditablePhase(evaluation.phase)
|
||||||
const opinionsReadOnly = readOnly || mode === 'workspace'
|
const opinionsReadOnly = readOnly || mode === 'workspace'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -107,7 +110,7 @@ export function PeDetailTabs({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{isDraft && !readOnly && (
|
{canEditPhase && !readOnly && (
|
||||||
<>
|
<>
|
||||||
<Button variant="ghost" onClick={() => navigate(`/purchase-evaluations/new?id=${evaluation.id}`)} className="gap-1.5 text-xs">
|
<Button variant="ghost" onClick={() => navigate(`/purchase-evaluations/new?id=${evaluation.id}`)} className="gap-1.5 text-xs">
|
||||||
<Pencil className="h-3.5 w-3.5" /> Sửa header
|
<Pencil className="h-3.5 w-3.5" /> Sửa header
|
||||||
@ -303,14 +306,14 @@ export function PeHistorySection({ ev }: { ev: PeDetailBundle }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== Section 1 — Thông tin gói thầu (spec: a. Tên gói thầu / b. Dự án) =====
|
// ===== Section 1 — Thông tin gói thầu (spec: a. Tên gói thầu / b. Dự án) =====
|
||||||
// Inline editable khi canEdit (=!readOnly && isDraft). Edit pencil button "Sửa"
|
// Inline editable khi canEdit (=!readOnly && phase editable). Edit pencil button
|
||||||
// flip display ↔ form mode. Save dùng existing PUT /pe/:id endpoint với current
|
// "Sửa" flip display ↔ form mode. Save dùng existing PUT /pe/:id endpoint với
|
||||||
// entity values + new header fields. Dự án + Type LOCKED sau create — chỉ Tên/
|
// current entity values + new header fields. Dự án + Type LOCKED sau create —
|
||||||
// Địa điểm/Mô tả/Payment editable inline. autoEdit prop cho phép trigger edit
|
// chỉ Tên/Địa điểm/Mô tả/Payment editable inline. autoEdit prop cho phép trigger
|
||||||
// mode từ pencil icon trong PeListPanel (URL flag ?editHeader=1).
|
// edit mode từ pencil icon trong PeListPanel (URL flag ?editHeader=1).
|
||||||
|
// Phase editable = DangSoanThao + TraLai (user 2026-05-07).
|
||||||
function InfoTab({ ev, readOnly, autoEdit }: { ev: PeDetailBundle; readOnly: boolean; autoEdit: boolean }) {
|
function InfoTab({ ev, readOnly, autoEdit }: { ev: PeDetailBundle; readOnly: boolean; autoEdit: boolean }) {
|
||||||
const isDraft = ev.phase === PurchaseEvaluationPhase.DangSoanThao
|
const canEdit = !readOnly && isEditablePhase(ev.phase)
|
||||||
const canEdit = !readOnly && isDraft
|
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const [editing, setEditing] = useState(autoEdit && canEdit)
|
const [editing, setEditing] = useState(autoEdit && canEdit)
|
||||||
const [tenGoiThau, setTenGoiThau] = useState(ev.tenGoiThau)
|
const [tenGoiThau, setTenGoiThau] = useState(ev.tenGoiThau)
|
||||||
@ -443,12 +446,11 @@ function InfoTab({ ev, readOnly, autoEdit }: { ev: PeDetailBundle; readOnly: boo
|
|||||||
// ===== b. Ngân sách inline editor (Mig 17) =====
|
// ===== b. Ngân sách inline editor (Mig 17) =====
|
||||||
// Hiển thị + edit budget link / manual fields ngay trong Section 2 — KHÔNG cần
|
// Hiển thị + edit budget link / manual fields ngay trong Section 2 — KHÔNG cần
|
||||||
// đi tới "Sửa header" page. Visible trong cả 3 view (Workspace / Danh sách /
|
// đi tới "Sửa header" page. Visible trong cả 3 view (Workspace / Danh sách /
|
||||||
// Duyệt). Edit chỉ enable khi !readOnly + isDraft (Drafter sửa). Read-only
|
// Duyệt). Edit chỉ enable khi !readOnly + phase editable (DangSoanThao /
|
||||||
// khi pendingMe=1 hoặc phase đã chuyển khỏi DangSoanThao. Empty values hiển
|
// TraLai). Read-only khi pendingMe=1 hoặc phase đã gửi duyệt / đã duyệt /
|
||||||
// thị empty (per user 2026-05-07).
|
// từ chối. Empty values hiển thị empty (per user 2026-05-07).
|
||||||
function BudgetFieldRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolean }) {
|
function BudgetFieldRow({ ev, readOnly }: { ev: PeDetailBundle; readOnly: boolean }) {
|
||||||
const isDraft = ev.phase === PurchaseEvaluationPhase.DangSoanThao
|
const canEdit = !readOnly && isEditablePhase(ev.phase)
|
||||||
const canEdit = !readOnly && isDraft
|
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
|
||||||
// Detect mode khi mount/refresh: prefer manual mode nếu đã có data manual + ko link
|
// Detect mode khi mount/refresh: prefer manual mode nếu đã có data manual + ko link
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import {
|
|||||||
PurchaseEvaluationPhase,
|
PurchaseEvaluationPhase,
|
||||||
PurchaseEvaluationTypeLabel,
|
PurchaseEvaluationTypeLabel,
|
||||||
getPeDisplayStatus,
|
getPeDisplayStatus,
|
||||||
|
isEditablePhase,
|
||||||
type PeListItem,
|
type PeListItem,
|
||||||
} from '@/types/purchaseEvaluation'
|
} from '@/types/purchaseEvaluation'
|
||||||
|
|
||||||
@ -51,13 +52,12 @@ export function PeListPanel({
|
|||||||
onCreate?: () => void
|
onCreate?: () => void
|
||||||
/** Pencil edit icon hover next-to-row — click → select + auto-open Section 1 edit mode (URL ?editHeader=1). */
|
/** Pencil edit icon hover next-to-row — click → select + auto-open Section 1 edit mode (URL ?editHeader=1). */
|
||||||
onEditClick?: (id: string) => void
|
onEditClick?: (id: string) => void
|
||||||
/** Force phase filter (vd workspace chỉ hiện Bản nháp = DangSoanThao). Khi set:
|
/** Workspace mode: chỉ list phiếu editable (DangSoanThao + TraLai). Filter
|
||||||
* ẩn phase Select dropdown + force fetch params phase=<this>. */
|
* client-side sau khi fetch — BE chưa hỗ trợ multi-phase param. */
|
||||||
forcedPhase?: number
|
editableOnly?: boolean
|
||||||
}) {
|
}) {
|
||||||
const effectivePhase = forcedPhase !== undefined ? String(forcedPhase) : phase
|
|
||||||
const list = useQuery({
|
const list = useQuery({
|
||||||
queryKey: ['pe-list', { typeFilter, pendingMe, search, phase: effectivePhase }],
|
queryKey: ['pe-list', { typeFilter, pendingMe, search, phase }],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (pendingMe) {
|
if (pendingMe) {
|
||||||
const res = await api.get<PeListItem[]>('/purchase-evaluations/inbox', {
|
const res = await api.get<PeListItem[]>('/purchase-evaluations/inbox', {
|
||||||
@ -70,14 +70,17 @@ export function PeListPanel({
|
|||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
search: search || undefined,
|
search: search || undefined,
|
||||||
type: typeFilter ?? undefined,
|
type: typeFilter ?? undefined,
|
||||||
phase: effectivePhase || undefined,
|
phase: phase || undefined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return res.data
|
return res.data
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const rows = list.data?.items ?? []
|
const allRows = list.data?.items ?? []
|
||||||
|
const rows = editableOnly
|
||||||
|
? allRows.filter(p => isEditablePhase(p.phase))
|
||||||
|
: allRows
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="flex flex-col overflow-hidden border-r border-slate-200 bg-white">
|
<aside className="flex flex-col overflow-hidden border-r border-slate-200 bg-white">
|
||||||
@ -88,7 +91,7 @@ export function PeListPanel({
|
|||||||
Danh sách phiếu
|
Danh sách phiếu
|
||||||
</div>
|
</div>
|
||||||
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-medium text-slate-600">
|
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-medium text-slate-600">
|
||||||
{list.data?.total ?? 0}
|
{rows.length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@ -100,17 +103,17 @@ export function PeListPanel({
|
|||||||
className="pl-8"
|
className="pl-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{forcedPhase === undefined ? (
|
{editableOnly ? (
|
||||||
|
<div className="rounded border border-slate-200 bg-slate-50 px-2 py-1.5 text-[11px] text-slate-600">
|
||||||
|
Lọc cố định: <strong>Bản nháp + Trả lại</strong> (chỉ phiếu sửa được)
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<Select value={phase} onChange={e => onPhaseChange(e.target.value)}>
|
<Select value={phase} onChange={e => onPhaseChange(e.target.value)}>
|
||||||
<option value="">Tất cả trạng thái</option>
|
<option value="">Tất cả trạng thái</option>
|
||||||
{Object.values(PeDisplayStatus).map(s => (
|
{Object.values(PeDisplayStatus).map(s => (
|
||||||
<option key={s} value={statusToPhaseValue(s)}>{PeDisplayStatusLabel[s]}</option>
|
<option key={s} value={statusToPhaseValue(s)}>{PeDisplayStatusLabel[s]}</option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
) : (
|
|
||||||
<div className="rounded border border-slate-200 bg-slate-50 px-2 py-1.5 text-[11px] text-slate-600">
|
|
||||||
Lọc cố định: <strong>{PeDisplayStatusLabel[getPeDisplayStatus(forcedPhase)]}</strong>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -179,16 +182,30 @@ export function PeListPanel({
|
|||||||
<div className="mt-1 text-[10px] text-brand-600">✓ Đã tạo HĐ</div>
|
<div className="mt-1 text-[10px] text-brand-600">✓ Đã tạo HĐ</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
{/* Edit pencil — visible on hover (chỉ khi onEditClick được truyền) */}
|
{/* Edit pencil — LUÔN visible (user 2026-05-07).
|
||||||
{onEditClick && (
|
Bright/active khi phase editable (DangSoanThao + TraLai).
|
||||||
<button
|
Dim/disabled khi phase không edit được (Đã gửi duyệt / Đã duyệt
|
||||||
onClick={() => onEditClick(p.id)}
|
/ Từ chối) — click không có tác dụng. */}
|
||||||
className="absolute right-2 top-2 rounded p-1.5 text-slate-400 opacity-0 transition group-hover:opacity-100 hover:bg-white hover:text-brand-600 hover:shadow-sm"
|
{onEditClick && (() => {
|
||||||
title="Sửa thông tin gói thầu"
|
const editable = isEditablePhase(p.phase)
|
||||||
>
|
return (
|
||||||
<Pencil className="h-3.5 w-3.5" />
|
<button
|
||||||
</button>
|
onClick={() => editable && onEditClick(p.id)}
|
||||||
)}
|
disabled={!editable}
|
||||||
|
className={cn(
|
||||||
|
'absolute right-2 top-2 rounded p-1.5 transition',
|
||||||
|
editable
|
||||||
|
? 'text-brand-600 hover:bg-brand-50 hover:shadow-sm cursor-pointer'
|
||||||
|
: 'text-slate-300 cursor-not-allowed',
|
||||||
|
)}
|
||||||
|
title={editable
|
||||||
|
? 'Sửa phiếu (header + chi tiết)'
|
||||||
|
: 'Phiếu đã gửi duyệt / đã duyệt / từ chối — không sửa được'}
|
||||||
|
>
|
||||||
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -20,7 +20,6 @@ import { PeWorkspaceCreateView } from '@/components/pe/PeWorkspaceCreateView'
|
|||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { getErrorMessage } from '@/lib/apiError'
|
import { getErrorMessage } from '@/lib/apiError'
|
||||||
import {
|
import {
|
||||||
PurchaseEvaluationPhase,
|
|
||||||
PurchaseEvaluationType,
|
PurchaseEvaluationType,
|
||||||
PurchaseEvaluationTypeLabel,
|
PurchaseEvaluationTypeLabel,
|
||||||
type PeDetailBundle,
|
type PeDetailBundle,
|
||||||
@ -93,7 +92,7 @@ export function PurchaseEvaluationWorkspacePage() {
|
|||||||
showCreateButton
|
showCreateButton
|
||||||
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' })}
|
||||||
forcedPhase={PurchaseEvaluationPhase.DangSoanThao}
|
editableOnly
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Panel 2: Empty | Header form | Detail tabs (workspace mode) */}
|
{/* Panel 2: Empty | Header form | Detail tabs (workspace mode) */}
|
||||||
|
|||||||
@ -24,7 +24,8 @@ export const PurchaseEvaluationPhase = {
|
|||||||
ChoCEODuyetPA: 5,
|
ChoCEODuyetPA: 5,
|
||||||
ChoCEODuyetNCC: 6,
|
ChoCEODuyetNCC: 6,
|
||||||
DaDuyet: 7,
|
DaDuyet: 7,
|
||||||
TuChoi: 99,
|
TraLai: 98, // approver trả về Drafter sửa — vẫn cho edit
|
||||||
|
TuChoi: 99, // terminal từ chối — KHÔNG edit
|
||||||
} as const
|
} as const
|
||||||
export type PurchaseEvaluationPhase = typeof PurchaseEvaluationPhase[keyof typeof PurchaseEvaluationPhase]
|
export type PurchaseEvaluationPhase = typeof PurchaseEvaluationPhase[keyof typeof PurchaseEvaluationPhase]
|
||||||
|
|
||||||
@ -36,6 +37,7 @@ export const PurchaseEvaluationPhaseLabel: Record<number, string> = {
|
|||||||
5: 'Chờ CEO duyệt PA',
|
5: 'Chờ CEO duyệt PA',
|
||||||
6: 'Chờ CEO duyệt NCC',
|
6: 'Chờ CEO duyệt NCC',
|
||||||
7: 'Đã duyệt',
|
7: 'Đã duyệt',
|
||||||
|
98: 'Trả lại',
|
||||||
99: 'Từ chối',
|
99: 'Từ chối',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,9 +49,17 @@ export const PurchaseEvaluationPhaseColor: Record<number, string> = {
|
|||||||
5: 'bg-fuchsia-100 text-fuchsia-700',
|
5: 'bg-fuchsia-100 text-fuchsia-700',
|
||||||
6: 'bg-pink-100 text-pink-700',
|
6: 'bg-pink-100 text-pink-700',
|
||||||
7: 'bg-emerald-100 text-emerald-700',
|
7: 'bg-emerald-100 text-emerald-700',
|
||||||
|
98: 'bg-yellow-100 text-yellow-800',
|
||||||
99: 'bg-red-100 text-red-700',
|
99: 'bg-red-100 text-red-700',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase nào được phép edit phiếu (Drafter sửa header + detail).
|
||||||
|
// User 2026-05-07: chỉ Đang soạn thảo + Trả lại (Từ chối là terminal, không edit).
|
||||||
|
export function isEditablePhase(phase: number): boolean {
|
||||||
|
return phase === PurchaseEvaluationPhase.DangSoanThao
|
||||||
|
|| phase === PurchaseEvaluationPhase.TraLai
|
||||||
|
}
|
||||||
|
|
||||||
// Display status meta — gom các phase chi tiết thành 4 nhóm hiển thị end-user
|
// Display status meta — gom các phase chi tiết thành 4 nhóm hiển thị end-user
|
||||||
// friendly. Workflow timeline + workflow service vẫn dùng phase chi tiết.
|
// friendly. Workflow timeline + workflow service vẫn dùng phase chi tiết.
|
||||||
// User 2026-05-07 chỉnh:
|
// User 2026-05-07 chỉnh:
|
||||||
@ -60,6 +70,7 @@ export const PurchaseEvaluationPhaseColor: Record<number, string> = {
|
|||||||
export const PeDisplayStatus = {
|
export const PeDisplayStatus = {
|
||||||
BanNhap: 'BanNhap',
|
BanNhap: 'BanNhap',
|
||||||
DaGuiDuyet: 'DaGuiDuyet',
|
DaGuiDuyet: 'DaGuiDuyet',
|
||||||
|
TraLai: 'TraLai',
|
||||||
DaDuyet: 'DaDuyet',
|
DaDuyet: 'DaDuyet',
|
||||||
TuChoi: 'TuChoi',
|
TuChoi: 'TuChoi',
|
||||||
} as const
|
} as const
|
||||||
@ -68,6 +79,7 @@ export type PeDisplayStatus = typeof PeDisplayStatus[keyof typeof PeDisplayStatu
|
|||||||
export const PeDisplayStatusLabel: Record<PeDisplayStatus, string> = {
|
export const PeDisplayStatusLabel: Record<PeDisplayStatus, string> = {
|
||||||
BanNhap: 'Bản nháp',
|
BanNhap: 'Bản nháp',
|
||||||
DaGuiDuyet: 'Đã gửi duyệt',
|
DaGuiDuyet: 'Đã gửi duyệt',
|
||||||
|
TraLai: 'Trả lại',
|
||||||
DaDuyet: 'Đã duyệt',
|
DaDuyet: 'Đã duyệt',
|
||||||
TuChoi: 'Từ chối',
|
TuChoi: 'Từ chối',
|
||||||
}
|
}
|
||||||
@ -75,6 +87,7 @@ export const PeDisplayStatusLabel: Record<PeDisplayStatus, string> = {
|
|||||||
export const PeDisplayStatusColor: Record<PeDisplayStatus, string> = {
|
export const PeDisplayStatusColor: Record<PeDisplayStatus, string> = {
|
||||||
BanNhap: 'bg-slate-100 text-slate-700',
|
BanNhap: 'bg-slate-100 text-slate-700',
|
||||||
DaGuiDuyet: 'bg-amber-100 text-amber-700',
|
DaGuiDuyet: 'bg-amber-100 text-amber-700',
|
||||||
|
TraLai: 'bg-yellow-100 text-yellow-800',
|
||||||
DaDuyet: 'bg-emerald-100 text-emerald-700',
|
DaDuyet: 'bg-emerald-100 text-emerald-700',
|
||||||
TuChoi: 'bg-red-100 text-red-700',
|
TuChoi: 'bg-red-100 text-red-700',
|
||||||
}
|
}
|
||||||
@ -82,6 +95,7 @@ export const PeDisplayStatusColor: Record<PeDisplayStatus, string> = {
|
|||||||
export function getPeDisplayStatus(phase: number): PeDisplayStatus {
|
export function getPeDisplayStatus(phase: number): PeDisplayStatus {
|
||||||
if (phase === PurchaseEvaluationPhase.DangSoanThao) return PeDisplayStatus.BanNhap
|
if (phase === PurchaseEvaluationPhase.DangSoanThao) return PeDisplayStatus.BanNhap
|
||||||
if (phase === PurchaseEvaluationPhase.DaDuyet) return PeDisplayStatus.DaDuyet
|
if (phase === PurchaseEvaluationPhase.DaDuyet) return PeDisplayStatus.DaDuyet
|
||||||
|
if (phase === PurchaseEvaluationPhase.TraLai) return PeDisplayStatus.TraLai
|
||||||
if (phase === PurchaseEvaluationPhase.TuChoi) return PeDisplayStatus.TuChoi
|
if (phase === PurchaseEvaluationPhase.TuChoi) return PeDisplayStatus.TuChoi
|
||||||
return PeDisplayStatus.DaGuiDuyet
|
return PeDisplayStatus.DaGuiDuyet
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,5 +16,6 @@ public enum PurchaseEvaluationPhase
|
|||||||
ChoCEODuyetPA = 5, // chỉ B (duyệt phương án trước)
|
ChoCEODuyetPA = 5, // chỉ B (duyệt phương án trước)
|
||||||
ChoCEODuyetNCC = 6, // chung cả A & B — duyệt chọn đơn vị
|
ChoCEODuyetNCC = 6, // chung cả A & B — duyệt chọn đơn vị
|
||||||
DaDuyet = 7, // terminal thành công
|
DaDuyet = 7, // terminal thành công
|
||||||
TuChoi = 99, // terminal từ chối
|
TraLai = 98, // approver trả về cho Drafter sửa (vẫn cho edit, khác TuChoi)
|
||||||
|
TuChoi = 99, // terminal từ chối — KHÔNG cho edit/thao tác
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user