[CLAUDE] Contract+FE-User: dot-5 menu 6-leaf y-chang-Duyet-NCC + GET /contracts/deleted + bo hero + "Tong quan quy trinh"
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m38s

- BE: ListDeletedContractsQuery (IgnoreQueryFilters + Where(IsDeleted) + IDOR mirror; LUAT thay so-dem theo reviewer FLAG-10) + GET /contracts/deleted [Contracts.Read]
- Seeder: Ct x7 +3 leaf (WfView/Approved/Deleted) + labelBackfill "Dang duyet" x7 + Khkk 6 leaf + Dashboard -> "Tong quan quy trinh" + reviewKeys +23 key
- FE user: matrix +type=3 (HD chung 1 bo V2) + route /contracts/workflow-matrix + Ct regex +3 action + MyContractsPage (phase/deleted + banner + an nut Tao + FLAG-2 guard selectContract khi deleted) + MenuLeaf depth-0 UPPERCASE + UserDashboardPage BO HERO (owner: "bo cai tren di, cai duoi OK") + title "Tong quan quy trinh"
- fe-admin: staticMap +2 Khkk (FLAG-1); leaf Ct_ moi van an theo isAdminHidden (da kiem CLEAN)
- reviewer PASS_WITH_FLAGS 10 FLAG (0H/4M/6L): fix 4M, park 6L co ghi (FLAG-4 dinh nghia "Da duyet" HD -> cho owner)
- build fe x2 + BE PASS; test 562 PASS

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-07-29 14:40:10 +07:00
parent ceeebf3c3b
commit ea962b1e59
10 changed files with 173 additions and 371 deletions

View File

