[CLAUDE] Supplier: Mig 62 expand 30-field + seed 4 NCC + FE form 2-app
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m7s

Phase A of the NCC/TP Excel-import backlog (anh Kiet FDC). Supplier
entity 9 -> 27 fields to hold the 30-column supplier database.

Backend:
- Supplier +18 nullable cols; SupplierType +CaHai=6 (append-only);
  new nullable SupplierStatus enum.
- Mig 62 ExpandSupplierFields (3-file): AddColumn x18 + guarded un-cram
  Sql (clear legacy crammed Note on the 4 seeded rows, exact-match).
  No new table (89), reversible Down.
- Seed fill-nulls-if-exists: populate 30 fields for TRUONGGIANG/TANPHU/
  TGN/DONGDUONG from Excel; idempotent, never clobbers existing cols.
- DTO + Create/Update commands + validators (MaximumLength-only, no
  NotEmpty so open-auth Create minimal payload stays valid) + 2 query
  projections.

Frontend (fe-admin + fe-user, byte-identical SHA-mirror):
- types/master.ts +18 fields +SupplierStatus; SuppliersPage form 4
  section groups + Status select + 3 NAS links via new shared PathLink
  (extracted from PE HoSoLink render+Copy). All new fields optional.

Excel upload auto-map = Phase B (deferred). Tests test-after (UAT mode).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-07-06 16:06:11 +07:00
parent 6d217f77ed
commit 778fc98b2f
21 changed files with 7440 additions and 23 deletions

View File

