[CLAUDE] Contract: KHKK man duyet — bo "(N dong)" + cot STT + ma hang muc (Mig 72)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 7m2s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 7m2s
Anh giao qua anh prod man duyet KHKK, 2 diem khoanh do:
(1) "Bo 1 dong di nhe" -> go hau to "(N dong)" khoi title Section 2.
(2) "Hien thi stt va ma hang muc (Du an) - ma hang muc chi tiet"
-> bang 3->4 cot (+STT cot rieng, anh chot), cot HANG MUC them dong
ma "<ma du an> - <ma chi tiet>" nam TREN ten.
Mig 72 AddMaHangMucToContractSigningPlanLines (anh chot DENORM co migration):
- +cot MaHangMuc nvarchar(20) = BANG nguon ContractCatalogEntry.Code.
- backfill Sql() idempotent (WHERE MaHangMuc IS NULL), COY KHONG loc IsDeleted:
hang muc xoa mem SAU ngay lap phieu thi dong phieu van phai giu duoc ma —
do chinh la ly do cot nay la denorm. Loc di la bo trong dung dong can cuu nhat.
- ten bang doi chieu voi ToTable() cua CA HAI configuration truoc khi viet
(Mig 60 tung backfill soi NHAM BANG, chay sach, phai de Mig 61 va — gotcha #78/#79).
PeWorkItemCode KHONG denorm — noi join WorkItems san co o Features.cs:929, 0 cot moi.
Chup lai vao KHKK la chup mot thu von da la con tro loose-Guid qua PE (Mig 49).
3 write-site denorm sua DU: :527 moi null · :695 gan lai · :805 tao moi.
Sot 1 cho = de dong co ten ma khong co ma, chi lo ra o luong it ai bam.
FE 2 app SHA IDENTICAL (verify identical @HEAD TRUOC khi copy de).
Build BE 0 warning 0 error · 2 app build sach · 644/644 test pass.
Lint 42 van de = NO CO SAN, 0 cai o 2 file luot nay dung.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@ -66,6 +66,18 @@ function moneyOrDash(n: number | null | undefined): ReactNode {
|
||||
return n == null ? DASH : formatVnd(n)
|
||||
}
|
||||
|
||||
/**
|
||||
* [S177 — anh: "hiển thị stt và mã hạng mục (Dự án) - mã hạng mục chi tiết"]
|
||||
* Ghép `<mã hạng mục dự án> - <mã hạng mục chi tiết>` cho dòng bảng hạng mục.
|
||||
* 🔴 LỌC rồi mới nối, KHÔNG nối rồi mới cắt: thiếu một vế mà cứ chèn " - " thì ra
|
||||
* "DAO_MONG - " hoặc " - A1-01" — dấu nối mồ côi trông như dữ liệu hỏng chứ không như
|
||||
* thiếu dữ liệu. Cả hai vế rỗng ⇒ trả null để chỗ gọi BỎ HẲN dòng mã, không vẽ ô trống.
|
||||
*/
|
||||
function joinMaHangMuc(maDuAn: string | null, maChiTiet: string | null): string | null {
|
||||
const parts = [maDuAn, maChiTiet].filter((s): s is string => s != null && s !== '')
|
||||
return parts.length === 0 ? null : parts.join(' - ')
|
||||
}
|
||||
|
||||
function formatDateTime(iso: string): string {
|
||||
return new Date(iso).toLocaleString('vi-VN')
|
||||
}
|
||||
@ -456,8 +468,11 @@ export function KhkkDetailContent({
|
||||
</Section>
|
||||
|
||||
{/* ─────────────── 2 ─────────────── */}
|
||||
{/* [S177 — anh: "Bỏ 1 dòng đi nhé"] Bỏ hậu tố "(N dòng)" khỏi `title`: bảng ngay dưới
|
||||
đã có cột STT nên số dòng tự đọc được ở dòng cuối, đếm thêm trên tiêu đề là nói
|
||||
hai lần. (Chú thích phải nằm NGOÀI thẻ — JSX không nhận comment giữa attribute.) */}
|
||||
<Section
|
||||
title={`2. Hạng mục (${plan.lines.length} dòng)`}
|
||||
title="2. Hạng mục"
|
||||
icon={<Users className="h-4 w-4" />}
|
||||
accent="var(--color-teal-500)"
|
||||
head="text-teal-700"
|
||||
@ -487,21 +502,29 @@ export function KhkkDetailContent({
|
||||
(đúng ý đồ của `overflow-x-auto` bọc ngoài) thay vì bóp chữ. */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[880px] table-fixed text-xs">
|
||||
{/* [S177] 3 → 4 cột (+STT). Bề rộng lấy 6% từ cột hạng mục, KHÔNG đụng 2 cột
|
||||
bên phải: cột Giá phải giữ nguyên bề rộng thì số mới còn thẳng hàng giữa
|
||||
các phiếu — đó chính là lý do `table-fixed` được chốt ở S175. */}
|
||||
<colgroup>
|
||||
<col className="w-[52%]" />
|
||||
<col className="w-[6%]" />
|
||||
<col className="w-[46%]" />
|
||||
<col className="w-[32%]" />
|
||||
<col className="w-[16%]" />
|
||||
</colgroup>
|
||||
<thead className="border-b border-slate-200 bg-slate-50/70">
|
||||
<tr>
|
||||
<th className="label-eyebrow px-3 py-2 text-right">STT</th>
|
||||
<th className="label-eyebrow px-3 py-2 text-left">Hạng mục</th>
|
||||
<th className="label-eyebrow px-3 py-2 text-right">Giá</th>
|
||||
<th className="label-eyebrow px-3 py-2 text-right"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{plan.lines.map((l) => (
|
||||
{plan.lines.map((l, i) => (
|
||||
<tr key={l.id} className="border-b border-slate-100 last:border-0 hover:bg-slate-50/60">
|
||||
{/* [S177] STT = thứ tự HIỂN THỊ, không phải id nghiệp vụ. BE đã sắp
|
||||
`OrderBy(CreatedAt).ThenBy(Id)` nên thứ tự này ổn định giữa 2 lần tải. */}
|
||||
<td className="px-3 py-2 text-right align-top tabular-nums text-slate-400">{i + 1}</td>
|
||||
<td className="px-3 py-2 text-slate-700">
|
||||
{isDraft && !l.contractId ? (
|
||||
<>
|
||||
@ -538,6 +561,15 @@ export function KhkkDetailContent({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* [S177 — anh: "mã hạng mục (Dự án) - mã hạng mục chi tiết"]
|
||||
Mã đứng TRÊN tên: mã ngắn + đều nên mắt quét dọc được cả cột,
|
||||
còn tên thì dài ngắn khác nhau. `font-mono` để các mã thẳng cột.
|
||||
Vắng cả hai mã ⇒ `joinMaHangMuc` trả null ⇒ không vẽ dòng này. */}
|
||||
{joinMaHangMuc(plan.peWorkItemCode, l.maHangMuc) && (
|
||||
<div className="font-mono text-[11px] font-medium text-slate-500">
|
||||
{joinMaHangMuc(plan.peWorkItemCode, l.maHangMuc)}
|
||||
</div>
|
||||
)}
|
||||
<div>{orDash(l.tenHangMuc)}</div>
|
||||
{l.note && <div className="mt-0.5 text-[11px] text-slate-500">{l.note}</div>}
|
||||
</>
|
||||
@ -584,7 +616,9 @@ export function KhkkDetailContent({
|
||||
còn được dùng làm DẤU phân biệt dòng-gốc ⟂ dòng-thêm-tay ở nút xoá trên. */}
|
||||
<tfoot>
|
||||
<tr className="border-t-2 border-slate-300 bg-slate-50">
|
||||
<td className="px-3 py-2 text-right text-xs font-semibold uppercase tracking-wide text-slate-600">Tổng</td>
|
||||
{/* [S177] `colSpan={2}` nuốt cột STT mới — thiếu nó thì chữ "Tổng" tụt sang
|
||||
ô hạng mục và con số Tổng rơi lệch một cột so với cột Giá bên trên. */}
|
||||
<td colSpan={2} className="px-3 py-2 text-right text-xs font-semibold uppercase tracking-wide text-slate-600">Tổng</td>
|
||||
<td className="px-3 py-2 text-right text-sm font-bold tabular-nums text-brand-800">
|
||||
{formatVnd(plan.lines.reduce((s, l) => s + (l.proposedAmount ?? 0), 0))}
|
||||
</td>
|
||||
|
||||
@ -225,6 +225,9 @@ export interface KhkkLineDto {
|
||||
// tiết hiện "—"; ĐƯỜNG GÁN (Lines-editor) thuộc **K4b**, K2 chỉ hiển thị.
|
||||
catalogEntryId: string | null
|
||||
tenHangMuc: string | null
|
||||
// [Mig 72 S177] `ContractSigningPlanLineDto` append-CUỐI — MÃ hạng mục ("A1-01"), DENORM
|
||||
// cùng lúc với `tenHangMuc`. null = dòng chưa gán hạng mục ⇒ hiển thị lùi về tên.
|
||||
maHangMuc: string | null
|
||||
}
|
||||
|
||||
export interface KhkkDossierItemDto {
|
||||
@ -396,6 +399,13 @@ export interface KhkkDetailDto extends KhkkListItemDto {
|
||||
finalizeLevelName: string | null
|
||||
/** Lịch sử duyệt (BE đã lọc `decision = Pending`). */
|
||||
approvals: KhkkApprovalDto[]
|
||||
/**
|
||||
* [S177] MÃ của hạng mục CÔNG VIỆC DỰ ÁN ("DAO_MONG") — cặp mã của `peWorkItemName`.
|
||||
* 🔴 Khai Ở ĐÂY chứ KHÔNG ở `KhkkListItemDto`, dù `peWorkItemName` nằm trên đó: BE chỉ
|
||||
* trả `PeWorkItemCode` trên `ContractSigningPlanDetailDto` (append-CUỐI), list KHÔNG có.
|
||||
* Khai nhầm lên list là hứa một field mà payload danh sách không bao giờ mang.
|
||||
*/
|
||||
peWorkItemCode: string | null
|
||||
}
|
||||
|
||||
// [K7 S167 — vá-4b] Tập id HỢP ĐỒNG mà một phiếu KHKK đã đẻ ra. Gom từ MỌI nguồn CÓ THẬT
|
||||
|
||||
@ -66,6 +66,18 @@ function moneyOrDash(n: number | null | undefined): ReactNode {
|
||||
return n == null ? DASH : formatVnd(n)
|
||||
}
|
||||
|
||||
/**
|
||||
* [S177 — anh: "hiển thị stt và mã hạng mục (Dự án) - mã hạng mục chi tiết"]
|
||||
* Ghép `<mã hạng mục dự án> - <mã hạng mục chi tiết>` cho dòng bảng hạng mục.
|
||||
* 🔴 LỌC rồi mới nối, KHÔNG nối rồi mới cắt: thiếu một vế mà cứ chèn " - " thì ra
|
||||
* "DAO_MONG - " hoặc " - A1-01" — dấu nối mồ côi trông như dữ liệu hỏng chứ không như
|
||||
* thiếu dữ liệu. Cả hai vế rỗng ⇒ trả null để chỗ gọi BỎ HẲN dòng mã, không vẽ ô trống.
|
||||
*/
|
||||
function joinMaHangMuc(maDuAn: string | null, maChiTiet: string | null): string | null {
|
||||
const parts = [maDuAn, maChiTiet].filter((s): s is string => s != null && s !== '')
|
||||
return parts.length === 0 ? null : parts.join(' - ')
|
||||
}
|
||||
|
||||
function formatDateTime(iso: string): string {
|
||||
return new Date(iso).toLocaleString('vi-VN')
|
||||
}
|
||||
@ -456,8 +468,11 @@ export function KhkkDetailContent({
|
||||
</Section>
|
||||
|
||||
{/* ─────────────── 2 ─────────────── */}
|
||||
{/* [S177 — anh: "Bỏ 1 dòng đi nhé"] Bỏ hậu tố "(N dòng)" khỏi `title`: bảng ngay dưới
|
||||
đã có cột STT nên số dòng tự đọc được ở dòng cuối, đếm thêm trên tiêu đề là nói
|
||||
hai lần. (Chú thích phải nằm NGOÀI thẻ — JSX không nhận comment giữa attribute.) */}
|
||||
<Section
|
||||
title={`2. Hạng mục (${plan.lines.length} dòng)`}
|
||||
title="2. Hạng mục"
|
||||
icon={<Users className="h-4 w-4" />}
|
||||
accent="var(--color-teal-500)"
|
||||
head="text-teal-700"
|
||||
@ -487,21 +502,29 @@ export function KhkkDetailContent({
|
||||
(đúng ý đồ của `overflow-x-auto` bọc ngoài) thay vì bóp chữ. */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[880px] table-fixed text-xs">
|
||||
{/* [S177] 3 → 4 cột (+STT). Bề rộng lấy 6% từ cột hạng mục, KHÔNG đụng 2 cột
|
||||
bên phải: cột Giá phải giữ nguyên bề rộng thì số mới còn thẳng hàng giữa
|
||||
các phiếu — đó chính là lý do `table-fixed` được chốt ở S175. */}
|
||||
<colgroup>
|
||||
<col className="w-[52%]" />
|
||||
<col className="w-[6%]" />
|
||||
<col className="w-[46%]" />
|
||||
<col className="w-[32%]" />
|
||||
<col className="w-[16%]" />
|
||||
</colgroup>
|
||||
<thead className="border-b border-slate-200 bg-slate-50/70">
|
||||
<tr>
|
||||
<th className="label-eyebrow px-3 py-2 text-right">STT</th>
|
||||
<th className="label-eyebrow px-3 py-2 text-left">Hạng mục</th>
|
||||
<th className="label-eyebrow px-3 py-2 text-right">Giá</th>
|
||||
<th className="label-eyebrow px-3 py-2 text-right"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{plan.lines.map((l) => (
|
||||
{plan.lines.map((l, i) => (
|
||||
<tr key={l.id} className="border-b border-slate-100 last:border-0 hover:bg-slate-50/60">
|
||||
{/* [S177] STT = thứ tự HIỂN THỊ, không phải id nghiệp vụ. BE đã sắp
|
||||
`OrderBy(CreatedAt).ThenBy(Id)` nên thứ tự này ổn định giữa 2 lần tải. */}
|
||||
<td className="px-3 py-2 text-right align-top tabular-nums text-slate-400">{i + 1}</td>
|
||||
<td className="px-3 py-2 text-slate-700">
|
||||
{isDraft && !l.contractId ? (
|
||||
<>
|
||||
@ -538,6 +561,15 @@ export function KhkkDetailContent({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* [S177 — anh: "mã hạng mục (Dự án) - mã hạng mục chi tiết"]
|
||||
Mã đứng TRÊN tên: mã ngắn + đều nên mắt quét dọc được cả cột,
|
||||
còn tên thì dài ngắn khác nhau. `font-mono` để các mã thẳng cột.
|
||||
Vắng cả hai mã ⇒ `joinMaHangMuc` trả null ⇒ không vẽ dòng này. */}
|
||||
{joinMaHangMuc(plan.peWorkItemCode, l.maHangMuc) && (
|
||||
<div className="font-mono text-[11px] font-medium text-slate-500">
|
||||
{joinMaHangMuc(plan.peWorkItemCode, l.maHangMuc)}
|
||||
</div>
|
||||
)}
|
||||
<div>{orDash(l.tenHangMuc)}</div>
|
||||
{l.note && <div className="mt-0.5 text-[11px] text-slate-500">{l.note}</div>}
|
||||
</>
|
||||
@ -584,7 +616,9 @@ export function KhkkDetailContent({
|
||||
còn được dùng làm DẤU phân biệt dòng-gốc ⟂ dòng-thêm-tay ở nút xoá trên. */}
|
||||
<tfoot>
|
||||
<tr className="border-t-2 border-slate-300 bg-slate-50">
|
||||
<td className="px-3 py-2 text-right text-xs font-semibold uppercase tracking-wide text-slate-600">Tổng</td>
|
||||
{/* [S177] `colSpan={2}` nuốt cột STT mới — thiếu nó thì chữ "Tổng" tụt sang
|
||||
ô hạng mục và con số Tổng rơi lệch một cột so với cột Giá bên trên. */}
|
||||
<td colSpan={2} className="px-3 py-2 text-right text-xs font-semibold uppercase tracking-wide text-slate-600">Tổng</td>
|
||||
<td className="px-3 py-2 text-right text-sm font-bold tabular-nums text-brand-800">
|
||||
{formatVnd(plan.lines.reduce((s, l) => s + (l.proposedAmount ?? 0), 0))}
|
||||
</td>
|
||||
|
||||
@ -225,6 +225,9 @@ export interface KhkkLineDto {
|
||||
// tiết hiện "—"; ĐƯỜNG GÁN (Lines-editor) thuộc **K4b**, K2 chỉ hiển thị.
|
||||
catalogEntryId: string | null
|
||||
tenHangMuc: string | null
|
||||
// [Mig 72 S177] `ContractSigningPlanLineDto` append-CUỐI — MÃ hạng mục ("A1-01"), DENORM
|
||||
// cùng lúc với `tenHangMuc`. null = dòng chưa gán hạng mục ⇒ hiển thị lùi về tên.
|
||||
maHangMuc: string | null
|
||||
}
|
||||
|
||||
export interface KhkkDossierItemDto {
|
||||
@ -396,6 +399,13 @@ export interface KhkkDetailDto extends KhkkListItemDto {
|
||||
finalizeLevelName: string | null
|
||||
/** Lịch sử duyệt (BE đã lọc `decision = Pending`). */
|
||||
approvals: KhkkApprovalDto[]
|
||||
/**
|
||||
* [S177] MÃ của hạng mục CÔNG VIỆC DỰ ÁN ("DAO_MONG") — cặp mã của `peWorkItemName`.
|
||||
* 🔴 Khai Ở ĐÂY chứ KHÔNG ở `KhkkListItemDto`, dù `peWorkItemName` nằm trên đó: BE chỉ
|
||||
* trả `PeWorkItemCode` trên `ContractSigningPlanDetailDto` (append-CUỐI), list KHÔNG có.
|
||||
* Khai nhầm lên list là hứa một field mà payload danh sách không bao giờ mang.
|
||||
*/
|
||||
peWorkItemCode: string | null
|
||||
}
|
||||
|
||||
// [K7 S167 — vá-4b] Tập id HỢP ĐỒNG mà một phiếu KHKK đã đẻ ra. Gom từ MỌI nguồn CÓ THẬT
|
||||
|
||||
@ -87,7 +87,11 @@ public record ContractSigningPlanLineDto(
|
||||
// [K2 S164] append-CUỐI — hạng mục SP-002 của dòng. `TenHangMuc` là DENORM lúc gán
|
||||
// (đừng join ngược lấy tên mới: danh mục đổi tên không được viết lại lịch sử phiếu).
|
||||
Guid? CatalogEntryId,
|
||||
string? TenHangMuc);
|
||||
string? TenHangMuc,
|
||||
// [Mig 72 — S177] append-CUỐI. MÃ hạng mục ("A1-01"), DENORM cùng lúc với `TenHangMuc`
|
||||
// ngay trên — KHÔNG join ngược `ContractCatalogEntries` lấy mã hiện tại (ghép mã-mới với
|
||||
// tên-lúc-ký là sai lệch khó thấy hơn cả để trôi cả hai; xem entity `:29-35`).
|
||||
string? MaHangMuc);
|
||||
|
||||
public record ContractSigningPlanDossierItemDto(
|
||||
Guid Id,
|
||||
@ -220,7 +224,12 @@ public record ContractSigningPlanDetailDto(
|
||||
bool EndedByLevelFinalize,
|
||||
string? FinalizeStepName,
|
||||
string? FinalizeLevelName,
|
||||
List<ContractSigningPlanApprovalDto> Approvals);
|
||||
List<ContractSigningPlanApprovalDto> Approvals,
|
||||
// [S177] append-CUỐI. MÃ của hạng mục CÔNG VIỆC DỰ ÁN ("DAO_MONG") — cặp mã của
|
||||
// `PeWorkItemName` ở `:178`. 🔴 KHÔNG denorm: khác `Line.MaHangMuc`, cái này lấy qua
|
||||
// JOIN SỐNG `WorkItems` ngay tại handler (`:929`) y như cái tên đã lấy — chụp lại vào
|
||||
// KHKK là chụp một thứ vốn đã là con trỏ loose-Guid qua PE (Mig 49).
|
||||
string? PeWorkItemCode);
|
||||
|
||||
/// Dòng picker "PE đã duyệt — chờ lập kế hoạch ký kết".
|
||||
public record ApprovedPeAwaitingPlanDto(
|
||||
@ -525,6 +534,7 @@ public class CreateContractSigningPlanCommandHandler(
|
||||
// (`ContractSigningPlanWorkflowService.SubmitAsync`) chặn trình khi còn dòng NULL.
|
||||
CatalogEntryId = null,
|
||||
TenHangMuc = null,
|
||||
MaHangMuc = null, // [Mig 72] cặp mã+tên đi liền — mồi thì cùng NULL
|
||||
PeReferenceAmount = peReference,
|
||||
// Mồi ProposedAmount = số PE. W2 CHƯA có đường sửa dòng giá (spec §②-2 không
|
||||
// liệt kê command nào cho Line) ⇒ để 0 thì cả phiếu là 0 và không ai sửa được.
|
||||
@ -693,6 +703,7 @@ public class UpdateContractSigningPlanLineCommandHandler(
|
||||
// Denorm tên TẠI THỜI ĐIỂM GÁN (khuôn `SignedByFullName` Mig 26 — entity `:25-27`):
|
||||
// danh mục đổi tên sau KHÔNG viết lại lịch sử phiếu.
|
||||
line.TenHangMuc = entry.TenVi;
|
||||
line.MaHangMuc = entry.Code; // [Mig 72] chụp CÙNG LÚC với tên — xem entity `:29-35`
|
||||
changes.Add($"hạng mục → '{entry.Code} — {entry.TenVi}'");
|
||||
}
|
||||
|
||||
@ -803,6 +814,7 @@ public class AddContractSigningPlanLineCommandHandler(
|
||||
SupplierId = request.SupplierId,
|
||||
CatalogEntryId = entry.Id,
|
||||
TenHangMuc = entry.TenVi,
|
||||
MaHangMuc = entry.Code, // [Mig 72] chụp CÙNG LÚC với tên — xem entity `:29-35`
|
||||
// 🔴 = 0, KHÔNG copy số tham chiếu của NCC. `PeReferenceAmount` là Σ báo giá ĐƯỢC
|
||||
// CHỌN của CẢ NCC đó trên phiếu PE, đã nằm trọn ở dòng gốc (`:512-515`). Lặp lại ở
|
||||
// dòng thêm tay = ĐẾM ĐÔI tiền tham chiếu của dự án. Nó cũng là DẤU phân biệt
|
||||
@ -923,12 +935,16 @@ public class GetContractSigningPlanQueryHandler(IApplicationDbContext db, ICurre
|
||||
.Select(p => new { p.MaPhieu, p.TenGoiThau, p.WorkItemId })
|
||||
.FirstOrDefaultAsync(ct);
|
||||
// `WorkItemId` là loose-Guid (Mig 49 — KHÔNG FK vật lý) ⇒ join tay, null-safe.
|
||||
var peWorkItemName = pe != null && pe.WorkItemId != null
|
||||
// [S177] Lấy LUÔN `Code` trong cùng 1 lượt đọc thay vì thêm query thứ hai — anh muốn
|
||||
// hiện "mã hạng mục (Dự án) - mã hạng mục chi tiết" trên từng dòng bảng hạng mục.
|
||||
var peWorkItem = pe != null && pe.WorkItemId != null
|
||||
? await db.WorkItems.AsNoTracking()
|
||||
.Where(w => w.Id == pe.WorkItemId)
|
||||
.Select(w => w.Name)
|
||||
.Select(w => new { w.Name, w.Code })
|
||||
.FirstOrDefaultAsync(ct)
|
||||
: null;
|
||||
var peWorkItemName = peWorkItem?.Name;
|
||||
var peWorkItemCode = peWorkItem?.Code;
|
||||
var project = await db.Projects.AsNoTracking()
|
||||
.Where(p => p.Id == plan.ProjectId)
|
||||
.Select(p => new { p.Name, p.Code })
|
||||
@ -1045,7 +1061,7 @@ public class GetContractSigningPlanQueryHandler(IApplicationDbContext db, ICurre
|
||||
// [K2 S164] Tên hạng mục lấy từ CỘT DENORM của chính dòng, KHÔNG join ngược sang
|
||||
// `ContractCatalogEntries`: danh mục sửa tên/xoá mềm sau ngày ký không được phép
|
||||
// viết lại nội dung phiếu đã lập (khuôn `SignedByFullName` Mig 26).
|
||||
l.CatalogEntryId, l.TenHangMuc))
|
||||
l.CatalogEntryId, l.TenHangMuc, l.MaHangMuc))
|
||||
.ToList();
|
||||
|
||||
var dossierItems = plan.DossierItems
|
||||
@ -1144,7 +1160,9 @@ public class GetContractSigningPlanQueryHandler(IApplicationDbContext db, ICurre
|
||||
// bảo FE "đọc `ApprovedAmount` + khoá ô" thay vì đọc `ProposedAmount`.
|
||||
plan.Phase == ContractSigningPlanPhase.DaDuyet,
|
||||
// [S171 BE-1/BE-1b/BE-2] 5 field nuôi panel 3.
|
||||
wfVersion, endedByLevelFinalize, finalizeStepName, finalizeLevelName, approvals);
|
||||
wfVersion, endedByLevelFinalize, finalizeStepName, finalizeLevelName, approvals,
|
||||
// [S177] mã hạng mục CÔNG VIỆC DỰ ÁN — cùng lượt đọc `WorkItems` với `peWorkItemName`.
|
||||
peWorkItemCode);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -26,6 +26,15 @@ public class ContractSigningPlanLine : AuditableEntity
|
||||
// phiếu (khuôn `SignedByFullName` Mig 26). Đọc để hiển thị; đừng join ngược lấy tên mới.
|
||||
public string? TenHangMuc { get; set; }
|
||||
|
||||
// [Mig 72 — S177] Denorm MÃ hạng mục ("A1-01"), chụp CÙNG LÚC với `TenHangMuc` ngay trên.
|
||||
// 🔴 Cặp mã+tên phải denorm CÙNG NHAU, không tách: denorm tên mà join ngược lấy mã thì
|
||||
// một dòng phiếu cũ sẽ hiện tên-lúc-ký ghép mã-hiện-tại — sai lệch khó thấy hơn cả việc
|
||||
// để trôi cả hai. Mọi write-site của `TenHangMuc` PHẢI ghi kèm field này.
|
||||
// NULL = dòng chưa gán hạng mục (`CatalogEntryId` cũng NULL). Dòng cũ trước Mig 72 đã được
|
||||
// backfill, KỂ CẢ khi hạng mục đó nay đã xoá mềm — backfill cố ý không lọc `IsDeleted`.
|
||||
// FE hiển thị lùi về tên khi thiếu mã, không để trống ô.
|
||||
public string? MaHangMuc { get; set; }
|
||||
|
||||
// SNAPSHOT tổng báo giá của NCC này trên phiếu PE (SUM Quote.IsSelected) tại thời
|
||||
// điểm TẠO kế hoạch — chụp 1 lần, KHÔNG đọc live (PE sửa sau không được làm trôi
|
||||
// số của kế hoạch đã lập; cùng triết lý freeze ngân sách Mig 67).
|
||||
|
||||
@ -22,6 +22,9 @@ public class ContractSigningPlanLineConfiguration : IEntityTypeConfiguration<Con
|
||||
// — denorm mà hẹp hơn nguồn thì tên dài nhất (A3-09 ~150 ký tự) vẫn lọt, nhưng để
|
||||
// BẰNG nguồn thì không bao giờ phải cắt. Validator lấy số từ ĐÂY (EF = source of truth).
|
||||
b.Property(x => x.TenHangMuc).HasMaxLength(500);
|
||||
// [Mig 72 — S177] Cùng lý lẽ BẰNG-NGUỒN như trên: `ContractCatalogEntry.Code` là 20
|
||||
// (`ContractCatalogEntryConfiguration.cs:18`) ⇒ denorm cũng 20, không bao giờ phải cắt.
|
||||
b.Property(x => x.MaHangMuc).HasMaxLength(20);
|
||||
|
||||
// [K2 Mig 71 — S164] UNIQUE đổi (Plan, Supplier) → (Plan, Supplier, CatalogEntry):
|
||||
// 1 NCC được nhiều dòng trên cùng phiếu, mỗi dòng 1 hạng mục khác nhau.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,45 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMaHangMucToContractSigningPlanLines : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "MaHangMuc",
|
||||
table: "ContractSigningPlanLines",
|
||||
type: "nvarchar(20)",
|
||||
maxLength: 20,
|
||||
nullable: true);
|
||||
|
||||
// [S177] BACKFILL dòng phiếu ĐÃ CÓ: chép `Code` của hạng mục đang trỏ tới.
|
||||
// 🔴 Tên bảng đã đối chiếu với `ToTable(...)` của CẢ HAI configuration trước khi
|
||||
// viết — Mig 60 từng backfill soi NHẦM BẢNG, chạy sạch, không ném lỗi, phải đẻ
|
||||
// Mig 61 vá (gotcha #78/#79). UPDATE sai bảng chỉ đụng 0 dòng rồi báo thành công.
|
||||
// 🔴 CỐ Ý KHÔNG lọc `c.IsDeleted = 0`: hạng mục bị xoá mềm SAU ngày lập phiếu thì
|
||||
// dòng phiếu vẫn phải giữ được mã của nó — đó chính là lý do cột này là denorm.
|
||||
// Lọc đi là tự tay bỏ trống đúng những dòng cần cứu nhất.
|
||||
// 🔴 Idempotent nhờ `MaHangMuc IS NULL`: chạy lại không đè dòng đã có mã.
|
||||
migrationBuilder.Sql(@"
|
||||
UPDATE l
|
||||
SET l.MaHangMuc = c.Code
|
||||
FROM ContractSigningPlanLines l
|
||||
INNER JOIN ContractCatalogEntries c ON c.Id = l.CatalogEntryId
|
||||
WHERE l.MaHangMuc IS NULL
|
||||
AND l.CatalogEntryId IS NOT NULL;");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaHangMuc",
|
||||
table: "ContractSigningPlanLines");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -742,6 +742,10 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("MaHangMuc")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
Reference in New Issue
Block a user