[CLAUDE] FE: PE detail flat layout — Panel 2 gop 3 section, Panel 3 them approvals + history
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m59s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m59s
User request: 'cho tat ca cai nay the hien tren dung 1 man hinh nhe, cai duyet va lich su thi dua sang panel 3'. Panel 2 (PeDetailTabs): truoc 5 tab (Info/NCC/Items/Approvals/History). Sau bo tabs, flat render 3 section stack doc voi divider + title uppercase: Thong tin → NCC tham gia (N) → Hang muc + Bao gia (N) Panel 3 (PeWorkflowPanel): truoc chi workflow timeline + transition btn. Sau them 2 section ben duoi: Workflow timeline → Lich su duyet (PeApprovalsSection) → Lich su thay doi (PeHistorySection) Export PeApprovalsSection + PeHistorySection tu PeDetailTabs — reuse ApprovalsTab + HistoryTab logic cu, wrap them <h3> section title. Dong bo ca fe-admin + fe-user (copy identical file).
This commit is contained in:
@ -1,6 +1,7 @@
|
|||||||
// Detail tabs cho 1 phiếu Duyệt NCC: Thông tin / NCC / Hạng mục + Báo giá /
|
// Detail content cho 1 phiếu Duyệt NCC. Flat render (no tabs): Thông tin +
|
||||||
// Duyệt / Lịch sử. Inline action dialog để add NCC, add Detail, upsert Quote,
|
// NCC + Hạng mục + Báo giá stack vertically trong 1 màn hình.
|
||||||
// select winner.
|
// Duyệt history + Lịch sử thay đổi → moved to Panel 3 (xem PeWorkflowPanel
|
||||||
|
// → PeApprovalsSection + PeHistorySection).
|
||||||
import { useState } from 'react'
|
import { 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'
|
||||||
@ -27,10 +28,10 @@ import {
|
|||||||
} from '@/types/purchaseEvaluation'
|
} from '@/types/purchaseEvaluation'
|
||||||
import type { Supplier } from '@/types/master'
|
import type { Supplier } from '@/types/master'
|
||||||
|
|
||||||
type TabKey = 'info' | 'suppliers' | 'items' | 'approvals' | 'history'
|
|
||||||
|
|
||||||
const fmtMoney = (v: number) => v.toLocaleString('vi-VN')
|
const fmtMoney = (v: number) => v.toLocaleString('vi-VN')
|
||||||
|
|
||||||
|
// Main detail content — flat render 3 section không tabs.
|
||||||
|
// Tên giữ PeDetailTabs để không break callsite (rename gây churn).
|
||||||
export function PeDetailTabs({
|
export function PeDetailTabs({
|
||||||
evaluation,
|
evaluation,
|
||||||
onBack,
|
onBack,
|
||||||
@ -40,7 +41,6 @@ export function PeDetailTabs({
|
|||||||
onBack: () => void
|
onBack: () => void
|
||||||
onDelete: () => void
|
onDelete: () => void
|
||||||
}) {
|
}) {
|
||||||
const [tab, setTab] = useState<TabKey>('info')
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const isDraft = evaluation.phase === PurchaseEvaluationPhase.DangSoanThao
|
const isDraft = evaluation.phase === PurchaseEvaluationPhase.DangSoanThao
|
||||||
|
|
||||||
@ -83,42 +83,50 @@ export function PeDetailTabs({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex gap-1 border-b border-slate-200 px-3 pt-2">
|
<div className="divide-y divide-slate-200">
|
||||||
{(
|
<Section title="Thông tin">
|
||||||
[
|
<InfoTab ev={evaluation} />
|
||||||
['info', 'Thông tin'],
|
</Section>
|
||||||
['suppliers', `NCC (${evaluation.suppliers.length})`],
|
<Section title={`NCC tham gia (${evaluation.suppliers.length})`}>
|
||||||
['items', `Hạng mục (${evaluation.details.length})`],
|
<SuppliersTab ev={evaluation} />
|
||||||
['approvals', `Duyệt (${evaluation.approvals.length})`],
|
</Section>
|
||||||
['history', 'Lịch sử'],
|
<Section title={`Hạng mục + Báo giá (${evaluation.details.length})`}>
|
||||||
] as const
|
<ItemsTab ev={evaluation} />
|
||||||
).map(([k, lbl]) => (
|
</Section>
|
||||||
<button
|
|
||||||
key={k}
|
|
||||||
onClick={() => setTab(k)}
|
|
||||||
className={cn(
|
|
||||||
'rounded-t-md border-b-2 px-3 py-1.5 text-xs font-medium transition',
|
|
||||||
tab === k
|
|
||||||
? 'border-brand-500 text-brand-700'
|
|
||||||
: 'border-transparent text-slate-500 hover:text-slate-700',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{lbl}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div className="p-5">
|
|
||||||
{tab === 'info' && <InfoTab ev={evaluation} />}
|
|
||||||
{tab === 'suppliers' && <SuppliersTab ev={evaluation} />}
|
|
||||||
{tab === 'items' && <ItemsTab ev={evaluation} />}
|
|
||||||
{tab === 'approvals' && <ApprovalsTab ev={evaluation} />}
|
|
||||||
{tab === 'history' && <HistoryTab ev={evaluation} />}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<section className="px-5 py-4">
|
||||||
|
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-500">{title}</h3>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Exports cho Panel 3 — Approvals history + Changelog =====
|
||||||
|
|
||||||
|
export function PeApprovalsSection({ ev }: { ev: PeDetailBundle }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2 text-sm font-semibold text-slate-900">Lịch sử duyệt ({ev.approvals.length})</h3>
|
||||||
|
<ApprovalsTab ev={ev} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PeHistorySection({ ev }: { ev: PeDetailBundle }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2 text-sm font-semibold text-slate-900">Lịch sử thay đổi</h3>
|
||||||
|
<HistoryTab ev={ev} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ===== Tab: Thông tin =====
|
// ===== Tab: Thông tin =====
|
||||||
function InfoTab({ ev }: { ev: PeDetailBundle }) {
|
function InfoTab({ ev }: { ev: PeDetailBundle }) {
|
||||||
const canCreateContract = ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.selectedSupplierId
|
const canCreateContract = ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.selectedSupplierId
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
// Panel 3: workflow + transition buttons. Pulls nextPhases từ BE bundle
|
// Panel 3: workflow timeline + transition buttons + approval history + changelog.
|
||||||
// (single source of truth) → render per-phase action button.
|
// Pulls nextPhases từ BE bundle (single source of truth) → render per-phase
|
||||||
|
// action button. Approvals + History moved here from PeDetailTabs (2 section
|
||||||
|
// dưới cùng) để Panel 2 tập trung hiển thị nội dung phiếu (Info + NCC + Items).
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
@ -16,6 +18,7 @@ import {
|
|||||||
PurchaseEvaluationPhaseLabel,
|
PurchaseEvaluationPhaseLabel,
|
||||||
type PeDetailBundle,
|
type PeDetailBundle,
|
||||||
} from '@/types/purchaseEvaluation'
|
} from '@/types/purchaseEvaluation'
|
||||||
|
import { PeApprovalsSection, PeHistorySection } from './PeDetailTabs'
|
||||||
|
|
||||||
export function PeWorkflowPanel({ evaluation }: { evaluation: PeDetailBundle }) {
|
export function PeWorkflowPanel({ evaluation }: { evaluation: PeDetailBundle }) {
|
||||||
const [target, setTarget] = useState<number | null>(null)
|
const [target, setTarget] = useState<number | null>(null)
|
||||||
@ -112,6 +115,14 @@ export function PeWorkflowPanel({ evaluation }: { evaluation: PeDetailBundle })
|
|||||||
<Textarea value={comment} onChange={e => setComment(e.target.value)} rows={3} />
|
<Textarea value={comment} onChange={e => setComment(e.target.value)} rows={3} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="border-t border-slate-200 pt-4">
|
||||||
|
<PeApprovalsSection ev={evaluation} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-slate-200 pt-4">
|
||||||
|
<PeHistorySection ev={evaluation} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
// Detail tabs cho 1 phiếu Duyệt NCC: Thông tin / NCC / Hạng mục + Báo giá /
|
// Detail content cho 1 phiếu Duyệt NCC. Flat render (no tabs): Thông tin +
|
||||||
// Duyệt / Lịch sử. Inline action dialog để add NCC, add Detail, upsert Quote,
|
// NCC + Hạng mục + Báo giá stack vertically trong 1 màn hình.
|
||||||
// select winner.
|
// Duyệt history + Lịch sử thay đổi → moved to Panel 3 (xem PeWorkflowPanel
|
||||||
|
// → PeApprovalsSection + PeHistorySection).
|
||||||
import { useState } from 'react'
|
import { 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'
|
||||||
@ -27,10 +28,10 @@ import {
|
|||||||
} from '@/types/purchaseEvaluation'
|
} from '@/types/purchaseEvaluation'
|
||||||
import type { Supplier } from '@/types/master'
|
import type { Supplier } from '@/types/master'
|
||||||
|
|
||||||
type TabKey = 'info' | 'suppliers' | 'items' | 'approvals' | 'history'
|
|
||||||
|
|
||||||
const fmtMoney = (v: number) => v.toLocaleString('vi-VN')
|
const fmtMoney = (v: number) => v.toLocaleString('vi-VN')
|
||||||
|
|
||||||
|
// Main detail content — flat render 3 section không tabs.
|
||||||
|
// Tên giữ PeDetailTabs để không break callsite (rename gây churn).
|
||||||
export function PeDetailTabs({
|
export function PeDetailTabs({
|
||||||
evaluation,
|
evaluation,
|
||||||
onBack,
|
onBack,
|
||||||
@ -40,7 +41,6 @@ export function PeDetailTabs({
|
|||||||
onBack: () => void
|
onBack: () => void
|
||||||
onDelete: () => void
|
onDelete: () => void
|
||||||
}) {
|
}) {
|
||||||
const [tab, setTab] = useState<TabKey>('info')
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const isDraft = evaluation.phase === PurchaseEvaluationPhase.DangSoanThao
|
const isDraft = evaluation.phase === PurchaseEvaluationPhase.DangSoanThao
|
||||||
|
|
||||||
@ -83,42 +83,50 @@ export function PeDetailTabs({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex gap-1 border-b border-slate-200 px-3 pt-2">
|
<div className="divide-y divide-slate-200">
|
||||||
{(
|
<Section title="Thông tin">
|
||||||
[
|
<InfoTab ev={evaluation} />
|
||||||
['info', 'Thông tin'],
|
</Section>
|
||||||
['suppliers', `NCC (${evaluation.suppliers.length})`],
|
<Section title={`NCC tham gia (${evaluation.suppliers.length})`}>
|
||||||
['items', `Hạng mục (${evaluation.details.length})`],
|
<SuppliersTab ev={evaluation} />
|
||||||
['approvals', `Duyệt (${evaluation.approvals.length})`],
|
</Section>
|
||||||
['history', 'Lịch sử'],
|
<Section title={`Hạng mục + Báo giá (${evaluation.details.length})`}>
|
||||||
] as const
|
<ItemsTab ev={evaluation} />
|
||||||
).map(([k, lbl]) => (
|
</Section>
|
||||||
<button
|
|
||||||
key={k}
|
|
||||||
onClick={() => setTab(k)}
|
|
||||||
className={cn(
|
|
||||||
'rounded-t-md border-b-2 px-3 py-1.5 text-xs font-medium transition',
|
|
||||||
tab === k
|
|
||||||
? 'border-brand-500 text-brand-700'
|
|
||||||
: 'border-transparent text-slate-500 hover:text-slate-700',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{lbl}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div className="p-5">
|
|
||||||
{tab === 'info' && <InfoTab ev={evaluation} />}
|
|
||||||
{tab === 'suppliers' && <SuppliersTab ev={evaluation} />}
|
|
||||||
{tab === 'items' && <ItemsTab ev={evaluation} />}
|
|
||||||
{tab === 'approvals' && <ApprovalsTab ev={evaluation} />}
|
|
||||||
{tab === 'history' && <HistoryTab ev={evaluation} />}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<section className="px-5 py-4">
|
||||||
|
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-500">{title}</h3>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Exports cho Panel 3 — Approvals history + Changelog =====
|
||||||
|
|
||||||
|
export function PeApprovalsSection({ ev }: { ev: PeDetailBundle }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2 text-sm font-semibold text-slate-900">Lịch sử duyệt ({ev.approvals.length})</h3>
|
||||||
|
<ApprovalsTab ev={ev} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PeHistorySection({ ev }: { ev: PeDetailBundle }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-2 text-sm font-semibold text-slate-900">Lịch sử thay đổi</h3>
|
||||||
|
<HistoryTab ev={ev} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ===== Tab: Thông tin =====
|
// ===== Tab: Thông tin =====
|
||||||
function InfoTab({ ev }: { ev: PeDetailBundle }) {
|
function InfoTab({ ev }: { ev: PeDetailBundle }) {
|
||||||
const canCreateContract = ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.selectedSupplierId
|
const canCreateContract = ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.selectedSupplierId
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
// Panel 3: workflow + transition buttons. Pulls nextPhases từ BE bundle
|
// Panel 3: workflow timeline + transition buttons + approval history + changelog.
|
||||||
// (single source of truth) → render per-phase action button.
|
// Pulls nextPhases từ BE bundle (single source of truth) → render per-phase
|
||||||
|
// action button. Approvals + History moved here from PeDetailTabs (2 section
|
||||||
|
// dưới cùng) để Panel 2 tập trung hiển thị nội dung phiếu (Info + NCC + Items).
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
@ -16,6 +18,7 @@ import {
|
|||||||
PurchaseEvaluationPhaseLabel,
|
PurchaseEvaluationPhaseLabel,
|
||||||
type PeDetailBundle,
|
type PeDetailBundle,
|
||||||
} from '@/types/purchaseEvaluation'
|
} from '@/types/purchaseEvaluation'
|
||||||
|
import { PeApprovalsSection, PeHistorySection } from './PeDetailTabs'
|
||||||
|
|
||||||
export function PeWorkflowPanel({ evaluation }: { evaluation: PeDetailBundle }) {
|
export function PeWorkflowPanel({ evaluation }: { evaluation: PeDetailBundle }) {
|
||||||
const [target, setTarget] = useState<number | null>(null)
|
const [target, setTarget] = useState<number | null>(null)
|
||||||
@ -112,6 +115,14 @@ export function PeWorkflowPanel({ evaluation }: { evaluation: PeDetailBundle })
|
|||||||
<Textarea value={comment} onChange={e => setComment(e.target.value)} rows={3} />
|
<Textarea value={comment} onChange={e => setComment(e.target.value)} rows={3} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="border-t border-slate-200 pt-4">
|
||||||
|
<PeApprovalsSection ev={evaluation} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-slate-200 pt-4">
|
||||||
|
<PeHistorySection ev={evaluation} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user