@ -70,12 +70,13 @@ Bearer từ `POST api.solutions.com.vn/api/auth/login` → status matrix expecte
## 📅 Recent activity (compressed — full verbatim → `archive/2026-06.md` via `archive/_INDEX.md`) ## 📅 Recent activity (compressed — full verbatim → `archive/2026-06.md` via `archive/_INDEX.md`)
- **2026-07-03 S101 H19-adopt Lane-2 (mapping + model-tier compliance, WF1 investigate — VERDICT SOUND):** ⭐ Mapping CORRECT: reviewer=final-review-gate (Category 1-6 adversarial + Smart-Friend anti-rubberstamp `reviewer.md:268-276` + live-curl deploy-verify = H19 §3a "cổng rà-soát cuối" exact) · investigator-codebase=ground-truth-lane (`investigator-codebase.md:23-26` file:line-evidence + sqlcmd + grep + "if uncertain say uncertain" = H19 §3b "xác-minh sự-thật chống bịa-số/dẫn-chứng"). NO better-fit: investigator-api=EXTERNAL-docs (WebFetch, out-of-scope internal-truth) · database-agent=narrow DB-lens (subset không phải general fact-verify). **Model-tier checklist 12-pt: SE meets A1✅(settings pin `claude-fable-5[1m]` — `[1m]` VALID in settings.json ≠ frontmatter; Fable-down→Opus 1M H5-fallback = A1/A2 frontier-1M allowed) A2✅ A3✅(all-inherit=all-top, 5-fn map) A4/A5/A6 N/A-by-form(no tier-2/helper layer) B1✅ B2✅ B3✅(11 role-names SE-own, 0 copied) C1✅(11/11 `model: inherit` grep) C2✅(hmw.js `tier` field) C3✅(0 `[1m]` in FRONTMATTER model: line — 4 grep hits ALL prose: README:9/12/16 Upgrade-notes + database-agent:46 checklist).** ⚠️ Toggle-marker template EXACT-exists: `.claude/hmw-mode.on` gitignored AFTER `!.claude/**` last-match-wins (`.gitignore:87`); new `.claude/model-real-mode.on` MUST copy placement or gets un-ignored. ⚠️ Ảo-ensemble reuse: hmw.js `parallel()` :97 + synthesis machinery PRESENT (grep 10 hits) — but "S101 WF2 3-lane" is LIVE-mechanism NOT persisted artifact (runs/ = old 06-18 only, HMW-ephemeral S94+ pattern). VACUOUS-MET nuance TRUE: under all-inherit+Fable, 2 positions ALREADY top → H19 §4 "allow reach top-model" adds 0 runtime-delta; new value = toggle depth-control + Fable-outage multi-Opus>single-Opus. Tag `[s101-h19-lane2, mapping-sound, 12pt-compliance, marker-template, vacuous-met]`. - **2026-07-06 Supplier 30-field invest+review WF `wf_5ac2cdb0` (anh Kiệt FDC — 🅰️ schema go-live):** ⭐ schema-domain lane **#53-GARBLED (null, "no StructuredOutput")** nhưng full design RECOVERED từ chính AS-10 memory-write của nó (reverted sau harvest — `feedback_agent_return_garble_recover`); em-main authored authoritative schema. **6 ground-truth correction vs brief:** (1) **ClosedXML 0.105.0 = project Excel-standard, KHÔNG phải OpenXml raw**`csproj:8` + mọi path (`XlsxRenderer.cs:18`/`ContractExcelExporter.cs:56`/`AttendanceReportExcelExporter.cs:14`) dùng `XLWorkbook`; `cell.GetString()` auto-decode shared-string → "shared-string pitfall" MOOT (cả 2 pkg referenced, OpenXml KHÔNG dùng cho read). (2) **reuse-existing-cols > parallel-new-cols** — map TÊN CÔNG TY→`Name`(cũ), ĐỊA CHỈ XUẤT HĐ→`Address`, SĐT→`Phone`; thêm cột MỚI chỉ cho TYPE thật-mới (OfficeAddress/MailingAddress/ContactPhone); tạo companyName/invoiceAddress = ORPHAN cột seed live. (3) **`UpdateSupplierCommand.cs:49-57` blind absolute-set cả 9 incl Note = latent #73 clobber** → import (🅱️) PHẢI ghi entity direct + fill-where-non-empty, KHÔNG route qua Update cmd. (4) **seed idempotent-by-Code (`DbInitializer.cs:2781`) SKIP existing → cột mới NULL** → population = seed **fill-nulls-if-exists** (KHÔNG seed-insert-alone). (5) **`SupplierDto` hand-projected 2 site** (`GetSupplierQuery.cs:21`+`ListSuppliersQuery.cs:48-50` positional, no AutoMapper) → +6-file thread mỗi cột. (6) FE: **`Dialog.tsx:48` đã `max-h-70vh overflow-auto`** → form-expand cần 0 layout-code (go-live win lớn nhất); HoSoLink `toFileUrl`/`PathWithCopy` LOCAL-không-export trong `PeDetailTabs.tsx:2087/2098` → extract shared `ui/PathLink.tsx`. Design chốt: +18 nullable AddColumn (net 89), `SupplierType +CaHai=6` APPEND, new `SupplierStatus` enum nullable, Mig 62. Reviews 3× PASS_WITH_CONCERNS (2 goLiveSafe, import-robustness=false chờ 🅱️). Tag `[supplier-30field, wf-invest, closedxml-not-openxml, reuse-cols, hand-dto-2site, update-blind-set-73, seed-fillnulls, dialog-autoscroll]`.
- **2026-07-02 (S100 H18-adopt WF1 `wf_50a817ce-a69` — 3 lane ∥, on-behalf em main APPEND B3, return-only 0-residual):** L1 measurer-coverage: 3/3 script (`mfe-eval:216`/`selfimprove:133-137`/`archive-gate:106`) **GLOB-LIVE 0-hardcode** — agent thứ 12 tự-cử; ⭐ ghost-wire-class MỚI: **config-key defined-but-unread** (`memory-budget.json:54-55` `role_file_glob`/`diary_path_pattern` — script re-hardcode giá trị TRÙNG → không detector nào flag vì cả 2 phía "đúng"; catch = grep key-name across scripts/) + latent skip-if-no-`tools:` (`selfimprove:147`, 0 affected 11/11 có). L2 ghost-wire+taxonomy: MAST=0 repo-wide (grep PHẢI case-sensitive `\bMAST\b` — insensitive dính "Master" noise); cron monthly = ghost-wire THẬT (CronList=0 + scheduler RỖNG 05-04 log + S96 chạy 13:04 em-main in-session); convention "auto = step em-main kích, no-OS-hook" nhất quán 5 nơi. L3 ledger-đo: `_ledger.md` true-orphan-catch **0/15 lifetime** (single-day artifact 06-18, 15 row/14 folder — mig54-R2 share) · mode-A coverage 0/8 S93+ · ⭐ "S71 `harvest/` rỗng" = **criterion-wording KHÔNG phải incident** (đính chính claim em-main) · repo có 5 "ledger" khác tên → sweep grep `_ledger` EXACT · hmw.js 0-touch. Tag `[s100-h18-wf1, 3-lane, ghostwire-configkey-class, ledger-0-15, on-behalf]`. - **[→ git+session-log 07-03 + adap-reports] S101 H19-adopt Lane-2:** map reviewer=review-gate / investigator=ground-truth-lane; 12-pt tier-checklist ALL✅ (11/11 inherit, C3 no-`[1m]`-in-frontmatter); toggle-marker MUST copy `.gitignore:87` last-match placement; VACUOUS-MET all-inherit+Fable. *(verbatim → archive/2026-07.md)*
- **2026-07-02 S98 verify-lane V2 post-flush (VERDICT PASS):** detector 28→25 (derived-stale 11→8; wikilink 14 + vocab-fork 3 giữ by-design). **8 derived-stale còn = 100% FP-class count-token soft-net:** CLAUDE.md:84 tests-tree "(6 test…)" · ef-core SKILL :94/:95 ("2/1 migration tách/GỘP" history) + :103/:111/:114 ("4/3/1 bảng" Budget-defer/Mig-15 module-local) + :162/:179 (heading "Revert 1 migration" + comment "Script từ 1 migration") — 0 genuine drift vs canonical 61/89/440/79; SKILL edit sáng (+rows 60/61 @:82-83) chỉ shift line-number FP cũ, 0 flag MỚI. ⭐ Grep gotcha: **"Fable RESTORED" exact-string CHỈ STATUS:24 + session-start:29; agents/README dùng biến-thể "Fable 5 1M RESTORED" (:3) + "Fable-restore" (:12)** — exact-phrase grep miss README dù substance đủ. HANDOFF:5 mega-line: bundle mEGqlNWc/Dnrp4PU1=#484, CeTA7Z82 chỉ trong note "= #483" (1 line match duy nhất cả file). 3 on-behalf append đúng-chỗ: database-agent :36 (cuối Log = EOF) · reviewer addendum TRONG-entry S97-bis :68 (câu cuối + marker on-behalf @S98) · impl-fe S97-Part2 :16 giữa badge:9/S97:19. cicd "S98" = 2× "(curate S98)" stub :73/:74, 0 material-tag. Ledger :7 Mode-A ephemeral note (wf mode-A vắng ledger-row ≠ orphan) + bảng 15 row :11-25 nguyên. - **[→ git+session-log 07-02 + adap-reports] S100 H18-adopt WF1:** ⭐ ghost-wire-class **config-key-defined-but-unread** (`budget.json:54-55` script re-hardcode same-value→no-flag; catch=grep-key-across-scripts); cron=ghost-wire (CronList=0); `_ledger` orphan-catch 0/15-lifetime. *(verbatim → archive/2026-07.md)*
- **2026-07-01 S97 session-end feedback-audit (VERDICT CLEAN):** Grep-verified capture readiness pre-close. ✅ gotcha #77/#78/#79 present `docs/gotchas.md:1301/1317/1325` (format `### N.` count=79 = STATUS header — no self-count drift; all 3 have Triệu-chứng+Cơ-chế+Fix+**Quy-tắc** root-cause). ✅ #53 return-garble **x6 (cicd 5/6)** in BOTH `feedback_agent_return_garble_recover.md` (Why-line ×5→×6 + **structural root-cause: cicd MEMORY self-target ~17.1KB→curate-at-return→thinking-step=message-cuối=return-value**; fix=archive-cicd-sớm NOT instruction) AND S97-log Parts 1/2/4 (lần-4/5/6). cicd MEMORY LIVE=17.5KB (confirms self-target). ✅ Mig 60 wrong-table BUG = 2-fix present: code (Mig 61 `20260701142752` query Changelogs) + guard (gotcha #79 "xác định ĐÚNG bảng writer" + "verify marker-count prod trước"). Mig 60 code `:29-31` confirmed `PurchaseEvaluationApprovals a...a.Comment LIKE` = wrong. Both Mig 60/61 have 3-file rule. ✅ git status CLEAN, 0 stray `.claude` misland, 0 `git add -A` in log, S97 workflows HMW-ephemeral (NOT persisted run-folders — consistent S94+ pattern, runs/ only old 06-18). ✅ Product decisions captured (STATUS/HANDOFF/log + commit-msg): "duyệt là fix"→#480 price-lock, "fix trọn"→Mig 60 endsBeforeCeo-runtime, "hiện luôn CEO duyệt"→#483 badge, Supplier-backlog "15-18 field"→STATUS Next-up. **MINOR only:** S97-log per-Part State footers show progression (Part-1 `:47` "88 bảng gotcha 77" stale vs session-end "89/79" Part-4 `:113`) — intentional per-part snapshot but mid-log reader sees stale count. No BLOCKING/MAJOR gaps. - **[→ git+session-log 07-02] S98 verify-lane V2:** detector 28→25, 8 derived-stale=100% FP count-token-soft-net vs canonical 61/89/440/79; grep-gotcha "Fable RESTORED" exact misses README variant ":3 Fable 5 1M RESTORED". *(verbatim → archive/2026-07.md)*
- **2026-07-01 Supplier Excel-import GAP audit (anh Kiệt FDC — 30-col "Database NCC & Nhà thầu phụ" cho bên Pro):** ⭐ **Entity CỰC LEAN 9 domain-prop** (`Domain/Master/Supplier.cs:5-16`: Code·Name·Type·TaxCode·Phone·Email·Address·ContactPerson·Note) + 8 audit inherited (Base/AuditableEntity). EF `SupplierConfiguration.cs`: Code len50 UNIQUE-filtered `[IsDeleted]=0`(:24, Mig47 #57), Name200, TaxCode20, Phone30, Email100, Address500, ContactPerson200, Note1000. **enum `SupplierType.cs:3-10` = NhaCungCap1/NhaThauPhu2/ToDoi3/DonViDichVu4/ChuDauTu5 — KHÔNG có "Cả hai/Both"** (FE mirror `fe-admin/types/master.ts:11-16` identical). **GAP 30→entity: 6✅ · 7⚠collision · 17❌ no-home** — Excel splits Address×2(invoice/office), Phone×2(company/contact), ContactPerson+role; ❌ Fax·Bank-acct×2·Legal-rep+role·GUQ+3-NAS-links·Status/blacklist·Referral·PMH-owner. ⚠️ **S55 seed `SeedRealMasterDataAsync` (`DbInitializer.cs:2768-2787`) crams NĐD/bank/status/category vào free-text `Note`** (:2771-73 TRUONGGIANG/TANPHU/TGN) — proof entity thiếu home. Seed **UNGATED** (called :128, idempotent-key=`Code` :2780) — KHÁC demo-master (`SeedDemoMasterDataAsync` GATED `DemoSeed:Disabled` :117, #75). POST any-auth (`SuppliersController.cs:37-42` S59). FE SHA-identical 2-app (`c1760788`, 253L, 9 fields). **Prod 20 NCC** (Type1×3+Type2×17; type3/4/5 wiped go-live S91 → "Cả hai" enum genuinely-needed). Precedent NAS-link = `PE.HoSoLink nvarchar(1000)` Mig52. Fork→em-main: core-widen ~15col+CaHai=6 vs satellite `SupplierDetail`. → _INDEX pending. - **[→ git+session-log 07-01] S97 feedback-audit:** #77/#78/#79 present `gotchas.md:1301/1317/1325`; #53-garble ×6 = cicd MEMORY-self-target ~17.5KB structural-root-cause; Mig 60 wrong-table Approvals→Mig 61 Changelogs both present. *(verbatim → archive/2026-07.md)*
- **2026-06-29 S93 Harness-16 MFE ground-truth (WF1 `wf_4c63e1bd`):** ⭐ MFE denominator extraction (deterministic, status-field KHÔNG grep): marks Active(-High) (`ACTIVE-MARKS.md`) + AS-class + Active-Guards (`error-ledger.md` Verified✅) + recurring-gotcha (`gotchas.md`); **dedup-by-AS-cross-reference** (gotcha/guard mà AS-row trỏ `#N`/`AS-N` → gộp) = **29 item**. Per-role sub-coverage: denom = numbered anti-pattern/NEVER trong `agents/<role>.md`; numer = diary `agent-memory/<role>/MEMORY.md` (KHÔNG project/user — bẫy trùng-tên H15-v3) match exact-id OR ≥2 content-word sau stop-list (đánh bại leading-verb-9/9-giả). Codified `scripts/mfe-eval.ps1` + engine §H. (WF1 self-write reverted AS-10; em-main harvest single-writer.) → _INDEX. - **[→ 07-06 entry above + session-log] 2026-07-01 Supplier GAP predecessor:** SUPERSEDED by 07-06 schema-design entry above (same anchors: 9-prop lean, seed-cram→Note, HoSoLink NAS precedent, prod 20-NCC). *(verbatim → archive/2026-07.md)*
- **[→ `scripts/mfe-eval.ps1` §H + session-log 06-29] S93 MFE ground-truth:** denominator=marks/AS/guards/gotcha dedup-by-AS-xref=29; per-role coverage denom=numbered-anti-pattern numer=diary-match exact-id OR ≥2-content-word (beats leading-verb-giả). *(verbatim → archive/2026-07.md)*
- **PE FROZEN-Mig59 patchwork + go-live recon cluster (S87→S89, 8 entries → `archive/2026-06.md`):** AREA-2 budget-matrix (`SetPeCcmBudgetPeriodCommand` band-aid misplaced in `PeSuggestedPriceFeatures.cs:153-199`; PRO=drafter-flow vs CCM=role-flow authz asymmetry Section-B row3; "Dự án" col intentional FE-only "—") · AREA-3 finalize/price (AllowApproverFinalize AUTO Mig58 + finalizeByCcmDelegation OPT-IN Mig54 coexist, AUTO-first; ApplyApprovedPriceOnFinalize allow-list {Ncc,ProMin,ProMax,Ccm}; isSystem-branch DEAD via public ApproveV2Async) · AREA-4 UpdatePeDraft (**LATENT #73-class BUG:** HoSoLink absolute-set `PurchaseEvaluationFeatures.cs:280` but PeHeaderForm PUT OMITS → clears; 2 PUT call-sites mutually destructive; no HoSoLink-preserve test) · AREA-5 urgent/notify/TZ (SignalR push BYPASSES UtcDateTimeJsonConverter benign-Kind.Utc; urgent SET/UNSET symmetric fail-closed; possible double-notify-then-skip drafter=Cấp1) · AREA-6 FE-consumers (all 6 PE-comp+3 pages SHA-identical 2-app; residual single-winner `PeListPanel.tsx:168` blank ≥2 joint; #70 stale-echo PRO row3/8 no-peFetching) · S89 AllowApproverFinalize-NOTE feasibility (**GAP: BE field MỚI cần** — flag chỉ ở `currentLevelOptions` current-level; approvalFlow+list+dashboard DTO KHÔNG carry → expose `EndsBeforeCeo`+FinalizeStep/LevelName; FE note đầu `PeWorkflowPanel.tsx` flow `<ol>`) · 06-24 3-change (winner=SINGLE `SelectedSupplierId`, winnerQuoteTotal DERIVED 3-site; budget=0 blocked 2-layer; terminal=NEW per-Level bool mirror SkipToFinal) · 06-23 D2/D4/D5 UAT (urgent authz BẤT-ĐỐI-XỨNG gắn-role/gỡ-DeptMgr; PE-create dropdown `PeWorkspaceCreateView.tsx:94`; workflow-required 2-tầng). - **PE FROZEN-Mig59 patchwork + go-live recon cluster (S87→S89, 8 entries → `archive/2026-06.md`):** AREA-2 budget-matrix (`SetPeCcmBudgetPeriodCommand` band-aid misplaced in `PeSuggestedPriceFeatures.cs:153-199`; PRO=drafter-flow vs CCM=role-flow authz asymmetry Section-B row3; "Dự án" col intentional FE-only "—") · AREA-3 finalize/price (AllowApproverFinalize AUTO Mig58 + finalizeByCcmDelegation OPT-IN Mig54 coexist, AUTO-first; ApplyApprovedPriceOnFinalize allow-list {Ncc,ProMin,ProMax,Ccm}; isSystem-branch DEAD via public ApproveV2Async) · AREA-4 UpdatePeDraft (**LATENT #73-class BUG:** HoSoLink absolute-set `PurchaseEvaluationFeatures.cs:280` but PeHeaderForm PUT OMITS → clears; 2 PUT call-sites mutually destructive; no HoSoLink-preserve test) · AREA-5 urgent/notify/TZ (SignalR push BYPASSES UtcDateTimeJsonConverter benign-Kind.Utc; urgent SET/UNSET symmetric fail-closed; possible double-notify-then-skip drafter=Cấp1) · AREA-6 FE-consumers (all 6 PE-comp+3 pages SHA-identical 2-app; residual single-winner `PeListPanel.tsx:168` blank ≥2 joint; #70 stale-echo PRO row3/8 no-peFetching) · S89 AllowApproverFinalize-NOTE feasibility (**GAP: BE field MỚI cần** — flag chỉ ở `currentLevelOptions` current-level; approvalFlow+list+dashboard DTO KHÔNG carry → expose `EndsBeforeCeo`+FinalizeStep/LevelName; FE note đầu `PeWorkflowPanel.tsx` flow `<ol>`) · 06-24 3-change (winner=SINGLE `SelectedSupplierId`, winnerQuoteTotal DERIVED 3-site; budget=0 blocked 2-layer; terminal=NEW per-Level bool mirror SkipToFinal) · 06-23 D2/D4/D5 UAT (urgent authz BẤT-ĐỐI-XỨNG gắn-role/gỡ-DeptMgr; PE-create dropdown `PeWorkspaceCreateView.tsx:94`; workflow-required 2-tầng).
- **2026-06-20 governance-landing map (RC-sig + User-Mark H12/13 + objective-criteria):** ⭐ WHERE-to-land 3 AI_INFRA gov broadcasts. `harness-11-engine.md` = CANONICAL engine (D5/D6/D7 safety-tier + D8 one-direction-lock + D9 single-writer + D10 Bash-residual + CAVEAT no-OS-hook). `error-ledger.md` = §L.a AS-1..13 + §L.b 7-step. adap-report FORMAT richest = `2026-06-18-Governance-harness-11.md`. rules.md §6.6 = objective-criteria. **report-before-stamp ⊂ D7 (extend, đừng duplicate → C3 vocab-fork).** → _INDEX. - **2026-06-20 governance-landing map (RC-sig + User-Mark H12/13 + objective-criteria):** ⭐ WHERE-to-land 3 AI_INFRA gov broadcasts. `harness-11-engine.md` = CANONICAL engine (D5/D6/D7 safety-tier + D8 one-direction-lock + D9 single-writer + D10 Bash-residual + CAVEAT no-OS-hook). `error-ledger.md` = §L.a AS-1..13 + §L.b 7-step. adap-report FORMAT richest = `2026-06-18-Governance-harness-11.md`. rules.md §6.6 = objective-criteria. **report-before-stamp ⊂ D7 (extend, đừng duplicate → C3 vocab-fork).** → _INDEX.
@ -91,5 +92,5 @@ Bearer từ `POST api.solutions.com.vn/api/auth/login` → status matrix expecte
## 🔄 Curate trigger ## 🔄 Curate trigger
- >~30KB → archive recent → L2 `archive/<period>.md` (byte-exact append) + `_INDEX.md` substring pointer. Stale >3mo → remove. - >~30KB → archive recent → L2 `archive/<period>.md` (byte-exact append) + `_INDEX.md` substring pointer. Stale >3mo → remove.
- **Last curate: 2026-06-20 S80 (em-main, archive-gate keep-floor-hit → manual)** (39.8→~14KB): moved 15 recon entries (lines 73-105 byte-exact via `sed`) → `archive/2026-06.md` (33→67KB) + `_INDEX.md` +15 table-row pointers (all verified count=1). KEPT foundation + 2 newest 06-20 + compressed recon-clusters + 3 git-stubs; UPDATED stale counts (40→57 mig, 84→88 tables, 130→354 test, 55→71 gotcha, 7→11 sub). gist NOT updated (em-main distill later). - **Last curate: 2026-07-06 (em-main single-writer, hook-triggered @21.4KB)** — 6 stale governance/superseded entries (S93/S97/S98/S100/S101 + 07-01 Supplier-predecessor) verbatim → `archive/2026-07.md`, replaced with pointer-stubs. L1 back under target.
- **Prev curate: 2026-06-18 S71** (29.8→23.2KB FIFO-trim 3 + gist gen:2). **Prev S70** (47.0→24.1KB dark-matter recovery + built _INDEX/gist). **Prev S40** (35.7→~20KB). - **Prev curate: 2026-06-18 S71** (29.8→23.2KB FIFO-trim 3 + gist gen:2). **Prev S70** (47.0→24.1KB dark-matter recovery + built _INDEX/gist). **Prev S40** (35.7→~20KB).

View File

@ -0,0 +1,11 @@
# investigator-codebase — archive 2026-07 (L2 verbatim, frozen additive)
## Curated 2026-07-06 (em-main, hook-triggered L1->L2)
- **2026-07-03 S101 H19-adopt Lane-2 (mapping + model-tier compliance, WF1 investigate — VERDICT SOUND):** ⭐ Mapping CORRECT: reviewer=final-review-gate (Category 1-6 adversarial + Smart-Friend anti-rubberstamp `reviewer.md:268-276` + live-curl deploy-verify = H19 §3a "cổng rà-soát cuối" exact) · investigator-codebase=ground-truth-lane (`investigator-codebase.md:23-26` file:line-evidence + sqlcmd + grep + "if uncertain say uncertain" = H19 §3b "xác-minh sự-thật chống bịa-số/dẫn-chứng"). NO better-fit: investigator-api=EXTERNAL-docs (WebFetch, out-of-scope internal-truth) · database-agent=narrow DB-lens (subset không phải general fact-verify). **Model-tier checklist 12-pt: SE meets A1✅(settings pin `claude-fable-5[1m]` — `[1m]` VALID in settings.json ≠ frontmatter; Fable-down→Opus 1M H5-fallback = A1/A2 frontier-1M allowed) A2✅ A3✅(all-inherit=all-top, 5-fn map) A4/A5/A6 N/A-by-form(no tier-2/helper layer) B1✅ B2✅ B3✅(11 role-names SE-own, 0 copied) C1✅(11/11 `model: inherit` grep) C2✅(hmw.js `tier` field) C3✅(0 `[1m]` in FRONTMATTER model: line — 4 grep hits ALL prose: README:9/12/16 Upgrade-notes + database-agent:46 checklist).** ⚠️ Toggle-marker template EXACT-exists: `.claude/hmw-mode.on` gitignored AFTER `!.claude/**` last-match-wins (`.gitignore:87`); new `.claude/model-real-mode.on` MUST copy placement or gets un-ignored. ⚠️ Ảo-ensemble reuse: hmw.js `parallel()` :97 + synthesis machinery PRESENT (grep 10 hits) — but "S101 WF2 3-lane" is LIVE-mechanism NOT persisted artifact (runs/ = old 06-18 only, HMW-ephemeral S94+ pattern). VACUOUS-MET nuance TRUE: under all-inherit+Fable, 2 positions ALREADY top → H19 §4 "allow reach top-model" adds 0 runtime-delta; new value = toggle depth-control + Fable-outage multi-Opus>single-Opus. Tag `[s101-h19-lane2, mapping-sound, 12pt-compliance, marker-template, vacuous-met]`.
- **2026-07-02 (S100 H18-adopt WF1 `wf_50a817ce-a69` — 3 lane ∥, on-behalf em main APPEND B3, return-only 0-residual):** L1 measurer-coverage: 3/3 script (`mfe-eval:216`/`selfimprove:133-137`/`archive-gate:106`) **GLOB-LIVE 0-hardcode** — agent thứ 12 tự-cử; ⭐ ghost-wire-class MỚI: **config-key defined-but-unread** (`memory-budget.json:54-55` `role_file_glob`/`diary_path_pattern` — script re-hardcode giá trị TRÙNG → không detector nào flag vì cả 2 phía "đúng"; catch = grep key-name across scripts/) + latent skip-if-no-`tools:` (`selfimprove:147`, 0 affected 11/11 có). L2 ghost-wire+taxonomy: MAST=0 repo-wide (grep PHẢI case-sensitive `\bMAST\b` — insensitive dính "Master" noise); cron monthly = ghost-wire THẬT (CronList=0 + scheduler RỖNG 05-04 log + S96 chạy 13:04 em-main in-session); convention "auto = step em-main kích, no-OS-hook" nhất quán 5 nơi. L3 ledger-đo: `_ledger.md` true-orphan-catch **0/15 lifetime** (single-day artifact 06-18, 15 row/14 folder — mig54-R2 share) · mode-A coverage 0/8 S93+ · ⭐ "S71 `harvest/` rỗng" = **criterion-wording KHÔNG phải incident** (đính chính claim em-main) · repo có 5 "ledger" khác tên → sweep grep `_ledger` EXACT · hmw.js 0-touch. Tag `[s100-h18-wf1, 3-lane, ghostwire-configkey-class, ledger-0-15, on-behalf]`.
- **2026-07-02 S98 verify-lane V2 post-flush (VERDICT PASS):** detector 28→25 (derived-stale 11→8; wikilink 14 + vocab-fork 3 giữ by-design). **8 derived-stale còn = 100% FP-class count-token soft-net:** CLAUDE.md:84 tests-tree "(6 test…)" · ef-core SKILL :94/:95 ("2/1 migration tách/GỘP" history) + :103/:111/:114 ("4/3/1 bảng" Budget-defer/Mig-15 module-local) + :162/:179 (heading "Revert 1 migration" + comment "Script từ 1 migration") — 0 genuine drift vs canonical 61/89/440/79; SKILL edit sáng (+rows 60/61 @:82-83) chỉ shift line-number FP cũ, 0 flag MỚI. ⭐ Grep gotcha: **"Fable RESTORED" exact-string CHỈ STATUS:24 + session-start:29; agents/README dùng biến-thể "Fable 5 1M RESTORED" (:3) + "Fable-restore" (:12)** — exact-phrase grep miss README dù substance đủ. HANDOFF:5 mega-line: bundle mEGqlNWc/Dnrp4PU1=#484, CeTA7Z82 chỉ trong note "= #483" (1 line match duy nhất cả file). 3 on-behalf append đúng-chỗ: database-agent :36 (cuối Log = EOF) · reviewer addendum TRONG-entry S97-bis :68 (câu cuối + marker on-behalf @S98) · impl-fe S97-Part2 :16 giữa badge:9/S97:19. cicd "S98" = 2× "(curate S98)" stub :73/:74, 0 material-tag. Ledger :7 Mode-A ephemeral note (wf mode-A vắng ledger-row ≠ orphan) + bảng 15 row :11-25 nguyên.
- **2026-07-01 S97 session-end feedback-audit (VERDICT CLEAN):** Grep-verified capture readiness pre-close. ✅ gotcha #77/#78/#79 present `docs/gotchas.md:1301/1317/1325` (format `### N.` count=79 = STATUS header — no self-count drift; all 3 have Triệu-chứng+Cơ-chế+Fix+**Quy-tắc** root-cause). ✅ #53 return-garble **x6 (cicd 5/6)** in BOTH `feedback_agent_return_garble_recover.md` (Why-line ×5→×6 + **structural root-cause: cicd MEMORY self-target ~17.1KB→curate-at-return→thinking-step=message-cuối=return-value**; fix=archive-cicd-sớm NOT instruction) AND S97-log Parts 1/2/4 (lần-4/5/6). cicd MEMORY LIVE=17.5KB (confirms self-target). ✅ Mig 60 wrong-table BUG = 2-fix present: code (Mig 61 `20260701142752` query Changelogs) + guard (gotcha #79 "xác định ĐÚNG bảng writer" + "verify marker-count prod trước"). Mig 60 code `:29-31` confirmed `PurchaseEvaluationApprovals a...a.Comment LIKE` = wrong. Both Mig 60/61 have 3-file rule. ✅ git status CLEAN, 0 stray `.claude` misland, 0 `git add -A` in log, S97 workflows HMW-ephemeral (NOT persisted run-folders — consistent S94+ pattern, runs/ only old 06-18). ✅ Product decisions captured (STATUS/HANDOFF/log + commit-msg): "duyệt là fix"→#480 price-lock, "fix trọn"→Mig 60 endsBeforeCeo-runtime, "hiện luôn CEO duyệt"→#483 badge, Supplier-backlog "15-18 field"→STATUS Next-up. **MINOR only:** S97-log per-Part State footers show progression (Part-1 `:47` "88 bảng gotcha 77" stale vs session-end "89/79" Part-4 `:113`) — intentional per-part snapshot but mid-log reader sees stale count. No BLOCKING/MAJOR gaps.
- **2026-07-01 Supplier Excel-import GAP audit (anh Kiệt FDC — 30-col "Database NCC & Nhà thầu phụ" cho bên Pro):** ⭐ **Entity CỰC LEAN 9 domain-prop** (`Domain/Master/Supplier.cs:5-16`: Code·Name·Type·TaxCode·Phone·Email·Address·ContactPerson·Note) + 8 audit inherited (Base/AuditableEntity). EF `SupplierConfiguration.cs`: Code len50 UNIQUE-filtered `[IsDeleted]=0`(:24, Mig47 #57), Name200, TaxCode20, Phone30, Email100, Address500, ContactPerson200, Note1000. **enum `SupplierType.cs:3-10` = NhaCungCap1/NhaThauPhu2/ToDoi3/DonViDichVu4/ChuDauTu5 — KHÔNG có "Cả hai/Both"** (FE mirror `fe-admin/types/master.ts:11-16` identical). **GAP 30→entity: 6✅ · 7⚠collision · 17❌ no-home** — Excel splits Address×2(invoice/office), Phone×2(company/contact), ContactPerson+role; ❌ Fax·Bank-acct×2·Legal-rep+role·GUQ+3-NAS-links·Status/blacklist·Referral·PMH-owner. ⚠️ **S55 seed `SeedRealMasterDataAsync` (`DbInitializer.cs:2768-2787`) crams NĐD/bank/status/category vào free-text `Note`** (:2771-73 TRUONGGIANG/TANPHU/TGN) — proof entity thiếu home. Seed **UNGATED** (called :128, idempotent-key=`Code` :2780) — KHÁC demo-master (`SeedDemoMasterDataAsync` GATED `DemoSeed:Disabled` :117, #75). POST any-auth (`SuppliersController.cs:37-42` S59). FE SHA-identical 2-app (`c1760788`, 253L, 9 fields). **Prod 20 NCC** (Type1×3+Type2×17; type3/4/5 wiped go-live S91 → "Cả hai" enum genuinely-needed). Precedent NAS-link = `PE.HoSoLink nvarchar(1000)` Mig52. Fork→em-main: core-widen ~15col+CaHai=6 vs satellite `SupplierDetail`. → _INDEX pending.
- **2026-06-29 S93 Harness-16 MFE ground-truth (WF1 `wf_4c63e1bd`):** ⭐ MFE denominator extraction (deterministic, status-field KHÔNG grep): marks Active(-High) (`ACTIVE-MARKS.md`) + AS-class + Active-Guards (`error-ledger.md` Verified✅) + recurring-gotcha (`gotchas.md`); **dedup-by-AS-cross-reference** (gotcha/guard mà AS-row trỏ `#N`/`AS-N` → gộp) = **29 item**. Per-role sub-coverage: denom = numbered anti-pattern/NEVER trong `agents/<role>.md`; numer = diary `agent-memory/<role>/MEMORY.md` (KHÔNG project/user — bẫy trùng-tên H15-v3) match exact-id OR ≥2 content-word sau stop-list (đánh bại leading-verb-9/9-giả). Codified `scripts/mfe-eval.ps1` + engine §H. (WF1 self-write reverted AS-10; em-main harvest single-writer.) → _INDEX.

View File

@ -0,0 +1,68 @@
import { useState } from 'react'
import { toast } from 'sonner'
import { Button } from '@/components/ui/Button'
// Đổi đường dẫn Windows → URL file:// (O:\DATA\x → file:///O:/DATA/x · \\srv\share → file://srv/share).
function toFileUrl(p: string): string {
const slashed = p.trim().replace(/\\/g, '/')
const url = slashed.startsWith('//') ? 'file:' + slashed : 'file:///' + slashed
return encodeURI(url)
}
// Link đường dẫn hồ sơ / NAS dùng chung (trích từ PeDetailTabs).
// - Link web (http/https, vd SharePoint) → bấm mở thẳng tab mới.
// - Đường dẫn ổ cứng/ổ mạng (O:\…, \\server) → render LINK file:// để BẤM-THỬ mở File
// Explorer (chạy nếu máy/trình duyệt cho phép: Edge bật policy IntranetFileLinksEnabled
// qua GPO + host trong Intranet Zone) + nút Copy dự phòng (default Chrome/Edge CHẶN
// https→file:// → bấm no-op → khi đó dùng Copy dán vào File Explorer, máy đã map ổ mạng
// là mở ngay).
export function PathLink({ path }: { path: string }) {
const [copied, setCopied] = useState(false)
const trimmed = path.trim()
if (/^https?:\/\//i.test(trimmed)) {
return (
<a
href={path}
target="_blank"
rel="noopener noreferrer"
className="break-all text-sm text-brand-600 hover:underline"
>
{path}
</a>
)
}
const copy = async () => {
try {
await navigator.clipboard.writeText(path)
setCopied(true)
setTimeout(() => setCopied(false), 1500)
} catch {
toast.error('Không copy được — vui lòng bôi đen đường dẫn rồi Ctrl+C.')
}
}
return (
<div className="flex max-w-2xl items-start gap-2">
<a
href={toFileUrl(path)}
target="_blank"
rel="noopener noreferrer"
className="min-w-0 flex-1 break-all rounded bg-slate-50 px-2 py-1 text-[13px] text-brand-700 ring-1 ring-slate-200 transition hover:bg-brand-50 hover:underline"
title="Bấm mở trong File Explorer (cần Edge + IT bật policy). Không mở được thì bấm Copy rồi dán vào File Explorer."
>
{path}
</a>
<Button
type="button"
variant="outline"
onClick={copy}
className="h-8 shrink-0 px-2.5 text-xs"
title="Copy đường dẫn rồi dán vào File Explorer (This PC)"
>
{copied ? '✓ Đã copy' : 'Copy'}
</Button>
</div>
)
}

View File

@ -11,10 +11,17 @@ import { Label } from '@/components/ui/Label'
import { Select } from '@/components/ui/Select' import { Select } from '@/components/ui/Select'
import { Textarea } from '@/components/ui/Textarea' import { Textarea } from '@/components/ui/Textarea'
import { Dialog } from '@/components/ui/Dialog' import { Dialog } from '@/components/ui/Dialog'
import { PathLink } from '@/components/ui/PathLink'
import { api } from '@/lib/api' import { api } from '@/lib/api'
import { getErrorMessage } from '@/lib/apiError' import { getErrorMessage } from '@/lib/apiError'
import { MenuKeys } from '@/lib/menuKeys' import { MenuKeys } from '@/lib/menuKeys'
import { SupplierType, SupplierTypeLabel, type Paged, type Supplier } from '@/types/master' import {
SupplierStatusLabel,
SupplierType,
SupplierTypeLabel,
type Paged,
type Supplier,
} from '@/types/master'
type FormState = { type FormState = {
id?: string id?: string
@ -27,11 +34,34 @@ type FormState = {
address: string address: string
contactPerson: string contactPerson: string
note: string note: string
packageCategory: string
officeAddress: string
mailingAddress: string
fax: string
bankAccount: string
secondaryBankAccount: string
legalRepresentative: string
legalRepTitle: string
authorizationNote: string
linkGuq: string
linkGpkd: string
linkHsnl: string
contactTitle: string
contactPhone: string
mailRecipient: string
referralSource: string
ownerPmh: string
status: string
} }
const emptyForm: FormState = { const emptyForm: FormState = {
code: '', name: '', type: SupplierType.NhaCungCap, code: '', name: '', type: SupplierType.NhaCungCap,
taxCode: '', phone: '', email: '', address: '', contactPerson: '', note: '', taxCode: '', phone: '', email: '', address: '', contactPerson: '', note: '',
packageCategory: '', officeAddress: '', mailingAddress: '', fax: '',
bankAccount: '', secondaryBankAccount: '', legalRepresentative: '', legalRepTitle: '',
authorizationNote: '', linkGuq: '', linkGpkd: '', linkHsnl: '',
contactTitle: '', contactPhone: '', mailRecipient: '', referralSource: '', ownerPmh: '',
status: '',
} }
export function SuppliersPage() { export function SuppliersPage() {
@ -68,6 +98,24 @@ export function SuppliersPage() {
address: data.address || null, address: data.address || null,
contactPerson: data.contactPerson || null, contactPerson: data.contactPerson || null,
note: data.note || null, note: data.note || null,
packageCategory: data.packageCategory || null,
officeAddress: data.officeAddress || null,
mailingAddress: data.mailingAddress || null,
fax: data.fax || null,
bankAccount: data.bankAccount || null,
secondaryBankAccount: data.secondaryBankAccount || null,
legalRepresentative: data.legalRepresentative || null,
legalRepTitle: data.legalRepTitle || null,
authorizationNote: data.authorizationNote || null,
linkGuq: data.linkGuq || null,
linkGpkd: data.linkGpkd || null,
linkHsnl: data.linkHsnl || null,
contactTitle: data.contactTitle || null,
contactPhone: data.contactPhone || null,
mailRecipient: data.mailRecipient || null,
referralSource: data.referralSource || null,
ownerPmh: data.ownerPmh || null,
status: data.status ? Number(data.status) : null,
} }
if (data.id) { if (data.id) {
await api.put(`/suppliers/${data.id}`, payload) await api.put(`/suppliers/${data.id}`, payload)
@ -103,6 +151,15 @@ export function SuppliersPage() {
id: s.id, code: s.code, name: s.name, type: s.type, id: s.id, code: s.code, name: s.name, type: s.type,
taxCode: s.taxCode ?? '', phone: s.phone ?? '', email: s.email ?? '', taxCode: s.taxCode ?? '', phone: s.phone ?? '', email: s.email ?? '',
address: s.address ?? '', contactPerson: s.contactPerson ?? '', note: s.note ?? '', address: s.address ?? '', contactPerson: s.contactPerson ?? '', note: s.note ?? '',
packageCategory: s.packageCategory ?? '', officeAddress: s.officeAddress ?? '',
mailingAddress: s.mailingAddress ?? '', fax: s.fax ?? '',
bankAccount: s.bankAccount ?? '', secondaryBankAccount: s.secondaryBankAccount ?? '',
legalRepresentative: s.legalRepresentative ?? '', legalRepTitle: s.legalRepTitle ?? '',
authorizationNote: s.authorizationNote ?? '', linkGuq: s.linkGuq ?? '',
linkGpkd: s.linkGpkd ?? '', linkHsnl: s.linkHsnl ?? '',
contactTitle: s.contactTitle ?? '', contactPhone: s.contactPhone ?? '',
mailRecipient: s.mailRecipient ?? '', referralSource: s.referralSource ?? '',
ownerPmh: s.ownerPmh ?? '', status: s.status != null ? String(s.status) : '',
}) })
setOpen(true) setOpen(true)
} }
@ -246,6 +303,114 @@ export function SuppliersPage() {
<Label>Ghi chú</Label> <Label>Ghi chú</Label>
<Textarea rows={3} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /> <Textarea rows={3} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} />
</div> </div>
<div className="col-span-2 mt-2 border-t pt-3 text-sm font-semibold text-slate-600">
Đnh danh
</div>
<div className="col-span-2 space-y-1.5">
<Label>Gói thầu</Label>
<Input value={form.packageCategory} onChange={e => setForm({ ...form, packageCategory: e.target.value })} />
</div>
<div className="col-span-2 mt-2 border-t pt-3 text-sm font-semibold text-slate-600">
Pháp &amp; Hợp đng
</div>
<div className="col-span-2 space-y-1.5">
<Label>Đa chỉ văn phòng</Label>
<Input value={form.officeAddress} onChange={e => setForm({ ...form, officeAddress: e.target.value })} />
</div>
<div className="col-span-2 space-y-1.5">
<Label>Đa chỉ gửi thư</Label>
<Input value={form.mailingAddress} onChange={e => setForm({ ...form, mailingAddress: e.target.value })} />
</div>
<div className="space-y-1.5">
<Label>Fax</Label>
<Input value={form.fax} onChange={e => setForm({ ...form, fax: e.target.value })} />
</div>
<div className="space-y-1.5">
<Label>Người đi diện pháp luật</Label>
<Input
value={form.legalRepresentative}
onChange={e => setForm({ ...form, legalRepresentative: e.target.value })}
/>
</div>
<div className="space-y-1.5">
<Label>Chức vụ đi diện</Label>
<Input value={form.legalRepTitle} onChange={e => setForm({ ...form, legalRepTitle: e.target.value })} />
</div>
<div className="col-span-2 space-y-1.5">
<Label>Số tài khoản ngân hàng</Label>
<Input value={form.bankAccount} onChange={e => setForm({ ...form, bankAccount: e.target.value })} />
</div>
<div className="col-span-2 space-y-1.5">
<Label>Số tài khoản phụ</Label>
<Input
value={form.secondaryBankAccount}
onChange={e => setForm({ ...form, secondaryBankAccount: e.target.value })}
/>
</div>
<div className="col-span-2 space-y-1.5">
<Label>Giấy y quyền (ghi chú)</Label>
<Textarea
rows={2}
value={form.authorizationNote}
onChange={e => setForm({ ...form, authorizationNote: e.target.value })}
/>
</div>
<div className="col-span-2 space-y-1.5">
<Label>Link GUQ</Label>
<Input value={form.linkGuq} onChange={e => setForm({ ...form, linkGuq: e.target.value })} />
{form.linkGuq.trim() && <PathLink path={form.linkGuq} />}
</div>
<div className="col-span-2 space-y-1.5">
<Label>Link GPKD</Label>
<Input value={form.linkGpkd} onChange={e => setForm({ ...form, linkGpkd: e.target.value })} />
{form.linkGpkd.trim() && <PathLink path={form.linkGpkd} />}
</div>
<div className="col-span-2 space-y-1.5">
<Label>Link HSNL</Label>
<Input value={form.linkHsnl} onChange={e => setForm({ ...form, linkHsnl: e.target.value })} />
{form.linkHsnl.trim() && <PathLink path={form.linkHsnl} />}
</div>
<div className="col-span-2 mt-2 border-t pt-3 text-sm font-semibold text-slate-600">
Liên hệ
</div>
<div className="space-y-1.5">
<Label>Chức vụ người liên hệ</Label>
<Input value={form.contactTitle} onChange={e => setForm({ ...form, contactTitle: e.target.value })} />
</div>
<div className="space-y-1.5">
<Label>SĐT người liên hệ</Label>
<Input value={form.contactPhone} onChange={e => setForm({ ...form, contactPhone: e.target.value })} />
</div>
<div className="col-span-2 space-y-1.5">
<Label>Người nhận thư / SĐT</Label>
<Input value={form.mailRecipient} onChange={e => setForm({ ...form, mailRecipient: e.target.value })} />
</div>
<div className="space-y-1.5">
<Label>Nguồn giới thiệu</Label>
<Input value={form.referralSource} onChange={e => setForm({ ...form, referralSource: e.target.value })} />
</div>
<div className="space-y-1.5">
<Label>Người phụ trách (PMH)</Label>
<Input value={form.ownerPmh} onChange={e => setForm({ ...form, ownerPmh: e.target.value })} />
</div>
<div className="col-span-2 mt-2 border-t pt-3 text-sm font-semibold text-slate-600">
Tình trạng
</div>
<div className="space-y-1.5">
<Label>Tình trạng hiện tại</Label>
<Select value={form.status} onChange={e => setForm({ ...form, status: e.target.value })}>
<option value=""> Chưa phân loại </option>
{Object.entries(SupplierStatusLabel).map(([v, l]) => (
<option key={v} value={v}>
{l}
</option>
))}
</Select>
</div>
</form> </form>
</Dialog> </Dialog>
</div> </div>

View File

@ -14,6 +14,7 @@ export const SupplierType = {
ToDoi: 3, ToDoi: 3,
DonViDichVu: 4, DonViDichVu: 4,
ChuDauTu: 5, ChuDauTu: 5,
CaHai: 6,
} as const } as const
export type SupplierType = typeof SupplierType[keyof typeof SupplierType] export type SupplierType = typeof SupplierType[keyof typeof SupplierType]
@ -24,6 +25,21 @@ export const SupplierTypeLabel: Record<SupplierType, string> = {
3: 'Tổ đội', 3: 'Tổ đội',
4: 'Đơn vị dịch vụ', 4: 'Đơn vị dịch vụ',
5: 'Chủ đầu tư', 5: 'Chủ đầu tư',
6: 'Cả hai (NCC & Thầu phụ)',
}
export const SupplierStatus = {
DangHoatDong: 1,
Blacklist: 2,
NgungHopTac: 3,
} as const
export type SupplierStatus = typeof SupplierStatus[keyof typeof SupplierStatus]
export const SupplierStatusLabel: Record<SupplierStatus, string> = {
1: 'Đang hoạt động',
2: 'Blacklist',
3: 'Ngừng hợp tác',
} }
export type Supplier = { export type Supplier = {
@ -37,6 +53,28 @@ export type Supplier = {
address: string | null address: string | null
contactPerson: string | null contactPerson: string | null
note: string | null note: string | null
// Định danh
packageCategory: string | null
// Pháp lý & Hợp đồng
officeAddress: string | null
mailingAddress: string | null
fax: string | null
bankAccount: string | null
secondaryBankAccount: string | null
legalRepresentative: string | null
legalRepTitle: string | null
authorizationNote: string | null
linkGuq: string | null
linkGpkd: string | null
linkHsnl: string | null
// Liên hệ
contactTitle: string | null
contactPhone: string | null
mailRecipient: string | null
referralSource: string | null
ownerPmh: string | null
// Tình trạng
status: SupplierStatus | null
createdAt: string createdAt: string
updatedAt: string | null updatedAt: string | null
} }

View File

@ -0,0 +1,68 @@
import { useState } from 'react'
import { toast } from 'sonner'
import { Button } from '@/components/ui/Button'
// Đổi đường dẫn Windows → URL file:// (O:\DATA\x → file:///O:/DATA/x · \\srv\share → file://srv/share).
function toFileUrl(p: string): string {
const slashed = p.trim().replace(/\\/g, '/')
const url = slashed.startsWith('//') ? 'file:' + slashed : 'file:///' + slashed
return encodeURI(url)
}
// Link đường dẫn hồ sơ / NAS dùng chung (trích từ PeDetailTabs).
// - Link web (http/https, vd SharePoint) → bấm mở thẳng tab mới.
// - Đường dẫn ổ cứng/ổ mạng (O:\…, \\server) → render LINK file:// để BẤM-THỬ mở File
// Explorer (chạy nếu máy/trình duyệt cho phép: Edge bật policy IntranetFileLinksEnabled
// qua GPO + host trong Intranet Zone) + nút Copy dự phòng (default Chrome/Edge CHẶN
// https→file:// → bấm no-op → khi đó dùng Copy dán vào File Explorer, máy đã map ổ mạng
// là mở ngay).
export function PathLink({ path }: { path: string }) {
const [copied, setCopied] = useState(false)
const trimmed = path.trim()
if (/^https?:\/\//i.test(trimmed)) {
return (
<a
href={path}
target="_blank"
rel="noopener noreferrer"
className="break-all text-sm text-brand-600 hover:underline"
>
{path}
</a>
)
}
const copy = async () => {
try {
await navigator.clipboard.writeText(path)
setCopied(true)
setTimeout(() => setCopied(false), 1500)
} catch {
toast.error('Không copy được — vui lòng bôi đen đường dẫn rồi Ctrl+C.')
}
}
return (
<div className="flex max-w-2xl items-start gap-2">
<a
href={toFileUrl(path)}
target="_blank"
rel="noopener noreferrer"
className="min-w-0 flex-1 break-all rounded bg-slate-50 px-2 py-1 text-[13px] text-brand-700 ring-1 ring-slate-200 transition hover:bg-brand-50 hover:underline"
title="Bấm mở trong File Explorer (cần Edge + IT bật policy). Không mở được thì bấm Copy rồi dán vào File Explorer."
>
{path}
</a>
<Button
type="button"
variant="outline"
onClick={copy}
className="h-8 shrink-0 px-2.5 text-xs"
title="Copy đường dẫn rồi dán vào File Explorer (This PC)"
>
{copied ? '✓ Đã copy' : 'Copy'}
</Button>
</div>
)
}

View File

@ -11,10 +11,17 @@ import { Label } from '@/components/ui/Label'
import { Select } from '@/components/ui/Select' import { Select } from '@/components/ui/Select'
import { Textarea } from '@/components/ui/Textarea' import { Textarea } from '@/components/ui/Textarea'
import { Dialog } from '@/components/ui/Dialog' import { Dialog } from '@/components/ui/Dialog'
import { PathLink } from '@/components/ui/PathLink'
import { api } from '@/lib/api' import { api } from '@/lib/api'
import { getErrorMessage } from '@/lib/apiError' import { getErrorMessage } from '@/lib/apiError'
import { MenuKeys } from '@/lib/menuKeys' import { MenuKeys } from '@/lib/menuKeys'
import { SupplierType, SupplierTypeLabel, type Paged, type Supplier } from '@/types/master' import {
SupplierStatusLabel,
SupplierType,
SupplierTypeLabel,
type Paged,
type Supplier,
} from '@/types/master'
type FormState = { type FormState = {
id?: string id?: string
@ -27,11 +34,34 @@ type FormState = {
address: string address: string
contactPerson: string contactPerson: string
note: string note: string
packageCategory: string
officeAddress: string
mailingAddress: string
fax: string
bankAccount: string
secondaryBankAccount: string
legalRepresentative: string
legalRepTitle: string
authorizationNote: string
linkGuq: string
linkGpkd: string
linkHsnl: string
contactTitle: string
contactPhone: string
mailRecipient: string
referralSource: string
ownerPmh: string
status: string
} }
const emptyForm: FormState = { const emptyForm: FormState = {
code: '', name: '', type: SupplierType.NhaCungCap, code: '', name: '', type: SupplierType.NhaCungCap,
taxCode: '', phone: '', email: '', address: '', contactPerson: '', note: '', taxCode: '', phone: '', email: '', address: '', contactPerson: '', note: '',
packageCategory: '', officeAddress: '', mailingAddress: '', fax: '',
bankAccount: '', secondaryBankAccount: '', legalRepresentative: '', legalRepTitle: '',
authorizationNote: '', linkGuq: '', linkGpkd: '', linkHsnl: '',
contactTitle: '', contactPhone: '', mailRecipient: '', referralSource: '', ownerPmh: '',
status: '',
} }
export function SuppliersPage() { export function SuppliersPage() {
@ -68,6 +98,24 @@ export function SuppliersPage() {
address: data.address || null, address: data.address || null,
contactPerson: data.contactPerson || null, contactPerson: data.contactPerson || null,
note: data.note || null, note: data.note || null,
packageCategory: data.packageCategory || null,
officeAddress: data.officeAddress || null,
mailingAddress: data.mailingAddress || null,
fax: data.fax || null,
bankAccount: data.bankAccount || null,
secondaryBankAccount: data.secondaryBankAccount || null,
legalRepresentative: data.legalRepresentative || null,
legalRepTitle: data.legalRepTitle || null,
authorizationNote: data.authorizationNote || null,
linkGuq: data.linkGuq || null,
linkGpkd: data.linkGpkd || null,
linkHsnl: data.linkHsnl || null,
contactTitle: data.contactTitle || null,
contactPhone: data.contactPhone || null,
mailRecipient: data.mailRecipient || null,
referralSource: data.referralSource || null,
ownerPmh: data.ownerPmh || null,
status: data.status ? Number(data.status) : null,
} }
if (data.id) { if (data.id) {
await api.put(`/suppliers/${data.id}`, payload) await api.put(`/suppliers/${data.id}`, payload)
@ -103,6 +151,15 @@ export function SuppliersPage() {
id: s.id, code: s.code, name: s.name, type: s.type, id: s.id, code: s.code, name: s.name, type: s.type,
taxCode: s.taxCode ?? '', phone: s.phone ?? '', email: s.email ?? '', taxCode: s.taxCode ?? '', phone: s.phone ?? '', email: s.email ?? '',
address: s.address ?? '', contactPerson: s.contactPerson ?? '', note: s.note ?? '', address: s.address ?? '', contactPerson: s.contactPerson ?? '', note: s.note ?? '',
packageCategory: s.packageCategory ?? '', officeAddress: s.officeAddress ?? '',
mailingAddress: s.mailingAddress ?? '', fax: s.fax ?? '',
bankAccount: s.bankAccount ?? '', secondaryBankAccount: s.secondaryBankAccount ?? '',
legalRepresentative: s.legalRepresentative ?? '', legalRepTitle: s.legalRepTitle ?? '',
authorizationNote: s.authorizationNote ?? '', linkGuq: s.linkGuq ?? '',
linkGpkd: s.linkGpkd ?? '', linkHsnl: s.linkHsnl ?? '',
contactTitle: s.contactTitle ?? '', contactPhone: s.contactPhone ?? '',
mailRecipient: s.mailRecipient ?? '', referralSource: s.referralSource ?? '',
ownerPmh: s.ownerPmh ?? '', status: s.status != null ? String(s.status) : '',
}) })
setOpen(true) setOpen(true)
} }
@ -246,6 +303,114 @@ export function SuppliersPage() {
<Label>Ghi chú</Label> <Label>Ghi chú</Label>
<Textarea rows={3} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} /> <Textarea rows={3} value={form.note} onChange={e => setForm({ ...form, note: e.target.value })} />
</div> </div>
<div className="col-span-2 mt-2 border-t pt-3 text-sm font-semibold text-slate-600">
Đnh danh
</div>
<div className="col-span-2 space-y-1.5">
<Label>Gói thầu</Label>
<Input value={form.packageCategory} onChange={e => setForm({ ...form, packageCategory: e.target.value })} />
</div>
<div className="col-span-2 mt-2 border-t pt-3 text-sm font-semibold text-slate-600">
Pháp &amp; Hợp đng
</div>
<div className="col-span-2 space-y-1.5">
<Label>Đa chỉ văn phòng</Label>
<Input value={form.officeAddress} onChange={e => setForm({ ...form, officeAddress: e.target.value })} />
</div>
<div className="col-span-2 space-y-1.5">
<Label>Đa chỉ gửi thư</Label>
<Input value={form.mailingAddress} onChange={e => setForm({ ...form, mailingAddress: e.target.value })} />
</div>
<div className="space-y-1.5">
<Label>Fax</Label>
<Input value={form.fax} onChange={e => setForm({ ...form, fax: e.target.value })} />
</div>
<div className="space-y-1.5">
<Label>Người đi diện pháp luật</Label>
<Input
value={form.legalRepresentative}
onChange={e => setForm({ ...form, legalRepresentative: e.target.value })}
/>
</div>
<div className="space-y-1.5">
<Label>Chức vụ đi diện</Label>
<Input value={form.legalRepTitle} onChange={e => setForm({ ...form, legalRepTitle: e.target.value })} />
</div>
<div className="col-span-2 space-y-1.5">
<Label>Số tài khoản ngân hàng</Label>
<Input value={form.bankAccount} onChange={e => setForm({ ...form, bankAccount: e.target.value })} />
</div>
<div className="col-span-2 space-y-1.5">
<Label>Số tài khoản phụ</Label>
<Input
value={form.secondaryBankAccount}
onChange={e => setForm({ ...form, secondaryBankAccount: e.target.value })}
/>
</div>
<div className="col-span-2 space-y-1.5">
<Label>Giấy y quyền (ghi chú)</Label>
<Textarea
rows={2}
value={form.authorizationNote}
onChange={e => setForm({ ...form, authorizationNote: e.target.value })}
/>
</div>
<div className="col-span-2 space-y-1.5">
<Label>Link GUQ</Label>
<Input value={form.linkGuq} onChange={e => setForm({ ...form, linkGuq: e.target.value })} />
{form.linkGuq.trim() && <PathLink path={form.linkGuq} />}
</div>
<div className="col-span-2 space-y-1.5">
<Label>Link GPKD</Label>
<Input value={form.linkGpkd} onChange={e => setForm({ ...form, linkGpkd: e.target.value })} />
{form.linkGpkd.trim() && <PathLink path={form.linkGpkd} />}
</div>
<div className="col-span-2 space-y-1.5">
<Label>Link HSNL</Label>
<Input value={form.linkHsnl} onChange={e => setForm({ ...form, linkHsnl: e.target.value })} />
{form.linkHsnl.trim() && <PathLink path={form.linkHsnl} />}
</div>
<div className="col-span-2 mt-2 border-t pt-3 text-sm font-semibold text-slate-600">
Liên hệ
</div>
<div className="space-y-1.5">
<Label>Chức vụ người liên hệ</Label>
<Input value={form.contactTitle} onChange={e => setForm({ ...form, contactTitle: e.target.value })} />
</div>
<div className="space-y-1.5">
<Label>SĐT người liên hệ</Label>
<Input value={form.contactPhone} onChange={e => setForm({ ...form, contactPhone: e.target.value })} />
</div>
<div className="col-span-2 space-y-1.5">
<Label>Người nhận thư / SĐT</Label>
<Input value={form.mailRecipient} onChange={e => setForm({ ...form, mailRecipient: e.target.value })} />
</div>
<div className="space-y-1.5">
<Label>Nguồn giới thiệu</Label>
<Input value={form.referralSource} onChange={e => setForm({ ...form, referralSource: e.target.value })} />
</div>
<div className="space-y-1.5">
<Label>Người phụ trách (PMH)</Label>
<Input value={form.ownerPmh} onChange={e => setForm({ ...form, ownerPmh: e.target.value })} />
</div>
<div className="col-span-2 mt-2 border-t pt-3 text-sm font-semibold text-slate-600">
Tình trạng
</div>
<div className="space-y-1.5">
<Label>Tình trạng hiện tại</Label>
<Select value={form.status} onChange={e => setForm({ ...form, status: e.target.value })}>
<option value=""> Chưa phân loại </option>
{Object.entries(SupplierStatusLabel).map(([v, l]) => (
<option key={v} value={v}>
{l}
</option>
))}
</Select>
</div>
</form> </form>
</Dialog> </Dialog>
</div> </div>

