[CLAUDE] PurchaseEvaluation: tree Panel 1 doi nhom theo anh Kiet FDC "Du an (Nam) > Hang muc cong viec > Phieu" (bo tang NCC, SHA256 mirror x2 app) + wipe testing data prod S59
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m27s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m27s
- Tree Panel 1: moi cap (Du an, Nam-tao-phieu) = 1 folder label "Ten du an (Nam)", level 2 = Hang muc cong viec (WorkItemId Mig 49, phieu cu null -> "(Chua gan hang muc)"), bo tang NCC khoi tree (van hien o card/detail). Expand-state localStorage key v2. - scripts/s59-wipe-testing-data.sql DA CHAY prod (anh chot AskUserQuestion): xoa 10 PE + 7 HD [DEMO] + 64 notif + 1 workflow V2 cu inactive; reset PeSeq/CtSeq -> ma moi tu 001; GIU master 70/86/22 + users 55 + templates 9 + 7 workflow ghim. Uploads orphan da don.
This commit is contained in:
@ -115,64 +115,60 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
// Đã duyệt/Từ chối loại bỏ). BE /inbox hiện loose UAT có thể trả phiếu Nháp →
|
// Đã duyệt/Từ chối loại bỏ). BE /inbox hiện loose UAT có thể trả phiếu Nháp →
|
||||||
// FE filter để UX đúng kỳ vọng. Phân quyền strict V2 BE pending Session 18+.
|
// FE filter để UX đúng kỳ vọng. Phân quyền strict V2 BE pending Session 18+.
|
||||||
|
|
||||||
// Plan AG5 — Group 3-level Project > Năm > NCC > PE (anh feedback 2026-05-21:
|
// S59 — anh Kiệt FDC (Zalo 2026-06-11): "Tên dự án ( 2026 ) -> Hạng mục công việc -> Phiếu cần duyệt"
|
||||||
// "Folder cấp dưới dự án là theo năm và dưới năm là theo NCC"). Filter pendingMe TRƯỚC group.
|
// (mirror cấu trúc folder Outlook FDC, thứ tự label "Dự án - Năm"). Thay tree AG5 cũ (Project > Năm > NCC):
|
||||||
// Year extract từ createdAt.getFullYear(). NCC = selectedSupplierName fallback "(Chưa chọn NCC)"
|
// mỗi cặp (Dự án, Năm-tạo-phiếu) = 1 folder label "Tên dự án (Năm)", level 2 = Hạng mục công việc
|
||||||
// khi PE chưa DaDuyet. Sort: Project A-Z (vi) + Year DESC + NCC A-Z (vi) + PE createdAt DESC.
|
// (WorkItemId Mig 49, phiếu cũ chưa gắn → "(Chưa gắn hạng mục)"). NCC bỏ khỏi tree — vẫn hiện ở card/detail.
|
||||||
type SupplierGroup = {
|
// Filter pendingMe TRƯỚC group. Sort: Project A-Z (vi) + Năm DESC + Hạng mục A-Z (vi) + PE createdAt DESC.
|
||||||
supplierId: string | null
|
type WorkItemGroup = {
|
||||||
supplierName: string
|
workItemId: string | null
|
||||||
|
workItemName: string
|
||||||
items: PeListItem[]
|
items: PeListItem[]
|
||||||
}
|
}
|
||||||
type YearGroup = {
|
type ProjectYearGroup = {
|
||||||
year: number
|
|
||||||
suppliers: SupplierGroup[]
|
|
||||||
totalCount: number
|
|
||||||
}
|
|
||||||
type ProjectGroup = {
|
|
||||||
projectId: string | null
|
projectId: string | null
|
||||||
projectName: string
|
projectName: string
|
||||||
years: YearGroup[]
|
year: number
|
||||||
|
label: string
|
||||||
|
workItems: WorkItemGroup[]
|
||||||
totalCount: number
|
totalCount: number
|
||||||
}
|
}
|
||||||
const projectGroups = useMemo<ProjectGroup[]>(() => {
|
const projectGroups = useMemo<ProjectYearGroup[]>(() => {
|
||||||
const filtered = pendingMe
|
const filtered = pendingMe
|
||||||
? allRows.filter(p => getPeDisplayStatus(p.phase) === PeDisplayStatus.DaGuiDuyet)
|
? allRows.filter(p => getPeDisplayStatus(p.phase) === PeDisplayStatus.DaGuiDuyet)
|
||||||
: allRows
|
: allRows
|
||||||
const projectMap = new Map<string, ProjectGroup>()
|
const groupMap = new Map<string, ProjectYearGroup>()
|
||||||
for (const p of filtered) {
|
for (const p of filtered) {
|
||||||
const projKey = p.projectId ?? '__no_project__'
|
|
||||||
const projName = p.projectName?.trim() || '(Dự án đã xoá)'
|
|
||||||
if (!projectMap.has(projKey)) {
|
|
||||||
projectMap.set(projKey, { projectId: p.projectId ?? null, projectName: projName, years: [], totalCount: 0 })
|
|
||||||
}
|
|
||||||
const pg = projectMap.get(projKey)!
|
|
||||||
const year = new Date(p.createdAt).getFullYear()
|
const year = new Date(p.createdAt).getFullYear()
|
||||||
let yg = pg.years.find(y => y.year === year)
|
const projName = p.projectName?.trim() || '(Dự án đã xoá)'
|
||||||
if (!yg) {
|
const groupKey = `${p.projectId ?? '__no_project__'}::y${year}`
|
||||||
yg = { year, suppliers: [], totalCount: 0 }
|
if (!groupMap.has(groupKey)) {
|
||||||
pg.years.push(yg)
|
groupMap.set(groupKey, {
|
||||||
|
projectId: p.projectId ?? null,
|
||||||
|
projectName: projName,
|
||||||
|
year,
|
||||||
|
label: `${projName} (${year})`,
|
||||||
|
workItems: [],
|
||||||
|
totalCount: 0,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const supKey = p.selectedSupplierId ?? '__no_supplier__'
|
const pg = groupMap.get(groupKey)!
|
||||||
const supName = p.selectedSupplierName?.trim() || '(Chưa chọn NCC)'
|
const wiKey = p.workItemId ?? '__no_workitem__'
|
||||||
let sg = yg.suppliers.find(s => (s.supplierId ?? '__no_supplier__') === supKey)
|
const wiName = p.workItemName?.trim() || '(Chưa gắn hạng mục)'
|
||||||
if (!sg) {
|
let wg = pg.workItems.find(w => (w.workItemId ?? '__no_workitem__') === wiKey)
|
||||||
sg = { supplierId: p.selectedSupplierId ?? null, supplierName: supName, items: [] }
|
if (!wg) {
|
||||||
yg.suppliers.push(sg)
|
wg = { workItemId: p.workItemId ?? null, workItemName: wiName, items: [] }
|
||||||
|
pg.workItems.push(wg)
|
||||||
}
|
}
|
||||||
sg.items.push(p)
|
wg.items.push(p)
|
||||||
yg.totalCount++
|
|
||||||
pg.totalCount++
|
pg.totalCount++
|
||||||
}
|
}
|
||||||
const arr = Array.from(projectMap.values())
|
const arr = Array.from(groupMap.values())
|
||||||
arr.sort((a, b) => a.projectName.localeCompare(b.projectName, 'vi'))
|
arr.sort((a, b) => a.projectName.localeCompare(b.projectName, 'vi') || b.year - a.year)
|
||||||
for (const pg of arr) {
|
for (const pg of arr) {
|
||||||
pg.years.sort((a, b) => b.year - a.year)
|
pg.workItems.sort((a, b) => a.workItemName.localeCompare(b.workItemName, 'vi'))
|
||||||
for (const yg of pg.years) {
|
for (const wg of pg.workItems) {
|
||||||
yg.suppliers.sort((a, b) => a.supplierName.localeCompare(b.supplierName, 'vi'))
|
wg.items.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||||
for (const sg of yg.suppliers) {
|
|
||||||
sg.items.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return arr
|
return arr
|
||||||
@ -181,9 +177,9 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
// Total row count cho header badge (pendingMe đếm filtered, Danh sách đếm BE total).
|
// Total row count cho header badge (pendingMe đếm filtered, Danh sách đếm BE total).
|
||||||
const totalRowCount = projectGroups.reduce((sum, pg) => sum + pg.totalCount, 0)
|
const totalRowCount = projectGroups.reduce((sum, pg) => sum + pg.totalCount, 0)
|
||||||
|
|
||||||
// Plan AG2 — Expand state localStorage Set<string> (projectId only, drop ::gtKey suffix).
|
// Plan AG2 — Expand state localStorage Set<string>. Default empty Set (all collapse).
|
||||||
// Default empty Set (all collapse). Single-PE project skip <details> wrapper (render flat).
|
// S59 key v2: node scheme đổi (Dự án+Năm gộp 1 node + Hạng mục thay NCC) → key cũ vô nghĩa.
|
||||||
const STORAGE_KEY = 'pe_list_expanded_projects'
|
const STORAGE_KEY = 'pe_list_expanded_projects_v2'
|
||||||
const [expandedSet, setExpandedSet] = useState<Set<string>>(() => {
|
const [expandedSet, setExpandedSet] = useState<Set<string>>(() => {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(STORAGE_KEY)
|
const raw = localStorage.getItem(STORAGE_KEY)
|
||||||
@ -285,13 +281,11 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
<EmptyState icon={ClipboardCheck} title="Chưa có phiếu" description="Tạo phiếu mới để bắt đầu quy trình." />
|
<EmptyState icon={ClipboardCheck} title="Chưa có phiếu" description="Tạo phiếu mới để bắt đầu quy trình." />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Plan AG5 — Tree view 3-level Project > Năm > NCC > PE (anh feedback 2026-05-21:
|
{/* S59 — Tree view theo anh Kiệt FDC: 📁 "Tên dự án (Năm)" (bg-slate-50) > 🧱 Hạng mục
|
||||||
"Folder cấp dưới dự án là theo năm và dưới năm là theo NCC").
|
công việc > PE card. 2 layer <details>, named groups group/proj + group/wi cho chevron. */}
|
||||||
3 layer <details>: 📁 Project (bg-slate-50) > 📅 Năm > 🏢 NCC > PE card.
|
|
||||||
Tailwind v3 named groups group/proj + group/year + group/sup cho chevron rotation. */}
|
|
||||||
<div className="divide-y divide-slate-100">
|
<div className="divide-y divide-slate-100">
|
||||||
{projectGroups.map(pg => {
|
{projectGroups.map(pg => {
|
||||||
const projKey = pg.projectId ?? '__no_project__'
|
const projKey = `${pg.projectId ?? '__no_project__'}::y${pg.year}`
|
||||||
return (
|
return (
|
||||||
<details
|
<details
|
||||||
key={projKey}
|
key={projKey}
|
||||||
@ -302,92 +296,72 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
<summary className="flex cursor-pointer items-center gap-1.5 bg-slate-50 px-3 py-2 hover:bg-slate-100 [&::-webkit-details-marker]:hidden">
|
<summary className="flex cursor-pointer items-center gap-1.5 bg-slate-50 px-3 py-2 hover:bg-slate-100 [&::-webkit-details-marker]:hidden">
|
||||||
<svg className="h-3 w-3 shrink-0 text-slate-500 transition-transform group-open/proj:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
<svg className="h-3 w-3 shrink-0 text-slate-500 transition-transform group-open/proj:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
||||||
<span className="text-base">📁</span>
|
<span className="text-base">📁</span>
|
||||||
<span className="flex-1 truncate text-[13px] font-medium text-slate-900">{pg.projectName}</span>
|
<span className="flex-1 truncate text-[13px] font-medium text-slate-900">{pg.label}</span>
|
||||||
<span className="rounded-full bg-slate-200 px-2 py-0.5 text-[10px] font-medium text-slate-700">{pg.totalCount}</span>
|
<span className="rounded-full bg-slate-200 px-2 py-0.5 text-[10px] font-medium text-slate-700">{pg.totalCount}</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div className="ml-3 border-l border-slate-200">
|
<div className="ml-3 border-l border-slate-200">
|
||||||
{pg.years.map(yg => {
|
{pg.workItems.map(wg => {
|
||||||
const yearKey = `${projKey}::y${yg.year}`
|
const wiKey = `${projKey}::w${wg.workItemId ?? '_none_'}`
|
||||||
return (
|
return (
|
||||||
<details
|
<details
|
||||||
key={yearKey}
|
key={wiKey}
|
||||||
open={isExpanded(yearKey)}
|
open={isExpanded(wiKey)}
|
||||||
onToggle={e => toggleExpand(yearKey, (e.currentTarget as HTMLDetailsElement).open)}
|
onToggle={e => toggleExpand(wiKey, (e.currentTarget as HTMLDetailsElement).open)}
|
||||||
className="group/year"
|
className="group/wi"
|
||||||
>
|
>
|
||||||
<summary className="flex cursor-pointer items-center gap-1.5 px-3 py-1.5 hover:bg-slate-50 [&::-webkit-details-marker]:hidden">
|
<summary className="flex cursor-pointer items-center gap-1.5 px-3 py-1.5 hover:bg-slate-50 [&::-webkit-details-marker]:hidden">
|
||||||
<svg className="h-3 w-3 shrink-0 text-slate-400 transition-transform group-open/year:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
<svg className="h-3 w-3 shrink-0 text-slate-400 transition-transform group-open/wi:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
||||||
<span className="text-sm">📅</span>
|
<span className="text-sm">🧱</span>
|
||||||
<span className="flex-1 truncate text-[12px] font-medium text-slate-700">Năm {yg.year}</span>
|
<span className={cn('flex-1 truncate text-[12px]', wg.workItemId ? 'font-medium text-slate-700' : 'italic text-slate-400')}>{wg.workItemName}</span>
|
||||||
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-600">{yg.totalCount}</span>
|
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-600">{wg.items.length}</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div className="ml-3 border-l border-slate-200">
|
<ul className="ml-3 divide-y divide-slate-100 border-l border-slate-200">
|
||||||
{yg.suppliers.map(sg => {
|
{wg.items.map(p => (
|
||||||
const supKey = `${yearKey}::s${sg.supplierId ?? '_none_'}`
|
<li key={p.id}>
|
||||||
return (
|
<button
|
||||||
<details
|
onClick={() => selectRow(p.id)}
|
||||||
key={supKey}
|
className={cn(
|
||||||
open={isExpanded(supKey)}
|
'block w-full px-3 py-2 text-left transition hover:bg-slate-50',
|
||||||
onToggle={e => toggleExpand(supKey, (e.currentTarget as HTMLDetailsElement).open)}
|
selectedId === p.id && 'bg-brand-50 ring-1 ring-inset ring-brand-200',
|
||||||
className="group/sup"
|
)}
|
||||||
>
|
>
|
||||||
<summary className="flex cursor-pointer items-center gap-1.5 px-3 py-1.5 hover:bg-slate-50 [&::-webkit-details-marker]:hidden">
|
{/* Plan AG6 — compact card 3 row: title+badge / mã+time / drafter+dept+contract */}
|
||||||
<svg className="h-3 w-3 shrink-0 text-slate-400 transition-transform group-open/sup:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
<div className="flex items-start justify-between gap-2">
|
||||||
<span className="text-sm">🏢</span>
|
<div className="min-w-0 flex-1 truncate text-[13px] font-medium text-slate-900">{p.tenGoiThau}</div>
|
||||||
<span className={cn('flex-1 truncate text-[12px]', sg.supplierId ? 'text-slate-700' : 'italic text-slate-400')}>{sg.supplierName}</span>
|
<span
|
||||||
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-600">{sg.items.length}</span>
|
className={cn(
|
||||||
</summary>
|
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium',
|
||||||
<ul className="ml-3 divide-y divide-slate-100 border-l border-slate-200">
|
PeDisplayStatusColor[getPeDisplayStatus(p.phase)],
|
||||||
{sg.items.map(p => (
|
)}
|
||||||
<li key={p.id}>
|
>
|
||||||
<button
|
{PeDisplayStatusLabel[getPeDisplayStatus(p.phase)]}
|
||||||
onClick={() => selectRow(p.id)}
|
</span>
|
||||||
className={cn(
|
</div>
|
||||||
'block w-full px-3 py-2 text-left transition hover:bg-slate-50',
|
<div className="mt-0.5 flex items-center gap-1.5 text-[11px] text-slate-500">
|
||||||
selectedId === p.id && 'bg-brand-50 ring-1 ring-inset ring-brand-200',
|
<span className="font-mono">{p.maPhieu ?? '—'}</span>
|
||||||
)}
|
<span className="text-slate-300">·</span>
|
||||||
>
|
{/* S23 t2 UAT: BE list sort theo UpdatedAt DESC (fallback CreatedAt). */}
|
||||||
{/* Plan AG6 — compact card 3 row: title+badge / mã+time / drafter+dept+contract */}
|
<span title={`Tạo lúc ${new Date(p.createdAt).toLocaleString('vi-VN')}`}>
|
||||||
<div className="flex items-start justify-between gap-2">
|
{new Date(p.createdAt).toLocaleString('vi-VN', {
|
||||||
<div className="min-w-0 flex-1 truncate text-[13px] font-medium text-slate-900">{p.tenGoiThau}</div>
|
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||||
<span
|
hour: '2-digit', minute: '2-digit',
|
||||||
className={cn(
|
})}
|
||||||
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium',
|
</span>
|
||||||
PeDisplayStatusColor[getPeDisplayStatus(p.phase)],
|
</div>
|
||||||
)}
|
{(p.drafterName || p.departmentName || p.contractId) && (
|
||||||
>
|
<div className="mt-0.5 flex items-center justify-between gap-2 text-[11px]">
|
||||||
{PeDisplayStatusLabel[getPeDisplayStatus(p.phase)]}
|
<span className="min-w-0 flex-1 truncate text-slate-500">
|
||||||
</span>
|
{p.drafterName && <>👤 {p.drafterName}</>}
|
||||||
</div>
|
{p.drafterName && p.departmentName && <span className="text-slate-300"> · </span>}
|
||||||
<div className="mt-0.5 flex items-center gap-1.5 text-[11px] text-slate-500">
|
{p.departmentName && <span className="text-slate-400">{p.departmentName}</span>}
|
||||||
<span className="font-mono">{p.maPhieu ?? '—'}</span>
|
</span>
|
||||||
<span className="text-slate-300">·</span>
|
{p.contractId && <span className="shrink-0 text-[10px] font-medium text-brand-600">✓ HĐ</span>}
|
||||||
{/* S23 t2 UAT: BE list sort theo UpdatedAt DESC (fallback CreatedAt). */}
|
</div>
|
||||||
<span title={`Tạo lúc ${new Date(p.createdAt).toLocaleString('vi-VN')}`}>
|
)}
|
||||||
{new Date(p.createdAt).toLocaleString('vi-VN', {
|
</button>
|
||||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
</li>
|
||||||
hour: '2-digit', minute: '2-digit',
|
))}
|
||||||
})}
|
</ul>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{(p.drafterName || p.departmentName || p.contractId) && (
|
|
||||||
<div className="mt-0.5 flex items-center justify-between gap-2 text-[11px]">
|
|
||||||
<span className="min-w-0 flex-1 truncate text-slate-500">
|
|
||||||
{p.drafterName && <>👤 {p.drafterName}</>}
|
|
||||||
{p.drafterName && p.departmentName && <span className="text-slate-300"> · </span>}
|
|
||||||
{p.departmentName && <span className="text-slate-400">{p.departmentName}</span>}
|
|
||||||
</span>
|
|
||||||
{p.contractId && <span className="shrink-0 text-[10px] font-medium text-brand-600">✓ HĐ</span>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</details>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</details>
|
</details>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -115,64 +115,60 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
// Đã duyệt/Từ chối loại bỏ). BE /inbox hiện loose UAT có thể trả phiếu Nháp →
|
// Đã duyệt/Từ chối loại bỏ). BE /inbox hiện loose UAT có thể trả phiếu Nháp →
|
||||||
// FE filter để UX đúng kỳ vọng. Phân quyền strict V2 BE pending Session 18+.
|
// FE filter để UX đúng kỳ vọng. Phân quyền strict V2 BE pending Session 18+.
|
||||||
|
|
||||||
// Plan AG5 — Group 3-level Project > Năm > NCC > PE (anh feedback 2026-05-21:
|
// S59 — anh Kiệt FDC (Zalo 2026-06-11): "Tên dự án ( 2026 ) -> Hạng mục công việc -> Phiếu cần duyệt"
|
||||||
// "Folder cấp dưới dự án là theo năm và dưới năm là theo NCC"). Filter pendingMe TRƯỚC group.
|
// (mirror cấu trúc folder Outlook FDC, thứ tự label "Dự án - Năm"). Thay tree AG5 cũ (Project > Năm > NCC):
|
||||||
// Year extract từ createdAt.getFullYear(). NCC = selectedSupplierName fallback "(Chưa chọn NCC)"
|
// mỗi cặp (Dự án, Năm-tạo-phiếu) = 1 folder label "Tên dự án (Năm)", level 2 = Hạng mục công việc
|
||||||
// khi PE chưa DaDuyet. Sort: Project A-Z (vi) + Year DESC + NCC A-Z (vi) + PE createdAt DESC.
|
// (WorkItemId Mig 49, phiếu cũ chưa gắn → "(Chưa gắn hạng mục)"). NCC bỏ khỏi tree — vẫn hiện ở card/detail.
|
||||||
type SupplierGroup = {
|
// Filter pendingMe TRƯỚC group. Sort: Project A-Z (vi) + Năm DESC + Hạng mục A-Z (vi) + PE createdAt DESC.
|
||||||
supplierId: string | null
|
type WorkItemGroup = {
|
||||||
supplierName: string
|
workItemId: string | null
|
||||||
|
workItemName: string
|
||||||
items: PeListItem[]
|
items: PeListItem[]
|
||||||
}
|
}
|
||||||
type YearGroup = {
|
type ProjectYearGroup = {
|
||||||
year: number
|
|
||||||
suppliers: SupplierGroup[]
|
|
||||||
totalCount: number
|
|
||||||
}
|
|
||||||
type ProjectGroup = {
|
|
||||||
projectId: string | null
|
projectId: string | null
|
||||||
projectName: string
|
projectName: string
|
||||||
years: YearGroup[]
|
year: number
|
||||||
|
label: string
|
||||||
|
workItems: WorkItemGroup[]
|
||||||
totalCount: number
|
totalCount: number
|
||||||
}
|
}
|
||||||
const projectGroups = useMemo<ProjectGroup[]>(() => {
|
const projectGroups = useMemo<ProjectYearGroup[]>(() => {
|
||||||
const filtered = pendingMe
|
const filtered = pendingMe
|
||||||
? allRows.filter(p => getPeDisplayStatus(p.phase) === PeDisplayStatus.DaGuiDuyet)
|
? allRows.filter(p => getPeDisplayStatus(p.phase) === PeDisplayStatus.DaGuiDuyet)
|
||||||
: allRows
|
: allRows
|
||||||
const projectMap = new Map<string, ProjectGroup>()
|
const groupMap = new Map<string, ProjectYearGroup>()
|
||||||
for (const p of filtered) {
|
for (const p of filtered) {
|
||||||
const projKey = p.projectId ?? '__no_project__'
|
|
||||||
const projName = p.projectName?.trim() || '(Dự án đã xoá)'
|
|
||||||
if (!projectMap.has(projKey)) {
|
|
||||||
projectMap.set(projKey, { projectId: p.projectId ?? null, projectName: projName, years: [], totalCount: 0 })
|
|
||||||
}
|
|
||||||
const pg = projectMap.get(projKey)!
|
|
||||||
const year = new Date(p.createdAt).getFullYear()
|
const year = new Date(p.createdAt).getFullYear()
|
||||||
let yg = pg.years.find(y => y.year === year)
|
const projName = p.projectName?.trim() || '(Dự án đã xoá)'
|
||||||
if (!yg) {
|
const groupKey = `${p.projectId ?? '__no_project__'}::y${year}`
|
||||||
yg = { year, suppliers: [], totalCount: 0 }
|
if (!groupMap.has(groupKey)) {
|
||||||
pg.years.push(yg)
|
groupMap.set(groupKey, {
|
||||||
|
projectId: p.projectId ?? null,
|
||||||
|
projectName: projName,
|
||||||
|
year,
|
||||||
|
label: `${projName} (${year})`,
|
||||||
|
workItems: [],
|
||||||
|
totalCount: 0,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const supKey = p.selectedSupplierId ?? '__no_supplier__'
|
const pg = groupMap.get(groupKey)!
|
||||||
const supName = p.selectedSupplierName?.trim() || '(Chưa chọn NCC)'
|
const wiKey = p.workItemId ?? '__no_workitem__'
|
||||||
let sg = yg.suppliers.find(s => (s.supplierId ?? '__no_supplier__') === supKey)
|
const wiName = p.workItemName?.trim() || '(Chưa gắn hạng mục)'
|
||||||
if (!sg) {
|
let wg = pg.workItems.find(w => (w.workItemId ?? '__no_workitem__') === wiKey)
|
||||||
sg = { supplierId: p.selectedSupplierId ?? null, supplierName: supName, items: [] }
|
if (!wg) {
|
||||||
yg.suppliers.push(sg)
|
wg = { workItemId: p.workItemId ?? null, workItemName: wiName, items: [] }
|
||||||
|
pg.workItems.push(wg)
|
||||||
}
|
}
|
||||||
sg.items.push(p)
|
wg.items.push(p)
|
||||||
yg.totalCount++
|
|
||||||
pg.totalCount++
|
pg.totalCount++
|
||||||
}
|
}
|
||||||
const arr = Array.from(projectMap.values())
|
const arr = Array.from(groupMap.values())
|
||||||
arr.sort((a, b) => a.projectName.localeCompare(b.projectName, 'vi'))
|
arr.sort((a, b) => a.projectName.localeCompare(b.projectName, 'vi') || b.year - a.year)
|
||||||
for (const pg of arr) {
|
for (const pg of arr) {
|
||||||
pg.years.sort((a, b) => b.year - a.year)
|
pg.workItems.sort((a, b) => a.workItemName.localeCompare(b.workItemName, 'vi'))
|
||||||
for (const yg of pg.years) {
|
for (const wg of pg.workItems) {
|
||||||
yg.suppliers.sort((a, b) => a.supplierName.localeCompare(b.supplierName, 'vi'))
|
wg.items.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||||
for (const sg of yg.suppliers) {
|
|
||||||
sg.items.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return arr
|
return arr
|
||||||
@ -181,9 +177,9 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
// Total row count cho header badge (pendingMe đếm filtered, Danh sách đếm BE total).
|
// Total row count cho header badge (pendingMe đếm filtered, Danh sách đếm BE total).
|
||||||
const totalRowCount = projectGroups.reduce((sum, pg) => sum + pg.totalCount, 0)
|
const totalRowCount = projectGroups.reduce((sum, pg) => sum + pg.totalCount, 0)
|
||||||
|
|
||||||
// Plan AG2 — Expand state localStorage Set<string> (projectId only, drop ::gtKey suffix).
|
// Plan AG2 — Expand state localStorage Set<string>. Default empty Set (all collapse).
|
||||||
// Default empty Set (all collapse). Single-PE project skip <details> wrapper (render flat).
|
// S59 key v2: node scheme đổi (Dự án+Năm gộp 1 node + Hạng mục thay NCC) → key cũ vô nghĩa.
|
||||||
const STORAGE_KEY = 'pe_list_expanded_projects'
|
const STORAGE_KEY = 'pe_list_expanded_projects_v2'
|
||||||
const [expandedSet, setExpandedSet] = useState<Set<string>>(() => {
|
const [expandedSet, setExpandedSet] = useState<Set<string>>(() => {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(STORAGE_KEY)
|
const raw = localStorage.getItem(STORAGE_KEY)
|
||||||
@ -285,13 +281,11 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
<EmptyState icon={ClipboardCheck} title="Chưa có phiếu" description="Tạo phiếu mới để bắt đầu quy trình." />
|
<EmptyState icon={ClipboardCheck} title="Chưa có phiếu" description="Tạo phiếu mới để bắt đầu quy trình." />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Plan AG5 — Tree view 3-level Project > Năm > NCC > PE (anh feedback 2026-05-21:
|
{/* S59 — Tree view theo anh Kiệt FDC: 📁 "Tên dự án (Năm)" (bg-slate-50) > 🧱 Hạng mục
|
||||||
"Folder cấp dưới dự án là theo năm và dưới năm là theo NCC").
|
công việc > PE card. 2 layer <details>, named groups group/proj + group/wi cho chevron. */}
|
||||||
3 layer <details>: 📁 Project (bg-slate-50) > 📅 Năm > 🏢 NCC > PE card.
|
|
||||||
Tailwind v3 named groups group/proj + group/year + group/sup cho chevron rotation. */}
|
|
||||||
<div className="divide-y divide-slate-100">
|
<div className="divide-y divide-slate-100">
|
||||||
{projectGroups.map(pg => {
|
{projectGroups.map(pg => {
|
||||||
const projKey = pg.projectId ?? '__no_project__'
|
const projKey = `${pg.projectId ?? '__no_project__'}::y${pg.year}`
|
||||||
return (
|
return (
|
||||||
<details
|
<details
|
||||||
key={projKey}
|
key={projKey}
|
||||||
@ -302,92 +296,72 @@ export function PurchaseEvaluationsListPage() {
|
|||||||
<summary className="flex cursor-pointer items-center gap-1.5 bg-slate-50 px-3 py-2 hover:bg-slate-100 [&::-webkit-details-marker]:hidden">
|
<summary className="flex cursor-pointer items-center gap-1.5 bg-slate-50 px-3 py-2 hover:bg-slate-100 [&::-webkit-details-marker]:hidden">
|
||||||
<svg className="h-3 w-3 shrink-0 text-slate-500 transition-transform group-open/proj:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
<svg className="h-3 w-3 shrink-0 text-slate-500 transition-transform group-open/proj:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
||||||
<span className="text-base">📁</span>
|
<span className="text-base">📁</span>
|
||||||
<span className="flex-1 truncate text-[13px] font-medium text-slate-900">{pg.projectName}</span>
|
<span className="flex-1 truncate text-[13px] font-medium text-slate-900">{pg.label}</span>
|
||||||
<span className="rounded-full bg-slate-200 px-2 py-0.5 text-[10px] font-medium text-slate-700">{pg.totalCount}</span>
|
<span className="rounded-full bg-slate-200 px-2 py-0.5 text-[10px] font-medium text-slate-700">{pg.totalCount}</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div className="ml-3 border-l border-slate-200">
|
<div className="ml-3 border-l border-slate-200">
|
||||||
{pg.years.map(yg => {
|
{pg.workItems.map(wg => {
|
||||||
const yearKey = `${projKey}::y${yg.year}`
|
const wiKey = `${projKey}::w${wg.workItemId ?? '_none_'}`
|
||||||
return (
|
return (
|
||||||
<details
|
<details
|
||||||
key={yearKey}
|
key={wiKey}
|
||||||
open={isExpanded(yearKey)}
|
open={isExpanded(wiKey)}
|
||||||
onToggle={e => toggleExpand(yearKey, (e.currentTarget as HTMLDetailsElement).open)}
|
onToggle={e => toggleExpand(wiKey, (e.currentTarget as HTMLDetailsElement).open)}
|
||||||
className="group/year"
|
className="group/wi"
|
||||||
>
|
>
|
||||||
<summary className="flex cursor-pointer items-center gap-1.5 px-3 py-1.5 hover:bg-slate-50 [&::-webkit-details-marker]:hidden">
|
<summary className="flex cursor-pointer items-center gap-1.5 px-3 py-1.5 hover:bg-slate-50 [&::-webkit-details-marker]:hidden">
|
||||||
<svg className="h-3 w-3 shrink-0 text-slate-400 transition-transform group-open/year:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
<svg className="h-3 w-3 shrink-0 text-slate-400 transition-transform group-open/wi:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
||||||
<span className="text-sm">📅</span>
|
<span className="text-sm">🧱</span>
|
||||||
<span className="flex-1 truncate text-[12px] font-medium text-slate-700">Năm {yg.year}</span>
|
<span className={cn('flex-1 truncate text-[12px]', wg.workItemId ? 'font-medium text-slate-700' : 'italic text-slate-400')}>{wg.workItemName}</span>
|
||||||
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-600">{yg.totalCount}</span>
|
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-600">{wg.items.length}</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div className="ml-3 border-l border-slate-200">
|
<ul className="ml-3 divide-y divide-slate-100 border-l border-slate-200">
|
||||||
{yg.suppliers.map(sg => {
|
{wg.items.map(p => (
|
||||||
const supKey = `${yearKey}::s${sg.supplierId ?? '_none_'}`
|
<li key={p.id}>
|
||||||
return (
|
<button
|
||||||
<details
|
onClick={() => selectRow(p.id)}
|
||||||
key={supKey}
|
className={cn(
|
||||||
open={isExpanded(supKey)}
|
'block w-full px-3 py-2 text-left transition hover:bg-slate-50',
|
||||||
onToggle={e => toggleExpand(supKey, (e.currentTarget as HTMLDetailsElement).open)}
|
selectedId === p.id && 'bg-brand-50 ring-1 ring-inset ring-brand-200',
|
||||||
className="group/sup"
|
)}
|
||||||
>
|
>
|
||||||
<summary className="flex cursor-pointer items-center gap-1.5 px-3 py-1.5 hover:bg-slate-50 [&::-webkit-details-marker]:hidden">
|
{/* Plan AG6 — compact card 3 row: title+badge / mã+time / drafter+dept+contract */}
|
||||||
<svg className="h-3 w-3 shrink-0 text-slate-400 transition-transform group-open/sup:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
<div className="flex items-start justify-between gap-2">
|
||||||
<span className="text-sm">🏢</span>
|
<div className="min-w-0 flex-1 truncate text-[13px] font-medium text-slate-900">{p.tenGoiThau}</div>
|
||||||
<span className={cn('flex-1 truncate text-[12px]', sg.supplierId ? 'text-slate-700' : 'italic text-slate-400')}>{sg.supplierName}</span>
|
<span
|
||||||
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] text-slate-600">{sg.items.length}</span>
|
className={cn(
|
||||||
</summary>
|
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium',
|
||||||
<ul className="ml-3 divide-y divide-slate-100 border-l border-slate-200">
|
PeDisplayStatusColor[getPeDisplayStatus(p.phase)],
|
||||||
{sg.items.map(p => (
|
)}
|
||||||
<li key={p.id}>
|
>
|
||||||
<button
|
{PeDisplayStatusLabel[getPeDisplayStatus(p.phase)]}
|
||||||
onClick={() => selectRow(p.id)}
|
</span>
|
||||||
className={cn(
|
</div>
|
||||||
'block w-full px-3 py-2 text-left transition hover:bg-slate-50',
|
<div className="mt-0.5 flex items-center gap-1.5 text-[11px] text-slate-500">
|
||||||
selectedId === p.id && 'bg-brand-50 ring-1 ring-inset ring-brand-200',
|
<span className="font-mono">{p.maPhieu ?? '—'}</span>
|
||||||
)}
|
<span className="text-slate-300">·</span>
|
||||||
>
|
{/* S23 t2 UAT: BE list sort theo UpdatedAt DESC (fallback CreatedAt). */}
|
||||||
{/* Plan AG6 — compact card 3 row: title+badge / mã+time / drafter+dept+contract */}
|
<span title={`Tạo lúc ${new Date(p.createdAt).toLocaleString('vi-VN')}`}>
|
||||||
<div className="flex items-start justify-between gap-2">
|
{new Date(p.createdAt).toLocaleString('vi-VN', {
|
||||||
<div className="min-w-0 flex-1 truncate text-[13px] font-medium text-slate-900">{p.tenGoiThau}</div>
|
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||||
<span
|
hour: '2-digit', minute: '2-digit',
|
||||||
className={cn(
|
})}
|
||||||
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium',
|
</span>
|
||||||
PeDisplayStatusColor[getPeDisplayStatus(p.phase)],
|
</div>
|
||||||
)}
|
{(p.drafterName || p.departmentName || p.contractId) && (
|
||||||
>
|
<div className="mt-0.5 flex items-center justify-between gap-2 text-[11px]">
|
||||||
{PeDisplayStatusLabel[getPeDisplayStatus(p.phase)]}
|
<span className="min-w-0 flex-1 truncate text-slate-500">
|
||||||
</span>
|
{p.drafterName && <>👤 {p.drafterName}</>}
|
||||||
</div>
|
{p.drafterName && p.departmentName && <span className="text-slate-300"> · </span>}
|
||||||
<div className="mt-0.5 flex items-center gap-1.5 text-[11px] text-slate-500">
|
{p.departmentName && <span className="text-slate-400">{p.departmentName}</span>}
|
||||||
<span className="font-mono">{p.maPhieu ?? '—'}</span>
|
</span>
|
||||||
<span className="text-slate-300">·</span>
|
{p.contractId && <span className="shrink-0 text-[10px] font-medium text-brand-600">✓ HĐ</span>}
|
||||||
{/* S23 t2 UAT: BE list sort theo UpdatedAt DESC (fallback CreatedAt). */}
|
</div>
|
||||||
<span title={`Tạo lúc ${new Date(p.createdAt).toLocaleString('vi-VN')}`}>
|
)}
|
||||||
{new Date(p.createdAt).toLocaleString('vi-VN', {
|
</button>
|
||||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
</li>
|
||||||
hour: '2-digit', minute: '2-digit',
|
))}
|
||||||
})}
|
</ul>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{(p.drafterName || p.departmentName || p.contractId) && (
|
|
||||||
<div className="mt-0.5 flex items-center justify-between gap-2 text-[11px]">
|
|
||||||
<span className="min-w-0 flex-1 truncate text-slate-500">
|
|
||||||
{p.drafterName && <>👤 {p.drafterName}</>}
|
|
||||||
{p.drafterName && p.departmentName && <span className="text-slate-300"> · </span>}
|
|
||||||
{p.departmentName && <span className="text-slate-400">{p.departmentName}</span>}
|
|
||||||
</span>
|
|
||||||
{p.contractId && <span className="shrink-0 text-[10px] font-medium text-brand-600">✓ HĐ</span>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</details>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</details>
|
</details>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
77
scripts/s59-wipe-testing-data.sql
Normal file
77
scripts/s59-wipe-testing-data.sql
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- S59 (2026-06-11) — Wipe transactional TESTING data trước khi team vào testing
|
||||||
|
-- Chỉ đạo: anh Kiệt FDC (Zalo 14:28 "xoá các cái testing trước đi em").
|
||||||
|
-- Anh chốt (AskUserQuestion S59): wipe 3 cụm + reset bộ đếm mã về 0.
|
||||||
|
--
|
||||||
|
-- XÓA: 10 PurchaseEvaluations (A/031-040, cascade child) · 7 Contracts [DEMO]
|
||||||
|
-- (cascade) · 64 Notifications · 1 ApprovalWorkflow V2 cũ (inactive+unghim)
|
||||||
|
-- · PurchaseEvaluationCodeSequences + ContractCodeSequences (generator tự
|
||||||
|
-- INSERT LastSeq=1 khi thiếu row → phiếu/HĐ thật đầu tiên = .../001).
|
||||||
|
-- GIỮ: Projects/WorkItems/Suppliers/Departments (master S55) · Users/Roles/
|
||||||
|
-- Permissions/MenuItems · ContractTemplates/Clauses · ApprovalWorkflows
|
||||||
|
-- GHIM (7 active) · WorkflowDefinitions V1 + PE V1 defs (policy, không
|
||||||
|
-- phải transactional) · HRM catalogs · EmployeeProfiles.
|
||||||
|
-- FK: PE child ALL CASCADE trừ Quotes NO_ACTION (multi-path) — single DELETE
|
||||||
|
-- PurchaseEvaluations vẫn pass vì cascade Details→Quotes dọn trước
|
||||||
|
-- end-of-statement check (proven scripts/plan-r-cleanup.sql S23).
|
||||||
|
-- Resurrect-safe: SeedDemoContracts/PurchaseEvaluations nằm trong gate
|
||||||
|
-- DemoSeed:Disabled=true (DbInitializer.cs) → không seed lại sau restart.
|
||||||
|
-- Run: sqlcmd -S .\SQLEXPRESS -d SolutionErp -i s59-wipe-testing-data.sql
|
||||||
|
-- ============================================================================
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
PRINT '=== BEFORE ===';
|
||||||
|
SELECT 'PE' AS T, COUNT(*) AS C FROM PurchaseEvaluations
|
||||||
|
UNION ALL SELECT 'Contracts', COUNT(*) FROM Contracts
|
||||||
|
UNION ALL SELECT 'Notifications', COUNT(*) FROM Notifications
|
||||||
|
UNION ALL SELECT 'AwV2_total', COUNT(*) FROM ApprovalWorkflows
|
||||||
|
UNION ALL SELECT 'PeSeq', COUNT(*) FROM PurchaseEvaluationCodeSequences
|
||||||
|
UNION ALL SELECT 'CtSeq', COUNT(*) FROM ContractCodeSequences;
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- 1. Phiếu PE (cascade: Suppliers/Details/Quotes/Approvals/Changelogs/
|
||||||
|
-- Attachments/LevelOpinions/DepartmentOpinions)
|
||||||
|
DELETE FROM PurchaseEvaluations;
|
||||||
|
|
||||||
|
-- 2. Hợp đồng [DEMO] (cascade: Approvals + 7 bảng type-details + Comments/Attachments)
|
||||||
|
DELETE FROM Contracts;
|
||||||
|
|
||||||
|
-- 3. Thông báo
|
||||||
|
DELETE FROM Notifications;
|
||||||
|
|
||||||
|
-- 4. Workflow V2 cũ không dùng (QT-DN-V2-001 v1 inactive+unghim — PE pin đã gone
|
||||||
|
-- ở bước 1 nên FK Restrict released). 7 workflow GHIM active KHÔNG match filter.
|
||||||
|
DELETE FROM ApprovalWorkflows WHERE IsActive = 0 AND IsUserSelectable = 0;
|
||||||
|
|
||||||
|
-- 5. Reset bộ đếm mã (DELETE row → generator INSERT lại LastSeq=1)
|
||||||
|
DELETE FROM PurchaseEvaluationCodeSequences;
|
||||||
|
DELETE FROM ContractCodeSequences;
|
||||||
|
|
||||||
|
PRINT '=== AFTER (expect 0 các bảng transactional, KEEP_* giữ nguyên) ===';
|
||||||
|
SELECT 'PE' AS T, COUNT(*) AS C FROM PurchaseEvaluations
|
||||||
|
UNION ALL SELECT 'PeSuppliers', COUNT(*) FROM PurchaseEvaluationSuppliers
|
||||||
|
UNION ALL SELECT 'PeDetails', COUNT(*) FROM PurchaseEvaluationDetails
|
||||||
|
UNION ALL SELECT 'PeQuotes', COUNT(*) FROM PurchaseEvaluationQuotes
|
||||||
|
UNION ALL SELECT 'PeApprovals', COUNT(*) FROM PurchaseEvaluationApprovals
|
||||||
|
UNION ALL SELECT 'PeChangelogs', COUNT(*) FROM PurchaseEvaluationChangelogs
|
||||||
|
UNION ALL SELECT 'PeAttachments', COUNT(*) FROM PurchaseEvaluationAttachments
|
||||||
|
UNION ALL SELECT 'PeLevelOpinions', COUNT(*) FROM PurchaseEvaluationLevelOpinions
|
||||||
|
UNION ALL SELECT 'Contracts', COUNT(*) FROM Contracts
|
||||||
|
UNION ALL SELECT 'CtApprovals', COUNT(*) FROM ContractApprovals
|
||||||
|
UNION ALL SELECT 'Notifications', COUNT(*) FROM Notifications
|
||||||
|
UNION ALL SELECT 'AwV2_total', COUNT(*) FROM ApprovalWorkflows
|
||||||
|
UNION ALL SELECT 'AwV2_active_ghim', COUNT(*) FROM ApprovalWorkflows WHERE IsActive = 1
|
||||||
|
UNION ALL SELECT 'PeSeq', COUNT(*) FROM PurchaseEvaluationCodeSequences
|
||||||
|
UNION ALL SELECT 'CtSeq', COUNT(*) FROM ContractCodeSequences
|
||||||
|
UNION ALL SELECT 'KEEP_Projects', COUNT(*) FROM Projects
|
||||||
|
UNION ALL SELECT 'KEEP_WorkItems', COUNT(*) FROM WorkItems
|
||||||
|
UNION ALL SELECT 'KEEP_Suppliers', COUNT(*) FROM Suppliers
|
||||||
|
UNION ALL SELECT 'KEEP_Users', COUNT(*) FROM Users
|
||||||
|
UNION ALL SELECT 'KEEP_Templates', COUNT(*) FROM ContractTemplates
|
||||||
|
UNION ALL SELECT 'KEEP_WfDefV1', COUNT(*) FROM WorkflowDefinitions
|
||||||
|
UNION ALL SELECT 'KEEP_PeWfDefV1', COUNT(*) FROM PurchaseEvaluationWorkflowDefinitions;
|
||||||
|
|
||||||
|
COMMIT TRANSACTION;
|
||||||
|
PRINT 'S59 wipe COMMITTED.';
|
||||||
Reference in New Issue
Block a user