[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
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:
@ -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).
|
||||||
|
|||||||
@ -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.
|
||||||
68
fe-admin/src/components/ui/PathLink.tsx
Normal file
68
fe-admin/src/components/ui/PathLink.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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 lý & 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>
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
68
fe-user/src/components/ui/PathLink.tsx
Normal file
68
fe-user/src/components/ui/PathLink.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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 lý & 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>
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/Backend/SolutionErp.Domain/Master/SupplierStatus.cs
Normal file
8
src/Backend/SolutionErp.Domain/Master/SupplierStatus.cs
Normal 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
|
||||||
|
}
|
||||||
@ -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ũ)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)");
|
||||||
|
|||||||
Reference in New Issue
Block a user