View File

@ -14,6 +14,7 @@ export const SupplierType = {
ToDoi: 3, ToDoi: 3,
DonViDichVu: 4, DonViDichVu: 4,
ChuDauTu: 5, ChuDauTu: 5,
CaHai: 6,
} as const } as const
export type SupplierType = typeof SupplierType[keyof typeof SupplierType] export type SupplierType = typeof SupplierType[keyof typeof SupplierType]
@ -24,6 +25,21 @@ export const SupplierTypeLabel: Record<SupplierType, string> = {
3: 'Tổ đội', 3: 'Tổ đội',
4: 'Đơn vị dịch vụ', 4: 'Đơn vị dịch vụ',
5: 'Chủ đầu tư', 5: 'Chủ đầu tư',
6: 'Cả hai (NCC & Thầu phụ)',
}
export const SupplierStatus = {
DangHoatDong: 1,
Blacklist: 2,
NgungHopTac: 3,
} as const
export type SupplierStatus = typeof SupplierStatus[keyof typeof SupplierStatus]
export const SupplierStatusLabel: Record<SupplierStatus, string> = {
1: 'Đang hoạt động',
2: 'Blacklist',
3: 'Ngừng hợp tác',
} }
export type Supplier = { export type Supplier = {
@ -37,6 +53,28 @@ export type Supplier = {
address: string | null address: string | null
contactPerson: string | null contactPerson: string | null
note: string | null note: string | null
// Định danh
packageCategory: string | null
// Pháp lý & Hợp đồng
officeAddress: string | null
mailingAddress: string | null
fax: string | null
bankAccount: string | null
secondaryBankAccount: string | null
legalRepresentative: string | null
legalRepTitle: string | null
authorizationNote: string | null
linkGuq: string | null
linkGpkd: string | null
linkHsnl: string | null
// Liên hệ
contactTitle: string | null
contactPhone: string | null
mailRecipient: string | null
referralSource: string | null
ownerPmh: string | null
// Tình trạng
status: SupplierStatus | null
createdAt: string createdAt: string
updatedAt: string | null updatedAt: string | null
} }

View File

@ -16,7 +16,27 @@ public record CreateSupplierCommand(
string? Email, string? Email,
string? Address, string? Address,
string? ContactPerson, string? ContactPerson,
string? Note) : IRequest<Guid>; string? Note,
// S? go-live expansion — 18 field bổ sung, tất cả TRAILING OPTIONAL (= null)
// để payload tối thiểu cũ + call-site positional 9-arg vẫn hợp lệ (mọi field optional).
string? PackageCategory = null,
string? OfficeAddress = null,
string? MailingAddress = null,
string? Fax = null,
string? BankAccount = null,
string? SecondaryBankAccount = null,
string? LegalRepresentative = null,
string? LegalRepTitle = null,
string? AuthorizationNote = null,
string? LinkGuq = null,
string? LinkGpkd = null,
string? LinkHsnl = null,
string? ContactTitle = null,
string? ContactPhone = null,
string? MailRecipient = null,
string? ReferralSource = null,
string? OwnerPmh = null,
SupplierStatus? Status = null) : IRequest<Guid>;
public class CreateSupplierCommandValidator : AbstractValidator<CreateSupplierCommand> public class CreateSupplierCommandValidator : AbstractValidator<CreateSupplierCommand>
{ {
@ -31,6 +51,24 @@ public class CreateSupplierCommandValidator : AbstractValidator<CreateSupplierCo
RuleFor(x => x.Address).MaximumLength(500); RuleFor(x => x.Address).MaximumLength(500);
RuleFor(x => x.ContactPerson).MaximumLength(200); RuleFor(x => x.ContactPerson).MaximumLength(200);
RuleFor(x => x.Note).MaximumLength(1000); RuleFor(x => x.Note).MaximumLength(1000);
// S? — new fields OPTIONAL: MaximumLength ONLY, KHÔNG NotEmpty (Create open-auth, minimal payload phải hợp lệ).
RuleFor(x => x.PackageCategory).MaximumLength(300);
RuleFor(x => x.OfficeAddress).MaximumLength(500);
RuleFor(x => x.MailingAddress).MaximumLength(500);
RuleFor(x => x.Fax).MaximumLength(50);
RuleFor(x => x.BankAccount).MaximumLength(500);
RuleFor(x => x.SecondaryBankAccount).MaximumLength(500);
RuleFor(x => x.LegalRepresentative).MaximumLength(200);
RuleFor(x => x.LegalRepTitle).MaximumLength(150);
RuleFor(x => x.AuthorizationNote).MaximumLength(1000);
RuleFor(x => x.LinkGuq).MaximumLength(1000);
RuleFor(x => x.LinkGpkd).MaximumLength(1000);
RuleFor(x => x.LinkHsnl).MaximumLength(1000);
RuleFor(x => x.ContactTitle).MaximumLength(150);
RuleFor(x => x.ContactPhone).MaximumLength(50);
RuleFor(x => x.MailRecipient).MaximumLength(200);
RuleFor(x => x.ReferralSource).MaximumLength(300);
RuleFor(x => x.OwnerPmh).MaximumLength(150);
} }
} }
@ -56,6 +94,24 @@ public class CreateSupplierCommandHandler : IRequestHandler<CreateSupplierComman
Address = request.Address, Address = request.Address,
ContactPerson = request.ContactPerson, ContactPerson = request.ContactPerson,
Note = request.Note, Note = request.Note,
PackageCategory = request.PackageCategory,
OfficeAddress = request.OfficeAddress,
MailingAddress = request.MailingAddress,
Fax = request.Fax,
BankAccount = request.BankAccount,
SecondaryBankAccount = request.SecondaryBankAccount,
LegalRepresentative = request.LegalRepresentative,
LegalRepTitle = request.LegalRepTitle,
AuthorizationNote = request.AuthorizationNote,
LinkGuq = request.LinkGuq,
LinkGpkd = request.LinkGpkd,
LinkHsnl = request.LinkHsnl,
ContactTitle = request.ContactTitle,
ContactPhone = request.ContactPhone,
MailRecipient = request.MailRecipient,
ReferralSource = request.ReferralSource,
OwnerPmh = request.OwnerPmh,
Status = request.Status,
}; };
_db.Suppliers.Add(entity); _db.Suppliers.Add(entity);
await _db.SaveChangesAsync(ct); await _db.SaveChangesAsync(ct);