@ -31,10 +31,12 @@ function resolvePath(key: string): string | null {
const staticMap: Record<string, string> = { const staticMap: Record<string, string> = {
// [S159-đợt3] skeleton GĐ2/GĐ4 — leaf trỏ trang "Sắp triển khai" (query giữ // [S159-đợt3] skeleton GĐ2/GĐ4 — leaf trỏ trang "Sắp triển khai" (query giữ
// active-state riêng từng mục; gotcha #50: thiếu route = sidebar drop SILENT). // active-state riêng từng mục; gotcha #50: thiếu route = sidebar drop SILENT).
Khkk_WfView: '/coming-soon?stage=2&m=wfview',
Khkk_List: '/coming-soon?stage=2&m=list', Khkk_List: '/coming-soon?stage=2&m=list',
Khkk_Create: '/coming-soon?stage=2&m=create', Khkk_Create: '/coming-soon?stage=2&m=create',
Khkk_Pending: '/coming-soon?stage=2&m=pending', Khkk_Pending: '/coming-soon?stage=2&m=pending',
Khkk_Approved: '/coming-soon?stage=2&m=approved', Khkk_Approved: '/coming-soon?stage=2&m=approved',
Khkk_Deleted: '/coming-soon?stage=2&m=deleted', // [S159-đợt5 FLAG-1] thiếu là leaf drop SILENT (gotcha #50)
Hdc_ThauPhu: '/coming-soon?stage=4&t=01', Hdc_ThauPhu: '/coming-soon?stage=4&t=01',
Hdc_GiaoKhoan: '/coming-soon?stage=4&t=02', Hdc_GiaoKhoan: '/coming-soon?stage=4&t=02',
Hdc_NhaCungCap: '/coming-soon?stage=4&t=03', Hdc_NhaCungCap: '/coming-soon?stage=4&t=03',

View File

@ -61,6 +61,8 @@ function App() {
<Route path="/my-contracts" element={<MyContractsPage />} /> <Route path="/my-contracts" element={<MyContractsPage />} />
<Route path="/purchase-evaluations" element={<PurchaseEvaluationsListPage />} /> <Route path="/purchase-evaluations" element={<PurchaseEvaluationsListPage />} />
<Route path="/purchase-evaluations/workflow-matrix" element={<WorkflowMatrixViewPage />} /> <Route path="/purchase-evaluations/workflow-matrix" element={<WorkflowMatrixViewPage />} />
{/* [S159-đợt5] alias cho leaf "Luồng duyệt" của 7 nhóm HĐ (?type=3) */}
<Route path="/contracts/workflow-matrix" element={<WorkflowMatrixViewPage />} />
<Route path="/purchase-evaluations/workspace" element={<PurchaseEvaluationWorkspacePage />} /> <Route path="/purchase-evaluations/workspace" element={<PurchaseEvaluationWorkspacePage />} />
<Route path="/purchase-evaluations/new" element={<PurchaseEvaluationCreatePage />} /> <Route path="/purchase-evaluations/new" element={<PurchaseEvaluationCreatePage />} />
<Route path="/purchase-evaluations/:id" element={<PurchaseEvaluationDetailPage />} /> <Route path="/purchase-evaluations/:id" element={<PurchaseEvaluationDetailPage />} />

View File

@ -75,6 +75,9 @@ function resolvePath(key: string): string | null {
// LESSON gotcha #50: key DB mới PHẢI có route ở đây, không thì sidebar drop SILENT. // LESSON gotcha #50: key DB mới PHẢI có route ở đây, không thì sidebar drop SILENT.
KeHoachKyKet: '/dashboard', KeHoachKyKet: '/dashboard',
HopDongCung: '/dashboard', HopDongCung: '/dashboard',
// [S159-đợt5] 2 leaf mới của skeleton GĐ2 (đủ 6 y chang Duyệt NCC)
Khkk_WfView: '/coming-soon?stage=2&m=wfview',
Khkk_Deleted: '/coming-soon?stage=2&m=deleted',
// [Plan CA Hotfix 1 S29 2026-05-22] 4 master + 4 catalog leaf moved từ // [Plan CA Hotfix 1 S29 2026-05-22] 4 master + 4 catalog leaf moved từ
// fe-admin → fe-user. resolvePath PHẢI có route mapping nếu không // fe-admin → fe-user. resolvePath PHẢI có route mapping nếu không
// MenuLeaf line 238 `if (!path) return null` → sidebar drop silent. // MenuLeaf line 238 `if (!path) return null` → sidebar drop silent.
@ -124,7 +127,9 @@ function resolvePath(key: string): string | null {
} }
if (staticMap[key]) return staticMap[key] if (staticMap[key]) return staticMap[key]
const match = key.match(/^Ct_([^_]+)_(List|Create|Pending)$/) // [S159-đợt5 owner "y chang Duyệt NCC"] +WfView/Approved/Deleted. WfView cả 7 loại
// đều ?type=3 (HĐ chung 1 bộ workflow V2) — thêm &ct=N để active-state riêng từng leaf.
const match = key.match(/^Ct_([^_]+)_(List|Create|Pending|WfView|Approved|Deleted)$/)
if (match) { if (match) {
const [, code, action] = match const [, code, action] = match
const typeInt = TYPE_CODE_TO_INT[code] const typeInt = TYPE_CODE_TO_INT[code]
@ -132,6 +137,9 @@ function resolvePath(key: string): string | null {
if (action === 'List') return `/my-contracts?type=${typeInt}` if (action === 'List') return `/my-contracts?type=${typeInt}`
if (action === 'Create') return `/contracts/new?type=${typeInt}` if (action === 'Create') return `/contracts/new?type=${typeInt}`
if (action === 'Pending') return `/inbox?type=${typeInt}` if (action === 'Pending') return `/inbox?type=${typeInt}`
if (action === 'WfView') return `/contracts/workflow-matrix?type=3&ct=${typeInt}`
if (action === 'Approved') return `/my-contracts?type=${typeInt}&phase=9`
if (action === 'Deleted') return `/my-contracts?type=${typeInt}&deleted=1`
} }
// Pe_<Code>_<Action> cho module Duyệt NCC (user side) // Pe_<Code>_<Action> cho module Duyệt NCC (user side)
@ -343,7 +351,15 @@ function MenuLeaf({ node, depth }: { node: MenuNode; depth: number }) {
// Density-first: active leaf gets a brand left-rail + tint (crisp // Density-first: active leaf gets a brand left-rail + tint (crisp
// selected affordance, NAMGROUP). Inactive stays quiet slate. // selected affordance, NAMGROUP). Inactive stays quiet slate.
'relative block rounded-md leading-snug transition-colors', 'relative block rounded-md leading-snug transition-colors',
isDeep ? 'px-3 py-1 text-[11px]' : 'px-3 py-1.5 text-[12px] font-medium', // [S159-đợt5 owner] leaf cấp-0 render qua MenuLeaf ("Tổng quan quy trình") viết
// HOA đồng bộ group header cấp-0 (khuôn MenuGroup :260). 🔴 KHAI ĐÚNG PHẠM VI
// (reviewer FLAG-3): "Hộp thư" KHÔNG đi qua đây — nó là USER_FIXED_TOP render
// bằng StaticLeaf (lớp CSS riêng, vẫn chữ thường). Đồng bộ nốt = quyết owner.
depth === 0
? 'px-3 py-2 text-[11px] font-semibold uppercase tracking-wide'
: isDeep
? 'px-3 py-1 text-[11px]'
: 'px-3 py-1.5 text-[12px] font-medium',
isActive isActive
? 'bg-brand-50 font-semibold text-brand-700 before:absolute before:inset-y-1 before:left-0 before:w-0.5 before:rounded-full before:bg-brand-600' ? 'bg-brand-50 font-semibold text-brand-700 before:absolute before:inset-y-1 before:left-0 before:w-0.5 before:rounded-full before:bg-brand-600'
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900', : 'text-slate-600 hover:bg-slate-100 hover:text-slate-900',

View File

@ -52,7 +52,6 @@ import { useNavigate } from 'react-router-dom'
import { import {
AlertTriangle, AlertTriangle,
ArrowRight, ArrowRight,
ChevronDown,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
ClipboardCheck, ClipboardCheck,
@ -65,7 +64,6 @@ import {
Search, Search,
SearchX, SearchX,
Stamp, Stamp,
Workflow,
X, X,
} from 'lucide-react' } from 'lucide-react'
import { PageHeader } from '@/components/PageHeader' import { PageHeader } from '@/components/PageHeader'
@ -82,7 +80,6 @@ import { ContractTypeLabel } from '@/types/forms'
import { PeFinalizeChip } from '@/components/pe/PeFinalizeChip' import { PeFinalizeChip } from '@/components/pe/PeFinalizeChip'
import { PeUrgentChips } from '@/components/pe/PeUrgentChips' import { PeUrgentChips } from '@/components/pe/PeUrgentChips'
import { import {
PurchaseEvaluationPhase,
getPeDisplayStatus, getPeDisplayStatus,
PeDisplayStatus, PeDisplayStatus,
PeDisplayStatusColor, PeDisplayStatusColor,
@ -90,14 +87,6 @@ import {
type PeListItem, type PeListItem,
} from '@/types/purchaseEvaluation' } from '@/types/purchaseEvaluation'
type MyDashboard = {
draftsInProgress: number
pendingMyApproval: number
dueSoon: number
overdue: number
draftsTotalValue: number
}
const PAGE_SIZE = 10 const PAGE_SIZE = 10
const fmtMoney = (v: number) => { const fmtMoney = (v: number) => {
@ -153,177 +142,6 @@ const stageOf = (n: number) => STAGES.find(s => s.n === n) ?? STAGES[0]
HERO — dải pipeline 4 giai đoạn (GIỮ NGUYÊN, owner đã duyệt) HERO — dải pipeline 4 giai đoạn (GIỮ NGUYÊN, owner đã duyệt)
───────────────────────────────────────────────────────────────────────────── */ ───────────────────────────────────────────────────────────────────────────── */
// Giai đoạn LIVE mang accent (brand / teal); giai đoạn "sắp triển khai" = khung
// nét đứt xám trung tính — MỜ nhưng KHÔNG đỏ/vàng, để không đọc nhầm thành lỗi.
const STAGE_TONE: Record<StageTone, {
rail: string; chipBg: string; chipFg: string
badge: string; head: string; row: string; num: string; link: string
}> = {
brand: {
rail: 'var(--color-brand-500)',
chipBg: 'var(--color-brand-50)',
chipFg: 'var(--color-brand-600)',
badge: 'bg-brand-50 text-brand-700 ring-brand-200',
head: 'text-brand-800!',
row: 'hover:bg-brand-50 focus-visible:ring-brand-500',
num: 'text-brand-700!',
link: 'text-brand-700 hover:text-brand-800 focus-visible:ring-brand-500',
},
teal: {
rail: 'var(--color-teal-500)',
chipBg: 'var(--color-teal-50)',
chipFg: 'var(--color-teal-700)',
badge: 'bg-teal-50 text-teal-700 ring-teal-100',
head: 'text-teal-700!',
row: 'hover:bg-teal-50 focus-visible:ring-teal-500',
num: 'text-teal-700!',
link: 'text-teal-700 hover:text-teal-600 focus-visible:ring-teal-500',
},
}
// Khung 1 giai đoạn — nhãn/icon/tone đọc TỪ `STAGES`, không truyền chuỗi rời.
function StageCard({ stage, children }: { stage: StageMeta; children: React.ReactNode }) {
const { n, title, stepRange, icon: Icon, tone } = stage
const t = tone ? STAGE_TONE[tone] : null
return (
<div
className={cn(
'flex h-full flex-col rounded-xl p-3.5',
t ? 'card-accent' : 'border border-dashed border-slate-300 bg-slate-50',
)}
style={t ? { ['--accent' as string]: t.rail } : undefined}
>
<div className="flex items-start justify-between gap-2">
<span
className={cn(
'rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide ring-1 ring-inset',
t ? t.badge : 'bg-white text-slate-600 ring-slate-300',
)}
>
Giai đoạn {n}
</span>
<span
className="icon-chip shrink-0"
style={
t
? { ['--chip-bg' as string]: t.chipBg, ['--chip-fg' as string]: t.chipFg }
: { ['--chip-bg' as string]: '#ffffff', ['--chip-fg' as string]: '#64748b' }
}
>
<Icon className="h-4 w-4" />
</span>
</div>
<h3 className={cn('mt-2 text-[15px] leading-tight', t ? t.head : 'text-slate-600!')}>{title}</h3>
<div className="mt-1 text-[11px] font-medium text-slate-500">{stepRange}</div>
<div className="mt-3 flex-1">{children}</div>
</div>
)
}
// Mũi tên nối giữa 2 giai đoạn — ngang trên desktop, xuống dòng trên mobile.
function StageArrow() {
return (
<div aria-hidden className="flex items-center justify-center py-0.5 lg:px-0.5">
<ChevronDown className="h-4 w-4 text-slate-400 lg:hidden" />
<ChevronRight className="hidden h-5 w-5 text-slate-400 lg:block" />
</div>
)
}
// 1 dòng số liệu trong giai đoạn LIVE — bấm được, mở đúng màn hình đã lọc sẵn.
// Là <button> ĐỘC LẬP (không lồng trong button cha) để giữ HTML/a11y hợp lệ.
function StageStatRow({
group,
label,
value,
tone,
loading,
error,
emphasis,
onClick,
}: {
// Tên giai đoạn — vào aria-label để 2 nút cùng chữ "Chờ tôi duyệt" ở GĐ1/GĐ3
// đọc ra KHÁC nhau trên screen-reader (reviewer FLAG-1, WCAG 2.4.6).
group: string
label: string
value: number | undefined
tone: StageTone
loading: boolean
error: boolean
emphasis?: 'accent' | 'danger'
onClick: () => void
}) {
const t = STAGE_TONE[tone]
const highlight = !loading && !error && typeof value === 'number' && value > 0 && emphasis
return (
<button
type="button"
aria-label={`${group}${label}${typeof value === 'number' ? `: ${value}` : ''}`}
onClick={onClick}
className={cn(
// py-1.5 → vùng bấm ~32px cao, thoải mái trên cảm ứng (WCAG 2.5.8 tối thiểu 24px).
'flex w-full items-center justify-between gap-2 rounded-md px-1.5 py-1.5 text-left transition',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1',
t.row,
)}
>
<span className="min-w-0 text-[11px] font-medium leading-tight text-slate-600">{label}</span>
{loading ? (
<span aria-hidden className="h-4 w-7 animate-pulse rounded bg-slate-200 motion-reduce:animate-none" />
) : error ? (
<span className="text-sm font-semibold text-slate-500" title="Không tải được số liệu">
</span>
) : (
<span
className={cn(
'stat-value shrink-0 text-base tabular-nums',
highlight === 'accent' && t.num,
highlight === 'danger' && 'text-accent-600!',
)}
>
{value ?? 0}
</span>
)}
</button>
)
}
// Link mở module của giai đoạn LIVE.
function StageLink({ label, tone, onClick }: { label: string; tone: StageTone; onClick: () => void }) {
const t = STAGE_TONE[tone]
return (
<button
type="button"
onClick={onClick}
className={cn(
'mt-2 inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[11px] font-semibold transition',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1',
t.link,
)}
>
{label}
<ArrowRight className="h-3.5 w-3.5" />
</button>
)
}
// Thân của giai đoạn chưa triển khai — badge trung tính + 1 dòng mô tả.
function StageSoon({ description, hint }: { description?: string; hint?: string }) {
return (
<div>
<span className="inline-flex items-center gap-1 rounded-full bg-slate-200/80 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-slate-600">
<Hourglass className="h-3 w-3" />
Sắp triển khai
</span>
<p className="mt-2 text-[11px] leading-relaxed text-slate-600">{description}</p>
{hint && <p className="mt-1.5 text-[11px] font-semibold text-slate-500">{hint}</p>}
</div>
)
}
/* ───────────────────────────────────────────────────────────────────────────── /* ─────────────────────────────────────────────────────────────────────────────
TẦNG LIÊN KẾT 2 — "Hành trình": mini-strip 4 chấm trên TỪNG DÒNG. TẦNG LIÊN KẾT 2 — "Hành trình": mini-strip 4 chấm trên TỪNG DÒNG.
Chấm của chính giai đoạn dòng đó mang trạng thái phiếu; 3 chấm còn lại mờ + Chấm của chính giai đoạn dòng đó mang trạng thái phiếu; 3 chấm còn lại mờ +
@ -1084,11 +902,6 @@ export function UserDashboardPage() {
/* ── Số liệu hero (5 query nền, y như trước — chỉ 2 query list rút pageSize 5 → 1 /* ── Số liệu hero (5 query nền, y như trước — chỉ 2 query list rút pageSize 5 → 1
vì 2 danh sách "gần đây" đã bị gỡ, hero chỉ cần `.total`). ───────────── */ vì 2 danh sách "gần đây" đã bị gỡ, hero chỉ cần `.total`). ───────────── */
const stats = useQuery({
queryKey: ['my-dashboard'],
queryFn: async () => (await api.get<MyDashboard>('/reports/my-dashboard')).data,
})
const peTotalQuery = useQuery({ const peTotalQuery = useQuery({
queryKey: ['pipeline-pe-total'], queryKey: ['pipeline-pe-total'],
queryFn: async () => queryFn: async () =>
@ -1102,46 +915,6 @@ export function UserDashboardPage() {
(await api.get<Paged<ContractListItem>>('/contracts', { params: { page: 1, pageSize: 1 } })).data.total, (await api.get<Paged<ContractListItem>>('/contracts', { params: { page: 1, pageSize: 1 } })).data.total,
}) })
// [S159] số phiếu Duyệt NCC ĐÃ DUYỆT (phase=7). pageSize=1 vì chỉ cần `total`.
const peApprovedQuery = useQuery({
queryKey: ['pipeline-pe-approved'],
queryFn: async () =>
(
await api.get<Paged<PeListItem>>('/purchase-evaluations', {
params: { page: 1, pageSize: 1, phase: PurchaseEvaluationPhase.DaDuyet },
})
).data.total,
})
// [S159] phiếu Duyệt NCC đang chờ CHÍNH TÔI duyệt. Endpoint /inbox trả mảng phẳng.
// 🔴 DÙNG CHUNG queryKey + queryFn với InboxPage:77-78 (reviewer FLAG-5) — cache chia sẻ,
// 2 màn luôn khớp số. CẤM đổi queryFn trả `.length` dưới key này: InboxPage cache MẢNG,
// trùng key khác shape = vỡ cache lẫn nhau ⇒ đếm bằng `select` (transform per-observer,
// không chạm cache).
const peInboxQuery = useQuery({
queryKey: ['pe-inbox'],
queryFn: async () => (await api.get<PeListItem[]>('/purchase-evaluations/inbox')).data,
select: d => d.length,
})
const s = stats.data
const peApproved = peApprovedQuery.data
const hasError =
stats.isError ||
contractTotalQuery.isError ||
peTotalQuery.isError ||
peApprovedQuery.isError ||
peInboxQuery.isError
const retryFailed = () => {
if (stats.isError) void stats.refetch()
if (contractTotalQuery.isError) void contractTotalQuery.refetch()
if (peTotalQuery.isError) void peTotalQuery.refetch()
if (peApprovedQuery.isError) void peApprovedQuery.refetch()
if (peInboxQuery.isError) void peInboxQuery.refetch()
}
/* ── Section "Danh sách phiếu toàn trình" ──────────────────────────────── */ /* ── Section "Danh sách phiếu toàn trình" ──────────────────────────────── */
const [stage, setStage] = useState(1) const [stage, setStage] = useState(1)
@ -1219,135 +992,13 @@ export function UserDashboardPage() {
return ( return (
<div className="p-6"> <div className="p-6">
<PageHeader <PageHeader
title="Tổng quan tất cả luồng duyệt" title="Tổng quan quy trình"
description={`Xin chào ${user?.fullName ?? user?.email ?? 'bạn'} — toàn bộ hành trình từ Duyệt NCC đến Ký kết hợp đồng, 4 giai đoạn. Bấm vào số liệu để mở đúng danh sách.`} description={`Xin chào ${user?.fullName ?? user?.email ?? 'bạn'} — toàn bộ hành trình từ Duyệt NCC đến Ký kết hợp đồng, 4 giai đoạn. Bấm vào số liệu để mở đúng danh sách.`}
/> />
{/* ── HERO: pipeline 4 giai đoạn ───────────────────────────────────── */} {/* [S159-dot5 owner: "O dep day, vay bo cai tren di nhe, cai duoi OK day"]
<section className="mb-6 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm"> HERO pipeline 4 card DA GO — tang tong-quan-lien-ket nay la thanh tab
<header className="mb-3 flex items-center gap-2.5"> 4 GD cua Danh sach chi tiet ben duoi (count + cot Hanh trinh per-row). */}
<span className="icon-chip shrink-0">
<Workflow className="h-4 w-4" />
</span>
<div className="min-w-0">
{/* Không dùng .label-eyebrow (brand-600 = 4.42:1, hụt AA 4.5) — brand-700
giữ đúng nhận diện mà đạt 5.6:1. */}
<div className="text-[11px] font-bold uppercase tracking-[0.07em] text-brand-700">
Quy trình mua sắm kết hợp đng
</div>
<h2 className="text-sm leading-tight">Toàn trình 4 giai đoạn · 21 bước</h2>
</div>
</header>
{hasError && (
<div className="mb-3 flex flex-wrap items-center gap-2 rounded-lg border border-amberx-500/40 bg-amberx-50 px-3 py-2 text-xs text-amberx-700">
<AlertTriangle className="h-4 w-4 shrink-0" />
<span className="min-w-0 flex-1">
Không tải đưc một vài số liệu. Các phần còn lại vẫn hiển thị bình thường.
</span>
<Button variant="outline" size="sm" onClick={retryFailed}>
<RefreshCw className="h-3.5 w-3.5" />
Thử lại
</Button>
</div>
)}
<div className="grid grid-cols-1 gap-2 lg:grid-cols-[1fr_auto_1fr_auto_1fr_auto_1fr] lg:items-stretch lg:gap-1.5">
{/* GĐ1 — LIVE */}
<StageCard stage={STAGES[0]}>
<div className="space-y-0.5">
<StageStatRow
group={STAGES[0].title}
label="Tổng phiếu"
value={peTotalQuery.data}
tone="brand"
loading={peTotalQuery.isLoading}
error={peTotalQuery.isError}
onClick={() => navigate('/purchase-evaluations')}
/>
<StageStatRow
group={STAGES[0].title}
label="Đã duyệt"
value={peApproved}
tone="brand"
loading={peApprovedQuery.isLoading}
error={peApprovedQuery.isError}
onClick={() => navigate(`/purchase-evaluations?phase=${PurchaseEvaluationPhase.DaDuyet}`)}
/>
<StageStatRow
group={STAGES[0].title}
label="Chờ tôi duyệt"
value={peInboxQuery.data}
tone="brand"
loading={peInboxQuery.isLoading}
error={peInboxQuery.isError}
emphasis="accent"
onClick={() => navigate('/purchase-evaluations?pendingMe=1')}
/>
</div>
<StageLink label="Mở Duyệt NCC" tone="brand" onClick={() => navigate('/purchase-evaluations')} />
</StageCard>
<StageArrow />
{/* GĐ2 — sắp triển khai */}
<StageCard stage={STAGES[1]}>
<StageSoon
description={STAGES[1].soonDesc}
hint={
typeof peApproved === 'number' && peApproved > 0
? `${peApproved} phiếu Duyệt NCC đã duyệt`
: undefined
}
/>
</StageCard>
<StageArrow />
{/* GĐ3 — LIVE */}
<StageCard stage={STAGES[2]}>
<div className="space-y-0.5">
<StageStatRow
group={STAGES[2].title}
label="Tổng hợp đồng"
value={contractTotalQuery.data}
tone="teal"
loading={contractTotalQuery.isLoading}
error={contractTotalQuery.isError}
onClick={() => navigate('/my-contracts')}
/>
<StageStatRow
group={STAGES[2].title}
label="Chờ tôi duyệt"
value={s?.pendingMyApproval}
tone="teal"
loading={stats.isLoading}
error={stats.isError}
emphasis="accent"
onClick={() => navigate('/inbox')}
/>
<StageStatRow
group={STAGES[2].title}
label="Đã quá hạn"
value={s?.overdue}
tone="teal"
loading={stats.isLoading}
error={stats.isError}
emphasis="danger"
onClick={() => navigate('/inbox')}
/>
</div>
<StageLink label="Mở Hợp đồng" tone="teal" onClick={() => navigate('/my-contracts')} />
</StageCard>
<StageArrow />
{/* GĐ4 — sắp triển khai */}
<StageCard stage={STAGES[3]}>
<StageSoon description={STAGES[3].soonDesc} />
</StageCard>
</div>
</section>
{/* ── DANH SÁCH PHIẾU TOÀN TRÌNH ───────────────────────────────────── */} {/* ── DANH SÁCH PHIẾU TOÀN TRÌNH ───────────────────────────────────── */}
<section className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm"> <section className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">

View File

@ -29,11 +29,24 @@ export function MyContractsPage() {
const typeFilter = searchParams.get('type') ? Number(searchParams.get('type')) : null const typeFilter = searchParams.get('type') ? Number(searchParams.get('type')) : null
const selectedId = searchParams.get('id') const selectedId = searchParams.get('id')
const search = searchParams.get('q') ?? '' const search = searchParams.get('q') ?? ''
// [S159-đợt5] leaf 'Đã duyệt' (?phase=9 — BE /contracts có sẵn param phase) và
// 'Đã xóa' (?deleted=1 — endpoint /contracts/deleted mới, chỉ-xem).
const phaseFilter = searchParams.get('phase') ? Number(searchParams.get('phase')) : null
const showDeleted = searchParams.get('deleted') === '1'
const list = useQuery({ const list = useQuery({
queryKey: ['my-contracts', typeFilter], queryKey: ['my-contracts', typeFilter, phaseFilter, showDeleted],
queryFn: async () => queryFn: async () =>
(await api.get<Paged<ContractListItem>>('/contracts', { params: { page: 1, pageSize: 100 } })).data, (
await api.get<Paged<ContractListItem>>(showDeleted ? '/contracts/deleted' : '/contracts', {
params: {
page: 1,
pageSize: 100,
...(showDeleted && typeFilter != null ? { type: typeFilter } : {}),
...(!showDeleted && phaseFilter != null ? { phase: phaseFilter } : {}),
},
})
).data,
}) })
const detail = useQuery({ const detail = useQuery({
@ -57,6 +70,10 @@ export function MyContractsPage() {
}, [list.data, typeFilter, search]) }, [list.data, typeFilter, search])
function selectContract(id: string) { function selectContract(id: string) {
// [S159-đợt5 FLAG-2 reviewer] Màn "Đã xóa" = CHỈ XEM DANH SÁCH (khuôn PE S155
// `disabled={deletedView}`): detail endpoint KHÔNG IgnoreQueryFilters ⇒ HĐ đã xóa
// 404 ⇒ desktop = 2 panel trắng, mobile = navigate sang trang 404. Chặn tại gốc.
if (showDeleted) return
// Desktop ≥ lg: cập nhật URL để render Panel 2/3 cạnh List. // Desktop ≥ lg: cập nhật URL để render Panel 2/3 cạnh List.
// Mobile: Panel 2/3 hidden → navigate fullpage /contracts/:id (UX khả dụng). // Mobile: Panel 2/3 hidden → navigate fullpage /contracts/:id (UX khả dụng).
if (typeof window !== 'undefined' && window.matchMedia('(min-width: 1024px)').matches) { if (typeof window !== 'undefined' && window.matchMedia('(min-width: 1024px)').matches) {
@ -96,10 +113,22 @@ export function MyContractsPage() {
{rows.length} {rows.length}
</span> </span>
</div> </div>
{showDeleted && (
<span className="rounded-full bg-slate-100 px-2.5 py-1 text-[11px] font-semibold text-slate-600 ring-1 ring-inset ring-slate-300">
Đã xóa chỉ xem
</span>
)}
{!showDeleted && phaseFilter === 9 && (
<span className="rounded-full bg-teal-50 px-2.5 py-1 text-[11px] font-semibold text-teal-700 ring-1 ring-inset ring-teal-200">
Đã phát hành
</span>
)}
{!showDeleted && (
<Button onClick={() => navigate(typeFilter ? `/contracts/new?type=${typeFilter}` : '/contracts/new')}> <Button onClick={() => navigate(typeFilter ? `/contracts/new?type=${typeFilter}` : '/contracts/new')}>
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
Tạo mới Tạo mới
</Button> </Button>
)}
</header> </header>
{/* 3-panel grid — flex-1 để fill phần còn lại của viewport */} {/* 3-panel grid — flex-1 để fill phần còn lại của viewport */}
@ -130,7 +159,7 @@ export function MyContractsPage() {
<EmptyState <EmptyState
icon={FileText} icon={FileText}
title="Không có HĐ" title="Không có HĐ"
description={typeFilter != null ? 'Loại HĐ này chưa có dữ liệu.' : 'Tạo HĐ mới để bắt đầu.'} description={showDeleted ? 'Chưa có HĐ nào bị xóa.' : typeFilter != null ? 'Loại HĐ này chưa có dữ liệu.' : 'Tạo HĐ mới để bắt đầu.'}
/> />
</div> </div>
)} )}

View File

@ -25,6 +25,7 @@ import type { AwAdminOverviewDto, AwDefinitionDto, AwLevelDto } from '@/types/ap
const TYPE_LABEL: Record<number, string> = { const TYPE_LABEL: Record<number, string> = {
1: 'Duyệt NCC', 1: 'Duyệt NCC',
2: 'Duyệt NCC + Giải pháp', 2: 'Duyệt NCC + Giải pháp',
3: 'Hợp đồng (chung mọi loại)', // [S159-đợt5]
} }
// Color palette per Step (Phòng) — cycle 5 màu để phân biệt Bước. Tailwind JIT // Color palette per Step (Phòng) — cycle 5 màu để phân biệt Bước. Tailwind JIT
@ -50,7 +51,9 @@ const LEVEL_PALETTE = [
export function WorkflowMatrixViewPage() { export function WorkflowMatrixViewPage() {
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const rawType = Number(searchParams.get('type')) const rawType = Number(searchParams.get('type'))
const typeInt = rawType === 1 || rawType === 2 ? rawType : 1 // [S159-đợt5] +3 = Contract (ApplicableType.Contract — HĐ dùng CHUNG 1 bộ workflow V2
// cho mọi ContractType ⇒ 7 leaf 'Luồng duyệt' của 7 nhóm HĐ đều trỏ về matrix này).
const typeInt = rawType === 1 || rawType === 2 || rawType === 3 ? rawType : 1
const { data, isLoading, isError } = useQuery({ const { data, isLoading, isError } = useQuery({
queryKey: ['workflow-matrix', typeInt], queryKey: ['workflow-matrix', typeInt],

View File

@ -23,6 +23,18 @@ public class ContractsController(IMediator mediator) : ControllerBase
CancellationToken ct = default) CancellationToken ct = default)
=> Ok(await mediator.Send(new ListContractsQuery(phase, supplierId, projectId) { Page = page, PageSize = pageSize, Search = search, SortDesc = sortDesc }, ct)); => Ok(await mediator.Send(new ListContractsQuery(phase, supplierId, projectId) { Page = page, PageSize = pageSize, Search = search, SortDesc = sortDesc }, ct));
// [S159-đợt5] Màn "Đã xóa" HĐ (chỉ-xem) — mirror PE /deleted (S155). Read-only,
// policy root Contracts.Read (chặt hơn [Authorize] trần của class — không phá gì).
[HttpGet("deleted")]
[Authorize(Policy = "Contracts.Read")]
public async Task<ActionResult<PagedResult<ContractListItemDto>>> ListDeleted(
[FromQuery] ContractType? type = null,
[FromQuery] int page = 1, [FromQuery] int pageSize = 20,
[FromQuery] string? search = null, [FromQuery] bool sortDesc = true,
CancellationToken ct = default)
=> Ok(await mediator.Send(new ListDeletedContractsQuery(type)
{ Page = page, PageSize = pageSize, Search = search, SortDesc = sortDesc }, ct));
[HttpGet("inbox")] [HttpGet("inbox")]
public async Task<ActionResult<List<ContractListItemDto>>> Inbox(CancellationToken ct) public async Task<ActionResult<List<ContractListItemDto>>> Inbox(CancellationToken ct)
=> Ok(await mediator.Send(new GetMyInboxQuery(), ct)); => Ok(await mediator.Send(new GetMyInboxQuery(), ct));

View File

@ -351,6 +351,64 @@ public class ListContractsQueryHandler(IApplicationDbContext db, ICurrentUser cu
} }
} }
// ========== LIST DELETED contracts — màn "Đã xóa" (chỉ-xem) ==========
// [S159-đợt5 — owner "y chang Duyệt NCC"] Cookie-cutter `ListDeletedPurchaseEvaluationsQuery`
// (PeSoftDeleteFeatures.cs). 🔴 LUẬT (không đếm số — reviewer FLAG-10: "chỗ THỨ HAI" là
// con số tự-lan, ApprovalWorkflowV2AdminFeatures cũng dùng Ignore ở lớp KHÁC): mọi nơi
// dùng `IgnoreQueryFilters` để LIỆT KÊ bản ghi đã xóa BẮT BUỘC kèm `.Where(x => x.IsDeleted)`
// vì Ignore GỠ filter chứ KHÔNG ĐẢO (bài 8a S155). Tự kiểm:
// grep -rn "IgnoreQueryFilters" src/Backend --include=*.cs
// IDOR scope MIRROR ListContractsQueryHandler (Drafter eligible-phase; phase ở đây =
// phase tại thời điểm xóa).
public record ListDeletedContractsQuery(ContractType? Type = null)
: PagedRequest, IRequest<PagedResult<ContractListItemDto>>;
public class ListDeletedContractsQueryHandler(IApplicationDbContext db, ICurrentUser currentUser)
: IRequestHandler<ListDeletedContractsQuery, PagedResult<ContractListItemDto>>
{
public async Task<PagedResult<ContractListItemDto>> Handle(ListDeletedContractsQuery request, CancellationToken ct)
{
var q = from c in db.Contracts.AsNoTracking().IgnoreQueryFilters()
.Where(x => x.IsDeleted)
join s in db.Suppliers.AsNoTracking() on c.SupplierId equals s.Id
join p in db.Projects.AsNoTracking() on c.ProjectId equals p.Id
select new { c, s, p };
if (!currentUser.Roles.Contains(AppRoles.Admin))
{
var userId = currentUser.UserId;
var eligiblePhases = ListContractsQueryHandler.GetEligiblePhases(currentUser.Roles);
q = q.Where(x => x.c.DrafterUserId == userId || eligiblePhases.Contains(x.c.Phase));
}
if (request.Type is not null) q = q.Where(x => x.c.Type == request.Type);
if (!string.IsNullOrWhiteSpace(request.Search))
{
var kw = request.Search.Trim();
q = q.Where(x =>
(x.c.MaHopDong != null && x.c.MaHopDong.Contains(kw)) ||
(x.c.TenHopDong != null && x.c.TenHopDong.Contains(kw)) ||
x.s.Name.Contains(kw) || x.p.Name.Contains(kw));
}
q = request.SortDesc ? q.OrderByDescending(x => x.c.DeletedAt) : q.OrderBy(x => x.c.DeletedAt);
var total = await q.CountAsync(ct);
var items = await q
.Skip((request.Page - 1) * request.PageSize).Take(request.PageSize)
.Select(x => new ContractListItemDto(
x.c.Id, x.c.MaHopDong, x.c.TenHopDong, x.c.Type, x.c.Phase,
x.c.SupplierId, x.s.Name,
x.c.ProjectId, x.p.Name,
x.c.GiaTri, x.c.SlaDeadline, x.c.CreatedAt))
.ToListAsync(ct);
return new PagedResult<ContractListItemDto>(items, total, request.Page, request.PageSize);
}
}
// ========== INBOX — HĐ chờ role/tôi xử lý ========== // ========== INBOX — HĐ chờ role/tôi xử lý ==========
public record GetMyInboxQuery : IRequest<List<ContractListItemDto>>; public record GetMyInboxQuery : IRequest<List<ContractListItemDto>>;

View File

@ -181,7 +181,14 @@ public class ListDeletedPurchaseEvaluationsQueryHandler(
{ {
// 🔴 8a — `IgnoreQueryFilters()` **GỠ** filter chứ KHÔNG ĐẢO nó. Thiếu // 🔴 8a — `IgnoreQueryFilters()` **GỠ** filter chứ KHÔNG ĐẢO nó. Thiếu
// `.Where(x => x.IsDeleted)` ⇒ màn "Đã xóa" liệt kê TOÀN BỘ phiếu sống lẫn chết. // `.Where(x => x.IsDeleted)` ⇒ màn "Đã xóa" liệt kê TOÀN BỘ phiếu sống lẫn chết.
// 🔴 D3 — đây là chỗ DUY NHẤT trong `src/Backend` dùng `IgnoreQueryFilters`: // 🔴 D3 (S159-đợt5, sửa lần 2 theo reviewer FLAG-10 — BỎ CON SỐ ĐẾM, nêu LUẬT):
// MỌI nơi dùng `IgnoreQueryFilters` để LIỆT KÊ bản ghi đã xóa BẮT BUỘC kèm
// `.Where(x => x.IsDeleted)`. Lớp list-deleted hiện: đây + `ListDeletedContractsQueryHandler`
// (ContractFeatures.cs). `ApprovalWorkflowV2AdminFeatures.cs` cũng dùng Ignore ở nhiều
// vị trí nhưng thuộc LỚP KHÁC (đọc *LevelOpinions + dò phiếu-cha-mồ-côi, không list-deleted).
// Tự kiểm: grep -rn "IgnoreQueryFilters" src/Backend --include=*.cs
// (Bài: "chỗ DUY NHẤT/THỨ HAI" là con số tự-lan — S155 ghi DUY NHẤT đã sai từ khi
// ApprovalWorkflowV2AdminFeatures dùng; đừng thay số sai cũ bằng số sai mới.)
// CẤM đưa vào helper dùng chung trả `IQueryable`, CẤM sửa // CẤM đưa vào helper dùng chung trả `IQueryable`, CẤM sửa
// `PurchaseEvaluationConfiguration.cs:84`, CẤM thêm cờ `includeDeleted` vào query list // `PurchaseEvaluationConfiguration.cs:84`, CẤM thêm cờ `includeDeleted` vào query list
// hiện có (cờ sẽ lan tới `PeBudgetAccumulator` = phá đúng thứ owner muốn). // hiện có (cờ sẽ lan tới `PeBudgetAccumulator` = phá đúng thứ owner muốn).

View File

@ -1753,7 +1753,7 @@ public static class DbInitializer
var tree = new List<(string Key, string Label, string? Parent, int Order, string Icon)> var tree = new List<(string Key, string Label, string? Parent, int Order, string Icon)>
{ {
(MenuKeys.Dashboard, "Tổng quan", null, 10, "LayoutDashboard"), (MenuKeys.Dashboard, "Tổng quan quy trình", null, 10, "LayoutDashboard"), // [S159-đợt5] owner: "đổi thành gì dễ hình dung? Tổng quan quy trình"
// [S58 2026-06-11] "Danh mục" 20→80: anh yêu cầu đưa xuống CUỐI sidebar // [S58 2026-06-11] "Danh mục" 20→80: anh yêu cầu đưa xuống CUỐI sidebar
// (sau Reports 50, trước vùng System 90 admin). Main upsert tự re-set // (sau Reports 50, trước vùng System 90 admin). Main upsert tự re-set
// Order trên DB cũ. Con (21-25) giữ nguyên — order chỉ so trong siblings. // Order trên DB cũ. Con (21-25) giữ nguyên — order chỉ so trong siblings.
@ -1777,10 +1777,13 @@ public static class DbInitializer
// Duyệt NCC" / "thêm Sub đánh số như Duyệt Hợp đồng"). Root CÓ CON ⇒ render // Duyệt NCC" / "thêm Sub đánh số như Duyệt Hợp đồng"). Root CÓ CON ⇒ render
// MenuGroup uppercase tự động. Leaves → /coming-soon (staticMap ×2 app). // MenuGroup uppercase tự động. Leaves → /coming-soon (staticMap ×2 app).
("Khkk_G1", "1. Kế hoạch ký kết HĐ (NCC-TP)", MenuKeys.KeHoachKyKet, 1, "FileCheck"), ("Khkk_G1", "1. Kế hoạch ký kết HĐ (NCC-TP)", MenuKeys.KeHoachKyKet, 1, "FileCheck"),
("Khkk_List", "Danh sách", "Khkk_G1", 1, "List"), // [S159-đợt5 owner] đủ 6 leaf "y chang Duyệt NCC" (WfView đầu + Deleted cuối).
("Khkk_Create", "Thao tác", "Khkk_G1", 2, "Plus"), ("Khkk_WfView", "Luồng duyệt", "Khkk_G1", 1, "Network"),
("Khkk_Pending", "Đang duyệt", "Khkk_G1", 3, "CheckCircle2"), ("Khkk_List", "Danh sách", "Khkk_G1", 2, "List"),
("Khkk_Approved", "Đã duyệt", "Khkk_G1", 4, "CheckCheck"), ("Khkk_Create", "Thao tác", "Khkk_G1", 3, "Plus"),
("Khkk_Pending", "Đang duyệt", "Khkk_G1", 4, "CheckCircle2"),
("Khkk_Approved", "Đã duyệt", "Khkk_G1", 5, "CheckCheck"),
("Khkk_Deleted", "Đã xóa", "Khkk_G1", 6, "Trash2"),
(MenuKeys.Forms, "Biểu mẫu", null, 40, "FileSpreadsheet"), (MenuKeys.Forms, "Biểu mẫu", null, 40, "FileSpreadsheet"),
(MenuKeys.Reports, "Báo cáo", null, 50, "BarChart3"), (MenuKeys.Reports, "Báo cáo", null, 50, "BarChart3"),
(MenuKeys.System, "Hệ thống", null, 90, "Settings"), (MenuKeys.System, "Hệ thống", null, 90, "Settings"),
@ -1863,9 +1866,14 @@ public static class DbInitializer
// đúng nhãn đã đánh số 01.-07. (leaf → /coming-soon?stage=4). // đúng nhãn đã đánh số 01.-07. (leaf → /coming-soon?stage=4).
tree.Add(("Hdc_" + code, label + " - Bảng cứng", MenuKeys.HopDongCung, hdcOrder++, "FileText")); // [S159-đợt4] owner vẽ: "01. HĐ Thầu phụ - Bảng cứng" tree.Add(("Hdc_" + code, label + " - Bảng cứng", MenuKeys.HopDongCung, hdcOrder++, "FileText")); // [S159-đợt4] owner vẽ: "01. HĐ Thầu phụ - Bảng cứng"
tree.Add((MenuKeys.ContractTypeGroup(code), label, MenuKeys.Contracts, order++, "FileText")); tree.Add((MenuKeys.ContractTypeGroup(code), label, MenuKeys.Contracts, order++, "FileText"));
// [S159-đợt5 owner "y chang Duyệt NCC"] mỗi loại HĐ đủ 6 leaf. Label "Đang duyệt"
// cho key CŨ đi qua labelBackfill (gotcha #11 — sửa chữ ở đây vô tác dụng DB đang chạy).
tree.Add(("Ct_" + code + "_WfView", "Luồng duyệt", MenuKeys.ContractTypeGroup(code), order++, "Network"));
tree.Add((MenuKeys.ContractTypeList(code), "Danh sách", MenuKeys.ContractTypeGroup(code), order++, "List")); tree.Add((MenuKeys.ContractTypeList(code), "Danh sách", MenuKeys.ContractTypeGroup(code), order++, "List"));
tree.Add((MenuKeys.ContractTypeCreate(code), "Thao tác", MenuKeys.ContractTypeGroup(code), order++, "Plus")); tree.Add((MenuKeys.ContractTypeCreate(code), "Thao tác", MenuKeys.ContractTypeGroup(code), order++, "Plus"));
tree.Add((MenuKeys.ContractTypePending(code),"Duyệt", MenuKeys.ContractTypeGroup(code), order++, "CheckCircle2")); tree.Add((MenuKeys.ContractTypePending(code),"Đang duyệt", MenuKeys.ContractTypeGroup(code), order++, "CheckCircle2"));
tree.Add(("Ct_" + code + "_Approved","Đã duyệt", MenuKeys.ContractTypeGroup(code), order++, "CheckCheck"));
tree.Add(("Ct_" + code + "_Deleted", "Đã xóa", MenuKeys.ContractTypeGroup(code), order++, "Trash2"));
} }
// Per-type workflow admin leaves dưới `Workflows` — mỗi loại là 1 // Per-type workflow admin leaves dưới `Workflows` — mỗi loại là 1
@ -1958,6 +1966,16 @@ public static class DbInitializer
// [S159] 2 root rename theo 4-GĐ (prod đã SQL cùng ngày ⇒ NO-OP trên prod). // [S159] 2 root rename theo 4-GĐ (prod đã SQL cùng ngày ⇒ NO-OP trên prod).
[MenuKeys.PurchaseEvaluations] = "Duyệt NCC", [MenuKeys.PurchaseEvaluations] = "Duyệt NCC",
[MenuKeys.Contracts] = "Duyệt hợp đồng", [MenuKeys.Contracts] = "Duyệt hợp đồng",
// [S159-đợt5] Sidebar "Tổng quan" → "Tổng quan quy trình" (owner đề tên, lead lấy).
[MenuKeys.Dashboard] = "Tổng quan quy trình",
// [S159-đợt5] 7 leaf Ct_X_Pending "Duyệt" → "Đang duyệt" (đồng khuôn PE — gotcha #11).
[MenuKeys.ContractTypePending("ThauPhu")] = "Đang duyệt",
[MenuKeys.ContractTypePending("GiaoKhoan")] = "Đang duyệt",
[MenuKeys.ContractTypePending("NhaCungCap")] = "Đang duyệt",
[MenuKeys.ContractTypePending("DichVu")] = "Đang duyệt",
[MenuKeys.ContractTypePending("MuaBan")] = "Đang duyệt",
[MenuKeys.ContractTypePending("NguyenTacNcc")] = "Đang duyệt",
[MenuKeys.ContractTypePending("NguyenTacDv")] = "Đang duyệt",
// [S159-đợt4] 7 leaf Hdc_ thêm hậu tố " - Bảng cứng" (owner vẽ; prod SQL cùng ngày). // [S159-đợt4] 7 leaf Hdc_ thêm hậu tố " - Bảng cứng" (owner vẽ; prod SQL cùng ngày).
["Hdc_ThauPhu"] = "01. HĐ Thầu phụ - Bảng cứng", ["Hdc_ThauPhu"] = "01. HĐ Thầu phụ - Bảng cứng",
["Hdc_GiaoKhoan"] = "02. HĐ Giao khoán - Bảng cứng", ["Hdc_GiaoKhoan"] = "02. HĐ Giao khoán - Bảng cứng",
@ -2142,9 +2160,12 @@ public static class DbInitializer
foreach (var c in MenuKeys.ContractTypeCodes) foreach (var c in MenuKeys.ContractTypeCodes)
{ {
yield return MenuKeys.ContractTypeGroup(c); yield return MenuKeys.ContractTypeGroup(c);
yield return "Ct_" + c + "_WfView"; // [S159-đợt5]
yield return MenuKeys.ContractTypeList(c); yield return MenuKeys.ContractTypeList(c);
yield return MenuKeys.ContractTypeCreate(c); yield return MenuKeys.ContractTypeCreate(c);
yield return MenuKeys.ContractTypePending(c); yield return MenuKeys.ContractTypePending(c);
yield return "Ct_" + c + "_Approved"; // [S159-đợt5]
yield return "Ct_" + c + "_Deleted"; // [S159-đợt5]
} }
} }
static IEnumerable<string> MasterMenuKeys() => new[] static IEnumerable<string> MasterMenuKeys() => new[]
@ -2155,7 +2176,8 @@ public static class DbInitializer
}; };
static IEnumerable<string> KhkkKeys() => new[] static IEnumerable<string> KhkkKeys() => new[]
{ {
"Khkk_G1", "Khkk_List", "Khkk_Create", "Khkk_Pending", "Khkk_Approved", "Khkk_G1", "Khkk_WfView", "Khkk_List", "Khkk_Create", "Khkk_Pending",
"Khkk_Approved", "Khkk_Deleted",
}; };
static IEnumerable<string> HdcKeys() static IEnumerable<string> HdcKeys()
=> MenuKeys.ContractTypeCodes.Select(c => "Hdc_" + c); => MenuKeys.ContractTypeCodes.Select(c => "Hdc_" + c);