View File

@ -17,7 +17,26 @@ public record UpdateSupplierCommand(
string? Email, string? Email,
string? Address, string? Address,
string? ContactPerson, string? ContactPerson,
string? Note) : IRequest; string? Note,
// S? go-live expansion — 18 field bổ sung, tất cả TRAILING OPTIONAL (= null). FE sửa tay gửi đủ field.
string? PackageCategory = null,
string? OfficeAddress = null,
string? MailingAddress = null,
string? Fax = null,
string? BankAccount = null,
string? SecondaryBankAccount = null,
string? LegalRepresentative = null,
string? LegalRepTitle = null,
string? AuthorizationNote = null,
string? LinkGuq = null,
string? LinkGpkd = null,
string? LinkHsnl = null,
string? ContactTitle = null,
string? ContactPhone = null,
string? MailRecipient = null,
string? ReferralSource = null,
string? OwnerPmh = null,
SupplierStatus? Status = null) : IRequest;
public class UpdateSupplierCommandValidator : AbstractValidator<UpdateSupplierCommand> public class UpdateSupplierCommandValidator : AbstractValidator<UpdateSupplierCommand>
{ {
@ -28,6 +47,24 @@ public class UpdateSupplierCommandValidator : AbstractValidator<UpdateSupplierCo
RuleFor(x => x.Name).NotEmpty().MaximumLength(200); RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.Type).IsInEnum(); RuleFor(x => x.Type).IsInEnum();
RuleFor(x => x.Email).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.Email)); RuleFor(x => x.Email).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.Email));
// S? — new fields OPTIONAL: MaximumLength ONLY, KHÔNG NotEmpty.
RuleFor(x => x.PackageCategory).MaximumLength(300);
RuleFor(x => x.OfficeAddress).MaximumLength(500);
RuleFor(x => x.MailingAddress).MaximumLength(500);
RuleFor(x => x.Fax).MaximumLength(50);
RuleFor(x => x.BankAccount).MaximumLength(500);
RuleFor(x => x.SecondaryBankAccount).MaximumLength(500);
RuleFor(x => x.LegalRepresentative).MaximumLength(200);
RuleFor(x => x.LegalRepTitle).MaximumLength(150);
RuleFor(x => x.AuthorizationNote).MaximumLength(1000);
RuleFor(x => x.LinkGuq).MaximumLength(1000);
RuleFor(x => x.LinkGpkd).MaximumLength(1000);
RuleFor(x => x.LinkHsnl).MaximumLength(1000);
RuleFor(x => x.ContactTitle).MaximumLength(150);
RuleFor(x => x.ContactPhone).MaximumLength(50);
RuleFor(x => x.MailRecipient).MaximumLength(200);
RuleFor(x => x.ReferralSource).MaximumLength(300);
RuleFor(x => x.OwnerPmh).MaximumLength(150);
} }
} }
@ -55,6 +92,24 @@ public class UpdateSupplierCommandHandler : IRequestHandler<UpdateSupplierComman
entity.Address = request.Address; entity.Address = request.Address;
entity.ContactPerson = request.ContactPerson; entity.ContactPerson = request.ContactPerson;
entity.Note = request.Note; entity.Note = request.Note;
entity.PackageCategory = request.PackageCategory;
entity.OfficeAddress = request.OfficeAddress;
entity.MailingAddress = request.MailingAddress;
entity.Fax = request.Fax;
entity.BankAccount = request.BankAccount;
entity.SecondaryBankAccount = request.SecondaryBankAccount;
entity.LegalRepresentative = request.LegalRepresentative;
entity.LegalRepTitle = request.LegalRepTitle;
entity.AuthorizationNote = request.AuthorizationNote;
entity.LinkGuq = request.LinkGuq;
entity.LinkGpkd = request.LinkGpkd;
entity.LinkHsnl = request.LinkHsnl;
entity.ContactTitle = request.ContactTitle;
entity.ContactPhone = request.ContactPhone;
entity.MailRecipient = request.MailRecipient;
entity.ReferralSource = request.ReferralSource;
entity.OwnerPmh = request.OwnerPmh;
entity.Status = request.Status;
await _db.SaveChangesAsync(ct); await _db.SaveChangesAsync(ct);
} }

View File

@ -13,5 +13,23 @@ public record SupplierDto(
string? Address, string? Address,
string? ContactPerson, string? ContactPerson,
string? Note, string? Note,
string? PackageCategory,
string? OfficeAddress,
string? MailingAddress,
string? Fax,
string? BankAccount,
string? SecondaryBankAccount,
string? LegalRepresentative,
string? LegalRepTitle,
string? AuthorizationNote,
string? LinkGuq,
string? LinkGpkd,
string? LinkHsnl,
string? ContactTitle,
string? ContactPhone,
string? MailRecipient,
string? ReferralSource,
string? OwnerPmh,
SupplierStatus? Status,
DateTime CreatedAt, DateTime CreatedAt,
DateTime? UpdatedAt); DateTime? UpdatedAt);

View File

@ -18,6 +18,10 @@ public class GetSupplierQueryHandler : IRequestHandler<GetSupplierQuery, Supplie
{ {
var x = await _db.Suppliers.AsNoTracking().FirstOrDefaultAsync(s => s.Id == request.Id, ct) var x = await _db.Suppliers.AsNoTracking().FirstOrDefaultAsync(s => s.Id == request.Id, ct)
?? throw new NotFoundException("Supplier", request.Id); ?? throw new NotFoundException("Supplier", request.Id);
return new SupplierDto(x.Id, x.Code, x.Name, x.Type, x.TaxCode, x.Phone, x.Email, x.Address, x.ContactPerson, x.Note, x.CreatedAt, x.UpdatedAt); return new SupplierDto(x.Id, x.Code, x.Name, x.Type, x.TaxCode, x.Phone, x.Email, x.Address, x.ContactPerson, x.Note,
x.PackageCategory, x.OfficeAddress, x.MailingAddress, x.Fax, x.BankAccount, x.SecondaryBankAccount,
x.LegalRepresentative, x.LegalRepTitle, x.AuthorizationNote, x.LinkGuq, x.LinkGpkd, x.LinkHsnl,
x.ContactTitle, x.ContactPhone, x.MailRecipient, x.ReferralSource, x.OwnerPmh, x.Status,
x.CreatedAt, x.UpdatedAt);
} }
} }

View File

@ -47,6 +47,9 @@ public class ListSuppliersQueryHandler : IRequestHandler<ListSuppliersQuery, Pag
.Take(request.PageSize) .Take(request.PageSize)
.Select(x => new SupplierDto( .Select(x => new SupplierDto(
x.Id, x.Code, x.Name, x.Type, x.TaxCode, x.Phone, x.Email, x.Address, x.ContactPerson, x.Note, x.Id, x.Code, x.Name, x.Type, x.TaxCode, x.Phone, x.Email, x.Address, x.ContactPerson, x.Note,
x.PackageCategory, x.OfficeAddress, x.MailingAddress, x.Fax, x.BankAccount, x.SecondaryBankAccount,
x.LegalRepresentative, x.LegalRepTitle, x.AuthorizationNote, x.LinkGuq, x.LinkGpkd, x.LinkHsnl,
x.ContactTitle, x.ContactPhone, x.MailRecipient, x.ReferralSource, x.OwnerPmh, x.Status,
x.CreatedAt, x.UpdatedAt)) x.CreatedAt, x.UpdatedAt))
.ToListAsync(ct); .ToListAsync(ct);

View File

@ -10,7 +10,27 @@ public class Supplier : AuditableEntity
public string? TaxCode { get; set; } // Mã số thuế public string? TaxCode { get; set; } // Mã số thuế
public string? Phone { get; set; } public string? Phone { get; set; }
public string? Email { get; set; } public string? Email { get; set; }
public string? Address { get; set; } public string? Address { get; set; } // Địa chỉ xuất HĐ (đăng ký)
public string? ContactPerson { get; set; } // Người liên hệ public string? ContactPerson { get; set; } // Người liên hệ chính
public string? Note { get; set; } public string? Note { get; set; } // Ghi chú / lý do blacklist
// ---- S? go-live expansion (18 field bổ sung, tất cả nullable — import Excel "Database NCC") ----
public string? PackageCategory { get; set; } // Gói thầu (free-text)
public string? OfficeAddress { get; set; } // Địa chỉ văn phòng
public string? MailingAddress { get; set; } // Địa chỉ gửi thư
public string? Fax { get; set; }
public string? BankAccount { get; set; } // Số TK + tên + CN ngân hàng (composite raw)
public string? SecondaryBankAccount { get; set; } // Số TK phụ
public string? LegalRepresentative { get; set; } // Người đại diện pháp luật
public string? LegalRepTitle { get; set; } // Chức vụ đại diện
public string? AuthorizationNote { get; set; } // Giấy ủy quyền (free text)
public string? LinkGuq { get; set; } // Link GUQ (NAS path)
public string? LinkGpkd { get; set; } // Link GPKD (NAS path)
public string? LinkHsnl { get; set; } // Link HSNL (NAS path)
public string? ContactTitle { get; set; } // Chức vụ người liên hệ
public string? ContactPhone { get; set; } // SĐT chính (người liên hệ)
public string? MailRecipient { get; set; } // Người nhận thư / SĐT (composite raw)
public string? ReferralSource { get; set; } // Nguồn giới thiệu
public string? OwnerPmh { get; set; } // Người phụ trách (PMH)
public SupplierStatus? Status { get; set; } // Tình trạng hiện tại (nullable = chưa phân loại)
} }

View File

@ -0,0 +1,8 @@
namespace SolutionErp.Domain.Master;
public enum SupplierStatus
{
DangHoatDong = 1, // Đang hoạt động
Blacklist = 2, // Blacklist
NgungHopTac = 3, // Ngừng hợp tác
}

View File

@ -7,4 +7,5 @@ public enum SupplierType
ToDoi = 3, // TĐ — Tổ đội ToDoi = 3, // TĐ — Tổ đội
DonViDichVu = 4, // ĐVDV — Đơn vị dịch vụ DonViDichVu = 4, // ĐVDV — Đơn vị dịch vụ
ChuDauTu = 5, // CĐT — Chủ đầu tư (đặc biệt, bypass quy trình CCM) ChuDauTu = 5, // CĐT — Chủ đầu tư (đặc biệt, bypass quy trình CCM)
CaHai = 6, // Cả hai — vừa NCC vừa NTP (append S? go-live; int 1-5 giữ nguyên trên phiếu cũ)
} }

View File

@ -21,6 +21,26 @@ public class SupplierConfiguration : IEntityTypeConfiguration<Supplier>
b.Property(x => x.ContactPerson).HasMaxLength(200); b.Property(x => x.ContactPerson).HasMaxLength(200);
b.Property(x => x.Note).HasMaxLength(1000); b.Property(x => x.Note).HasMaxLength(1000);
// S? go-live expansion — 18 field bổ sung (all nullable). MaxLength khớp spec §3.
b.Property(x => x.PackageCategory).HasMaxLength(300);
b.Property(x => x.OfficeAddress).HasMaxLength(500);
b.Property(x => x.MailingAddress).HasMaxLength(500);
b.Property(x => x.Fax).HasMaxLength(50);
b.Property(x => x.BankAccount).HasMaxLength(500);
b.Property(x => x.SecondaryBankAccount).HasMaxLength(500);
b.Property(x => x.LegalRepresentative).HasMaxLength(200);
b.Property(x => x.LegalRepTitle).HasMaxLength(150);
b.Property(x => x.AuthorizationNote).HasMaxLength(1000);
b.Property(x => x.LinkGuq).HasMaxLength(1000);
b.Property(x => x.LinkGpkd).HasMaxLength(1000);
b.Property(x => x.LinkHsnl).HasMaxLength(1000);
b.Property(x => x.ContactTitle).HasMaxLength(150);
b.Property(x => x.ContactPhone).HasMaxLength(50);
b.Property(x => x.MailRecipient).HasMaxLength(200);
b.Property(x => x.ReferralSource).HasMaxLength(300);
b.Property(x => x.OwnerPmh).HasMaxLength(150);
b.Property(x => x.Status).HasConversion<int>(); // nullable int enum
b.HasIndex(x => x.Code).IsUnique().HasFilter("[IsDeleted] = 0"); // Mig 47 (gotcha #57 EXT) — soft-deleted slot reusable, khớp HasQueryFilter !IsDeleted app-check b.HasIndex(x => x.Code).IsUnique().HasFilter("[IsDeleted] = 0"); // Mig 47 (gotcha #57 EXT) — soft-deleted slot reusable, khớp HasQueryFilter !IsDeleted app-check
b.HasIndex(x => x.Type); b.HasIndex(x => x.Type);

View File

@ -2765,26 +2765,62 @@ public static class DbInitializer
} }
if (addedWorkItems > 0) logger.LogInformation("Seeded {Count} real work items (S55 import)", addedWorkItems); if (addedWorkItems > 0) logger.LogInformation("Seeded {Count} real work items (S55 import)", addedWorkItems);
// ---------- SUPPLIERS (4) — (Code, Name, Type, TaxCode, Phone, Email, Address, ContactPerson, Note) ---------- [S96 2026-07-01: +DONGDUONG import từ Excel "Database NCC" anh Kiệt FDC; 15-18 field form (bank chi tiết/địa chỉ VP/link GPKD-HSNL-GUQ/PMH/nguồn GT...) NOTE-lại-từ-từ-tính = backlog schema Supplier mở rộng, xem docs/STATUS Next-up] // ---------- SUPPLIERS (4) — 30-field expansion (S? go-live, Mig 62) ----------
// Import Excel "Database NCC" anh Kiệt FDC. 9 field cũ (reuse) + 18 field mới +Status.
// Note = null cho cả 4 (đã tách legacy-cram sang các cột riêng; un-cram Sql() trong Mig 62).
// Idempotency = insert-if-missing + FILL-NULLS-if-exists: row đã tồn tại → CHỈ set các
// cột MỚI đang null (KHÔNG đè non-null) → go-live-safe, không clobber, idempotent.
var suppliers = new[] var suppliers = new[]
{ {
new Supplier { Code = "TRUONGGIANG", Name = "CÔNG TY CỔ PHẦN NHÔM KÍNH VÀ ĐẦU TƯ TRƯỜNG GIANG", Type = SupplierType.NhaThauPhu, TaxCode = "0312 251 859", Phone = "08 2253 1381", Email = "viethq1985@gmail.com", Address = "Số 77/46A Đường Chuyên Dùng Chính, Phường Phú Thuận, TP.HCM", ContactPerson = "Mr. Việt", Note = "Loại: Nhôm kính, alu; NĐD: Ông: HOÀNG QUỐC VIỆT - GIÁM ĐỐC ĐIỀU HÀNH; TK: 100212127 Ngân hàng Eximbank - Chi Nhánh Hòa Bình; Trạng thái: ✅ Đang hoạt động" }, new Supplier { Code = "TRUONGGIANG", Name = "CÔNG TY CỔ PHẦN NHÔM KÍNH VÀ ĐẦU TƯ TRƯỜNG GIANG", Type = SupplierType.NhaThauPhu, TaxCode = "0312 251 859", Phone = "08 2253 1381", Email = "viethq1985@gmail.com", Address = "77/46A Đường Chuyên Dùng Chính, Phường Phú Thuận, Thành phố Hồ Chí Minh, Việt Nam", ContactPerson = "Mr. Việt", Note = null, Status = SupplierStatus.DangHoatDong,
new Supplier { Code = "TANPHU", Name = "CÔNG TY TNHH SẢN XUẤT THƯƠNG MẠI SƠN TÂN PHÚ", Type = SupplierType.NhaThauPhu, TaxCode = "0305497393", Phone = "0902 686468", Email = "thu@tanphupaint.com", Address = "Số 49/5A Đường số 7, Khu phố 53, P. Linh Xuân, Thành phố Hồ Chí Minh, Việt Nam", ContactPerson = "Ms Thư", Note = "Loại: Sơn PU, Epoxy, liquid; NĐD: Ông ĐINH PHƯỚC THỌ - GIÁM ĐỐC; TK: 8683107979 tại Ngân hàng BIDV - CN Nam Bình Dương; Trạng thái: ✅ Đang hoạt động" }, PackageCategory = "10-Nhôm kính+Face Mặt dựng", BankAccount = "100212127 Ngân hàng Eximbank - Chi Nhánh Hòa Bình", LegalRepresentative = "Ông: HOÀNG QUỐC VIỆT", LegalRepTitle = "GIÁM ĐỐC ĐIỀU HÀNH", AuthorizationNote = "Theo giấy ủy quyền Số 01UQTGD-2022 ký ngày 01/04/2022 từ đại diện pháp luật của công ty là Ông Nguyễn Đăng Khoa, Chức Vụ: Tổng Giám Đốc", LinkGuq = @"01. GPKD_HSNL_GUQ\02.Trường giang\GIẤY ỦY QUYỀN ANH VIỆT.pdf", LinkGpkd = @"01. GPKD_HSNL_GUQ\02.Trường giang\GP TRƯỜNG GIANG 14.11.2024 THAY DOI L6.pdf", ContactTitle = "Giám đốc ĐH", ContactPhone = "0966 801 456", MailingAddress = "D32, ấp 1B, Xã Rạch Kiến, Tỉnh Tây Ninh (địa chỉ cũ: D32 ấp 1B, xã Long Hòa, Huyện Cần Đước, Tỉnh Long An)", MailRecipient = "MS Thủy / 036 405 0016", ReferralSource = "Tái sử dụng DA trước", OwnerPmh = "Bình" },
new Supplier { Code = "TGN", Name = "CÔNG TY CỔ PHẦN SIÊU THỊ VLXD THẾ GIỚI NHÀ", Type = SupplierType.NhaCungCap, TaxCode = "3603497972", Phone = "028 36206000", Email = "duong.ltt@tgngroup.vn", Address = "46-48 Nguyễn Cơ Thạch, Phường An Khánh, Thành phố Hồ Chí Minh,Việt Nam", ContactPerson = "Ms. Dương", Note = "Loại: Bê tông; NĐD: Ông TRẦN ANH ĐIỀN - TỔNG GIÁM ĐỐC; TK: 114002641307 tại VietinBank CN KCN Biên Hòa; Trạng thái: ✅ Đang hoạt động" }, new Supplier { Code = "TANPHU", Name = "CÔNG TY TNHH SẢN XUẤT THƯƠNG MẠI SƠN TÂN PHÚ", Type = SupplierType.NhaThauPhu, TaxCode = "0305497393", Phone = "0902 686468", Email = "thu@tanphupaint.com", Address = "Số 49/5A Đường số 7, Khu phố 53, P. Linh Xuân, Thành phố Hồ Chí Minh, Việt Nam", ContactPerson = "Ms Thư", Note = null, Status = SupplierStatus.DangHoatDong,
new Supplier { Code = "DONGDUONG", Name = "CÔNG TY CỔ PHẦN DỊCH VỤ CÔNG NGHỆ ĐÔNG DƯƠNG", Type = SupplierType.NhaThauPhu, Phone = "0913 912 094", Email = "hoangmi.tl@dongduongelevator.com", Address = "Số 11 Đường 27 Khu đô thị Vạn Phúc, Phường Hiệp Bình, Thành phố Hồ Chí Minh, Việt Nam", ContactPerson = "Ms.Mi", Note = "Loại: Thang máy, bàn nâng hạ; NĐD: Ông ĐOÀN MẬU QUẾ - GIÁM ĐỐC; TK: 110616498868 tại Ngân hàng TMCP Công thương (Vietinbank) CN Thủ Thiêm; GUQ: Số 02/2024/QĐ-ĐD ký 17/06/2024 (Bà Đoàn Phan Ngọc Bích - CT HĐQT); Trạng thái: ✅ Đang hoạt động" }, PackageCategory = "15-Sơn PU/Epoxy/liquid", BankAccount = "8683107979 tại Ngân hàng BIDV - CN Nam Bình Dương", LegalRepresentative = "Ông ĐINH PHƯỚC THỌ", LegalRepTitle = "GIÁM ĐỐC", LinkGpkd = @"01. GPKD_HSNL_GUQ\01.Sơn Tân Phú\BUSINESS LICENSE-TAN PHU PAINT CO LTD(1).pdf", LinkHsnl = @"01. GPKD_HSNL_GUQ\01.Sơn Tân Phú\HỒ SƠ NĂNG LỰC CÔNG TY SƠN TÂN PHÚ-04.2026.pdf", ContactTitle = "Kinh doanh", ContactPhone = "0908355222", ReferralSource = "Qua đối tác", OwnerPmh = "Bình" },
new Supplier { Code = "TGN", Name = "CÔNG TY CỔ PHẦN SIÊU THỊ VLXD THẾ GIỚI NHÀ", Type = SupplierType.NhaCungCap, TaxCode = "3603497972", Phone = "028 36206000", Email = "duong.ltt@tgngroup.vn", Address = "46-48 Nguyễn Cơ Thạch, Phường An Khánh, Thành phố Hồ Chí Minh,Việt Nam", ContactPerson = "Ms. Dương", Note = null, Status = SupplierStatus.DangHoatDong,
PackageCategory = "2-Bê tông", Fax = "0251.2814663", BankAccount = "114002641307 tại VietinBank CN KCN Biên Hòa", LegalRepresentative = "Ông TRẦN ANH ĐIỀN", LegalRepTitle = "TỔNG GIÁM ĐỐC", LinkGpkd = @"01. GPKD_HSNL_GUQ\03.TGN\GPKD 2026.pdf", LinkHsnl = @"01. GPKD_HSNL_GUQ\03.TGN\HỒ SƠ NĂNG LỰC TGN 2022.pdf", ContactTitle = "Kinh doanh", ContactPhone = "0906.303. 087", MailingAddress = "643 xa lộ hà nội, kp2, p. long bình, đồng nai", MailRecipient = "Ms Dương/ 0967 019 386", ReferralSource = "Tái sử dụng DA trước", OwnerPmh = "Bình" },
new Supplier { Code = "DONGDUONG", Name = "CÔNG TY CỔ PHẦN DỊCH VỤ CÔNG NGHỆ ĐÔNG DƯƠNG", Type = SupplierType.NhaThauPhu, TaxCode = null, Phone = "0913 912 094", Email = "hoangmi.tl@dongduongelevator.com", Address = "Số 11 Đường 27 Khu đô thị Vạn Phúc, Phường Hiệp Bình, Thành phố Hồ Chí Minh, Việt Nam", ContactPerson = "Ms.Mi", Note = null, Status = SupplierStatus.DangHoatDong,
PackageCategory = "6-Thang máy/ bàn nâng hạ", BankAccount = "110616498868 tại Ngân hàng TMCP Công thương (Vietinbank) CN Thủ Thiêm", LegalRepresentative = "Ông ĐOÀN MẬU QUẾ", LegalRepTitle = "GIÁM ĐỐC", AuthorizationNote = "Theo giấy ủy quyền Số 02/2024/QĐ-ĐD ký ngày 17/06/2024 từ đại diện pháp luật của công ty là Bà Đoàn Phan Ngọc Bích, Chức Vụ: Chủ Tịch Hồi Đồng Quản Trị", LinkGuq = @"01. GPKD_HSNL_GUQ\04. Đông Dương\GIẤY ỦY QUYỀN GIÁM ĐỐC_MR.QUẾ_NĂM 2026.pdf", LinkGpkd = @"01. GPKD_HSNL_GUQ\04. Đông Dương\4. LẦN THỨ 3_GIẤY ĐKKD_CÔNG NGHỆ ĐÔNG DƯƠNG_02.04.2026.pdf", LinkHsnl = @"01. GPKD_HSNL_GUQ\04. Đông Dương\HỒ SƠ NĂNG LỰC 2025.pdf", ContactTitle = "Kinh doanh", ContactPhone = "0903.632.625", MailRecipient = "Ms Mi/0903.632.625", ReferralSource = "Tái sử dụng DA trước", OwnerPmh = "Bình" },
}; };
var existingSupplierCodes = await db.Suppliers.Select(s => s.Code).ToListAsync(); // Fetch existing rows FULL (không chỉ Code) để fill-nulls trên các cột mới.
var existingSuppliers = await db.Suppliers.ToListAsync();
var existingByCode = existingSuppliers.ToDictionary(s => s.Code, StringComparer.Ordinal);
int addedSuppliers = 0; int addedSuppliers = 0;
int filledSuppliers = 0;
foreach (var s in suppliers) foreach (var s in suppliers)
{ {
if (existingSupplierCodes.Contains(s.Code)) continue; if (existingByCode.TryGetValue(s.Code, out var e))
{
// FILL-NULLS: chỉ set cột MỚI đang null (KHÔNG đè non-null; KHÔNG đụng 9 cột cũ).
bool changed = false;
if (e.PackageCategory is null && s.PackageCategory is not null) { e.PackageCategory = s.PackageCategory; changed = true; }
if (e.OfficeAddress is null && s.OfficeAddress is not null) { e.OfficeAddress = s.OfficeAddress; changed = true; }
if (e.MailingAddress is null && s.MailingAddress is not null) { e.MailingAddress = s.MailingAddress; changed = true; }
if (e.Fax is null && s.Fax is not null) { e.Fax = s.Fax; changed = true; }
if (e.BankAccount is null && s.BankAccount is not null) { e.BankAccount = s.BankAccount; changed = true; }
if (e.SecondaryBankAccount is null && s.SecondaryBankAccount is not null) { e.SecondaryBankAccount = s.SecondaryBankAccount; changed = true; }
if (e.LegalRepresentative is null && s.LegalRepresentative is not null) { e.LegalRepresentative = s.LegalRepresentative; changed = true; }
if (e.LegalRepTitle is null && s.LegalRepTitle is not null) { e.LegalRepTitle = s.LegalRepTitle; changed = true; }
if (e.AuthorizationNote is null && s.AuthorizationNote is not null) { e.AuthorizationNote = s.AuthorizationNote; changed = true; }
if (e.LinkGuq is null && s.LinkGuq is not null) { e.LinkGuq = s.LinkGuq; changed = true; }
if (e.LinkGpkd is null && s.LinkGpkd is not null) { e.LinkGpkd = s.LinkGpkd; changed = true; }
if (e.LinkHsnl is null && s.LinkHsnl is not null) { e.LinkHsnl = s.LinkHsnl; changed = true; }
if (e.ContactTitle is null && s.ContactTitle is not null) { e.ContactTitle = s.ContactTitle; changed = true; }
if (e.ContactPhone is null && s.ContactPhone is not null) { e.ContactPhone = s.ContactPhone; changed = true; }
if (e.MailRecipient is null && s.MailRecipient is not null) { e.MailRecipient = s.MailRecipient; changed = true; }
if (e.ReferralSource is null && s.ReferralSource is not null) { e.ReferralSource = s.ReferralSource; changed = true; }
if (e.OwnerPmh is null && s.OwnerPmh is not null) { e.OwnerPmh = s.OwnerPmh; changed = true; }
if (e.Status is null && s.Status is not null) { e.Status = s.Status; changed = true; }
if (changed) filledSuppliers++;
continue;
}
db.Suppliers.Add(s); db.Suppliers.Add(s);
addedSuppliers++; addedSuppliers++;
} }
if (addedSuppliers > 0) logger.LogInformation("Seeded {Count} real suppliers (S55 import)", addedSuppliers); if (addedSuppliers > 0) logger.LogInformation("Seeded {Count} real suppliers (S55 import)", addedSuppliers);
if (filledSuppliers > 0) logger.LogInformation("Filled new columns on {Count} existing suppliers (S? 30-field)", filledSuppliers);
if (addedProjects + addedWorkItems + addedSuppliers > 0) if (addedProjects + addedWorkItems + addedSuppliers + filledSuppliers > 0)
await db.SaveChangesAsync(); await db.SaveChangesAsync();
} }

View File

@ -0,0 +1,223 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class ExpandSupplierFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "AuthorizationNote",
table: "Suppliers",
type: "nvarchar(1000)",
maxLength: 1000,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "BankAccount",
table: "Suppliers",
type: "nvarchar(500)",
maxLength: 500,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ContactPhone",
table: "Suppliers",
type: "nvarchar(50)",
maxLength: 50,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ContactTitle",
table: "Suppliers",
type: "nvarchar(150)",
maxLength: 150,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Fax",
table: "Suppliers",
type: "nvarchar(50)",
maxLength: 50,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "LegalRepTitle",
table: "Suppliers",
type: "nvarchar(150)",
maxLength: 150,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "LegalRepresentative",
table: "Suppliers",
type: "nvarchar(200)",
maxLength: 200,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "LinkGpkd",
table: "Suppliers",
type: "nvarchar(1000)",
maxLength: 1000,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "LinkGuq",
table: "Suppliers",
type: "nvarchar(1000)",
maxLength: 1000,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "LinkHsnl",
table: "Suppliers",
type: "nvarchar(1000)",
maxLength: 1000,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "MailRecipient",
table: "Suppliers",
type: "nvarchar(200)",
maxLength: 200,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "MailingAddress",
table: "Suppliers",
type: "nvarchar(500)",
maxLength: 500,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "OfficeAddress",
table: "Suppliers",
type: "nvarchar(500)",
maxLength: 500,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "OwnerPmh",
table: "Suppliers",
type: "nvarchar(150)",
maxLength: 150,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "PackageCategory",
table: "Suppliers",
type: "nvarchar(300)",
maxLength: 300,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ReferralSource",
table: "Suppliers",
type: "nvarchar(300)",
maxLength: 300,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "SecondaryBankAccount",
table: "Suppliers",
type: "nvarchar(500)",
maxLength: 500,
nullable: true);
migrationBuilder.AddColumn<int>(
name: "Status",
table: "Suppliers",
type: "int",
nullable: true);
// One-time un-cram: clear the legacy concatenated Note on the 4 seeded suppliers now that
// their data lives in structured columns. Guarded exact-match prefix (Note LIKE N'Loại:%')
// so manually-edited Notes are never touched (spec §4; safe on existing prod rows only).
migrationBuilder.Sql(
"UPDATE Suppliers SET Note = NULL " +
"WHERE Code IN (N'TRUONGGIANG', N'TANPHU', N'TGN', N'DONGDUONG') " +
"AND Note LIKE N'Loại:%';");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AuthorizationNote",
table: "Suppliers");
migrationBuilder.DropColumn(
name: "BankAccount",
table: "Suppliers");
migrationBuilder.DropColumn(
name: "ContactPhone",
table: "Suppliers");
migrationBuilder.DropColumn(
name: "ContactTitle",
table: "Suppliers");
migrationBuilder.DropColumn(
name: "Fax",
table: "Suppliers");
migrationBuilder.DropColumn(
name: "LegalRepTitle",
table: "Suppliers");
migrationBuilder.DropColumn(
name: "LegalRepresentative",
table: "Suppliers");
migrationBuilder.DropColumn(
name: "LinkGpkd",
table: "Suppliers");
migrationBuilder.DropColumn(
name: "LinkGuq",
table: "Suppliers");
migrationBuilder.DropColumn(
name: "LinkHsnl",
table: "Suppliers");
migrationBuilder.DropColumn(
name: "MailRecipient",
table: "Suppliers");
migrationBuilder.DropColumn(
name: "MailingAddress",
table: "Suppliers");
migrationBuilder.DropColumn(
name: "OfficeAddress",
table: "Suppliers");
migrationBuilder.DropColumn(
name: "OwnerPmh",
table: "Suppliers");
migrationBuilder.DropColumn(
name: "PackageCategory",
table: "Suppliers");
migrationBuilder.DropColumn(
name: "ReferralSource",
table: "Suppliers");
migrationBuilder.DropColumn(
name: "SecondaryBankAccount",
table: "Suppliers");
migrationBuilder.DropColumn(
name: "Status",
table: "Suppliers");
}
}
}

View File

@ -3218,6 +3218,14 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
.HasMaxLength(500) .HasMaxLength(500)
.HasColumnType("nvarchar(500)"); .HasColumnType("nvarchar(500)");
b.Property<string>("AuthorizationNote")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<string>("BankAccount")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("Code") b.Property<string>("Code")
.IsRequired() .IsRequired()
.HasMaxLength(50) .HasMaxLength(50)
@ -3227,6 +3235,14 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
.HasMaxLength(200) .HasMaxLength(200)
.HasColumnType("nvarchar(200)"); .HasColumnType("nvarchar(200)");
b.Property<string>("ContactPhone")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ContactTitle")
.HasMaxLength(150)
.HasColumnType("nvarchar(150)");
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
@ -3243,9 +3259,41 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
.HasMaxLength(100) .HasMaxLength(100)
.HasColumnType("nvarchar(100)"); .HasColumnType("nvarchar(100)");
b.Property<string>("Fax")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<bool>("IsDeleted") b.Property<bool>("IsDeleted")
.HasColumnType("bit"); .HasColumnType("bit");
b.Property<string>("LegalRepTitle")
.HasMaxLength(150)
.HasColumnType("nvarchar(150)");
b.Property<string>("LegalRepresentative")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("LinkGpkd")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<string>("LinkGuq")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<string>("LinkHsnl")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<string>("MailRecipient")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("MailingAddress")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasMaxLength(200) .HasMaxLength(200)
@ -3255,10 +3303,33 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
.HasMaxLength(1000) .HasMaxLength(1000)
.HasColumnType("nvarchar(1000)"); .HasColumnType("nvarchar(1000)");
b.Property<string>("OfficeAddress")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("OwnerPmh")
.HasMaxLength(150)
.HasColumnType("nvarchar(150)");
b.Property<string>("PackageCategory")
.HasMaxLength(300)
.HasColumnType("nvarchar(300)");
b.Property<string>("Phone") b.Property<string>("Phone")
.HasMaxLength(30) .HasMaxLength(30)
.HasColumnType("nvarchar(30)"); .HasColumnType("nvarchar(30)");
b.Property<string>("ReferralSource")
.HasMaxLength(300)
.HasColumnType("nvarchar(300)");
b.Property<string>("SecondaryBankAccount")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int?>("Status")
.HasColumnType("int");
b.Property<string>("TaxCode") b.Property<string>("TaxCode")
.HasMaxLength(20) .HasMaxLength(20)
.HasColumnType("nvarchar(20)"); .HasColumnType("nvarchar(20)");