using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;  // Plan T S23 t10 — DemoSeed:Disabled flag
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SolutionErp.Application.Contracts.Services;
using SolutionErp.Domain.ApprovalWorkflowsV2;
using SolutionErp.Domain.Contracts;
using SolutionErp.Domain.Forms;
using SolutionErp.Domain.Hrm;
using SolutionErp.Domain.Identity;
using SolutionErp.Domain.Master;
using SolutionErp.Domain.Master.Catalogs;
using SolutionErp.Domain.Office;
using SolutionErp.Domain.PurchaseEvaluations;

namespace SolutionErp.Infrastructure.Persistence;

public static class DbInitializer
{
    public const string AdminEmail = "admin@solutions.com.vn";
    public const string AdminPassword = "Admin@123456";

    // Migration helper (Phase 6 rebrand): rename user email
    // @solutionerp.local → @solutions.com.vn. Idempotent — skip user đã rename.
    // Password + role + refresh token giữ nguyên. Chạy trước SeedAdmin.
    private static async Task BackfillUserEmailDomainAsync(
        UserManager<User> userManager, ILogger logger)
    {
        const string OldDomain = "@solutionerp.local";
        const string NewDomain = "@solutions.com.vn";
        var oldUsers = userManager.Users
            .Where(u => u.Email != null && u.Email.EndsWith(OldDomain))
            .ToList();
        if (oldUsers.Count == 0) return;

        var renamed = 0;
        foreach (var u in oldUsers)
        {
            var newEmail = u.Email!.Replace(OldDomain, NewDomain);
            var conflict = await userManager.FindByEmailAsync(newEmail);
            if (conflict != null && conflict.Id != u.Id) continue;
            u.Email = newEmail;
            u.NormalizedEmail = newEmail.ToUpperInvariant();
            u.UserName = newEmail;
            u.NormalizedUserName = newEmail.ToUpperInvariant();
            var res = await userManager.UpdateAsync(u);
            if (res.Succeeded) renamed++;
        }
        if (renamed > 0)
            logger.LogInformation("Backfilled {Count} user emails -> @solutions.com.vn", renamed);
    }

    public static async Task InitializeAsync(IServiceProvider services)
    {
        using var scope = services.CreateScope();
        var sp = scope.ServiceProvider;
        var logger = sp.GetRequiredService<ILoggerFactory>().CreateLogger("DbInitializer");
        var db = sp.GetRequiredService<ApplicationDbContext>();
        var userManager = sp.GetRequiredService<UserManager<User>>();
        var roleManager = sp.GetRequiredService<RoleManager<Role>>();

        logger.LogInformation("Applying migrations...");
        await db.Database.MigrateAsync();

        // Plan T S23 t10 (2026-05-15) — DemoSeed:Disabled flag. Khi true thì skip
        // 5 method auto-seed demo data:
        //   - SeedWorkflowDefinitionsAsync (V1 PE workflow sample: QT-DN-A + QT-DN-B)
        //   - SeedPurchaseEvaluationWorkflowsAsync (V1 PE workflow extended)
        //   - SeedDemoContractsAsync ([DEMO] HĐ 7-type sample)
        //   - SeedDemoPurchaseEvaluationsAsync ([DEMO] PE 4 sample)
        //   - SeedSampleApprovalWorkflowsV2Async (V2 sample mẫu UAT cho type B)
        //   - SeedSampleContractWorkflowV2Async (V2 sample mẫu UAT cho Contract — Mig 32 Plan B Chunk A2)
        // GIỮ (ungated): SeedRoles, SeedAdmin, SeedDepartments, SeedDemoUsers (chỉ user THẬT
        // — S89 đã gỡ user test khỏi mảng), SeedMenuTree, SeedAdminPermissions,
        // SeedContractTemplates (file template), SeedCatalogs, SeedRealMasterData (62 dự án +
        // 71 hạng mục + 3 NCC thật). [S91 2026-06-26] SeedDemoMasterData (Supplier/Project
        // DEMO) ĐÃ chuyển sang GATED — prod skip để không resurrect 18 NCC demo đã wipe go-live.
        var config = sp.GetRequiredService<IConfiguration>();
        var demoSeedDisabled = config.GetValue<bool>("DemoSeed:Disabled");
        if (demoSeedDisabled)
            logger.LogInformation("DemoSeed:Disabled=true — skip workflow + contracts + PE + sample V2 seed (Plan T S23 t10 + Plan B Chunk A2 Contract V2)");

        await SeedRolesAsync(roleManager, logger);
        // Phase 6 rebrand: rename user email @solutionerp.local → @solutions.com.vn
        // TRƯỚC SeedAdmin để SeedAdmin tìm theo new email thấy user đã rename → skip create.
        await BackfillUserEmailDomainAsync(userManager, logger);
        await SeedAdminAsync(userManager, logger);
        await SeedDepartmentsAsync(db, logger);
        await SeedDemoUsersAsync(db, userManager, logger);
        // Phase 11 P11-D (Mig 46 — Wave 2) — gán 2 sample user vào Phòng CNTT (IT staff)
        // cho round-robin auto-assign ticket. PHẢI sau SeedDemoUsersAsync (reconcile dept
        // trước → method này override về IT). Infrastructure data (NOT gated DemoSeed).
        await SeedItDepartmentStaffAsync(db, userManager, logger);
        // [S89 2026-06-25 — anh Kiệt FDC chốt] LockDemoSampleUsersAsync ĐÃ GỠ: 16 user mẫu +
        // ~21 user UAT-matrix đã XÓA hẳn khỏi prod (scripts/s89-delete-test-users.sql) + gỡ khỏi
        // SeedDemoUsersAsync → không còn user test để khóa (S57bis lock thay bằng xóa thật).
        // Plan B G-H1 (Mig 34 S33 2026-05-26) — seed EmployeeProfile 1-1 với
        // mọi user @solutions.com.vn. Idempotent. NOT gated DemoSeed flag
        // (infrastructure data, mirror Mig 32 SeedSampleContractWorkflowV2
        // gotcha #51 lesson). Bro UAT update field nhạy cảm qua FE Hồ sơ NS.
        await SeedDemoEmployeeProfilesAsync(db, userManager, logger);
        // Plan G-H2 (Mig 35 S34 2026-05-27) — Cấu hình HRM 4 catalog. Infrastructure
        // data (NOT gated DemoSeed flag) — Workflow Apps reference cần data ngày 1.
        await SeedHrmConfigsAsync(db, logger);
        // Plan G-O2 (Mig 36 S36 2026-05-28) — Phòng họp 4 sample room. Infrastructure
        // data (NOT gated DemoSeed flag, gotcha #51) — admin có thể edit/delete sau.
        await SeedMeetingRoomsAsync(db, logger);
        await SeedMenuTreeAsync(db, logger);
        await SeedAdminPermissionsAsync(db, roleManager, logger);
        // [S91 2026-06-26 — anh Kiệt FDC go-live] GATE demo master (Supplier/Project demo)
        // sau cờ DemoSeed:Disabled. Trước đây ungated → SeedDemoMasterDataAsync per-code
        // idempotent RESURRECT 18 NCC demo (NCC-VLXD/CDT-VIN/Sun Group…) mỗi restart sau khi
        // đã wipe (scripts/s91-wipe-golive.sql). Prod Disabled=true → SKIP → giữ wipe BỀN;
        // Dev Disabled=false → vẫn seed demo local. Master data THẬT (62 dự án + 71 hạng mục
        // + 3 NCC) đến từ SeedRealMasterDataAsync (ungated, dưới) — KHÔNG ảnh hưởng.
        if (!demoSeedDisabled)
            await SeedDemoMasterDataAsync(db, logger);
        await SeedContractTemplatesAsync(db, logger);
        if (!demoSeedDisabled)
        {
            await SeedWorkflowDefinitionsAsync(db, logger);
            await SeedPurchaseEvaluationWorkflowsAsync(db, logger);
        }
        await SeedCatalogsAsync(db, logger);
        // S55 (2026-06-09) — Master data thật của công ty (62 dự án + 71 hạng mục +
        // 3 NCC). NOT gated DemoSeed — coexist với demo data qua per-code idempotent.
        await SeedRealMasterDataAsync(db, logger);
        // [K1 — S164, Mig 70] Danh mục Hợp đồng SOL-CCM-SP-002 (86 dòng, 8 nhóm duyệt).
        // NOT gated DemoSeed — master data thật, prod cần ngay deploy đầu (gotcha #51).
        await SeedContractCatalogAsync(db, logger);

        // Backfill mã HĐ cho HĐ legacy chưa có (sau khi đổi policy gen-tại-create).
        // Idempotent: chỉ HĐ MaHopDong IS NULL được gen.
        var codeGen = sp.GetRequiredService<IContractCodeGenerator>();
        await BackfillContractCodesAsync(db, codeGen, logger);

        if (!demoSeedDisabled)
        {
            await SeedDemoContractsAsync(db, userManager, codeGen, logger);
            await SeedDemoPurchaseEvaluationsAsync(db, userManager, logger);
            await SeedSampleApprovalWorkflowsV2Async(db, userManager, logger);
        }

        // [Plan B S29 2026-05-22 Hotfix CICD] SeedSampleContractWorkflowV2Async
        // PROMOTED ra ngoài DemoSeed gate — đây là INFRASTRUCTURE workflow
        // (như Roles + Departments + Catalogs), KHÔNG phải demo data wipeable.
        // Production cần workflow V2 mẫu để Drafter create V2 contract end-to-end
        // (Workspace dropdown filter ApplicableType=3 + IsUserSelectable=true).
        // CICD Monitor S29 Plan B caught: nested inside gate → skip prod → V2
        // path BLOCKED. Admin có thể edit/delete/disable qua Designer sau seed.
        // Idempotent: skip nếu workflow QT-HD-V2-001 đã tồn tại.
        await SeedSampleContractWorkflowV2Async(db, userManager, logger);

        // Phase 10.3 G-O3 (Mig 38 — S37 2026-05-28). Infrastructure seed sample workflow
        // ApplicableType=ProposalGeneral=4 cho Drafter Workspace dropdown.
        // NOT gated DemoSeed per gotcha #51 INFRASTRUCTURE seed.
        await SeedSampleProposalWorkflowV2Async(db, userManager, logger);

        // Phase 11 P11-A (S42) — Sample workflow V2 cho 4 WorkflowApps module.
        // INFRASTRUCTURE seed NOT gated DemoSeed (gotcha #51) — mirror Proposal.
        await SeedSampleLeaveRequestWorkflowV2Async(db, userManager, logger);
        await SeedSampleOtRequestWorkflowV2Async(db, userManager, logger);
        await SeedSampleTravelRequestWorkflowV2Async(db, userManager, logger);
        await SeedSampleVehicleBookingWorkflowV2Async(db, userManager, logger);

        // [K3 nhịp-1 — S164/S165] 8 quy trình duyệt KHKK theo nhóm duyệt N1..N8
        // (ApplicableType=ContractSigningPlan=10). INFRASTRUCTURE seed NOT gated
        // DemoSeed (gotcha #51) — prod cần đủ 8 quy trình ngay deploy đầu, thiếu thì
        // Drafter không pick được quy trình lúc tạo phiếu KHKK. Idempotent per-Code.
        await SeedKhkkGroupWorkflowsAsync(db, userManager, logger);

        // [S187 W3 — YC-023/YC-024] 7 quy trình duyệt HĐ theo LOẠI (`HD-*`,
        // ApplicableType=Contract=3). Cùng lý do ungated + idempotent per-Code như KHKK
        // ngay trên (gotcha #51). Đặt SAU KHKK vì dùng chung `ProCcmBodApprovalRoster`
        // và cùng khuôn 3 trạm — đọc 2 seeder cạnh nhau thấy ngay chỗ chúng LỆCH
        // (cờ finalize: KHKK true / HĐ false).
        await SeedContractTypeWorkflowsAsync(db, userManager, logger);

        await WarnDefaultAdminPasswordAsync(userManager, logger);
    }

    // Seed sample workflow V2 cho ApplicableType=DuyetNccPhuongAn (B), giúp UAT
    // type B nhanh không cần admin tạo qua Designer trước. Idempotent — skip nếu
    // đã có ANY workflow B (admin đã tạo) HOẶC nếu thiếu test user / phòng CCM.
    // KHÔNG seed cho A (admin đã tạo + chốt qua Designer S17 — không clobber).
    private static async Task SeedSampleApprovalWorkflowsV2Async(
        ApplicationDbContext db, UserManager<User> userManager, ILogger logger)
    {
        var hasAnyB = await db.ApprovalWorkflows
            .AnyAsync(w => w.ApplicableType == ApprovalWorkflowApplicableType.DuyetNccPhuongAn);
        if (hasAnyB) return;

        var testUser = await userManager.FindByEmailAsync("nv.test@solutions.com.vn");
        if (testUser is null)
        {
            logger.LogWarning("SeedSampleApprovalWorkflowsV2Async: skip — test user nv.test@solutions.com.vn not found");
            return;
        }

        var ccmDept = await db.Departments.FirstOrDefaultAsync(d => d.Code == "CCM");

        var wf = new ApprovalWorkflow
        {
            Code = "QT-DN-PA-V2-001",
            Version = 1,
            ApplicableType = ApprovalWorkflowApplicableType.DuyetNccPhuongAn,
            Name = "Quy trình duyệt NCC và Giải pháp (mẫu UAT)",
            Description = "Sample seed cho UAT B — 1 Bước Phòng CCM × 1 Cấp NV test. Admin có thể clone tạo version mới qua Designer.",
            IsActive = true,
            IsUserSelectable = true,    // Mig 25 — sample mặc định cho user pick
            ActivatedAt = DateTime.UtcNow,
        };
        var step = new ApprovalWorkflowStep
        {
            ApprovalWorkflow = wf,
            Order = 1,
            Name = "Phòng CCM",
            DepartmentId = ccmDept?.Id,
        };
        var level = new ApprovalWorkflowLevel
        {
            Step = step,
            Order = 1,
            Name = "Cấp 1",
            ApproverUserId = testUser.Id,
        };
        wf.Steps.Add(step);
        step.Levels.Add(level);
        db.ApprovalWorkflows.Add(wf);
        await db.SaveChangesAsync();
        logger.LogInformation("Seeded sample ApprovalWorkflow V2 for DuyetNccPhuongAn: QT-DN-PA-V2-001 v01");
    }

    // [Plan B S29 2026-05-22 Chunk A2] Seed sample workflow V2 cho ApplicableType=Contract,
    // giúp UAT HĐ V2 nhanh không cần admin tạo qua Designer trước. Idempotent — skip
    // nếu đã có ANY workflow Contract (admin đã tạo) HOẶC nếu thiếu user CCM seed.
    // Mirror SeedSampleApprovalWorkflowsV2Async pattern (DuyetNccPhuongAn).
    private static async Task SeedSampleContractWorkflowV2Async(
        ApplicationDbContext db, UserManager<User> userManager, ILogger logger)
    {
        var hasAnyContract = await db.ApprovalWorkflows
            .AnyAsync(w => w.ApplicableType == ApprovalWorkflowApplicableType.Contract);
        if (hasAnyContract) return;

        var approver = await userManager.FindByEmailAsync("binh.le@solutions.com.vn");
        if (approver is null)
        {
            logger.LogWarning("SeedSampleContractWorkflowV2Async: skip — approver binh.le@solutions.com.vn (Lê Văn Bình CCM) not found");
            return;
        }

        var ccmDept = await db.Departments.FirstOrDefaultAsync(d => d.Code == "CCM");

        var wf = new ApprovalWorkflow
        {
            Code = "QT-HD-V2-001",
            Version = 1,
            ApplicableType = ApprovalWorkflowApplicableType.Contract,
            Name = "Quy trình duyệt HĐ mẫu UAT V2",
            Description = "Sample seed cho UAT HĐ V2 — 1 Bước Phòng CCM × 1 Cấp (Lê Văn Bình). Admin có thể clone tạo version mới qua Designer.",
            // [YC-030 Q5 S190] Anh chốt "Thay luôn — mỗi loại 1 quy trình tương tự KHKK":
            // 7 quy trình họ `HD-*` per-loại (seed @S188) là đường CHÍNH THỨC; bản mẫu chung
            // này TẮT từ lúc sinh cho env mới (env cũ đã có row ⇒ seeder skip, tắt bằng
            // UPDATE data cùng đợt deploy — bền vì idempotent-check là EXISTENCE, không
            // re-activate, né gotcha #75). Giữ khối seed làm mẫu-lịch-sử, KHÔNG xoá.
            IsActive = false,
            IsUserSelectable = false,   // Mig 25 — KHÔNG hiện trong Workspace dropdown
            ActivatedAt = DateTime.UtcNow,
        };
        var step = new ApprovalWorkflowStep
        {
            ApprovalWorkflow = wf,
            Order = 1,
            Name = "Bước 1 - Phòng CCM",
            DepartmentId = ccmDept?.Id,
        };
        var level = new ApprovalWorkflowLevel
        {
            Step = step,
            Order = 1,
            Name = "Cấp 1",
            ApproverUserId = approver.Id,
        };
        wf.Steps.Add(step);
        step.Levels.Add(level);
        db.ApprovalWorkflows.Add(wf);
        await db.SaveChangesAsync();
        logger.LogInformation("Seeded sample ApprovalWorkflow V2 for Contract: QT-HD-V2-001 v01");
    }

    // Phase 10.3 G-O3 (Mig 38 — S37) — Sample workflow Proposal cho UAT.
    // Mirror SeedSampleContractWorkflowV2Async pattern. INFRASTRUCTURE seed NOT gated.
    private static async Task SeedSampleProposalWorkflowV2Async(
        ApplicationDbContext db, UserManager<User> userManager, ILogger logger)
    {
        var hasAnyProposal = await db.ApprovalWorkflows
            .AnyAsync(w => w.ApplicableType == ApprovalWorkflowApplicableType.ProposalGeneral);
        if (hasAnyProposal) return;

        var approver = await userManager.FindByEmailAsync("binh.le@solutions.com.vn");
        if (approver is null)
        {
            logger.LogWarning("SeedSampleProposalWorkflowV2Async: skip — approver binh.le@solutions.com.vn not found");
            return;
        }

        var ccmDept = await db.Departments.FirstOrDefaultAsync(d => d.Code == "CCM");

        var wf = new ApprovalWorkflow
        {
            Code = "QT-DX-V2-001",
            Version = 1,
            ApplicableType = ApprovalWorkflowApplicableType.ProposalGeneral,
            Name = "Quy trình duyệt Đề xuất mẫu V2",
            Description = "Sample seed UAT — 1 Bước Phòng CCM × 1 Cấp (Lê Văn Bình). Admin có thể clone tạo version mới qua Designer.",
            IsActive = true,
            IsUserSelectable = true,
            ActivatedAt = DateTime.UtcNow,
        };
        var step = new ApprovalWorkflowStep
        {
            ApprovalWorkflow = wf,
            Order = 1,
            Name = "Bước 1 - Phòng CCM",
            DepartmentId = ccmDept?.Id,
        };
        var level = new ApprovalWorkflowLevel
        {
            Step = step,
            Order = 1,
            Name = "Cấp 1",
            ApproverUserId = approver.Id,
        };
        wf.Steps.Add(step);
        step.Levels.Add(level);
        db.ApprovalWorkflows.Add(wf);
        await db.SaveChangesAsync();
        logger.LogInformation("Seeded sample ApprovalWorkflow V2 for Proposal: QT-DX-V2-001 v01");
    }

    // Phase 11 P11-A (S42) — Sample workflow V2 cho 4 WorkflowApps module để UAT
    // test approval ngay (Leave/OT/Travel/Vehicle). Cookie-cutter mirror
    // SeedSampleProposalWorkflowV2Async CHÍNH XÁC. INFRASTRUCTURE seed NOT gated
    // DemoSeed (gotcha #51). Idempotent — skip nếu đã có ANY workflow cùng type.
    private static async Task SeedSampleLeaveRequestWorkflowV2Async(
        ApplicationDbContext db, UserManager<User> userManager, ILogger logger)
    {
        var hasAny = await db.ApprovalWorkflows
            .AnyAsync(w => w.ApplicableType == ApprovalWorkflowApplicableType.LeaveRequest);
        if (hasAny) return;

        var approver = await userManager.FindByEmailAsync("binh.le@solutions.com.vn");
        if (approver is null)
        {
            logger.LogWarning("SeedSampleLeaveRequestWorkflowV2Async: skip — approver binh.le@solutions.com.vn not found");
            return;
        }

        var ccmDept = await db.Departments.FirstOrDefaultAsync(d => d.Code == "CCM");

        var wf = new ApprovalWorkflow
        {
            Code = "QT-NP-V2-001",
            Version = 1,
            ApplicableType = ApprovalWorkflowApplicableType.LeaveRequest,
            Name = "Quy trình duyệt đơn nghỉ phép (mẫu)",
            Description = "Sample seed UAT — 1 Bước Phòng CCM × 1 Cấp (Lê Văn Bình). Admin có thể clone tạo version mới qua Designer.",
            IsActive = true,
            IsUserSelectable = true,
            ActivatedAt = DateTime.UtcNow,
        };
        var step = new ApprovalWorkflowStep
        {
            ApprovalWorkflow = wf,
            Order = 1,
            Name = "Cấp duyệt",
            DepartmentId = ccmDept?.Id,
        };
        var level = new ApprovalWorkflowLevel
        {
            Step = step,
            Order = 1,
            Name = "Cấp 1",
            ApproverUserId = approver.Id,
        };
        wf.Steps.Add(step);
        step.Levels.Add(level);
        db.ApprovalWorkflows.Add(wf);
        await db.SaveChangesAsync();
        logger.LogInformation("Seeded sample ApprovalWorkflow V2 for LeaveRequest: QT-NP-V2-001 v01");
    }

    private static async Task SeedSampleOtRequestWorkflowV2Async(
        ApplicationDbContext db, UserManager<User> userManager, ILogger logger)
    {
        var hasAny = await db.ApprovalWorkflows
            .AnyAsync(w => w.ApplicableType == ApprovalWorkflowApplicableType.OtRequest);
        if (hasAny) return;

        var approver = await userManager.FindByEmailAsync("binh.le@solutions.com.vn");
        if (approver is null)
        {
            logger.LogWarning("SeedSampleOtRequestWorkflowV2Async: skip — approver binh.le@solutions.com.vn not found");
            return;
        }

        var ccmDept = await db.Departments.FirstOrDefaultAsync(d => d.Code == "CCM");

        var wf = new ApprovalWorkflow
        {
            Code = "QT-OT-V2-001",
            Version = 1,
            ApplicableType = ApprovalWorkflowApplicableType.OtRequest,
            Name = "Quy trình duyệt đơn OT (mẫu)",
            Description = "Sample seed UAT — 1 Bước Phòng CCM × 1 Cấp (Lê Văn Bình). Admin có thể clone tạo version mới qua Designer.",
            IsActive = true,
            IsUserSelectable = true,
            ActivatedAt = DateTime.UtcNow,
        };
        var step = new ApprovalWorkflowStep
        {
            ApprovalWorkflow = wf,
            Order = 1,
            Name = "Cấp duyệt",
            DepartmentId = ccmDept?.Id,
        };
        var level = new ApprovalWorkflowLevel
        {
            Step = step,
            Order = 1,
            Name = "Cấp 1",
            ApproverUserId = approver.Id,
        };
        wf.Steps.Add(step);
        step.Levels.Add(level);
        db.ApprovalWorkflows.Add(wf);
        await db.SaveChangesAsync();
        logger.LogInformation("Seeded sample ApprovalWorkflow V2 for OtRequest: QT-OT-V2-001 v01");
    }

    private static async Task SeedSampleTravelRequestWorkflowV2Async(
        ApplicationDbContext db, UserManager<User> userManager, ILogger logger)
    {
        var hasAny = await db.ApprovalWorkflows
            .AnyAsync(w => w.ApplicableType == ApprovalWorkflowApplicableType.TravelRequest);
        if (hasAny) return;

        var approver = await userManager.FindByEmailAsync("binh.le@solutions.com.vn");
        if (approver is null)
        {
            logger.LogWarning("SeedSampleTravelRequestWorkflowV2Async: skip — approver binh.le@solutions.com.vn not found");
            return;
        }

        var ccmDept = await db.Departments.FirstOrDefaultAsync(d => d.Code == "CCM");

        var wf = new ApprovalWorkflow
        {
            Code = "QT-CT-V2-001",
            Version = 1,
            ApplicableType = ApprovalWorkflowApplicableType.TravelRequest,
            Name = "Quy trình duyệt đơn công tác (mẫu)",
            Description = "Sample seed UAT — 1 Bước Phòng CCM × 1 Cấp (Lê Văn Bình). Admin có thể clone tạo version mới qua Designer.",
            IsActive = true,
            IsUserSelectable = true,
            ActivatedAt = DateTime.UtcNow,
        };
        var step = new ApprovalWorkflowStep
        {
            ApprovalWorkflow = wf,
            Order = 1,
            Name = "Cấp duyệt",
            DepartmentId = ccmDept?.Id,
        };
        var level = new ApprovalWorkflowLevel
        {
            Step = step,
            Order = 1,
            Name = "Cấp 1",
            ApproverUserId = approver.Id,
        };
        wf.Steps.Add(step);
        step.Levels.Add(level);
        db.ApprovalWorkflows.Add(wf);
        await db.SaveChangesAsync();
        logger.LogInformation("Seeded sample ApprovalWorkflow V2 for TravelRequest: QT-CT-V2-001 v01");
    }

    private static async Task SeedSampleVehicleBookingWorkflowV2Async(
        ApplicationDbContext db, UserManager<User> userManager, ILogger logger)
    {
        var hasAny = await db.ApprovalWorkflows
            .AnyAsync(w => w.ApplicableType == ApprovalWorkflowApplicableType.VehicleBooking);
        if (hasAny) return;

        var approver = await userManager.FindByEmailAsync("binh.le@solutions.com.vn");
        if (approver is null)
        {
            logger.LogWarning("SeedSampleVehicleBookingWorkflowV2Async: skip — approver binh.le@solutions.com.vn not found");
            return;
        }

        var ccmDept = await db.Departments.FirstOrDefaultAsync(d => d.Code == "CCM");

        var wf = new ApprovalWorkflow
        {
            Code = "QT-XE-V2-001",
            Version = 1,
            ApplicableType = ApprovalWorkflowApplicableType.VehicleBooking,
            Name = "Quy trình duyệt đặt xe (mẫu)",
            Description = "Sample seed UAT — 1 Bước Phòng CCM × 1 Cấp (Lê Văn Bình). Admin có thể clone tạo version mới qua Designer.",
            IsActive = true,
            IsUserSelectable = true,
            ActivatedAt = DateTime.UtcNow,
        };
        var step = new ApprovalWorkflowStep
        {
            ApprovalWorkflow = wf,
            Order = 1,
            Name = "Cấp duyệt",
            DepartmentId = ccmDept?.Id,
        };
        var level = new ApprovalWorkflowLevel
        {
            Step = step,
            Order = 1,
            Name = "Cấp 1",
            ApproverUserId = approver.Id,
        };
        wf.Steps.Add(step);
        step.Levels.Add(level);
        db.ApprovalWorkflows.Add(wf);
        await db.SaveChangesAsync();
        logger.LogInformation("Seeded sample ApprovalWorkflow V2 for VehicleBooking: QT-XE-V2-001 v01");
    }

    // ============================================================================
    // [K3 nhịp-1 — S164/S165] 8 QUY TRÌNH DUYỆT KHKK theo NHÓM DUYỆT N1..N8
    // ============================================================================
    // Danh mục Hợp đồng SOL-CCM-SP-002 (Mig 70) chia 86 dòng vào 8 nhóm duyệt
    // (`ContractCatalogEntry.ApprovalGroup` 1..8); phiếu KHKK chốt nhóm lúc TẠO
    // (`ContractSigningPlan.ApprovalGroup`, Mig 71) ⇒ mỗi nhóm cần 1 quy trình V2
    // riêng để pin. Code `KHKK-N{n}`, ApplicableType=ContractSigningPlan (=10).
    //
    // ĐỘI HÌNH: 3 trạm PRO → CCM → BGĐ, sao đúng workflow Duyệt NCC prod bản LIVE
    // (`QT-DN-V2-001`, ApplicableType=1) theo owner OG-2 — "lấy đúng bộ người duyệt
    // đang cấu hình ở workflow Duyệt NCC prod". Cấp 1 mỗi trạm = OR-of-N (N row cùng
    // Order ⇒ bất kỳ ai trong cấp duyệt là xong cấp đó), Cấp 2 = trưởng phòng.
    //
    // INFRASTRUCTURE seed — NOT gated DemoSeed (gotcha #51).
    // Idempotent PER-CODE (`KHKK-N{n}`), CỐ Ý KHÔNG dùng khuôn `AnyAsync(w.Applicable
    // Type == ...)` của 6 seeder mẫu phía trên: ở đây 8 workflow DÙNG CHUNG 1 type,
    // check theo type sẽ chặn 7 nhóm còn lại ngay khi nhóm đầu vừa được tạo.
    //
    // Resolve người duyệt THEO EMAIL. User vắng mặt (DB dev thiếu người thật) ⇒ BỎ QUA
    // đúng slot đó + log warning, KHÔNG throw (seeder chạy lúc boot — throw = API chết).
    // Cấp không còn ai ⇒ bỏ cấp (Order được đánh LẠI liên tục 1,2,… để không vi phạm
    // ràng buộc sequential của Designer); Bước không còn cấp nào ⇒ bỏ bước; workflow
    // 0 bước vẫn TẠO (giữ mã cho phiếu pin + admin bổ sung người qua Designer) + log.
    // ĐỘI HÌNH 3 TRẠM DÙNG CHUNG — nguồn DUY NHẤT của "ai duyệt, trạm nào, thứ tự nào".
    //
    // Roster VERIFY LIVE trên prod 2026-07-31 (bản QT-DN-V2-001 IsActive=1) —
    // bước × phòng × [Cấp 1 OR-of-N] × [Cấp 2] × cờ finalize của Cấp 2.
    //
    // 🔴 [S187 W3] NÂNG từ biến cục bộ của seeder KHKK lên field dùng chung, vì họ quy trình
    //    HĐ (`SeedContractTypeWorkflowsAsync`, type-3) phải "SAO KHUÔN KHKK" theo owner (83).
    //    Chép mảng lần thứ hai = 2 bản 11 email đi lệch nhau theo thời gian (đúng bug-class mà
    //    `ContractSigningPlanWorkflowGuards:28-32` đang chống) ⇒ dùng CHUNG để 2 họ khớp
    //    by-construction. Đo 2026-08-10 trên Dev: 8/8 workflow `KHKK-N*` có chữ ký cấu trúc
    //    GIỐNG HỆT nhau (MD5 `7EB6CD21…` trên tập bước/phòng/cấp/email/cờ) ⇒ "khuôn chung" là
    //    mô tả ĐO ĐƯỢC, không phải giả định.
    // ⚠️ Muốn 2 họ KHÁC người nhau thì TÁCH thành 2 mảng đặt cạnh nhau — ĐỪNG sửa tại chỗ,
    //    sửa ở đây là đổi luôn họ kia mà không ai thấy. (Chỉnh người cho MỘT quy trình đang chạy
    //    là việc của admin trên Designer, không phải của seeder: seeder idempotent per-Code nên
    //    sau lần seed đầu nó không đụng vào DB nữa.)
    private static readonly (string StepName, string DeptCode, string[] Level1, string[] Level2, bool Level2Finalize)[]
        ProCcmBodApprovalRoster =
        [
            ("Phòng Cung Ứng", "PRO",
                new[]
                {
                    "phuong.nguyen@solutions.com.vn",     // Nguyễn Thị Bích Phượng
                    "long.chau@solutions.com.vn",         // Châu Tạ Kim Long
                    "duy.nguyen@solutions.com.vn",        // Nguyễn Văn Duy
                    "binh.lethanh@solutions.com.vn",      // Bình Lê Thanh (⟂ binh.le bên CCM)
                },
                new[] { "tra.bui@solutions.com.vn" },     // Bùi Lê Thủy Trà (TP.PRO)
                false),

            ("P. Kiểm soát Chi phí", "CCM",
                new[]
                {
                    "luu.tran@solutions.com.vn",          // Trần Xuân Lưu
                    "truong.letrandang@solutions.com.vn", // Lê Trần Đăng Trường (⟂ truong.nguyen bên BGĐ)
                    "binh.le@solutions.com.vn",           // Lê Văn Bình
                    "nguyen.ho@solutions.com.vn",         // Hồ Thị Nữ Nguyên
                },
                new[] { "chuong.phan@solutions.com.vn" }, // Phan Văn Chương (TP.CCM)
                // 🔴 AllowApproverFinalize=true (owner OG-3/OG-9: a Chương quyết lên CEO hay
                // kết thúc). Nhánh service port ở nhịp sau CÙNG WAVE trước push nên không
                // phải config-lie #78.
                // 🔴 [S187 W3] Cờ này CHỈ ăn cho họ KHKK (type-10). Họ HĐ (type-3) truyền
                //    `applyLevel2Finalize: false` — xem chú thích tại `SeedContractTypeWorkflowsAsync`:
                //    máy duyệt type-3 (`ContractWorkflowService.ApproveV2Async`) KHÔNG có nhánh
                //    finalize (đo: 0 hit `AllowApproverFinalize`; control dương PE `:870` + KHKK
                //    `ContractSigningPlanWorkflowService.cs:286` đều CÓ) ⇒ seed `true` cho HĐ sẽ
                //    khoe trên Designer một quyền mà engine không thi hành = đúng #78.
                true),

            ("Ban Giám đốc", "BOD",
                new[] { "truong.nguyen@solutions.com.vn" },  // Nguyễn Văn Trường (CEO)
                Array.Empty<string>(),
                false),
        ];

    // Resolve email → UserId MỘT lượt cho cả roster (dùng lại cho MỌI workflow của họ).
    // Thiếu user ⇒ ghi warning + bỏ đúng slot đó, KHÔNG throw (seeder chạy lúc boot).
    private static async Task<Dictionary<string, Guid>> ResolveRosterApproversAsync(
        UserManager<User> userManager,
        (string StepName, string DeptCode, string[] Level1, string[] Level2, bool Level2Finalize)[] roster,
        string seeder, ILogger logger)
    {
        var usersByEmail = new Dictionary<string, Guid>(StringComparer.OrdinalIgnoreCase);
        var missingEmails = new List<string>();
        foreach (var email in roster.SelectMany(r => r.Level1.Concat(r.Level2))
                     .Distinct(StringComparer.OrdinalIgnoreCase))
        {
            var user = await userManager.FindByEmailAsync(email);
            if (user is null) { missingEmails.Add(email); continue; }
            usersByEmail[email] = user.Id;
        }
        if (missingEmails.Count > 0)
            logger.LogWarning(
                "{Seeder}: {Count} email người duyệt KHÔNG có user — bỏ qua slot đó: {Emails}",
                seeder, missingEmails.Count, string.Join(", ", missingEmails));
        return usersByEmail;
    }

    private static async Task<Dictionary<string, Guid?>> LoadDepartmentIdsByCodeAsync(ApplicationDbContext db) =>
        (await db.Departments.ToListAsync())
            .GroupBy(d => d.Code)
            .ToDictionary(g => g.Key, g => (Guid?)g.First().Id);

    // Dựng 3 trạm PRO → CCM → BGĐ theo `ProCcmBodApprovalRoster` vào MỘT workflow.
    // Cấp không còn ai ⇒ bỏ cấp (Order đánh LẠI liên tục 1,2,… để không vi phạm ràng buộc
    // sequential của Designer); Bước không còn cấp nào ⇒ bỏ bước + log.
    // `applyLevel2Finalize=false` ⇒ ép cờ finalize của Cấp 2 về false cho MỌI trạm (họ nào mà
    // engine chưa có nhánh finalize thì truyền false — xem chú thích cờ trong roster).
    private static void AddProCcmBodStepsToWorkflow(
        ApprovalWorkflow wf,
        IReadOnlyDictionary<string, Guid> usersByEmail,
        IReadOnlyDictionary<string, Guid?> deptIdByCode,
        bool applyLevel2Finalize,
        string seeder, string code, ILogger logger)
    {
        var stepOrder = 1;
        foreach (var r in ProCcmBodApprovalRoster)
        {
            var step = new ApprovalWorkflowStep
            {
                ApprovalWorkflow = wf,
                Order = stepOrder,
                Name = r.StepName,
                DepartmentId = deptIdByCode.GetValueOrDefault(r.DeptCode),
            };

            var levelOrder = 1;
            foreach (var (emails, finalize) in new[]
                     { (r.Level1, false), (r.Level2, applyLevel2Finalize && r.Level2Finalize) })
            {
                var approverIds = emails
                    .Where(usersByEmail.ContainsKey)
                    .Select(e => usersByEmail[e])
                    .ToArray();
                if (approverIds.Length == 0) continue;   // cấp rỗng → bỏ, KHÔNG chừa lỗ Order

                foreach (var approverId in approverIds)
                {
                    step.Levels.Add(new ApprovalWorkflowLevel
                    {
                        Step = step,
                        Order = levelOrder,             // N row cùng Order = OR-of-N trong 1 Cấp
                        Name = $"Cấp {levelOrder}",
                        ApproverUserId = approverId,
                        AllowApproverFinalize = finalize,
                    });
                }
                levelOrder++;
            }

            if (step.Levels.Count == 0)
            {
                logger.LogWarning(
                    "{Seeder}: {Code} bỏ Bước \"{Step}\" — 0 người duyệt resolve được",
                    seeder, code, r.StepName);
                continue;
            }

            wf.Steps.Add(step);
            stepOrder++;
        }
    }

    private static async Task SeedKhkkGroupWorkflowsAsync(
        ApplicationDbContext db, UserManager<User> userManager, ILogger logger)
    {
        var usersByEmail = await ResolveRosterApproversAsync(
            userManager, ProCcmBodApprovalRoster, nameof(SeedKhkkGroupWorkflowsAsync), logger);
        var deptIdByCode = await LoadDepartmentIdsByCodeAsync(db);

        var seeded = 0;
        for (var n = 1; n <= 8; n++)
        {
            var code = $"KHKK-N{n}";
            if (await db.ApprovalWorkflows.AnyAsync(w => w.Code == code)) continue;

            var wf = new ApprovalWorkflow
            {
                Code = code,
                Version = 1,
                ApplicableType = ApprovalWorkflowApplicableType.ContractSigningPlan,
                Name = $"Duyệt KHKK — Nhóm {n}",
                Description =
                    $"Quy trình duyệt Kế hoạch ký kết HĐ cho nhóm duyệt N{n} (danh mục SOL-CCM-SP-002). " +
                    "3 trạm PRO → CCM → BGĐ, đội hình sao từ workflow Duyệt NCC prod. " +
                    "Admin có thể chỉnh người/quyền tại chỗ hoặc tạo version mới qua Designer.",
                IsActive = true,
                IsUserSelectable = true,
                ActivatedAt = DateTime.UtcNow,
            };

            // [S187 W3] Vòng dựng trạm CŨ (inline) đã nâng nguyên xi thành `AddProCcmBodStepsToWorkflow`.
            // `applyLevel2Finalize: true` ⇒ biểu thức trong builder là `true && r.Level2Finalize`
            // = ĐÚNG bằng `r.Level2Finalize` của dòng cũ ⇒ hành vi KHKK không đổi một li.
            AddProCcmBodStepsToWorkflow(
                wf, usersByEmail, deptIdByCode, applyLevel2Finalize: true,
                nameof(SeedKhkkGroupWorkflowsAsync), code, logger);

            if (wf.Steps.Count == 0)
                logger.LogWarning(
                    "SeedKhkkGroupWorkflowsAsync: {Code} tạo RỖNG (0 Bước có người) — admin phải thêm người qua Designer",
                    code);

            db.ApprovalWorkflows.Add(wf);
            seeded++;
        }

        if (seeded > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation("Seeded {Count} KHKK group workflows (K3 SP-002)", seeded);
        }
    }

    // ============================================================================
    // [S187 W3 — YC-023/YC-024] 7 QUY TRÌNH DUYỆT HỢP ĐỒNG theo LOẠI HĐ
    // ============================================================================
    // Owner @S188: (80) trục = 7 LOẠI HĐ · (81) PA-A mỗi loại MỘT mã riêng, 0 cột mới ·
    // (83) đội hình SAO KHUÔN KHKK, anh tự chỉnh lại trong Admin Designer sau.
    // Mã + tên loại: `ContractApprovalWorkflowFamily` (Domain/Contracts) — NHÀ DUY NHẤT.
    // `ApplicableType = Contract` (=3) ⇒ Drafter pick được ở màn tạo HĐ (dropdown lọc
    // `IsUserSelectable && IsActive` theo type).
    //
    // INFRASTRUCTURE seed — NOT gated DemoSeed (gotcha #51): prod cần đủ 7 quy trình ngay
    // deploy đầu, thiếu thì HĐ loại đó không pin được quy trình nào.
    // Idempotent PER-CODE (`HD-*`) — CÙNG lý do như KHKK: 7 workflow DÙNG CHUNG 1 type nên
    // check theo `ApplicableType` sẽ chặn 6 loại còn lại ngay khi loại đầu vừa tạo (và ở đây
    // còn tệ hơn: type-3 ĐÃ có sẵn `QT-HD-V2-001` sample từ S29 ⇒ check-by-type = seed 0 cái).
    //
    // 🔴 CỜ `AllowApproverFinalize` CỐ Ý KHÔNG SAO NGUYÊN XI (`applyLevel2Finalize: false`):
    //    roster KHKK bật finalize ở Cấp 2 trạm CCM, nhưng máy duyệt type-3
    //    (`ContractWorkflowService`) đo ngày 2026-08-10 có **0 hit** `AllowApproverFinalize`
    //    (control dương: PE `PurchaseEvaluationWorkflowService.cs:870` CÓ · KHKK
    //    `ContractSigningPlanWorkflowService.cs:286` CÓ ⇒ thước sống, 0-hit là vắng THẬT).
    //    Seed `true` khi engine không thi hành = Designer khoe một quyền không có thật —
    //    đúng gotcha #78 (config-lie). Owner bật lại bằng 1 click trong Designer khi máy
    //    type-3 có nhánh finalize. TRẠM/NGƯỜI/THỨ TỰ/OR-of-N giữ NGUYÊN theo (83).
    private static async Task SeedContractTypeWorkflowsAsync(
        ApplicationDbContext db, UserManager<User> userManager, ILogger logger)
    {
        var usersByEmail = await ResolveRosterApproversAsync(
            userManager, ProCcmBodApprovalRoster, nameof(SeedContractTypeWorkflowsAsync), logger);
        var deptIdByCode = await LoadDepartmentIdsByCodeAsync(db);

        var seeded = 0;
        foreach (var (_, code, shortName) in ContractApprovalWorkflowFamily.Members)
        {
            if (await db.ApprovalWorkflows.AnyAsync(w => w.Code == code)) continue;

            var wf = new ApprovalWorkflow
            {
                Code = code,
                Version = 1,
                ApplicableType = ApprovalWorkflowApplicableType.Contract,
                Name = $"Duyệt Hợp đồng — {shortName}",
                Description =
                    $"Quy trình duyệt Hợp đồng loại \"{shortName}\". Đội hình sao khuôn quy trình " +
                    "duyệt KHKK (3 trạm PRO → CCM → BGĐ). Admin chỉnh người/quyền tại chỗ hoặc tạo " +
                    "version mới qua Designer.",
                IsActive = true,
                IsUserSelectable = true,
                ActivatedAt = DateTime.UtcNow,
            };

            AddProCcmBodStepsToWorkflow(
                wf, usersByEmail, deptIdByCode, applyLevel2Finalize: false,
                nameof(SeedContractTypeWorkflowsAsync), code, logger);

            if (wf.Steps.Count == 0)
                logger.LogWarning(
                    "SeedContractTypeWorkflowsAsync: {Code} tạo RỖNG (0 Bước có người) — admin phải thêm người qua Designer",
                    code);

            db.ApprovalWorkflows.Add(wf);
            seeded++;
        }

        if (seeded > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation("Seeded {Count} contract-type approval workflows (W3 YC-023)", seeded);
        }
    }

    // Seed 4 master catalogs với defaults cho user nhập liệu Details. Idempotent:
    // skip per-table nếu đã có row (admin có thể đã thêm/sửa — không clobber).
    private static async Task SeedCatalogsAsync(ApplicationDbContext db, ILogger logger)
    {
        // 1. UnitsOfMeasure (~17)
        if (!await db.UnitsOfMeasure.AnyAsync())
        {
            var units = new[]
            {
                ("m2", "Mét vuông"), ("m3", "Mét khối"), ("m", "Mét"), ("md", "Mét dài"),
                ("kg", "Kilogram"), ("tan", "Tấn"), ("g", "Gram"),
                ("l", "Lít"), ("ml", "Mililít"),
                ("cai", "Cái"), ("chiec", "Chiếc"), ("bo", "Bộ"), ("goi", "Gói"), ("hop", "Hộp"),
                ("ngc", "Ngày công"), ("h", "Giờ"), ("ca", "Ca"),
                ("nguoi", "Người"), ("luot", "Lượt"), ("lan", "Lần"),
            };
            foreach (var (code, name) in units)
                db.UnitsOfMeasure.Add(new UnitOfMeasure { Code = code, Name = name });
            await db.SaveChangesAsync();
            logger.LogInformation("Seed {Count} UnitsOfMeasure", units.Length);
        }

        // 2. MaterialItems (~15 demo)
        if (!await db.MaterialItems.AnyAsync())
        {
            var materials = new[]
            {
                ("XM-PCB40", "Xi măng PCB40", "Xi măng", "kg"),
                ("XM-PCB30", "Xi măng PCB30", "Xi măng", "kg"),
                ("CAT-VANG", "Cát vàng đổ bê tông", "Cát đá", "m3"),
                ("CAT-DEN", "Cát đen xây trát", "Cát đá", "m3"),
                ("DA-1x2", "Đá 1x2", "Cát đá", "m3"),
                ("DA-4x6", "Đá 4x6", "Cát đá", "m3"),
                ("THEP-D6", "Thép cuộn phi 6", "Sắt thép", "kg"),
                ("THEP-D10", "Thép cây phi 10", "Sắt thép", "kg"),
                ("THEP-D14", "Thép cây phi 14", "Sắt thép", "kg"),
                ("THEP-D18", "Thép cây phi 18", "Sắt thép", "kg"),
                ("GACH-XANH", "Gạch xây 4 lỗ", "Gạch ngói", "cai"),
                ("GACH-DAT-NUNG", "Gạch đặc đất nung", "Gạch ngói", "cai"),
                ("SON-LOT", "Sơn lót chống kiềm", "Sơn", "l"),
                ("SON-NGOAI", "Sơn ngoại thất", "Sơn", "l"),
                ("ONG-PVC-90", "Ống PVC phi 90", "Vật tư cấp thoát nước", "m"),
            };
            foreach (var (code, name, cat, unit) in materials)
                db.MaterialItems.Add(new MaterialItem { Code = code, Name = name, Category = cat, DefaultUnit = unit });
            await db.SaveChangesAsync();
            logger.LogInformation("Seed {Count} MaterialItems", materials.Length);
        }

        // 3. ServiceItems (~10 demo)
        if (!await db.ServiceItems.AnyAsync())
        {
            var services = new[]
            {
                ("VC-OTO", "Vận chuyển ô tô tải", "Vận chuyển", "ca"),
                ("VC-CAN-TRUC", "Cẩu tháp / cẩu tự hành", "Vận chuyển", "h"),
                ("BT-MAY", "Bảo trì máy móc thiết bị", "Bảo trì", "lan"),
                ("BT-DIEN", "Bảo trì hệ thống điện", "Bảo trì", "lan"),
                ("TV-THIET-KE", "Tư vấn thiết kế", "Tư vấn", "goi"),
                ("TV-GIAM-SAT", "Giám sát thi công", "Tư vấn", "ngc"),
                ("KIEM-DINH", "Kiểm định an toàn", "Kiểm định", "lan"),
                ("VS-CT", "Vệ sinh công trường", "Vệ sinh", "ca"),
                ("BV-CT", "Bảo vệ công trường", "Bảo vệ", "ngc"),
                ("THUE-MAY", "Thuê máy móc thiết bị", "Thuê thiết bị", "ca"),
            };
            foreach (var (code, name, cat, unit) in services)
                db.ServiceItems.Add(new ServiceItem { Code = code, Name = name, Category = cat, DefaultUnit = unit });
            await db.SaveChangesAsync();
            logger.LogInformation("Seed {Count} ServiceItems", services.Length);
        }

        // 4. WorkItems — KHÔNG seed demo (S59, chị Trà Sol chốt 2026-06-11: hạng mục
        //    công việc CHỈ dùng đúng 71 mã PMH công ty gửi — VT-01..16 + TP-01..30 +
        //    MEP-01..09 + TB-01..16, "không đẻ thêm mã của công ty khác vô").
        //    Nguồn duy nhất = SeedRealMasterDataAsync (per-code idempotent, ungated).
        //    15 mã demo cũ (Phần thô/Hoàn thiện/Cơ điện/Khác) đã DELETE prod S59.
    }

    // Backfill: re-assign supplier + project cho [DEMO] HĐ existing → đa dạng
    // theo DemoSupplierByType / DemoProjectByType. Idempotent (chỉ update nếu
    // mismatch). Hữu ích khi expand seed list rồi muốn HĐ cũ pick supplier mới.
    private static async Task BackfillDemoContractsSupplierProjectAsync(
        ApplicationDbContext db,
        IReadOnlyDictionary<string, Supplier> suppliersByCode,
        IReadOnlyDictionary<string, Project> projectsByCode,
        ILogger logger)
    {
        var demos = await db.Contracts
            .Where(c => c.TenHopDong != null && c.TenHopDong!.StartsWith("[DEMO]"))
            .ToListAsync();
        if (demos.Count == 0) return;

        int updated = 0;
        foreach (var c in demos)
        {
            var newSupplierId = c.SupplierId;
            var newProjectId = c.ProjectId;

            if (DemoSupplierByType.TryGetValue(c.Type, out var sCode)
                && suppliersByCode.TryGetValue(sCode, out var s)
                && c.SupplierId != s.Id)
            {
                newSupplierId = s.Id;
            }
            if (DemoProjectByType.TryGetValue(c.Type, out var pCode)
                && projectsByCode.TryGetValue(pCode, out var p)
                && c.ProjectId != p.Id)
            {
                newProjectId = p.Id;
            }

            if (newSupplierId != c.SupplierId || newProjectId != c.ProjectId)
            {
                c.SupplierId = newSupplierId;
                c.ProjectId = newProjectId;
                updated++;
            }
        }
        if (updated > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation("Backfill [DEMO] contracts supplier+project: {Count} updated.", updated);
        }
    }

    private static async Task BackfillContractCodesAsync(
        ApplicationDbContext db, IContractCodeGenerator codeGen, ILogger logger)
    {
        var orphanCount = await db.Contracts.CountAsync(c => c.MaHopDong == null);
        if (orphanCount == 0)
        {
            logger.LogInformation("Backfill mã HĐ: không có HĐ nào thiếu mã.");
            return;
        }

        logger.LogInformation("Backfill mã HĐ: {Count} HĐ thiếu mã, đang gen...", orphanCount);
        var orphans = await db.Contracts.Where(c => c.MaHopDong == null).ToListAsync();
        int success = 0, failed = 0;

        foreach (var contract in orphans)
        {
            var supplier = await db.Suppliers.FirstOrDefaultAsync(s => s.Id == contract.SupplierId);
            var project = await db.Projects.FirstOrDefaultAsync(p => p.Id == contract.ProjectId);
            if (supplier is null || project is null)
            {
                logger.LogWarning(
                    "Backfill HĐ {Id}: skip vì supplier/project missing.", contract.Id);
                failed++;
                continue;
            }

            try
            {
                contract.MaHopDong = await codeGen.GenerateAsync(contract, project.Code, supplier.Code);
                await db.SaveChangesAsync();
                logger.LogInformation("Backfill HĐ {Id} → {Code}", contract.Id, contract.MaHopDong);
                success++;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Backfill HĐ {Id} thất bại.", contract.Id);
                failed++;
            }
        }

        logger.LogInformation(
            "Backfill mã HĐ xong: {Success} thành công, {Failed} thất bại.", success, failed);
    }

    // Seed v01 per ContractType from hardcoded WorkflowPolicies. Idempotent:
    // skip if any active definition already exists for that type (admin may
    // have already created custom versions — don't clobber).
    private static async Task SeedWorkflowDefinitionsAsync(ApplicationDbContext db, ILogger logger)
    {
        var typeLabels = new Dictionary<ContractType, (string Code, string Name)>
        {
            [ContractType.HopDongThauPhu]          = ("QT-TP",   "Quy trình HĐ Thầu phụ"),
            [ContractType.HopDongGiaoKhoan]        = ("QT-GK",   "Quy trình HĐ Giao khoán"),
            [ContractType.HopDongNhaCungCap]       = ("QT-NCC",  "Quy trình HĐ Nhà cung cấp"),
            [ContractType.HopDongDichVu]           = ("QT-DV",   "Quy trình HĐ Dịch vụ"),
            [ContractType.HopDongMuaBan]           = ("QT-MB",   "Quy trình HĐ Mua bán"),
            [ContractType.HopDongNguyenTacNCC]     = ("QT-NTNCC","Quy trình HĐ Nguyên tắc NCC"),
            [ContractType.HopDongNguyenTacDichVu]  = ("QT-NTDV", "Quy trình HĐ Nguyên tắc Dịch vụ"),
        };

        var phaseNames = new Dictionary<ContractPhase, string>
        {
            [ContractPhase.DangSoanThao]    = "Soạn thảo",
            [ContractPhase.DangGopY]        = "Góp ý",
            [ContractPhase.DangDamPhan]     = "Đàm phán",
            [ContractPhase.DangInKy]        = "In ký",
            [ContractPhase.DangKiemTraCCM]  = "Kiểm tra CCM",
            [ContractPhase.DangTrinhKy]     = "Trình ký BOD",
            [ContractPhase.DangDongDau]     = "Đóng dấu",
            [ContractPhase.DaPhatHanh]      = "Phát hành",
        };

        var added = 0;
        foreach (var (type, info) in typeLabels)
        {
            var alreadyExists = await db.WorkflowDefinitions.AnyAsync(w => w.ContractType == type);
            if (alreadyExists) continue;

            var policy = WorkflowPolicyRegistry.For(type);
            var def = new WorkflowDefinition
            {
                Code = info.Code,
                Version = 1,
                ContractType = type,
                Name = $"{info.Name} (v01)",
                Description = policy.Description,
                IsActive = true,
                ActivatedAt = DateTime.UtcNow,
                Steps = policy.ActivePhases
                    .Where(p => p != ContractPhase.TuChoi)  // TuChoi is a terminal state, not a step
                    .Select((p, idx) =>
                    {
                        var roles = policy.Transitions
                            .Where(t => t.Key.To == p)
                            .SelectMany(t => t.Value)
                            .Distinct()
                            .ToList();
                        return new WorkflowStep
                        {
                            Order = idx + 1,
                            Phase = p,
                            Name = phaseNames.GetValueOrDefault(p, p.ToString()),
                            SlaDays = policy.PhaseSla.GetValueOrDefault(p) is TimeSpan s ? (int?)s.Days : null,
                            Approvers = roles.Select(r => new WorkflowStepApprover
                            {
                                Kind = WorkflowApproverKind.Role,
                                AssignmentValue = r,
                            }).ToList(),
                        };
                    })
                    .ToList(),
            };
            db.WorkflowDefinitions.Add(def);
            added++;
        }
        if (added > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation("Seeded {Count} workflow definitions (v01)", added);
        }
    }

    // Seed default workflow v01 cho 2 PurchaseEvaluationType (A NccOnly / B
    // NccWithPlan) — mirror SeedWorkflowDefinitionsAsync của HĐ. Admin có
    // thể tạo version mới qua /system/pe-workflows/:typeCode designer.
    private static async Task SeedPurchaseEvaluationWorkflowsAsync(ApplicationDbContext db, ILogger logger)
    {
        var typeLabels = new Dictionary<PurchaseEvaluationType, (string Code, string Name)>
        {
            [PurchaseEvaluationType.DuyetNcc]          = ("QT-DN-A", "Quy trình Duyệt NCC"),
            [PurchaseEvaluationType.DuyetNccPhuongAn]  = ("QT-DN-B", "Quy trình Duyệt NCC và Giải pháp"),
        };

        var phaseNames = new Dictionary<PurchaseEvaluationPhase, string>
        {
            [PurchaseEvaluationPhase.DangSoanThao]     = "Soạn thảo",
            [PurchaseEvaluationPhase.ChoPurchasing]    = "Purchasing tổng hợp",
            [PurchaseEvaluationPhase.ChoDuAn]          = "Dự án kiểm tra",
            [PurchaseEvaluationPhase.ChoCCM]           = "CCM kiểm tra ngân sách",
            [PurchaseEvaluationPhase.ChoCEODuyetPA]    = "CEO duyệt phương án",
            [PurchaseEvaluationPhase.ChoCEODuyetNCC]   = "CEO duyệt chọn đơn vị",
            [PurchaseEvaluationPhase.DaDuyet]          = "Đã duyệt",
        };

        var added = 0;
        foreach (var (type, info) in typeLabels)
        {
            var alreadyExists = await db.PurchaseEvaluationWorkflowDefinitions
                .AnyAsync(w => w.EvaluationType == type);
            if (alreadyExists) continue;

            var policy = PurchaseEvaluationPolicyRegistry.For(type);
            var def = new PurchaseEvaluationWorkflowDefinition
            {
                Code = info.Code,
                Version = 1,
                EvaluationType = type,
                Name = $"{info.Name} (v01)",
                Description = policy.Description,
                IsActive = true,
                ActivatedAt = DateTime.UtcNow,
                Steps = policy.ActivePhases
                    .Where(p => p != PurchaseEvaluationPhase.TuChoi && p != PurchaseEvaluationPhase.DaDuyet)
                    .Select((p, idx) =>
                    {
                        var roles = policy.Transitions
                            .Where(t => t.Key.To == p)
                            .SelectMany(t => t.Value)
                            .Distinct()
                            .ToList();
                        return new PurchaseEvaluationWorkflowStep
                        {
                            Order = idx + 1,
                            Phase = p,
                            Name = phaseNames.GetValueOrDefault(p, p.ToString()),
                            SlaDays = policy.PhaseSla.GetValueOrDefault(p) is TimeSpan s ? (int?)s.Days : null,
                            Approvers = roles.Select(r => new PurchaseEvaluationWorkflowStepApprover
                            {
                                Kind = WorkflowApproverKind.Role,
                                AssignmentValue = r,
                            }).ToList(),
                        };
                    })
                    .ToList(),
            };
            db.PurchaseEvaluationWorkflowDefinitions.Add(def);
            added++;
        }
        if (added > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation("Seeded {Count} PE workflow definitions (v01)", added);
        }
    }

    // Map ContractType → preferred supplier code (đa dạng dữ liệu demo theo
    // đúng business: ThauPhu/GiaoKhoan đi với NTP/TĐ, NCC/MuaBan/NTNcc đi
    // với NCC, DichVu/NTDv đi với DV).
    private static readonly Dictionary<ContractType, string> DemoSupplierByType = new()
    {
        [ContractType.HopDongThauPhu]          = "NTP-XD",
        [ContractType.HopDongGiaoKhoan]        = "TD-NEHOANG",
        [ContractType.HopDongNhaCungCap]       = "NCC-XIMANG",
        [ContractType.HopDongDichVu]           = "DV-VANCHUYEN",
        [ContractType.HopDongMuaBan]           = "NCC-DIEN",
        [ContractType.HopDongNguyenTacNCC]     = "NCC-THEP",
        [ContractType.HopDongNguyenTacDichVu]  = "DV-CLEAN",
    };

    // Map ContractType → preferred project code (đa dạng dự án per HĐ)
    private static readonly Dictionary<ContractType, string> DemoProjectByType = new()
    {
        [ContractType.HopDongThauPhu]          = "FLOCK01",
        [ContractType.HopDongGiaoKhoan]        = "FLOCK01",
        [ContractType.HopDongNhaCungCap]       = "VHOMES-OP",
        [ContractType.HopDongDichVu]           = "RESORT-PQ",
        [ContractType.HopDongMuaBan]           = "BWTOWER",
        [ContractType.HopDongNguyenTacNCC]     = "ECOPARK-VL",
        [ContractType.HopDongNguyenTacDichVu]  = "WAREHOUSE-LB",
    };

    // Seed 7 demo HĐ — 1 per ContractType, varied phases (Đang soạn / Góp ý /
    // Đã in ký / Đã phát hành), với Details + Approvals + Comments để admin
    // login thấy ngay sample data hoạt động full E2E.
    //
    // Idempotent 2 paths:
    //   1. Lần đầu (no [DEMO] HĐ): create 7 HĐ varied supplier+project per type
    //   2. Đã có [DEMO] HĐ: chạy backfill — update supplier+project nếu mismatched
    //      với DemoSupplierByType/DemoProjectByType (đa dạng dữ liệu).
    private static async Task SeedDemoContractsAsync(
        ApplicationDbContext db, UserManager<User> userManager,
        IContractCodeGenerator codeGen, ILogger logger)
    {
        // Lookup supplier + project map theo Code (cần cả 2 path)
        var suppliersByCode = await db.Suppliers.ToDictionaryAsync(s => s.Code, s => s);
        var projectsByCode = await db.Projects.ToDictionaryAsync(p => p.Code, p => p);
        if (suppliersByCode.Count == 0 || projectsByCode.Count == 0)
        {
            logger.LogWarning("SeedDemoContracts: skip — thiếu Supplier/Project.");
            return;
        }

        // Path 2: backfill nếu đã có [DEMO] HĐ
        if (await db.Contracts.AnyAsync(c => c.TenHopDong != null && c.TenHopDong!.StartsWith("[DEMO]")))
        {
            await BackfillDemoContractsSupplierProjectAsync(db, suppliersByCode, projectsByCode, logger);
            return;
        }

        // Path 1: tạo mới (lần đầu)
        // Default fallback supplier + project nếu type-specific không có (shouldn't happen với expanded seed)
        var fallbackSupplier = suppliersByCode.Values.First();
        var fallbackProject = projectsByCode.Values.First();

        var qsHoang = await userManager.FindByEmailAsync("qs.hoang@solutions.com.vn");
        var ccmTran = await userManager.FindByEmailAsync("ccm.tran@solutions.com.vn");
        var bodHuynh = await userManager.FindByEmailAsync("bod.huynh@solutions.com.vn");
        var hraDang = await userManager.FindByEmailAsync("hra.dang@solutions.com.vn");
        var qsDeptId = (await db.Departments.FirstOrDefaultAsync(d => d.Code == "QS"))?.Id;
        var nowUtc = DateTime.UtcNow;

        async Task<Contract> createDemoAsync(
            ContractType type, ContractPhase finalPhase, string ten, decimal giaTri, string? noiDung)
        {
            var activeWfId = await db.WorkflowDefinitions.AsNoTracking()
                .Where(w => w.ContractType == type && w.IsActive)
                .Select(w => (Guid?)w.Id)
                .FirstOrDefaultAsync();

            // Pick supplier + project theo type (đa dạng dữ liệu demo)
            var supplier = DemoSupplierByType.TryGetValue(type, out var sCode) && suppliersByCode.TryGetValue(sCode, out var s) ? s : fallbackSupplier;
            var project = DemoProjectByType.TryGetValue(type, out var pCode) && projectsByCode.TryGetValue(pCode, out var p) ? p : fallbackProject;

            var c = new Contract
            {
                Type = type,
                Phase = ContractPhase.DangSoanThao,        // Tạo ở phase 2, transition lên sau
                SupplierId = supplier.Id,
                ProjectId = project.Id,
                DepartmentId = qsDeptId,
                DrafterUserId = qsHoang?.Id,
                GiaTri = giaTri,
                TenHopDong = $"[DEMO] {ten}",
                NoiDung = noiDung,
                BypassProcurementAndCCM = false,
                WorkflowDefinitionId = activeWfId,
                SlaDeadline = nowUtc.AddDays(7),
            };
            c.MaHopDong = await codeGen.GenerateAsync(c, project.Code, supplier.Code);
            db.Contracts.Add(c);
            await db.SaveChangesAsync();

            // Mock approvals + Phase advance qua manual write (skip workflow service vì
            // service expect ICurrentUser context). Thực tế chỉ cần ContractApproval
            // history + cập nhật Phase entity.
            ContractPhase[] flow = type is ContractType.HopDongDichVu or ContractType.HopDongMuaBan
                or ContractType.HopDongNguyenTacNCC or ContractType.HopDongNguyenTacDichVu
                ? [ContractPhase.DangGopY, ContractPhase.DangDamPhan, ContractPhase.DangInKy,
                   ContractPhase.DangTrinhKy, ContractPhase.DangDongDau, ContractPhase.DaPhatHanh]
                : [ContractPhase.DangGopY, ContractPhase.DangDamPhan, ContractPhase.DangInKy,
                   ContractPhase.DangKiemTraCCM, ContractPhase.DangTrinhKy,
                   ContractPhase.DangDongDau, ContractPhase.DaPhatHanh];

            var current = ContractPhase.DangSoanThao;
            foreach (var next in flow)
            {
                if (next > finalPhase) break;
                Guid? actorId = next switch
                {
                    ContractPhase.DangKiemTraCCM => ccmTran?.Id,
                    ContractPhase.DangTrinhKy or ContractPhase.DangDongDau => bodHuynh?.Id,
                    ContractPhase.DaPhatHanh => hraDang?.Id,
                    _ => qsHoang?.Id,
                };
                db.ContractApprovals.Add(new ContractApproval
                {
                    ContractId = c.Id,
                    FromPhase = current,
                    ToPhase = next,
                    ApproverUserId = actorId,
                    Decision = ApprovalDecision.Approve,
                    Comment = next == ContractPhase.DangGopY ? "Demo seed — chuyển góp ý" : null,
                    ApprovedAt = nowUtc,
                });
                current = next;
            }
            c.Phase = current;
            return c;
        }

        async Task addThauPhuDetail(Guid cid, int order, string hangMuc, string dvt, decimal kl, decimal dg)
        {
            db.ThauPhuDetails.Add(new Domain.Contracts.Details.ThauPhuDetail
            {
                ContractId = cid, Order = order, HangMuc = hangMuc, DonViTinh = dvt,
                KhoiLuong = kl, DonGia = dg, ThanhTien = kl * dg,
            });
        }
        async Task addNccDetail(Guid cid, int order, string ma, string ten, string dvt, decimal sl, decimal dg)
        {
            db.NhaCungCapDetails.Add(new Domain.Contracts.Details.NhaCungCapDetail
            {
                ContractId = cid, Order = order, MaSP = ma, TenSP = ten, DonViTinh = dvt,
                SoLuong = sl, DonGia = dg, ThanhTien = sl * dg,
            });
        }
        async Task addMuaBanDetail(Guid cid, int order, string ma, string ten, string dvt, decimal sl, decimal dg, decimal vat)
        {
            db.MuaBanDetails.Add(new Domain.Contracts.Details.MuaBanDetail
            {
                ContractId = cid, Order = order, MaSP = ma, TenSP = ten, DonViTinh = dvt,
                SoLuong = sl, DonGia = dg, ThueVAT = vat, ThanhTien = sl * dg * (1 + vat / 100m),
            });
        }
        async Task addDichVuDetail(Guid cid, int order, string ma, string ten, string dvt, decimal tg, decimal dg)
        {
            db.DichVuDetails.Add(new Domain.Contracts.Details.DichVuDetail
            {
                ContractId = cid, Order = order, MaDichVu = ma, TenDichVu = ten, DonViTinh = dvt,
                ThoiGian = tg, DonGia = dg, ThanhTien = tg * dg,
            });
        }

        // 1. HĐ Thầu phụ — DangSoanThao (đang soạn)
        var c1 = await createDemoAsync(
            ContractType.HopDongThauPhu, ContractPhase.DangSoanThao,
            "Thi công móng + cột tầng 1 — FLOCK 01", 850_000_000m,
            "Bao gồm đào móng, đổ bê tông móng cột, lắp cốt thép.");
        await addThauPhuDetail(c1.Id, 1, "Đào móng công trình", "m3", 120, 250_000);
        await addThauPhuDetail(c1.Id, 2, "Đổ bê tông móng cột", "m3", 80, 1_800_000);
        await addThauPhuDetail(c1.Id, 3, "Lắp dựng cốt thép", "kg", 5000, 18_000);

        // 2. HĐ Giao khoán — DangGopY (đang góp ý)
        var c2 = await createDemoAsync(
            ContractType.HopDongGiaoKhoan, ContractPhase.DangGopY,
            "Khoán nhân công xây tường + trát + sơn — FLOCK 01", 320_000_000m,
            "Phần hoàn thiện tầng 2 + 3.");
        db.GiaoKhoanDetails.Add(new Domain.Contracts.Details.GiaoKhoanDetail
        {
            ContractId = c2.Id, Order = 1, MaCongViec = "XAY-TUONG", TenCongViec = "Xây tường gạch 110",
            DonViTinh = "m2", KhoiLuong = 800, DonGia = 180_000, ThanhTien = 144_000_000m,
        });
        db.GiaoKhoanDetails.Add(new Domain.Contracts.Details.GiaoKhoanDetail
        {
            ContractId = c2.Id, Order = 2, MaCongViec = "TRAT-TUONG", TenCongViec = "Trát tường 2 mặt",
            DonViTinh = "m2", KhoiLuong = 1600, DonGia = 80_000, ThanhTien = 128_000_000m,
        });
        db.GiaoKhoanDetails.Add(new Domain.Contracts.Details.GiaoKhoanDetail
        {
            ContractId = c2.Id, Order = 3, MaCongViec = "SON-NUOC", TenCongViec = "Sơn nước nội thất",
            DonViTinh = "m2", KhoiLuong = 1600, DonGia = 30_000, ThanhTien = 48_000_000m,
        });

        // 3. HĐ Nhà cung cấp — DangInKy (đang in ký)
        var c3 = await createDemoAsync(
            ContractType.HopDongNhaCungCap, ContractPhase.DangInKy,
            "Cung cấp xi măng + sắt thép Q2/2026", 1_200_000_000m,
            "Đợt 1: 200 tấn xi măng + 50 tấn thép.");
        await addNccDetail(c3.Id, 1, "XM-PCB40", "Xi măng PCB40 50kg", "tan", 200, 1_800_000);
        await addNccDetail(c3.Id, 2, "THEP-D14", "Thép cây phi 14", "kg", 30000, 18_500);
        await addNccDetail(c3.Id, 3, "THEP-D18", "Thép cây phi 18", "kg", 20000, 18_800);

        // 4. HĐ Dịch vụ — DangTrinhKy (đang trình ký BOD)
        var c4 = await createDemoAsync(
            ContractType.HopDongDichVu, ContractPhase.DangTrinhKy,
            "Thuê cẩu tháp 6 tháng — FLOCK 01", 540_000_000m,
            "Cẩu tháp Liebherr — Q2-Q3/2026.");
        await addDichVuDetail(c4.Id, 1, "VC-CAN-TRUC", "Cẩu tháp Liebherr 320 EC-H", "thang", 6, 90_000_000);

        // 5. HĐ Mua bán — DaPhatHanh (đã phát hành — full flow)
        var c5 = await createDemoAsync(
            ContractType.HopDongMuaBan, ContractPhase.DaPhatHanh,
            "Mua máy phát điện 250kVA", 850_000_000m,
            "1 máy phát điện chính + 1 dự phòng cho công trường.");
        await addMuaBanDetail(c5.Id, 1, "MP-250KVA", "Máy phát điện Cummins 250kVA", "cai", 1, 720_000_000, 10);
        await addMuaBanDetail(c5.Id, 2, "MP-100KVA", "Máy phát điện dự phòng 100kVA", "cai", 1, 50_000_000, 10);

        // 6. HĐ Nguyên tắc NCC — DangGopY (framework agreement)
        var c6 = await createDemoAsync(
            ContractType.HopDongNguyenTacNCC, ContractPhase.DangGopY,
            "HĐ nguyên tắc cung cấp vật tư xây dựng 2026", 0m,
            "Khung giá tham chiếu — đặt mua qua PO theo từng đợt.");
        db.NguyenTacNccDetails.Add(new Domain.Contracts.Details.NguyenTacNccDetail
        {
            ContractId = c6.Id, Order = 1, NhomSP = "Xi măng",
            TenSP = "Xi măng PCB40 50kg", DonViTinh = "tan",
            DonGiaToiThieu = 1_700_000, DonGiaToiDa = 1_900_000,
            DieuKienThanhToan = "Net 30 — chuyển khoản",
        });
        db.NguyenTacNccDetails.Add(new Domain.Contracts.Details.NguyenTacNccDetail
        {
            ContractId = c6.Id, Order = 2, NhomSP = "Sắt thép",
            TenSP = "Thép cây phi 10-32", DonViTinh = "kg",
            DonGiaToiThieu = 17_500, DonGiaToiDa = 19_500,
            DieuKienThanhToan = "Net 30 — chuyển khoản",
        });

        // 7. HĐ Nguyên tắc Dịch vụ — DangSoanThao
        var c7 = await createDemoAsync(
            ContractType.HopDongNguyenTacDichVu, ContractPhase.DangSoanThao,
            "HĐ nguyên tắc bảo trì thiết bị 2026", 0m,
            "Khung dịch vụ bảo trì máy móc — gọi theo nhu cầu.");
        db.NguyenTacDvDetails.Add(new Domain.Contracts.Details.NguyenTacDvDetail
        {
            ContractId = c7.Id, Order = 1, LoaiDichVu = "Bảo trì",
            TenDichVu = "Bảo trì máy phát điện", DonViTinh = "lan",
            DonGiaToiThieu = 5_000_000, DonGiaToiDa = 8_000_000,
            SLA = "Phản hồi 24h, hoàn thành 72h",
        });

        // Comments demo cho HĐ #2 (DangGopY)
        if (ccmTran is not null)
        {
            db.ContractComments.Add(new ContractComment
            {
                ContractId = c2.Id, UserId = ccmTran.Id, Phase = ContractPhase.DangGopY,
                Content = "[DEMO] CCM góp ý: Đề nghị bổ sung điều khoản phạt chậm tiến độ + bảo hành 12 tháng.",
            });
        }
        if (qsHoang is not null)
        {
            db.ContractComments.Add(new ContractComment
            {
                ContractId = c2.Id, UserId = qsHoang.Id, Phase = ContractPhase.DangGopY,
                Content = "[DEMO] Drafter phản hồi: Đã ghi nhận, sẽ cập nhật bản v2.",
            });
        }

        await db.SaveChangesAsync();
        logger.LogInformation("Seed 7 demo contracts: {C1} {C2} {C3} {C4} {C5} {C6} {C7}",
            c1.MaHopDong, c2.MaHopDong, c3.MaHopDong, c4.MaHopDong, c5.MaHopDong, c6.MaHopDong, c7.MaHopDong);
    }

    // Seed 4 demo PE phiếu — varied Type (A/B) × Phase (DangSoanThao / mid /
    // ChoCEODuyetNCC / DaDuyet). Idempotent: skip nếu MaPhieu prefix `[DEMO]`
    // đã tồn tại (KHÔNG backfill — vì Quotes/Approvals nested khó merge).
    //
    // Mỗi phiếu có:
    //   - 2-3 NCC tham gia thầu (Suppliers)
    //   - 2-4 hạng mục (Details — group A.I/A.II/A.III)
    //   - N×M Quotes (báo giá per supplier × hạng mục)
    //   - Approval history theo phase đã đi qua
    //   - Phiếu DaDuyet có SelectedSupplierId (winner)
    //
    // MaPhieu format tạm hardcode `[DEMO]-A-001` etc. — sau này MaPhieu atomic
    // sequence (migration 13) sẽ replace.
    private static async Task SeedDemoPurchaseEvaluationsAsync(
        ApplicationDbContext db, UserManager<User> userManager, ILogger logger)
    {
        // Skip nếu đã có demo PE
        if (await db.PurchaseEvaluations.AnyAsync(p => p.MaPhieu != null && p.MaPhieu!.StartsWith("[DEMO]")))
        {
            logger.LogInformation("SeedDemoPurchaseEvaluationsAsync: skip — đã có [DEMO] PE.");
            return;
        }

        // Lookup masters
        var suppliersByCode = await db.Suppliers.ToDictionaryAsync(s => s.Code, s => s);
        var projectsByCode = await db.Projects.ToDictionaryAsync(p => p.Code, p => p);
        if (suppliersByCode.Count < 3 || projectsByCode.Count == 0)
        {
            logger.LogWarning("SeedDemoPurchaseEvaluationsAsync: skip — thiếu Supplier/Project master.");
            return;
        }

        // Lookup actor users (per role)
        var qsHoang = await userManager.FindByEmailAsync("qs.hoang@solutions.com.vn");
        var proPham = await userManager.FindByEmailAsync("pro.pham@solutions.com.vn");
        var ccmTran = await userManager.FindByEmailAsync("ccm.tran@solutions.com.vn");
        var pmNguyen = await userManager.FindByEmailAsync("pm.nguyen@solutions.com.vn");
        var bodHuynh = await userManager.FindByEmailAsync("bod.huynh@solutions.com.vn");
        var qsDeptId = (await db.Departments.FirstOrDefaultAsync(d => d.Code == "QS"))?.Id;
        var nowUtc = DateTime.UtcNow;

        // Helper: build PE phiếu với Suppliers + Details + Quotes + Approvals
        async Task<PurchaseEvaluation> createDemoPeAsync(
            string maPhieu,
            PurchaseEvaluationType type,
            PurchaseEvaluationPhase finalPhase,
            string tenGoiThau,
            string projectCode,
            string? diaDiem,
            string? moTa,
            string[] supplierCodes,         // 2-3 supplier codes
            (string GroupCode, string GroupName, string ItemCode, string NoiDung, string Dvt, decimal KlNs, decimal KlTc, decimal DgNs)[] detailRows,
            // Quotes: index by [detailIdx][supplierIdx] → (BgVat, ChuaVat, IsSelected)
            (decimal BgVat, decimal ChuaVat, bool IsSelected)[][] quotesGrid,
            string? winnerSupplierCode = null,
            string? paymentTermsJson = null)
        {
            var activeWfId = await db.PurchaseEvaluationWorkflowDefinitions.AsNoTracking()
                .Where(w => w.EvaluationType == type && w.IsActive)
                .Select(w => (Guid?)w.Id)
                .FirstOrDefaultAsync();

            var project = projectsByCode.TryGetValue(projectCode, out var p) ? p : projectsByCode.Values.First();

            var pe = new PurchaseEvaluation
            {
                MaPhieu = $"[DEMO]-{maPhieu}",
                Type = type,
                Phase = PurchaseEvaluationPhase.DangSoanThao,   // tạo ở phase 1, transition sau
                TenGoiThau = $"[DEMO] {tenGoiThau}",
                ProjectId = project.Id,
                DepartmentId = qsDeptId,
                DrafterUserId = qsHoang?.Id,
                DiaDiem = diaDiem,
                MoTa = moTa,
                WorkflowDefinitionId = activeWfId,
                SlaDeadline = nowUtc.AddDays(7),
                PaymentTerms = paymentTermsJson,
            };
            db.PurchaseEvaluations.Add(pe);
            await db.SaveChangesAsync();

            // Suppliers
            var supplierEntities = new List<PurchaseEvaluationSupplier>();
            for (int i = 0; i < supplierCodes.Length; i++)
            {
                var sCode = supplierCodes[i];
                if (!suppliersByCode.TryGetValue(sCode, out var supplier)) continue;
                var pes = new PurchaseEvaluationSupplier
                {
                    PurchaseEvaluationId = pe.Id,
                    SupplierId = supplier.Id,
                    DisplayName = supplier.Name,
                    ContactName = supplier.ContactPerson,
                    ContactEmail = supplier.Email,
                    ContactPhone = supplier.Phone,
                    PaymentTermText = i == 0 ? "TGN-30 ngày" : i == 1 ? "TGN-45 ngày" : "Tiến độ",
                    Note = i == 0 ? "ĐÃ CHỐT SO SÁNH LẦN 1" : null,
                    Order = i + 1,
                };
                db.PurchaseEvaluationSuppliers.Add(pes);
                supplierEntities.Add(pes);
            }
            await db.SaveChangesAsync();

            // Details
            var detailEntities = new List<PurchaseEvaluationDetail>();
            for (int i = 0; i < detailRows.Length; i++)
            {
                var (gc, gn, ic, nd, dvt, klNs, klTc, dgNs) = detailRows[i];
                var det = new PurchaseEvaluationDetail
                {
                    PurchaseEvaluationId = pe.Id,
                    GroupCode = gc, GroupName = gn,
                    ItemCode = ic, NoiDung = nd, DonViTinh = dvt,
                    KhoiLuongNganSach = klNs,
                    KhoiLuongThiCong = klTc,
                    DonGiaNganSach = dgNs,
                    ThanhTienNganSach = klNs * dgNs,
                    Order = i + 1,
                };
                db.PurchaseEvaluationDetails.Add(det);
                detailEntities.Add(det);
            }
            await db.SaveChangesAsync();

            // Quotes: detailIdx × supplierIdx
            for (int di = 0; di < detailEntities.Count; di++)
            {
                if (di >= quotesGrid.Length) break;
                var row = quotesGrid[di];
                for (int si = 0; si < supplierEntities.Count; si++)
                {
                    if (si >= row.Length) break;
                    var (bgVat, chuaVat, isSelected) = row[si];
                    db.PurchaseEvaluationQuotes.Add(new PurchaseEvaluationQuote
                    {
                        PurchaseEvaluationDetailId = detailEntities[di].Id,
                        PurchaseEvaluationSupplierId = supplierEntities[si].Id,
                        BgVat = bgVat,
                        ChuaVat = chuaVat,
                        ThanhTien = detailEntities[di].KhoiLuongNganSach * chuaVat,
                        IsSelected = isSelected,
                    });
                }
            }
            await db.SaveChangesAsync();

            // [multi-NCC 2026-07-12 — A2(c)] Derive IsWinner mỗi supplier-row từ grid IsSelected
            // (INVARIANT: supplier.IsWinner == đơn vị có ÍT NHẤT 1 quote IsSelected). Đặt SAU khi
            // tạo quotes để seed đồng bộ nguồn-sự-thật (Quote.IsSelected) với derived flag.
            for (int si = 0; si < supplierEntities.Count; si++)
            {
                bool anySelected = false;
                for (int di = 0; di < detailEntities.Count && di < quotesGrid.Length; di++)
                {
                    var gridRow = quotesGrid[di];
                    if (si < gridRow.Length && gridRow[si].IsSelected) { anySelected = true; break; }
                }
                supplierEntities[si].IsWinner = anySelected;
            }
            await db.SaveChangesAsync();

            // Approval history theo flow của type
            PurchaseEvaluationPhase[] flow = type == PurchaseEvaluationType.DuyetNcc
                ? [PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.ChoCCM,
                   PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.DaDuyet]
                : [PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.ChoDuAn,
                   PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.ChoCEODuyetPA,
                   PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.DaDuyet];

            var current = PurchaseEvaluationPhase.DangSoanThao;
            foreach (var next in flow)
            {
                if (next > finalPhase && finalPhase != PurchaseEvaluationPhase.DaDuyet) break;
                if (next > finalPhase) break;
                Guid? actorId = next switch
                {
                    PurchaseEvaluationPhase.ChoPurchasing => qsHoang?.Id,    // Drafter submit
                    PurchaseEvaluationPhase.ChoDuAn => proPham?.Id,           // Procurement chuyển sang
                    PurchaseEvaluationPhase.ChoCCM => type == PurchaseEvaluationType.DuyetNcc
                        ? proPham?.Id                                          // A: Procurement
                        : pmNguyen?.Id,                                        // B: PM
                    PurchaseEvaluationPhase.ChoCEODuyetPA => ccmTran?.Id,    // CCM
                    PurchaseEvaluationPhase.ChoCEODuyetNCC => type == PurchaseEvaluationType.DuyetNcc
                        ? ccmTran?.Id                                          // A: CCM
                        : bodHuynh?.Id,                                        // B: CEO duyệt PA xong
                    PurchaseEvaluationPhase.DaDuyet => bodHuynh?.Id,         // CEO chốt
                    _ => qsHoang?.Id,
                };
                db.PurchaseEvaluationApprovals.Add(new PurchaseEvaluationApproval
                {
                    PurchaseEvaluationId = pe.Id,
                    FromPhase = current,
                    ToPhase = next,
                    ApproverUserId = actorId,
                    Decision = ApprovalDecision.Approve,
                    Comment = next switch
                    {
                        PurchaseEvaluationPhase.ChoPurchasing => "Demo seed — Drafter submit phiếu",
                        PurchaseEvaluationPhase.ChoCCM => "Demo seed — chuyển CCM kiểm tra ngân sách",
                        PurchaseEvaluationPhase.DaDuyet => "Demo seed — CEO duyệt chọn NCC",
                        _ => null,
                    },
                    ApprovedAt = nowUtc.AddMinutes(-(flow.Length - Array.IndexOf(flow, next)) * 30),
                });
                current = next;
            }
            pe.Phase = current;

            // [S133] Demo phiếu DaDuyet cũng phải có snapshot NS: fresh-DB migrate chạy TRƯỚC seed
            // → backfill Mig 67 no-op trên bảng rỗng; thiếu block này demo DaDuyet hiển thị live-drift
            // (reviewer S133 catch — mirror helper ApplyBudgetSnapshotOnFinalizeAsync, null-safe).
            if (current == PurchaseEvaluationPhase.DaDuyet)
            {
                var pairDemo = pe.WorkItemId is Guid wiDemo
                    ? await db.PeWorkItemBudgets
                        .FirstOrDefaultAsync(b => b.ProjectId == pe.ProjectId && b.WorkItemId == wiDemo)
                    : null;
                pe.ApprovedBudgetProInitialAmount = pairDemo?.ProInitialAmount;
                pe.ApprovedBudgetProAdjustmentAmount = pairDemo?.ProAdjustmentAmount;
                pe.ApprovedBudgetProNote = pairDemo?.ProNote;
                pe.ApprovedBudgetCcmInitialAmount = pairDemo?.InitialAmount;
                pe.ApprovedBudgetCcmAdjustmentAmount = pairDemo?.AdjustmentAmount;
                pe.ApprovedBudgetCcmNote = pairDemo?.CcmNote;
                pe.ApprovedBudgetSnapshotAt = nowUtc;
            }

            // Set winner nếu DaDuyet
            if (current == PurchaseEvaluationPhase.DaDuyet
                && winnerSupplierCode is not null
                && suppliersByCode.TryGetValue(winnerSupplierCode, out var winner))
            {
                pe.SelectedSupplierId = winner.Id;
            }

            return pe;
        }

        // ===========================================================
        // 1. Phiếu A — DangSoanThao (Drafter còn đang soạn)
        // ===========================================================
        var pe1 = await createDemoPeAsync(
            maPhieu: "A-001",
            type: PurchaseEvaluationType.DuyetNcc,
            finalPhase: PurchaseEvaluationPhase.DangSoanThao,
            tenGoiThau: "Cung cấp xi măng + sắt thép Q3/2026 — FLOCK 01",
            projectCode: "FLOCK01",
            diaDiem: "Lô K, KCN Lộc An",
            moTa: "Đợt 1: 200 tấn xi măng + 30 tấn thép cho phần thân.",
            supplierCodes: ["NCC-XIMANG", "NCC-THEP", "NCC-DIEN"],
            detailRows:
            [
                ("A.I", "Xi măng", "XM-001", "Xi măng PCB40 50kg", "tan", 200, 200, 1_800_000),
                ("A.I", "Xi măng", "XM-002", "Xi măng PCB30 50kg", "tan", 50, 50, 1_650_000),
                ("A.II", "Sắt thép", "TH-014", "Thép cây phi 14", "kg", 15000, 15000, 18_500),
                ("A.II", "Sắt thép", "TH-018", "Thép cây phi 18", "kg", 15000, 15000, 18_800),
            ],
            quotesGrid:
            [
                // Hạng 1 (xi măng PCB40) — chưa nhập (Drafter mới mở phiếu)
                [(0, 0, false), (0, 0, false), (0, 0, false)],
                [(0, 0, false), (0, 0, false), (0, 0, false)],
                [(0, 0, false), (0, 0, false), (0, 0, false)],
                [(0, 0, false), (0, 0, false), (0, 0, false)],
            ]);

        // ===========================================================
        // 2. Phiếu A — ChoCEODuyetNCC (sắp duyệt — đã có quotes + winner đề xuất)
        // ===========================================================
        var pe2 = await createDemoPeAsync(
            maPhieu: "A-002",
            type: PurchaseEvaluationType.DuyetNcc,
            finalPhase: PurchaseEvaluationPhase.ChoCEODuyetNCC,
            tenGoiThau: "Cung cấp gạch xây + cát đá Q2/2026 — Vinhomes Ocean Park",
            projectCode: "VHOMES-OP",
            diaDiem: "Vinhomes Ocean Park, Gia Lâm, HN",
            moTa: "Vật tư xây dựng đợt 2 cho block A1-A5.",
            supplierCodes: ["NCC-XIMANG", "NCC-THEP", "NCC-DIEN"],
            detailRows:
            [
                ("A.I", "Gạch", "GACH-001", "Gạch xây 4 lỗ 8x8x18", "vien", 50000, 50000, 1_500),
                ("A.II", "Cát đá", "CAT-001", "Cát vàng đổ bê tông", "m3", 200, 200, 350_000),
                ("A.II", "Cát đá", "DA-001", "Đá 1x2", "m3", 150, 150, 280_000),
            ],
            quotesGrid:
            [
                // Hạng 1: Gạch — supplier 1 thắng (báo giá thấp + cam kết)
                [(1_700, 1_550, true), (1_750, 1_590, false), (1_780, 1_618, false)],
                // Hạng 2: Cát vàng — supplier 1 thắng
                [(385_000, 350_000, true), (390_000, 354_000, false), (395_000, 359_000, false)],
                // Hạng 3: Đá 1x2 — supplier 1 thắng
                [(308_000, 280_000, true), (315_000, 286_000, false), (320_000, 290_000, false)],
            ]);

        // ===========================================================
        // 3. Phiếu A — DaDuyet (đã duyệt — chưa tạo HĐ — demo "Tạo HĐ từ phiếu")
        // ===========================================================
        var pe3 = await createDemoPeAsync(
            maPhieu: "A-003",
            type: PurchaseEvaluationType.DuyetNcc,
            finalPhase: PurchaseEvaluationPhase.DaDuyet,
            tenGoiThau: "Cung cấp ống PVC + phụ kiện cấp thoát nước — Resort Phú Quốc",
            projectCode: "RESORT-PQ",
            diaDiem: "Bãi Trường, Phú Quốc, Kiên Giang",
            moTa: "Hệ thống cấp thoát nước villa + biệt thự ven biển.",
            supplierCodes: ["NCC-XIMANG", "NCC-THEP"],
            detailRows:
            [
                ("A.I", "Ống nhựa", "ONG-090", "Ống PVC phi 90", "m", 2000, 2000, 65_000),
                ("A.I", "Ống nhựa", "ONG-110", "Ống PVC phi 110", "m", 1500, 1500, 95_000),
            ],
            quotesGrid:
            [
                [(71_500, 65_000, true), (72_600, 66_000, false)],
                [(104_500, 95_000, true), (106_700, 97_000, false)],
            ],
            winnerSupplierCode: "NCC-XIMANG",
            paymentTermsJson: """{"tamUng":20,"thanhToanTam":70,"quyetToan":10,"baoHanh":12,"hanMucCongNo":500000000,"danhGia":"Đối tác chiến lược 3 năm — uy tín cao."}""");

        // ===========================================================
        // 4. Phiếu B — ChoDuAn (5-step giữa chừng — Procurement đã chuyển)
        // ===========================================================
        var pe4 = await createDemoPeAsync(
            maPhieu: "B-001",
            type: PurchaseEvaluationType.DuyetNccPhuongAn,
            finalPhase: PurchaseEvaluationPhase.ChoDuAn,
            tenGoiThau: "Cung cấp + lắp đặt cẩu tháp 6 tháng — FLOCK 01",
            projectCode: "FLOCK01",
            diaDiem: "Lô K, KCN Lộc An",
            moTa: "Phương án A: thuê cẩu Liebherr 320 EC-H. Phương án B: thuê cẩu Potain MD 365.",
            supplierCodes: ["DV-VANCHUYEN", "DV-CLEAN", "NTP-XD"],
            detailRows:
            [
                ("A.I", "Cẩu tháp", "CT-001", "Cẩu tháp Liebherr 320 EC-H — phương án A", "thang", 6, 6, 90_000_000),
                ("A.II", "Vận chuyển", "VC-001", "Vận chuyển + lắp đặt cẩu tháp", "lan", 1, 1, 80_000_000),
                ("A.III", "Bảo trì", "BT-001", "Bảo trì cẩu tháp định kỳ", "lan", 6, 6, 5_000_000),
            ],
            quotesGrid:
            [
                [(99_000_000, 90_000_000, false), (104_500_000, 95_000_000, false), (96_800_000, 88_000_000, false)],
                [(88_000_000, 80_000_000, false), (93_500_000, 85_000_000, false), (82_500_000, 75_000_000, false)],
                [(5_500_000, 5_000_000, false), (5_775_000, 5_250_000, false), (5_280_000, 4_800_000, false)],
            ]);

        await db.SaveChangesAsync();
        logger.LogInformation("Seed 4 demo PE phiếu: {P1} {P2} {P3} {P4}",
            pe1.MaPhieu, pe2.MaPhieu, pe3.MaPhieu, pe4.MaPhieu);
    }

    // Phase 5.1 security: log warning nếu admin vẫn dùng password mặc định sau deploy production.
    private static async Task WarnDefaultAdminPasswordAsync(UserManager<User> userManager, ILogger logger)
    {
        var admin = await userManager.FindByEmailAsync(AdminEmail);
        if (admin is null) return;
        if (await userManager.CheckPasswordAsync(admin, AdminPassword))
        {
            logger.LogWarning(
                "⚠️  Admin user '{Email}' vẫn dùng password mặc định. ĐỔI NGAY trong production! " +
                "Xem docs/guides/security-checklist.md.", AdminEmail);
        }
    }

    // 12 role với ShortName (Vietnamese abbreviation) + Description (Vietnamese
    // full name). Identity Name = code English (kept cho FK + [Authorize] attr).
    // Idempotent: tạo role mới nếu chưa có, backfill ShortName/Description nếu null.
    private static readonly Dictionary<string, (string ShortName, string Description)> RoleLabels = new()
    {
        [AppRoles.Admin]            = ("QTV",   "Quản trị viên hệ thống"),
        [AppRoles.Drafter]          = ("NV.PB", "Nhân viên phòng ban (soạn thảo HĐ)"),
        [AppRoles.DeptManager]      = ("TPB",   "Trưởng phòng ban"),
        [AppRoles.ProjectManager]   = ("PM",    "Giám đốc dự án"),
        [AppRoles.Procurement]      = ("PRO",   "Phòng Cung ứng"),
        [AppRoles.CostControl]      = ("CCM",   "Phòng Kiểm soát chi phí"),
        [AppRoles.Finance]          = ("FIN",   "Phòng Tài chính"),
        [AppRoles.Accounting]       = ("ACT",   "Phòng Kế toán"),
        [AppRoles.Equipment]        = ("EQU",   "Phòng Thiết bị"),
        [AppRoles.Director]         = ("BOD",   "Ban Giám đốc"),
        [AppRoles.AuthorizedSigner] = ("NĐUQ",  "Người được Ủy quyền ký HĐ"),
        [AppRoles.HrAdmin]          = ("HRA",   "Phòng Nhân sự - Hành chính"),
        [AppRoles.CatalogManager]   = ("DM",    "Nhân viên Quản lý danh mục"),
    };

    private static async Task SeedRolesAsync(RoleManager<Role> roleManager, ILogger logger)
    {
        foreach (var roleName in AppRoles.All)
        {
            var (shortName, desc) = RoleLabels.TryGetValue(roleName, out var v)
                ? v : (roleName, roleName);

            var existing = await roleManager.FindByNameAsync(roleName);
            if (existing is null)
            {
                await roleManager.CreateAsync(new Role
                {
                    Name = roleName,
                    ShortName = shortName,
                    Description = desc,
                    CreatedAt = DateTime.UtcNow,
                });
                logger.LogInformation("Created role {Role} ({Short})", roleName, shortName);
            }
            else if (existing.ShortName != shortName || existing.Description != desc)
            {
                // Backfill labels for existing roles (post-migration)
                existing.ShortName = shortName;
                existing.Description = desc;
                await roleManager.UpdateAsync(existing);
                logger.LogInformation("Backfilled role {Role} labels", roleName);
            }
        }
    }

    private static async Task SeedAdminAsync(UserManager<User> userManager, ILogger logger)
    {
        var admin = await userManager.FindByEmailAsync(AdminEmail);
        if (admin is null)
        {
            admin = new User
            {
                UserName = AdminEmail,
                Email = AdminEmail,
                FullName = "Administrator",
                EmailConfirmed = true,
                IsActive = true,
                CreatedAt = DateTime.UtcNow,
            };
            var result = await userManager.CreateAsync(admin, AdminPassword);
            if (!result.Succeeded)
            {
                logger.LogError("Failed to seed admin: {Errors}", string.Join("; ", result.Errors.Select(e => e.Description)));
                return;
            }
            await userManager.AddToRoleAsync(admin, AppRoles.Admin);
            logger.LogInformation("Seeded admin user {Email}", AdminEmail);
        }
    }

    // 16 demo users covering full org chart — RECONCILE PATTERN (idempotent +
    // self-healing). Mỗi run:
    //   - Nếu user chưa có → create + assign roles + dept + position
    //   - Nếu user đã có → verify dept + position + roles, fix nếu drift
    //   - Try-catch per-user → 1 fail không abort 12 còn lại
    //   - Log chi tiết success/fixed/created/failed counts
    // Default password: User@1234567 (warn log để rotate prod).
    // [S58 2026-06-11] 11→12 ký tự: prod set Identity:Password:RequiredLength=12
    // (appsettings.Production.json) → CreateAsync silent-fail MỌI user dùng const này
    // trên prod từ trước tới giờ (named-person sample + nv.cao/nv.truong IT pool +
    // 5 real staff — root cause "helpdesk inert" S56). User Dev cũ giữ pass cũ
    // (const chỉ áp dụng lúc CREATE user mới, không đổi pass existing).
    private const string DemoUserPassword = "User@1234567";

    // [S57bis 2026-06-11] Khóa demo sample user — sếp yêu cầu "clear dữ liệu cũ",
    // anh chốt scope: CHỈ khóa user demo (GIỮ phiếu/HĐ/NCC/dự án demo). Ungated idempotent
    // (mirror SeedRealMasterDataAsync philosophy): chạy mọi startup → khóa BỀN kể cả ai
    // re-activate nhầm. Email không tồn tại trong DB → skip vô hại.
    // [S58 2026-06-11] +20 email UAT-matrix `{dept}.{nv|pp|tp}@` + `bod.{1,2}@` = population
    // prod THẬT (tạo TAY qua admin UI 2026-05-13 — chưa từng nằm trong seed). 14 email
    // named-person = population Dev (prod không có — DemoUserPassword cũ 11 ký tự fail
    // RequiredLength=12, đã fix). KHÔNG khóa theo pattern/regex: binh.le@/binh.lethanh@
    // là người thật nằm sát scheme demo — CHỈ exact-email list.
    // GIỮ ACTIVE có chủ đích (KHÔNG đưa vào list):
    //   - nv.cao + nv.truong : IT helpdesk round-robin pool (S52 P11-D) — khóa nốt SAU KHI
    //     anh gán ≥1 user thật vào Phòng CNTT (ops-pending S56), tránh helpdesk chết hẳn.
    //   - catalog.manager    : account chức năng quản danh mục dùng chung (Plan CA S29).
    //   - nv.test            : creds cicd-monitor smoke-verify prod sau deploy (anh chốt S58).
    //   - chuong.phan@solution.com.vn (domain thiếu 's', không trong seed): typo-dup của bản
    //     @solutions nhưng có thể là login thật anh Chương đang dùng — giữ, xác nhận rồi dọn sau.
    // Muốn mở lại 1 user có chủ đích → gỡ email khỏi list này + admin re-activate.
    // [S89 2026-06-25 — anh Kiệt FDC] LockDemoSampleUsersAsync ĐÃ XÓA: 16 user mẫu + ~21 user
    // UAT-matrix được XÓA HẲN khỏi prod (scripts/s89-delete-test-users.sql, backup trước) + gỡ
    // khỏi SeedDemoUsersAsync. S57bis "khóa demo sample" thay bằng "xóa thật" — chỉ còn 17 user
    // Solutions thật (admin + 16 real). Không còn user test để khóa.

    private static async Task SeedDemoUsersAsync(
        ApplicationDbContext db, UserManager<User> userManager, ILogger logger)
    {
        // Lookup department Id theo Code
        var depts = await db.Departments.ToDictionaryAsync(d => d.Code, d => d.Id);
        if (depts.Count == 0)
        {
            logger.LogWarning("SeedDemoUsersAsync: skip — no departments seeded yet (run SeedDepartmentsAsync first).");
            return;
        }

        Guid? deptId(string code) => depts.TryGetValue(code, out var id) ? id : null;

        var demoUsers = new[]
        {
            // (Email, FullName, DeptCode, Position, RoleNames[])
            // [S89 2026-06-25 — anh Kiệt FDC chốt] ĐÃ GỠ 16 user MẪU TEST (tên giả: bod.huynh/le/
            // tran · pm.nguyen/le · ccm.tran · pro.pham · fin.do · act.vu · equ.bui · hra.dang ·
            // qs.hoang/ngo · nv.cao/dinh/truong) — chỉ giữ user Solutions THẬT bên dưới. Đã xóa
            // khỏi prod qua scripts/s89-delete-test-users.sql (kèm ~21 user UAT-matrix tạo tay).
            // LockDemoSampleUsersAsync cũng đã gỡ (không còn user test để khóa).

            // ===== Solutions company users (real names — added 2026-04-25) =====
            // PRO — Phòng Cung ứng (5 user)
            ("tra.bui@solutions.com.vn",        "Bùi Lê Thủy Trà",         "PRO", "TP Cung ứng",                        new[] { AppRoles.Procurement, AppRoles.DeptManager }),
            ("phuong.nguyen@solutions.com.vn",  "Nguyễn Thị Bích Phượng",  "PRO", "Nhân viên Cung ứng",                 new[] { AppRoles.Procurement, AppRoles.Drafter }),
            ("thanh.lethanh@solutions.com.vn",  "Lê Thanh Bình",           "PRO", "Nhân viên Cung ứng",                 new[] { AppRoles.Procurement, AppRoles.Drafter }),
            ("long.chau@solutions.com.vn",      "Châu Tạ Kim Long",        "PRO", "Nhân viên Cung ứng",                 new[] { AppRoles.Procurement, AppRoles.Drafter }),
            ("duy.nguyen@solutions.com.vn",     "Nguyễn Văn Duy",          "PRO", "Nhân viên Cung ứng",                 new[] { AppRoles.Procurement, AppRoles.Drafter }),

            // CCM — Phòng Kiểm soát Chi phí (7 user)
            ("chuong.phan@solutions.com.vn",    "Phan Văn Chương",         "CCM", "TP Kiểm soát Chi phí",               new[] { AppRoles.CostControl, AppRoles.DeptManager }),
            ("binh.le@solutions.com.vn",        "Lê Văn Bình",             "CCM", "Nhân viên Kiểm soát Chi phí",        new[] { AppRoles.CostControl }),
            ("luu.tran@solutions.com.vn",       "Trần Xuân Lưu",           "CCM", "Nhân viên Kiểm soát Chi phí",        new[] { AppRoles.CostControl }),
            ("nguyen.ho@solutions.com.vn",      "Hồ Thị Nữ Nguyên",        "CCM", "Nhân viên Kiểm soát Chi phí",        new[] { AppRoles.CostControl }),
            ("anh.nguyen@solutions.com.vn",     "Nguyễn Thị Kim Ánh",      "CCM", "Nhân viên Kiểm soát Chi phí",        new[] { AppRoles.CostControl }),
            ("tring.le@solutions.com.vn",       "Lê Tự Đăng Trình",        "CCM", "Nhân viên Kiểm soát Chi phí",        new[] { AppRoles.CostControl }),
            ("truong.le@solutions.com.vn",      "Lê Trần Đăng Trường",     "CCM", "Nhân viên Kiểm soát Chi phí",        new[] { AppRoles.CostControl }),

            // ISO — đóng dấu HĐ (1 user, dept HRA)
            ("long.nguyen@solutions.com.vn",    "Nguyễn Hoàng Chánh Long", "HRA", "ISO — Đóng dấu HĐ",                   new[] { AppRoles.HrAdmin }),

            // CEO — Tổng Giám đốc (1 user, dept BOD)
            ("truong.nguyen@solutions.com.vn",  "Nguyễn Văn Trường",       "BOD", "Tổng Giám đốc (CEO)",                 new[] { AppRoles.Director }),

            // [Plan CA S29 2026-05-22] Demo user role CatalogManager — quản lý
            // danh mục dùng chung (Suppliers/Projects/Departments/Catalogs)
            // ở fe-user/eoffice. Admin có thể tạo thêm user gán role này qua
            // /system/users + /system/permissions. Dept PRO (Cung ứng) — vai trò
            // thường xuyên cập nhật NCC/vật tư/dịch vụ.
            ("catalog.manager@solutions.com.vn", "NV Quản lý Danh mục",     "PRO", "Nhân viên Quản lý Danh mục Dùng chung", new[] { AppRoles.CatalogManager }),
        };

        int created = 0, fixedExisting = 0, failed = 0;
        foreach (var (email, fullName, deptCode, position, roles) in demoUsers)
        {
            try
            {
                var existing = await userManager.FindByEmailAsync(email);
                if (existing is null)
                {
                    var user = new User
                    {
                        UserName = email,
                        Email = email,
                        FullName = fullName,
                        EmailConfirmed = true,
                        IsActive = true,
                        DepartmentId = deptId(deptCode),
                        Position = position,
                        CreatedAt = DateTime.UtcNow,
                    };
                    // [Plan CA S29 2026-05-22] CatalogManager role mới — password
                    // ≥12 chars per policy S22+2 (existing 30 demo user dùng
                    // DemoUserPassword 11 chars seed pre-S22+2 nên alive). User
                    // mới phải tuân policy mới hoặc CreateAsync fail trên prod.
                    var pwd = roles.Contains(AppRoles.CatalogManager) ? "CatalogMgr@2026" : DemoUserPassword;
                    var result = await userManager.CreateAsync(user, pwd);
                    if (!result.Succeeded)
                    {
                        logger.LogWarning("Demo user {Email} CREATE fail: {Err}",
                            email, string.Join("; ", result.Errors.Select(e => e.Description)));
                        failed++;
                        continue;
                    }
                    foreach (var role in roles)
                    {
                        var roleResult = await userManager.AddToRoleAsync(user, role);
                        if (!roleResult.Succeeded)
                            logger.LogWarning("Demo user {Email} AddRole {Role} fail: {Err}",
                                email, role, string.Join("; ", roleResult.Errors.Select(e => e.Description)));
                    }
                    created++;
                }
                else
                {
                    // Reconcile — verify dept + position + roles, fix nếu drift
                    bool changed = false;
                    var targetDeptId = deptId(deptCode);
                    if (existing.DepartmentId != targetDeptId)
                    {
                        existing.DepartmentId = targetDeptId; changed = true;
                    }
                    if (existing.Position != position)
                    {
                        existing.Position = position; changed = true;
                    }
                    if (existing.FullName != fullName)
                    {
                        existing.FullName = fullName; changed = true;
                    }
                    if (!existing.IsActive)
                    {
                        existing.IsActive = true; changed = true;
                    }
                    if (changed)
                    {
                        existing.UpdatedAt = DateTime.UtcNow;
                        await userManager.UpdateAsync(existing);
                    }

                    // Reconcile roles: ensure target roles có trong assigned set
                    var currentRoles = await userManager.GetRolesAsync(existing);
                    var missingRoles = roles.Where(r => !currentRoles.Contains(r)).ToList();
                    foreach (var role in missingRoles)
                    {
                        var addResult = await userManager.AddToRoleAsync(existing, role);
                        if (!addResult.Succeeded)
                            logger.LogWarning("Demo user {Email} AddRole {Role} fail: {Err}",
                                email, role, string.Join("; ", addResult.Errors.Select(e => e.Description)));
                        changed = true;
                    }

                    if (changed) fixedExisting++;
                }
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "SeedDemoUsersAsync: unhandled exception cho user {Email}", email);
                failed++;
            }
        }

        logger.LogInformation(
            "Demo users reconcile xong: {Created} created, {Fixed} fixed (drift), {Failed} failed. (Total demo: {Total}, password={Pwd} — ⚠ rotate prod)",
            created, fixedExisting, failed, demoUsers.Length, DemoUserPassword);
    }

    private static async Task SeedMenuTreeAsync(ApplicationDbContext db, ILogger logger)
    {
        // [S159 2026-07-29] UAT anh Kiệt: "cho đánh số thứ tự phía trước giúp anh — 01. HĐ thầu phụ"
        // ⇒ prefix 01.-07. theo THỨ TỰ hiển thị (= thứ tự ContractTypeCodes). Prod đã UPDATE
        // trực tiếp cùng ngày (prepend-only, 0 ký tự VN qua wire); labelBackfill dưới giữ
        // các env khác hội tụ (gotcha #11 — sửa tree.Add một mình là vô tác dụng trên DB đang chạy).
        var typeLabels = new Dictionary<string, string>
        {
            ["ThauPhu"] = "01. HĐ Thầu phụ",
            ["GiaoKhoan"] = "02. HĐ Giao khoán",
            ["NhaCungCap"] = "03. HĐ Nhà cung cấp",
            ["DichVu"] = "04. HĐ Dịch vụ",
            ["MuaBan"] = "05. HĐ Mua bán",
            ["NguyenTacNcc"] = "06. HĐ Nguyên tắc NCC",
            ["NguyenTacDv"] = "07. HĐ Nguyên tắc Dịch vụ",
        };

        var tree = new List<(string Key, string Label, string? Parent, int Order, string Icon)>
        {
            (MenuKeys.Dashboard,   "Tổng quan quy trình", null,          10, "LayoutDashboard"), // [S159-đợt5] owner: "đổi thành gì dễ hình dung? Tổng quan quy trình"
            // [S58 2026-06-11] "Danh mục" 20→80: anh yêu cầu đưa xuống CUỐI sidebar
            // (sau Reports 50, trước vùng System 90 admin). Main upsert tự re-set
            // Order trên DB cũ. Con (21-25) giữ nguyên — order chỉ so trong siblings.
            (MenuKeys.Master,      "Danh mục",         null,             80, "Database"),
            (MenuKeys.Suppliers,   "Nhà cung cấp",     MenuKeys.Master,  21, "Building2"),
            (MenuKeys.Projects,    "Dự án",            MenuKeys.Master,  22, "FolderKanban"),
            (MenuKeys.Departments, "Phòng ban",        MenuKeys.Master,  23, "Users"),
            // 4 master catalog cho Details add form (autocomplete suggestion)
            (MenuKeys.Catalogs,        "Danh mục chi tiết", MenuKeys.Master, 24, "Library"),
            (MenuKeys.CatalogUnits,    "Đơn vị tính",      MenuKeys.Catalogs, 1, "Ruler"),
            (MenuKeys.CatalogMaterials,"Vật tư / SP",      MenuKeys.Catalogs, 2, "Package"),
            (MenuKeys.CatalogServices, "Dịch vụ",          MenuKeys.Catalogs, 3, "Wrench"),
            (MenuKeys.CatalogWorkItems,"Hạng mục công việc", MenuKeys.Catalogs, 4, "ListChecks"),
            // [K1 Mig 70 — S164] Danh mục Hợp đồng SOL-CCM-SP-002 (86 dòng × 8 nhóm duyệt).
            // Leaf thứ 5 dưới nhóm "Danh mục chi tiết". Key CÓ trong MenuKeys.All ⇒ Admin
            // được grant tự động qua loop SeedAdminPermissionsAsync :2058.
            (MenuKeys.ContractCatalog, "Danh mục Hợp đồng", MenuKeys.Catalogs, 5, "FileSpreadsheet"),
            // [S57] Order 30→31: nhường slot 30 cho nhóm "Cá nhân" (đứng ngay sau Văn phòng số = 29, mirror Puro).
            (MenuKeys.Contracts,   "Duyệt hợp đồng",   null,             31, "FileText"), // [S159] rename theo 4-GĐ (owner tab strip)
            // [S159] 2 root PLACEHOLDER GĐ2/GĐ4 — menu = 4 giai đoạn (owner). FE staticMap trỏ
            // /dashboard. Prod đã INSERT trực tiếp cùng ngày (idempotent ở đây).
            // [S161 W1 — F-3] `KeHoachKyKet` nay ĐÃ VÀO MenuKeys.All (policy sống); `HopDongCung` vẫn ngoài.
            (MenuKeys.KeHoachKyKet, "Kế hoạch ký kết HĐ", null,   26, "FileSignature"),
            (MenuKeys.HopDongCung,  "Hợp đồng cứng",      null,   32, "Stamp"),
            // [S159-đợt3] skeleton menu GĐ2/GĐ4 (owner vẽ: "viết hoa + phân list như
            // Duyệt NCC" / "thêm Sub đánh số như Duyệt Hợp đồng"). Root CÓ CON ⇒ render
            // MenuGroup uppercase tự động. Leaves → /coming-soon (staticMap ×2 app).
            // [K4a — S166] Nhóm 1 trong 8 NHÓM DUYỆT (danh mục SP-002): nhãn skeleton S159
            // "1. Kế hoạch ký kết HĐ (NCC-TP)" → nhãn nhóm A1. Key `Khkk_G1` GIỮ NGUYÊN;
            // DB đang chạy hội tụ qua `labelBackfill` bên dưới (dòng này chỉ ăn với DB trống).
            // 7 nhóm còn lại (G2..G8) seed ở vòng lặp phía dưới (sau vòng `PeWf_*`).
            ("Khkk_G1",       "1. Thiết bị, máy móc, vật tư phụ (A1)", MenuKeys.KeHoachKyKet, 1, "FileCheck"),
            // [S159-đợt5 owner] đủ 6 leaf "y chang Duyệt NCC" (WfView đầu + Deleted cuối).
            ("Khkk_WfView",   "Luồng duyệt", "Khkk_G1", 1, "Network"),
            ("Khkk_List",     "Danh sách",   "Khkk_G1", 2, "List"),
            ("Khkk_Create",   "Thao tác",    "Khkk_G1", 3, "Plus"),
            ("Khkk_Pending",  "Đang duyệt",  "Khkk_G1", 4, "CheckCircle2"),
            ("Khkk_Approved", "Đã duyệt",    "Khkk_G1", 5, "CheckCheck"),
            ("Khkk_Deleted",  "Đã xóa",      "Khkk_G1", 6, "Trash2"),
            (MenuKeys.Forms,       "Biểu mẫu",         null,             40, "FileSpreadsheet"),
            (MenuKeys.Reports,     "Báo cáo",          null,             50, "BarChart3"),
            (MenuKeys.System,      "Hệ thống",         null,             90, "Settings"),
            (MenuKeys.Users,       "Người dùng",       MenuKeys.System,  91, "User"),
            (MenuKeys.Roles,       "Vai trò",          MenuKeys.System,  92, "Shield"),
            (MenuKeys.Permissions, "Phân quyền",       MenuKeys.System,  93, "KeyRound"),
            (MenuKeys.MenuVisibility, "Menu eOffice",  MenuKeys.System,  94, "Eye"),
            (MenuKeys.Workflows,   "Quy trình HĐ",     MenuKeys.System,  95, "GitBranch"),
            // Module Duyệt NCC (tiền-HĐ)
            (MenuKeys.PurchaseEvaluations, "Duyệt NCC", null, 25, "ClipboardCheck"), // [S159] rename theo 4-GĐ
            (MenuKeys.PeWorkflows,         "Quy trình Duyệt NCC",           MenuKeys.System,  95, "GitCompareArrows"),
            // Quy trình duyệt MỚI (Mig 22 — Session 17, 2026-05-08): schema riêng
            // UAT trước khi migrate hoàn toàn. Cấu trúc: Quy trình > Bước (Phòng) > Cấp (NV cụ thể).
            (MenuKeys.ApprovalWorkflowsV2,                "Quy trình duyệt (Mới)",        MenuKeys.System,             96, "Workflow"),
            (MenuKeys.ApprovalWorkflowDuyetNccV2,         "Duyệt NCC (Mới)",              MenuKeys.ApprovalWorkflowsV2, 1, "FileCheck"),
            (MenuKeys.ApprovalWorkflowDuyetNccPhuongAnV2, "Duyệt NCC và Giải pháp (Mới)", MenuKeys.ApprovalWorkflowsV2, 2, "FileCheck"),
            // [K3 nhịp-1 — S164/S165] 8 leaf Designer cho 8 nhóm duyệt KHKK (workflow
            // `KHKK-N{n}` type-10 seed ở SeedKhkkGroupWorkflowsAsync). Key CÓ trong
            // MenuKeys.All ⇒ Admin được grant tự động qua loop SeedAdminPermissionsAsync;
            // upsert dưới là idempotent per-Key (key đã có → chỉ re-set Order rồi continue).
            (MenuKeys.ApprovalWorkflowKhkkN1, "Duyệt KHKK — Nhóm 1", MenuKeys.ApprovalWorkflowsV2, 3, "FileSignature"),
            (MenuKeys.ApprovalWorkflowKhkkN2, "Duyệt KHKK — Nhóm 2", MenuKeys.ApprovalWorkflowsV2, 4, "FileSignature"),
            (MenuKeys.ApprovalWorkflowKhkkN3, "Duyệt KHKK — Nhóm 3", MenuKeys.ApprovalWorkflowsV2, 5, "FileSignature"),
            (MenuKeys.ApprovalWorkflowKhkkN4, "Duyệt KHKK — Nhóm 4", MenuKeys.ApprovalWorkflowsV2, 6, "FileSignature"),
            (MenuKeys.ApprovalWorkflowKhkkN5, "Duyệt KHKK — Nhóm 5", MenuKeys.ApprovalWorkflowsV2, 7, "FileSignature"),
            (MenuKeys.ApprovalWorkflowKhkkN6, "Duyệt KHKK — Nhóm 6", MenuKeys.ApprovalWorkflowsV2, 8, "FileSignature"),
            (MenuKeys.ApprovalWorkflowKhkkN7, "Duyệt KHKK — Nhóm 7", MenuKeys.ApprovalWorkflowsV2, 9, "FileSignature"),
            (MenuKeys.ApprovalWorkflowKhkkN8, "Duyệt KHKK — Nhóm 8", MenuKeys.ApprovalWorkflowsV2, 10, "FileSignature"),
            // [S61 Mig 50] Menu module Ngân sách cũ (Budgets + 3 leaf Bg_*) đã XÓA —
            // rows cũ trên DB cleanup qua SQL trong migration (idempotent DELETE).
            // Module Nhân sự (Phase 10.1 G-H1 — Mig 34 S33). Root operational HR.
            // [S57] "Cấu hình HRM" re-parent sang "Danh mục" (Master) — gom config 1 chỗ.
            // Hrm còn: Dashboard(1) → Hồ sơ(2), Dashboard đầu nhóm (khớp Puro).
            (MenuKeys.Hrm,           "Nhân sự",             null,            28, "UserCircle"),
            (MenuKeys.HrmHoSo,       "Hồ sơ Nhân sự",       MenuKeys.Hrm,     2, "ContactRound"),

            // Phase 10.2 G-H2 (Mig 35 — S34). Sub-group "Cấu hình HRM" + 6 catalog leaf.
            // [S57] parent Hrm → Master: nằm dưới "Danh mục" (order 25, sau Catalogs) để gom
            // toàn bộ config/catalog 1 chỗ. 6 leaf bên dưới giữ parent=HrmConfig nên theo cùng.
            // DB cũ propagate qua parentBackfill bên dưới (main upsert chỉ re-set Order).
            (MenuKeys.HrmConfig,           "Cấu hình HRM",        MenuKeys.Master,    25, "Settings2"),
            (MenuKeys.HrmConfigLeaveTypes, "Loại phép",           MenuKeys.HrmConfig,  1, "CalendarOff"),
            (MenuKeys.HrmConfigHolidays,   "Ngày lễ",             MenuKeys.HrmConfig,  2, "PartyPopper"),
            (MenuKeys.HrmConfigShifts,     "Ca làm việc",         MenuKeys.HrmConfig,  3, "Clock"),
            (MenuKeys.HrmConfigOtPolicies, "Chính sách OT",       MenuKeys.HrmConfig,  4, "TimerReset"),
            // Phase 11 P11-C (Mig 44 — S51). 2 catalog leaf xe công + tài xế.
            (MenuKeys.HrmConfigVehicles,   "Xe công",             MenuKeys.HrmConfig,  5, "Car"),
            (MenuKeys.HrmConfigDrivers,    "Tài xế",              MenuKeys.HrmConfig,  6, "UserCog"),

            // Module Văn phòng số (Phase 10.2 G-O1+ S34). 1 root + leaf Danh bạ.
            // Future leaf: Off_PhongHop (G-O2) + workflow apps Off_DeXuat/DonTu/DatXe/ItTicket.
            (MenuKeys.Off,           "Văn phòng số",        null,            29, "Briefcase"),
            // Bảng điều khiển Văn phòng số — leaf đầu nhóm (Order 0 = landing, đứng trước Danh bạ=1).
            (MenuKeys.OffDashboard,  "Bảng điều khiển Văn phòng số", MenuKeys.Off, 0, "LayoutDashboard"),
            (MenuKeys.OffDanhBa,     "Danh bạ nội bộ",      MenuKeys.Off,     1, "BookUser"),
            // Phase 10.2 G-O2 (Mig 36 — S36 2026-05-28). Sub-group "Phòng họp" + 3 leaf.
            (MenuKeys.OffPhongHop,       "Phòng họp",       MenuKeys.Off,         2, "CalendarRange"),
            (MenuKeys.OffPhongHopView,   "Xem lịch",        MenuKeys.OffPhongHop, 1, "CalendarDays"),
            (MenuKeys.OffPhongHopManage, "Quản lý phòng",   MenuKeys.OffPhongHop, 2, "Building2"),
            (MenuKeys.OffPhongHopBook,   "Đặt phòng",       MenuKeys.OffPhongHop, 3, "CalendarPlus"),
            // Phase 10.3 G-O3 (Mig 38 — S37 2026-05-28). Sub-group "Đề xuất" + 3 leaf.
            (MenuKeys.OffDeXuat,         "Đề xuất",         MenuKeys.Off,         3, "FileSignature"),
            (MenuKeys.OffDeXuatList,     "Danh sách",       MenuKeys.OffDeXuat,   1, "List"),
            (MenuKeys.OffDeXuatCreate,   "Tạo mới",         MenuKeys.OffDeXuat,   2, "Plus"),
            (MenuKeys.OffDeXuatInbox,    "Inbox duyệt",     MenuKeys.OffDeXuat,   3, "Inbox"),
            // Phase 10.3 G-O4+G-O5+G-O6+G-P1 (Mig 39+40 — S38 2026-05-28). Skeleton Workflow Apps.
            (MenuKeys.OffDonTu,          "Đơn từ",          MenuKeys.Off,         4, "FileText"),
            (MenuKeys.OffDonTuLeave,     "Nghỉ phép",       MenuKeys.OffDonTu,    1, "CalendarOff"),
            (MenuKeys.OffDonTuOt,        "Đăng ký OT",      MenuKeys.OffDonTu,    2, "Clock"),
            (MenuKeys.OffDonTuTravel,    "Công tác",        MenuKeys.OffDonTu,    3, "Plane"),
            (MenuKeys.OffDatXe,          "Đặt xe công",     MenuKeys.Off,         5, "Car"),
            (MenuKeys.OffItTicket,       "Ticket CNTT",     MenuKeys.Off,         6, "Ticket"),
            // [S57] "Báo cáo chấm công" giữ ở Văn phòng số (báo cáo admin, order 7 — lấp chỗ Chấm công rời đi).
            (MenuKeys.OffAttendanceReport, "Báo cáo chấm công", MenuKeys.Off,     7, "FileBarChart"),
            // [S57] Nhóm "Cá nhân" (mirror Puro). Root order 30 = ngay sau Văn phòng số (29).
            // "Chấm công" re-parent Off → Personal; với DB cũ propagate qua parentBackfill bên dưới
            // (main upsert chỉ re-set Order, KHÔNG đụng ParentKey).
            (MenuKeys.Personal,          "Cá nhân",         null,                30, "UserRound"),
            (MenuKeys.OffChamCong,       "Chấm công",       MenuKeys.Personal,   1, "Fingerprint"),
            // Phase 10.4 G-H3 — Dashboard NS dưới root Hrm. [S57] Order 1 = đầu nhóm (khớp Puro).
            (MenuKeys.HrmDashboard,      "Dashboard NS",    MenuKeys.Hrm,         1, "BarChart3"),
        };

        // Per-type sub-menu under Contracts: 1 group + 3 leaves each
        // (Danh sách / Thao tác / Duyệt).
        var order = 31;
        var hdcOrder = 1;
        foreach (var code in MenuKeys.ContractTypeCodes)
        {
            var label = typeLabels.GetValueOrDefault(code, code);
            // [S159-đợt3] GĐ4 skeleton — 1 leaf/loại HĐ dưới root Hợp đồng cứng, tái dùng
            // đúng nhãn đã đánh số 01.-07. (leaf → /coming-soon?stage=4).
            tree.Add(("Hdc_" + code, label + " - Bảng cứng", MenuKeys.HopDongCung, hdcOrder++, "FileText")); // [S159-đợt4] owner vẽ: "01. HĐ Thầu phụ - Bảng cứng"
            tree.Add((MenuKeys.ContractTypeGroup(code),  label,       MenuKeys.Contracts,                order++, "FileText"));
            // [S159-đợt5 owner "y chang Duyệt NCC"] mỗi loại HĐ đủ 6 leaf. Label "Đang duyệt"
            // cho key CŨ đi qua labelBackfill (gotcha #11 — sửa chữ ở đây vô tác dụng DB đang chạy).
            tree.Add(("Ct_" + code + "_WfView",  "Luồng duyệt", MenuKeys.ContractTypeGroup(code),  order++, "Network"));
            tree.Add((MenuKeys.ContractTypeList(code),   "Danh sách", MenuKeys.ContractTypeGroup(code),  order++, "List"));
            tree.Add((MenuKeys.ContractTypeCreate(code), "Thao tác",  MenuKeys.ContractTypeGroup(code),  order++, "Plus"));
            tree.Add((MenuKeys.ContractTypePending(code),"Đang duyệt", MenuKeys.ContractTypeGroup(code), order++, "CheckCircle2"));
            tree.Add(("Ct_" + code + "_Approved","Đã duyệt",   MenuKeys.ContractTypeGroup(code),  order++, "CheckCheck"));
            tree.Add(("Ct_" + code + "_Deleted", "Đã xóa",     MenuKeys.ContractTypeGroup(code),  order++, "Trash2"));
        }

        // Per-type workflow admin leaves dưới `Workflows` — mỗi loại là 1
        // menu item, click vào → mở designer scoped cho loại đó.
        var wfOrder = 95;
        foreach (var code in MenuKeys.ContractTypeCodes)
        {
            var label = typeLabels.GetValueOrDefault(code, code);
            tree.Add((MenuKeys.WorkflowTypeLeaf(code), label, MenuKeys.Workflows, wfOrder++, "FileText"));
        }

        // Pe_* group per PurchaseEvaluationType + 3 action leaves each
        var peTypeLabels = new Dictionary<string, string>
        {
            ["DuyetNcc"] = "Duyệt NCC",
            ["DuyetNccPhuongAn"] = "Duyệt NCC và Giải pháp",
        };
        var peOrder = 1;
        foreach (var code in MenuKeys.PurchaseEvaluationTypeCodes)
        {
            var label = peTypeLabels.GetValueOrDefault(code, code);
            tree.Add((MenuKeys.PurchaseEvaluationGroup(code),         label,         MenuKeys.PurchaseEvaluations, peOrder++, "FileCheck"));
            // [Plan AA S24 t1] "Luồng duyệt" leaf phía trên "Danh sách" — read-only
            // view ma trận phân quyền workflow V2 admin Designer ghim. User biết
            // trước khi tạo phiếu ai duyệt step nào.
            tree.Add((MenuKeys.PurchaseEvaluationWorkflowView(code),  "Luồng duyệt", MenuKeys.PurchaseEvaluationGroup(code), peOrder++, "Network"));
            tree.Add((MenuKeys.PurchaseEvaluationList(code),          "Danh sách",   MenuKeys.PurchaseEvaluationGroup(code), peOrder++, "List"));
            tree.Add((MenuKeys.PurchaseEvaluationCreate(code),        "Thao tác",    MenuKeys.PurchaseEvaluationGroup(code), peOrder++, "Plus"));
            // [S155 đợt 2] Nhãn ở đây CHỈ có tác dụng cho DB TRỐNG. Với DB đang chạy, upsert
            // `:2162-2179` (grep `existingItems.TryGetValue`) gặp key đã tồn tại thì chỉ update
            // `Order` rồi `continue` ⇒ rename "Duyệt" → "Đang duyệt" phải qua `labelBackfill`.
            tree.Add((MenuKeys.PurchaseEvaluationPending(code),        "Đang duyệt", MenuKeys.PurchaseEvaluationGroup(code), peOrder++, "CheckCircle2"));
            // 2 mục MỚI (owner chốt 2026-07-27) — key mới ⇒ đi nhánh INSERT bình thường.
            tree.Add((MenuKeys.PurchaseEvaluationApproved(code),       "Đã duyệt",   MenuKeys.PurchaseEvaluationGroup(code), peOrder++, "CheckCheck"));
            tree.Add((MenuKeys.PurchaseEvaluationDeleted(code),        "Đã xóa",     MenuKeys.PurchaseEvaluationGroup(code), peOrder++, "Trash2"));
        }

        // PE workflow admin leaves dưới `PeWorkflows`
        var peWfOrder = 96;
        foreach (var code in MenuKeys.PurchaseEvaluationTypeCodes)
        {
            var label = peTypeLabels.GetValueOrDefault(code, code);
            tree.Add((MenuKeys.PeWorkflowTypeLeaf(code), label, MenuKeys.PeWorkflows, peWfOrder++, "FileCheck"));
        }

        // [K4a — S166] Sidebar KHKK 8 NHÓM DUYỆT × 6 leaf — 49 row MỚI cho nhóm 2..8
        // (nhóm 1 = 7 row CŨ KHÔNG-infix nằm trong tree initializer trên: `Khkk_G1` +
        // `Khkk_WfView`…`Khkk_Deleted`, key GIỮ NGUYÊN — đổi = mồ côi permission-row, S155).
        // Nhãn 8 nhóm = bảng owner đã soát `danh-muc-sp002-transcribe.md:22-29` (nguồn: danh
        // mục SOL-CCM-SP-002 Mig 70 chia 86 dòng thành 8 nhóm duyệt). Nhãn/icon/Order của 6
        // leaf MIRROR Y nhóm 1 (Luồng duyệt/Danh sách/Thao tác/Đang duyệt/Đã duyệt/Đã xóa).
        // 🔴 Key sinh bằng CHÍNH `MenuKeys.KhkkGroup/KhkkLeaf` + duyệt CHÍNH `KhkkGroupNumbers`/
        //    `KhkkLeafActions` ⇒ mọi key trong `MenuKeys.All` đều có menu-row by-construction
        //    (thiếu row = Permission FK nổ lần boot đầu — invariant T6 K1). Thêm nhóm/action mà
        //    quên nhãn ⇒ KeyNotFoundException NGAY lúc seed (fail-fast, hơn là FK nổ về sau).
        // ⚠️ Đổi NHÃN sau khi đã seed: BẮT BUỘC thêm entry vào `labelBackfill` bên dưới +
        //    restart — sửa chữ tại đây VÔ TÁC DỤNG trên DB đang chạy (upsert chỉ re-set Order).
        var khkkGroupLabels = new Dictionary<int, string>
        {
            [2] = "2. Kết cấu, hạ tầng, phá dỡ & DV hỗ trợ thi công (A2-A3)",
            [3] = "3. Hoạt động văn phòng (A4)",
            [4] = "4. Vật tư (B1)",
            [5] = "5. Thầu phụ xây dựng (B2)",
            [6] = "6. Thầu phụ MEP (B3)",
            [7] = "7. BCH công trường (C1-C3)",
            [8] = "8. HĐ chưa ủy quyền — CEO (5.2)",
        };
        var khkkLeafMeta = new Dictionary<string, (string Label, string Icon)>
        {
            ["WfView"]   = ("Luồng duyệt", "Network"),
            ["List"]     = ("Danh sách",   "List"),
            ["Create"]   = ("Thao tác",    "Plus"),
            ["Pending"]  = ("Đang duyệt",  "CheckCircle2"),
            ["Approved"] = ("Đã duyệt",    "CheckCheck"),
            ["Deleted"]  = ("Đã xóa",      "Trash2"),
        };
        foreach (var n in MenuKeys.KhkkGroupNumbers)
        {
            var groupKey = MenuKeys.KhkkGroup(n);
            // Order nhóm = n (nhóm 1 đang Order 1 ⇒ 8 nhóm xếp 1..8 dưới root KeHoachKyKet).
            tree.Add((groupKey, khkkGroupLabels[n], MenuKeys.KeHoachKyKet, n, "FileCheck"));
            var khkkLeafOrder = 1;
            foreach (var action in MenuKeys.KhkkLeafActions)
            {
                var (leafLabel, leafIcon) = khkkLeafMeta[action];
                tree.Add((MenuKeys.KhkkLeaf(n, action), leafLabel, groupKey, khkkLeafOrder++, leafIcon));
            }
        }

        // [S187 W3 — YC-023/YC-024] 7 leaf Designer cho 7 quy trình duyệt HĐ theo LOẠI
        // (workflow `HD-*` type-3 seed ở `SeedContractTypeWorkflowsAsync`). Cùng cha
        // `ApprovalWorkflowsV2` với 8 leaf KHKK ngay trên, Order tiếp ngay sau (KHKK 3..10
        // ⇒ HĐ 11..17). Key CÓ trong `MenuKeys.All` ⇒ Admin được grant tự động qua loop
        // `SeedAdminPermissionsAsync`; upsert dưới idempotent per-Key.
        // 🔴 Key sinh bằng CHÍNH `MenuKeys.ApprovalWorkflowContractLeaf` trên CHÍNH
        //    `ContractApprovalWorkflowFamily.Members` — tức CÙNG bộ sinh với
        //    `MenuKeys.ContractWorkflowMenuKeys` đang nằm trong `All` ⇒ mọi key của họ đều
        //    có menu-row BY-CONSTRUCTION (invariant T6: key vào `All` mà thiếu menu-row =
        //    Permission FK nổ ngay lần boot đầu). Thêm loại HĐ thứ 8 chỉ phải sửa 1 nơi.
        // ⚠️ Nhãn ở đây chỉ ăn cho DB TRỐNG / key MỚI. Đổi nhãn sau khi đã seed BẮT BUỘC
        //    thêm entry vào `labelBackfill` bên dưới — upsert chỉ re-set `Order` (gotcha #11).
        var awV2ContractOrder = 11;
        foreach (var (_, familyCode, shortName) in ContractApprovalWorkflowFamily.Members)
        {
            var leafKey = MenuKeys.ApprovalWorkflowContractLeaf(
                ContractApprovalWorkflowFamily.KeyToken(familyCode));
            tree.Add((leafKey, $"QT Duyệt HĐ — {shortName}",
                MenuKeys.ApprovalWorkflowsV2, awV2ContractOrder++, "FileSignature"));
        }

        var existingItems = await db.MenuItems.ToDictionaryAsync(m => m.Key);
        var added = 0;
        var reordered = 0;
        foreach (var (key, label, parent, o, icon) in tree)
        {
            if (existingItems.TryGetValue(key, out var existing))
            {
                // [Plan AA S24 t1] Shift Order khi insert leaf mới giữa list cũ —
                // existing rows phải UPDATE Order theo seed list canonical.
                // Idempotent: skip nếu Order đã match. KHÔNG đụng Label / ParentKey
                // / Icon (label backfill section riêng phía dưới).
                if (existing.Order != o)
                {
                    existing.Order = o;
                    reordered++;
                }
                continue;
            }
            db.MenuItems.Add(new MenuItem { Key = key, Label = label, ParentKey = parent, Order = o, Icon = icon });
            added++;
        }
        if (added > 0 || reordered > 0)
        {
            await db.SaveChangesAsync();
            if (added > 0) logger.LogInformation("Seeded {Count} menu items", added);
            if (reordered > 0) logger.LogInformation("Reordered {Count} menu items", reordered);
        }

        // Backfill label updates for menu items renamed via business changes.
        // Add new entries here khi có rename — key → expected label. Idempotent.
        var labelBackfill = new Dictionary<string, string>
        {
            [MenuKeys.PurchaseEvaluationGroup("DuyetNcc")]         = "Duyệt NCC",
            [MenuKeys.PurchaseEvaluationGroup("DuyetNccPhuongAn")] = "Duyệt NCC và Giải pháp",
            [MenuKeys.PeWorkflowTypeLeaf("DuyetNcc")]              = "Duyệt NCC",
            [MenuKeys.PeWorkflowTypeLeaf("DuyetNccPhuongAn")]      = "Duyệt NCC và Giải pháp",
            // [S155 đợt 2] 🔴 Rename "Duyệt" → "Đang duyệt" (owner, ảnh menu 2026-07-27).
            // BẮT BUỘC đi qua đây: sửa chữ trong `tree.Add(...)` là VÔ TÁC DỤNG trên DB đang
            // chạy vì upsert `:2162-2179` (grep `existingItems.TryGetValue`) chỉ re-set `Order`.
            [MenuKeys.PurchaseEvaluationPending("DuyetNcc")]         = "Đang duyệt",
            [MenuKeys.PurchaseEvaluationPending("DuyetNccPhuongAn")] = "Đang duyệt",
            // [S159] 🔴 UAT anh Kiệt: đánh số 01.-07. nhóm HĐ (verbatim "01 . HĐ thầu phụ").
            // Prod đã prepend trực tiếp cùng ngày ⇒ trên prod các entry này NO-OP (equal);
            // dev/fresh-DB hội tụ qua đây. Nguồn số = thứ tự ContractTypeCodes (= [Order]).
            // [S159] 2 root rename theo 4-GĐ (prod đã SQL cùng ngày ⇒ NO-OP trên prod).
            [MenuKeys.PurchaseEvaluations] = "Duyệt NCC",
            [MenuKeys.Contracts]           = "Duyệt hợp đồng",
            // [S159-đợt5] Sidebar "Tổng quan" → "Tổng quan quy trình" (owner đề tên, lead lấy).
            [MenuKeys.Dashboard] = "Tổng quan quy trình",
            // [S159-đợt5] 7 leaf Ct_X_Pending "Duyệt" → "Đang duyệt" (đồng khuôn PE — gotcha #11).
            [MenuKeys.ContractTypePending("ThauPhu")]      = "Đang duyệt",
            [MenuKeys.ContractTypePending("GiaoKhoan")]    = "Đang duyệt",
            [MenuKeys.ContractTypePending("NhaCungCap")]   = "Đang duyệt",
            [MenuKeys.ContractTypePending("DichVu")]       = "Đang duyệt",
            [MenuKeys.ContractTypePending("MuaBan")]       = "Đang duyệt",
            [MenuKeys.ContractTypePending("NguyenTacNcc")] = "Đang duyệt",
            [MenuKeys.ContractTypePending("NguyenTacDv")]  = "Đang duyệt",
            // [S159-đợt4] 7 leaf Hdc_ thêm hậu tố " - Bảng cứng" (owner vẽ; prod SQL cùng ngày).
            ["Hdc_ThauPhu"]      = "01. HĐ Thầu phụ - Bảng cứng",
            ["Hdc_GiaoKhoan"]    = "02. HĐ Giao khoán - Bảng cứng",
            ["Hdc_NhaCungCap"]   = "03. HĐ Nhà cung cấp - Bảng cứng",
            ["Hdc_DichVu"]       = "04. HĐ Dịch vụ - Bảng cứng",
            ["Hdc_MuaBan"]       = "05. HĐ Mua bán - Bảng cứng",
            ["Hdc_NguyenTacNcc"] = "06. HĐ Nguyên tắc NCC - Bảng cứng",
            ["Hdc_NguyenTacDv"]  = "07. HĐ Nguyên tắc Dịch vụ - Bảng cứng",
            [MenuKeys.ContractTypeGroup("ThauPhu")]      = "01. HĐ Thầu phụ",
            [MenuKeys.ContractTypeGroup("GiaoKhoan")]    = "02. HĐ Giao khoán",
            [MenuKeys.ContractTypeGroup("NhaCungCap")]   = "03. HĐ Nhà cung cấp",
            [MenuKeys.ContractTypeGroup("DichVu")]       = "04. HĐ Dịch vụ",
            [MenuKeys.ContractTypeGroup("MuaBan")]       = "05. HĐ Mua bán",
            [MenuKeys.ContractTypeGroup("NguyenTacNcc")] = "06. HĐ Nguyên tắc NCC",
            [MenuKeys.ContractTypeGroup("NguyenTacDv")]  = "07. HĐ Nguyên tắc Dịch vụ",
            // [K4a — S166] Nhóm 1 KHKK đổi nhãn skeleton S159 ("1. Kế hoạch ký kết HĐ
            // (NCC-TP)") → nhãn nhóm duyệt A1, khớp bảng 8 nhãn owner đã soát. CHỈ đổi
            // Label, KHÔNG đổi key ⇒ 0 row mới, 0 permission-row mồ côi. 7 nhóm G2..G8 là
            // key MỚI nên đi nhánh INSERT của upsert, không cần entry backfill.
            ["Khkk_G1"] = "1. Thiết bị, máy móc, vật tư phụ (A1)",
        };
        var updatedLabels = 0;
        foreach (var (key, expectedLabel) in labelBackfill)
        {
            var item = await db.MenuItems.FirstOrDefaultAsync(m => m.Key == key);
            if (item != null && item.Label != expectedLabel)
            {
                item.Label = expectedLabel;
                updatedLabels++;
            }
        }
        if (updatedLabels > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation("Backfilled {Count} menu labels", updatedLabels);
        }

        // [S57] Re-parent backfill — chuyển node sang group khác trên DB cũ. Main
        // upsert phía trên CHỈ re-set Order, KHÔNG đụng ParentKey (xem comment trên),
        // nên đổi nhóm phải update ParentKey riêng. Idempotent. "Chấm công" Off → Cá nhân.
        var parentBackfill = new Dictionary<string, string?>
        {
            [MenuKeys.OffChamCong] = MenuKeys.Personal,   // [S57] Chấm công → Cá nhân
            [MenuKeys.HrmConfig]   = MenuKeys.Master,      // [S57] Cấu hình HRM → Danh mục (gom config 1 chỗ)
        };
        var reparented = 0;
        foreach (var (key, expectedParent) in parentBackfill)
        {
            var item = await db.MenuItems.FirstOrDefaultAsync(m => m.Key == key);
            if (item != null && item.ParentKey != expectedParent)
            {
                item.ParentKey = expectedParent;
                reparented++;
            }
        }
        if (reparented > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation("Re-parented {Count} menu items", reparented);
        }

        // Backfill WorkflowDefinition name cho B (Phương Án → Giải pháp rename).
        var wfB = await db.PurchaseEvaluationWorkflowDefinitions
            .FirstOrDefaultAsync(w => w.Code == "QT-DN-B" && w.Version == 1);
        if (wfB != null && wfB.Name.Contains("Phương Án"))
        {
            wfB.Name = wfB.Name.Replace("Quy trình Duyệt NCC - Phương Án", "Quy trình Duyệt NCC và Giải pháp");
            await db.SaveChangesAsync();
            logger.LogInformation("Backfilled WorkflowDefinition QT-DN-B name");
        }
    }

    private static async Task SeedAdminPermissionsAsync(ApplicationDbContext db, RoleManager<Role> roleManager, ILogger logger)
    {
        var adminRole = await roleManager.FindByNameAsync(AppRoles.Admin);
        if (adminRole is null) return;

        var existingMenuKeys = await db.Permissions
            .Where(p => p.RoleId == adminRole.Id)
            .Select(p => p.MenuKey)
            .ToListAsync();

        var added = 0;
        foreach (var menuKey in MenuKeys.All)
        {
            if (existingMenuKeys.Contains(menuKey)) continue;
            db.Permissions.Add(new Permission
            {
                RoleId = adminRole.Id,
                MenuKey = menuKey,
                CanRead = true, CanCreate = true, CanUpdate = true, CanDelete = true,
            });
            added++;
        }
        if (added > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation("Seeded {Count} admin permissions", added);
        }

        // Defaults cho Pe_* permissions per role — mỗi role business liên quan
        // PE module được grant CRUD level phù hợp lên 2 type (DuyetNcc /
        // DuyetNccPhuongAn).
        // Idempotent: skip per-(role,menuKey) đã có, chỉ add row mới.
        await SeedPurchaseEvaluationPermissionDefaultsAsync(db, roleManager, logger);

        // [Plan CA S29 2026-05-22] Role CatalogManager — quản lý danh mục dùng chung
        // (Master/Suppliers/Projects/Departments + 4 Catalogs leaf). Admin gán role
        // cho user nào cần CRUD danh mục sau khi move FE từ admin → eoffice.
        await SeedCatalogManagerPermissionsAsync(db, roleManager, logger);

        // [S57] Mở quyền XEM (Read-only) cho TẤT CẢ role để mọi bộ phận review/góp ý
        // các module HRM + Văn phòng số + Danh mục (master). KHÔNG đụng Duyệt NCC
        // (Pe_*/PeWf_*/AwV2 — sắp go-live, giữ phân quyền cũ), Contracts/System.
        // [S58] Scope grant THU HẸP còn Master/Catalogs/Pe_* — xem note trong method.
        await SeedAllRolesReviewReadPermissionsAsync(db, roleManager, logger);

        // [S58 2026-06-11] TẠM ẨN module chưa golive với user thường (anh yêu cầu,
        // screenshot eoffice): thu hồi quyền HRM (Hrm*) + Văn phòng số (Off*) +
        // Cá nhân (Personal) khỏi MỌI role TRỪ Admin. Menu tự ẩn cả 2 app
        // (GetMyMenuTree lọc CanRead). Chạy SAU grant seed để revoke thắng.
        // Mở lại sau golive: gỡ prefix khỏi revoke + thêm lại vào InReviewScope.
        // [K4a — S166] Đã soi revoker cho 49 key KHKK mới: filter chỉ bắt prefix `Hrm*`/`Off*`
        // + key `Personal` ⇒ **0 match `Khkk_*`** ⇒ 49 row CanRead vừa cấp KHÔNG bị lật. Phép
        // thử của khẳng định này = restart API ×2 rồi đếm lại row `Khkk%` (acceptance A5).
        await RevokeTemporarilyHiddenModulesAsync(db, roleManager, logger);

        // [S118 2026-07-14 — anh Kiệt FDC] CHẠY SAU revoke để THẮNG: NGOẠI-LỆ role Procurement
        // (Phòng Cung ứng) — mở lại Danh mục (Master/Suppliers/Projects/Departments/Catalog*) RIÊNG
        // cho PRO (cần quản lý + CÔNG BỐ NCC — Suppliers R+C+U). S92 "chỉ Admin thấy" GIỮ NGUYÊN cho
        // MỌI role khác (seeder chỉ đụng role Procurement). Dashboard/Reports KHÔNG cấp (anh chốt bỏ).
        await SeedProcurementMasterAccessAsync(db, roleManager, logger);

        // [W1 Mig 69 — S161 2026-07-29] GĐ2 "Kế hoạch ký kết HĐ" golive: nâng quyền GHI
        // (CanCreate+CanUpdate 13/13 role — O-A "hiển thị hết để góp ý"; CanDelete chỉ
        // Drafter+Admin) trên root KeHoachKyKet + 6 leaf Khkk_*. CHẠY SAU revoke để THẮNG.
        // 🔴 UPGRADE-IF-EXISTS bắt buộc (review F-S1 4-lane hội tụ): row đã seed CanRead-only
        // @S159 ⇒ nhánh insert skip-existing = NO-OP im lặng ⇒ 403 giết dry-run (gotcha #44).
        await SeedKeHoachKyKetAccessAsync(db, roleManager, logger);

        // [K7 — S167 2026-08-01] Cầu GĐ2 → GĐ3 golive: cấp `Contracts.Create` cho Drafter +
        // Procurement + Admin (câu III default — chờ anh ratify, đổi bộ vai TRONG method).
        // CHẠY SAU revoke cùng lý do 2 seeder trên. Không có nó thì nút "Đưa vào Hợp đồng"
        // 403 với 12/13 role (`SeedAllRolesReviewReadPermissionsAsync` để CanCreate=false cho key
        // `Contracts`; Admin qua nhờ bypass + prod sẵn row CanCreate=True — đo @gate-K7) —
        // nghiệm thu = restart THẬT rồi đếm row, không đọc code (#75/#76).
        await SeedContractCreateAccessAsync(db, roleManager, logger);

        // [S65 2026-06-16] CHẠY SAU revoke để THẮNG: mở lại quyền XEM "Hồ sơ Nhân sự"
        // (+ root nhóm "Nhân sự") cho MỌI role — anh chốt public module Nhân sự cho
        // user thường tra cứu hồ sơ. Dashboard NS (Hrm_Dashboard) + 6 catalog
        // Hrm_Config* VẪN ẨN (revoke che). Read-only (chỉ CanRead).
        // [S92 2026-06-29] TẮT re-grant "Hồ sơ Nhân sự" public (S65) — anh chốt nhóm
        // "Nhân sự" CHỈ Admin thấy. RevokeTemporarilyHiddenModulesAsync (chạy ngay trên,
        // S92 mở rộng phủ thêm Contracts/Master/Catalogs) là bước permission CUỐI → thắng
        // mọi grant. Bật lại module nào: bỏ comment call tương ứng + gỡ prefix khỏi revoke.
        // await SeedAllRolesHrmProfileReadPermissionsAsync(db, roleManager, logger);   // S65 — TẮT S92

        // [S69 2026-06-17] GOLIVE Văn phòng số: mở Read+Create module Office (self-service)
        // cho MỌI role — anh chốt "public văn phòng số cho all user eoffice". CHẠY SAU revoke
        // để THẮNG (mirror S65). Allow-list 16 key (loại Off_PhongHop_Manage admin-CRUD +
        // Off_AttendanceReport báo-cáo-riêng-tư + Off_ChamCong Cá-nhân — giữ ẩn). HRM (trừ
        // Hồ sơ NS) + Personal VẪN ẩn (anh chỉ mở Office).
        // [S92 2026-06-29] TẮT re-grant "Văn phòng số" public (S69) — anh chốt nhóm
        // "Văn phòng số" CHỈ Admin thấy.
        // await SeedAllRolesOfficeModulePermissionsAsync(db, roleManager, logger);     // S69 — TẮT S92
    }

    // [S57] Cấp CanRead (CHỈ xem) cho MỌI role trên menu HRM + Office + Master để mọi
    // bộ phận nhân viên thấy + review/góp ý. Additive idempotent → KHÔNG xóa quyền
    // sẵn có. Write vẫn khóa ở controller (Master: Admin+CatalogManager;
    // HRM-config/Catalogs/MeetingRoom: Admin).
    //
    // [S57bis] Mở Duyệt NCC (Pe_*) cho MỌI role: anh chốt "Xem + Tạo".
    //   - Key HRM/Office/Master/Catalogs : CanRead-only, skip-existing (giữ nguyên).
    //   - Key Pe_*                        : CanRead=true + CanCreate=true.
    //     Idempotent UPGRADE-ONLY: row Pe_* đã tồn tại (Pe defaults cũ seed 7 role)
    //     mà CanRead HOẶC CanCreate=false → NÂNG đúng 2 cờ đó lên true. KHÔNG hạ +
    //     KHÔNG đụng CanUpdate/CanDelete (additive — không phá quyền admin đã chỉnh
    //     cao hơn). Row chưa có → tạo mới CanRead+CanCreate=true, Update/Delete=false.
    private static async Task SeedAllRolesReviewReadPermissionsAsync(
        ApplicationDbContext db, RoleManager<Role> roleManager, ILogger logger)
    {
        // Scope grant = Master + Catalogs + Pe_*.
        // [S57bis] +Pe_* (Duyệt NCC) — semantics riêng read+create xử lý bên dưới.
        // [S58] BỎ Hrm*/Off*/Personal khỏi grant — sếp yêu cầu TẠM ẨN HRM + Văn phòng
        // số + Cá nhân với user thường (chưa golive các module này). Revoke tương ứng
        // ở RevokeTemporarilyHiddenModulesAsync (chạy sau). Loại trừ tự nhiên (không
        // match prefix): PeWf_* (ký tự thứ 3 'W' ≠ '_'), AwV2_*, Ct_*, Bg_*, Wf_*, System.
        // [S92 2026-06-29] Narrow: BỎ Master/Catalogs/Suppliers/Projects/Departments khỏi
        // grant-all-roles (anh chốt "Danh mục" CHỈ Admin). GIỮ Pe_* (Duyệt NCC read+create
        // cho mọi role — KHÔNG đụng). Master-data GET = any-auth (Suppliers/Projects/Catalogs
        // controller class-[Authorize]) nên form PE vẫn chọn Dự án/NCC/Hạng mục dù menu ẩn.
        // Row Master/Catalogs cũ trên prod được RevokeTemporarilyHiddenModulesAsync (dưới) gỡ.
        static bool InReviewScope(string key) => key.StartsWith("Pe_");

        // Phân biệt key Pe_* (read+create) vs read-only. Pe_* match cờ thứ-3 '_'
        // → "PeWf_*"/"PeWorkflows" KHÔNG match (loại admin Designer).
        static bool IsPeKey(string key) => key.StartsWith("Pe_");

        // [S159] các bộ key grant CanRead-only mọi role (xem chú thích tại reviewKeys).
        static IEnumerable<string> ContractMenuKeys()
        {
            yield return MenuKeys.Contracts;
            foreach (var c in MenuKeys.ContractTypeCodes)
            {
                yield return MenuKeys.ContractTypeGroup(c);
                yield return "Ct_" + c + "_WfView";   // [S159-đợt5]
                yield return MenuKeys.ContractTypeList(c);
                yield return MenuKeys.ContractTypeCreate(c);
                yield return MenuKeys.ContractTypePending(c);
                yield return "Ct_" + c + "_Approved"; // [S159-đợt5]
                yield return "Ct_" + c + "_Deleted";  // [S159-đợt5]
            }
        }
        static IEnumerable<string> MasterMenuKeys() => new[]
        {
            MenuKeys.Master, MenuKeys.Catalogs, MenuKeys.CatalogUnits, MenuKeys.CatalogMaterials,
            MenuKeys.CatalogServices, MenuKeys.CatalogWorkItems, MenuKeys.Departments,
            MenuKeys.Projects, MenuKeys.Suppliers,
        };
        // [S159] 7 key nhóm 1 (không-infix) + [K4a S166] 49 key nhóm 2..8 (7 group + 42 leaf,
        // sinh từ `MenuKeys.KhkkGroupMenuKeys` — CÙNG bộ sinh với `MenuKeys.All` và vòng seed
        // menu-row ⇒ không mirror tay). CanRead mọi role, skip-existing.
        // 🔴 LÝ DO GIỮ site này (đính chính lý-do sai đang lưu hành "group-key cần row để
        //    render"): group KHÔNG cần row CanRead của chính nó — `GetMyMenuTreeQuery.cs:96`
        //    `HasAccess(n) = n.CanRead || n.Children.Any(HasAccess)`. Cái BẮT BUỘC là 42 LEAF
        //    phải có row CanRead cho non-Admin: site-1 (loop MenuKeys.All) CHỈ cấp Admin, còn
        //    site-3 `SeedKeHoachKyKetAccessAsync` CHỈ đụng root + 6 leaf nhóm 1. Group vẫn liệt
        //    kê ở đây để có hàng tường minh trên ma trận Phân quyền (admin tự bật/tắt).
        // ⚠️ CanRead là ĐỦ cho 42 leaf mới, KHÔNG cần nhét chúng vào site-3: mọi endpoint KHKK
        //    gate bằng policy `KeHoachKyKet.{Read,Create,Update,Delete}` (gotcha #85 — gate FE
        //    cũng phải dùng đúng key đó, KHÔNG OR key con), và cờ GHI của root do site-3 phát
        //    (13/13 role C+U, Delete = Drafter+Admin). Leaf chỉ là điểm điều hướng.
        static IEnumerable<string> KhkkKeys() => new[]
        {
            "Khkk_G1", "Khkk_WfView", "Khkk_List", "Khkk_Create", "Khkk_Pending",
            "Khkk_Approved", "Khkk_Deleted",
        }.Concat(MenuKeys.KhkkGroupMenuKeys);
        static IEnumerable<string> HdcKeys()
            => MenuKeys.ContractTypeCodes.Select(c => "Hdc_" + c);

        // MenuKeys.All chứa root PurchaseEvaluations nhưng KHÔNG chứa Pe_* leaf
        // (sinh động qua factory). Build leaf giống SeedPurchaseEvaluationPermissionDefaultsAsync
        // để upgrade đúng row Pe_* thật trong DB (1 root + 5 leaf × 2 type).
        var peKeys = new List<string> { MenuKeys.PurchaseEvaluations };
        foreach (var typeCode in MenuKeys.PurchaseEvaluationTypeCodes)
        {
            peKeys.Add(MenuKeys.PurchaseEvaluationGroup(typeCode));
            peKeys.Add(MenuKeys.PurchaseEvaluationWorkflowView(typeCode));
            peKeys.Add(MenuKeys.PurchaseEvaluationList(typeCode));
            peKeys.Add(MenuKeys.PurchaseEvaluationCreate(typeCode));
            peKeys.Add(MenuKeys.PurchaseEvaluationPending(typeCode));
            // [S155 đợt 2] 2 mục menu mới — phải có mặt ở CẢ HAI danh sách permission
            // (đây + `SeedPurchaseEvaluationPermissionDefaultsAsync`), quên 1 bên = quyền lệch.
            peKeys.Add(MenuKeys.PurchaseEvaluationApproved(typeCode));
            peKeys.Add(MenuKeys.PurchaseEvaluationDeleted(typeCode));
        }

        var reviewKeys = MenuKeys.All.Where(InReviewScope)
            .Concat(peKeys)
            // [S159] 2 root placeholder GĐ2/GĐ4 — CanRead-only mọi role (IsPeKey=false
            // ⇒ nhánh read-only). Prod đã INSERT trực tiếp; đây giữ fresh-DB hội tụ.
            .Concat(new[] { MenuKeys.KeHoachKyKet, MenuKeys.HopDongCung })
            // [S159] skeleton GĐ2 (Khkk_*) + GĐ4 (Hdc_*) — leaves trỏ /coming-soon.
            .Concat(KhkkKeys()).Concat(HdcKeys())
            // [S159] owner "hiển thị hết": HĐ (Contracts+Ct_*) + DANH MỤC (Master-set)
            // CanRead-only mọi role — cặp với việc GỠ nhánh [S92] khỏi revoker ở trên.
            // Lưu ý: nhánh grant key-thường là skip-existing ⇒ trên prod row false
            // KHÔNG được nâng ở đây — re-grant prod đã chạy SQL trực tiếp cùng ngày;
            // đây là đường hội tụ cho fresh-DB/env khác.
            .Concat(ContractMenuKeys()).Concat(MasterMenuKeys())
            .Distinct()
            .ToArray();
        var roles = await roleManager.Roles.ToListAsync();

        // Load full rows (cần mutate CanRead/CanCreate cho Pe_* upgrade path).
        var existingRows = (await db.Permissions
                .Where(p => reviewKeys.Contains(p.MenuKey))
                .ToListAsync())
            .ToDictionary(p => (p.RoleId, p.MenuKey));

        var added = 0;
        var upgraded = 0;
        foreach (var role in roles)
        {
            foreach (var key in reviewKeys)
            {
                var isPe = IsPeKey(key);
                if (existingRows.TryGetValue((role.Id, key), out var row))
                {
                    // [S57bis] Pe_* upgrade-only: nâng CanRead/CanCreate nếu đang false.
                    if (isPe)
                    {
                        var changed = false;
                        if (!row.CanRead) { row.CanRead = true; changed = true; }
                        if (!row.CanCreate) { row.CanCreate = true; changed = true; }
                        if (changed) upgraded++;
                    }
                    // Key non-Pe: skip-existing (giữ nguyên như cũ).
                    continue;
                }

                db.Permissions.Add(new Permission
                {
                    RoleId = role.Id,
                    MenuKey = key,
                    CanRead = true,
                    CanCreate = isPe,            // [S57bis] Pe_* được Tạo; còn lại read-only
                    CanUpdate = false,
                    CanDelete = false,
                });
                added++;
            }
        }

        if (added > 0 || upgraded > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation(
                "Seeded all-roles review perms: {Added} added + {Upgraded} upgraded (Pe_* read+create) " +
                "({Keys} keys × {Roles} roles)",
                added, upgraded, reviewKeys.Length, roles.Count);
        }
    }

    // [S58 2026-06-11] Thu hồi quyền các module TẠM ẨN (chưa golive — anh yêu cầu từ
    // screenshot eoffice) khỏi mọi role non-Admin: HRM (Hrm* gồm Hrm_Config*) +
    // Văn phòng số (Off* gồm Off_ChamCong đã re-parent về Cá nhân) + root Cá nhân
    // (Personal). Set cả 4 cờ CRUD=false (KHÔNG xóa row — giữ vết, flip lại nhanh
    // khi golive). Admin GIỮ nguyên để quản trị/chuẩn bị dữ liệu.
    // Lưu ý mức che: ẩn menu (GetMyMenuTree lọc CanRead) + permission matrix; FE
    // KHÔNG có PermissionGuard per-route nên gõ URL trực tiếp vẫn render trang
    // (API self-service vẫn [Authorize] thường) — chấp nhận cho mức "tạm ẩn".
    // Idempotent: lần 2 không row nào còn cờ true → 0 update.
    private static async Task RevokeTemporarilyHiddenModulesAsync(
        ApplicationDbContext db, RoleManager<Role> roleManager, ILogger logger)
    {
        var adminRole = await roleManager.FindByNameAsync(AppRoles.Admin);
        if (adminRole is null)
        {
            logger.LogWarning("RevokeTemporarilyHiddenModulesAsync: skip — Admin role chưa seed.");
            return;
        }
        var adminRoleId = adminRole.Id;

        var rows = await db.Permissions
            .Where(p => p.RoleId != adminRoleId
                && (p.MenuKey.StartsWith("Hrm") || p.MenuKey.StartsWith("Off")
                    || p.MenuKey == MenuKeys.Personal)
                    // [S159 2026-07-29] 🔴 GỠ nhánh [S92] (Contracts+Ct_* / Master+Suppliers+
                    // Projects+Departments+Catalog*) — owner SUPERSEDE chính quyết định S92
                    // "chỉ Admin thấy": nay "đang phát triển → cho hiển thị hết để mọi người
                    // góp ý; Duyệt NCC để như cũ". Bài trả giá cùng ngày: grant 38 key bằng
                    // SQL tay bị CHÍNH revoker này lật lại ở app-restart kế (Run #423 —
                    // 447/494 row rơi, 11/13 role về 0; cicd-verify-1a47a61.md mục 9).
                    // Đúng chiều-ngược gotcha #75/#76: seeder ungated thắng data-change tay
                    // ⇒ đổi trạng-thái bền = đổi CODE, nghiệm thu = restart THẬT.
                    // Hrm*/Off*/Personal GIỮ trong revoker (vùng đang-làm, ý ❸ owner).
                && (p.CanRead || p.CanCreate || p.CanUpdate || p.CanDelete))
            .ToListAsync();

        foreach (var row in rows)
        {
            row.CanRead = false;
            row.CanCreate = false;
            row.CanUpdate = false;
            row.CanDelete = false;
        }

        if (rows.Count > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation(
                "Revoked {Count} permission rows — ẩn HRM/Văn phòng số/Cá nhân/Hợp đồng/Danh mục " +
                "khỏi non-Admin (chỉ Admin thấy — S58 + S92)",
                rows.Count);
        }
    }

    // [S118 2026-07-14 — anh Kiệt FDC] NGOẠI-LỆ role Procurement (Phòng Cung ứng): mở lại quyền
    // Danh mục (Master + Suppliers + Projects + Departments + Catalog*) RIÊNG cho role Procurement
    // — anh chốt PRO cần quản lý + CÔNG BỐ NCC (Suppliers R+C+U; S113 publish cần Suppliers.Update).
    // S92 "chỉ Admin thấy" GIỮ NGUYÊN cho MỌI role khác. CHẠY SAU RevokeTemporarilyHiddenModulesAsync
    // để THẮNG revoke (mirror pattern S65/S69). Dashboard/Reports CỐ Ý KHÔNG cấp (anh chốt bỏ —
    // Reports lộ tổng giá trị HĐ + top NCC/dự án theo giá trị, ngược S92; reviewer S118 bắt).
    //   - Suppliers: Read+Create+Update (quản lý + công bố NCC). KHÔNG Delete.
    //   - Master + Catalogs (group) + Projects + Departments + 4 Catalog leaf: Read-only.
    //   - UPGRADE-ONLY (mirror Hrm S65 / Office S69): row đã tồn tại (revoke vừa set false) → NÂNG
    //     cờ desired lên true. Row chưa có (DB mới) → tạo. KHÔNG hạ cờ + KHÔNG đụng role khác.
    // Thu hồi khi cần: xóa call ở SeedAsync — revoke sẽ tự che Master/Suppliers lại lần seed kế.
    private static async Task SeedProcurementMasterAccessAsync(
        ApplicationDbContext db, RoleManager<Role> roleManager, ILogger logger)
    {
        var role = await roleManager.FindByNameAsync(AppRoles.Procurement);
        if (role is null)
        {
            logger.LogWarning("SeedProcurementMasterAccessAsync: skip — Procurement role chưa seed.");
            return;
        }

        // (MenuKey, CanRead, CanCreate, CanUpdate) — CanDelete luôn giữ nguyên (KHÔNG cấp Delete).
        var grants = new (string Key, bool R, bool C, bool U)[]
        {
            (MenuKeys.Master,           true, false, false),
            (MenuKeys.Suppliers,        true, true,  true),   // quản lý + CÔNG BỐ NCC (S113 cần Update)
            (MenuKeys.Projects,         true, false, false),
            (MenuKeys.Departments,      true, false, false),
            (MenuKeys.Catalogs,         true, false, false),
            (MenuKeys.CatalogUnits,     true, false, false),
            (MenuKeys.CatalogMaterials, true, false, false),
            (MenuKeys.CatalogServices,  true, false, false),
            (MenuKeys.CatalogWorkItems, true, false, false),
        };
        var keys = grants.Select(g => g.Key).ToArray();

        var existing = (await db.Permissions
                .Where(p => p.RoleId == role.Id && keys.Contains(p.MenuKey))
                .ToListAsync())
            .ToDictionary(p => p.MenuKey);

        var added = 0;
        var upgraded = 0;
        foreach (var g in grants)
        {
            if (existing.TryGetValue(g.Key, out var row))
            {
                var changed = false;
                if (g.R && !row.CanRead)   { row.CanRead = true;   changed = true; }
                if (g.C && !row.CanCreate) { row.CanCreate = true; changed = true; }
                if (g.U && !row.CanUpdate) { row.CanUpdate = true; changed = true; }
                if (changed) upgraded++;
                continue;
            }

            db.Permissions.Add(new Permission
            {
                RoleId = role.Id,
                MenuKey = g.Key,
                CanRead = g.R,
                CanCreate = g.C,
                CanUpdate = g.U,
                CanDelete = false,
            });
            added++;
        }

        if (added > 0 || upgraded > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation(
                "Seeded Procurement master access: {Added} added + {Upgraded} upgraded " +
                "(Master/Suppliers/Projects/Departments/Catalog* — PRO exception to S92, S118)",
                added, upgraded);
        }
    }

    // [W1 Mig 69 — S161 2026-07-29] GĐ2 "Kế hoạch ký kết HĐ" — nâng quyền GHI cho MỌI role
    // trên root KeHoachKyKet + 6 leaf Khkk_* (O-A anh chốt "hiển thị hết để góp ý" ⇒ 13/13
    // role CanCreate+CanUpdate; CanDelete CHỈ Drafter+Admin — spec W1 §②-8).
    // Khuôn = SeedProcurementMasterAccessAsync (S118) MỞ RỘNG thêm vế CanDelete (khuôn gốc
    // không có — chốt-cuối S160 dặn). UPGRADE-ONLY: row tồn tại → NÂNG cờ desired, KHÔNG hạ
    // cờ nào; row chưa có (fresh DB) → tạo đủ cờ. CẤM SQL tay (#84 — grant tay bị revoker lật).
    // Thu hồi khi cần: xóa call ở SeedAsync — revoke sẽ tự che lại lần seed kế.
    private static async Task SeedKeHoachKyKetAccessAsync(
        ApplicationDbContext db, RoleManager<Role> roleManager, ILogger logger)
    {
        // Root policy-key + 6 leaf menu-key (khớp bộ seed menu-row S159; Khkk_G1 = group
        // điều hướng, CanRead-all đã có từ nhánh read-only — KHÔNG cần cờ ghi).
        var keys = new[]
        {
            MenuKeys.KeHoachKyKet,
            "Khkk_WfView", "Khkk_List", "Khkk_Create", "Khkk_Pending",
            "Khkk_Approved", "Khkk_Deleted",
        };

        var roles = await roleManager.Roles.ToListAsync();
        var existing = (await db.Permissions
                .Where(p => keys.Contains(p.MenuKey))
                .ToListAsync())
            .ToDictionary(p => (p.RoleId, p.MenuKey));

        var added = 0;
        var upgraded = 0;
        foreach (var role in roles)
        {
            var allowDelete = role.Name == AppRoles.Admin || role.Name == AppRoles.Drafter;
            foreach (var key in keys)
            {
                if (existing.TryGetValue((role.Id, key), out var row))
                {
                    var changed = false;
                    if (!row.CanRead)   { row.CanRead = true;   changed = true; }
                    if (!row.CanCreate) { row.CanCreate = true; changed = true; }
                    if (!row.CanUpdate) { row.CanUpdate = true; changed = true; }
                    if (allowDelete && !row.CanDelete) { row.CanDelete = true; changed = true; }
                    if (changed) upgraded++;
                    continue;
                }

                db.Permissions.Add(new Permission
                {
                    RoleId = role.Id,
                    MenuKey = key,
                    CanRead = true,
                    CanCreate = true,
                    CanUpdate = true,
                    CanDelete = allowDelete,
                });
                added++;
            }
        }

        if (added > 0 || upgraded > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation(
                "Seeded KeHoachKyKet access: {Added} added + {Upgraded} upgraded " +
                "(W1 GĐ2 golive — 13/13 role C+U, Delete=Drafter/Admin; UPGRADE-if-exists F-S1)",
                added, upgraded);
        }
    }

    // [K7 — S167 2026-08-01] Cấp `Contracts.Create` cho các vai được bấm "Đưa vào Hợp đồng"
    // (cầu GĐ2 → GĐ3, `ContractSigningPlansController` `POST {id}/create-contract`).
    //
    // 🔴 LÝ DO PHẢI CÓ HÀM NÀY: trước S167 **KHÔNG role nào** mang `Contracts.Create` —
    // `SeedAllRolesReviewReadPermissionsAsync` (grep tên hàm, đừng neo số dòng) tạo row key
    // `Contracts` với `CanCreate = isPe` mà `isPe` = false cho key này ⇒ nút bridge 403 với
    // 12/13 role (kể cả Drafter); Admin qua nhờ bypass và prod ĐÃ sẵn row CanCreate=True cho
    // Admin (đo prod @gate-K7). Đây là lỗ ĐO ĐƯỢC, không phải suy đoán.
    // CanRead nâng KÈM là CHỦ ĐÍCH, không phải tràn-nhãn: vai được tạo HĐ phải THẤY menu
    // "Hợp đồng" để mở HĐ vừa tạo từ cầu KHKK — CanRead là cờ hiển thị menu (2-tầng #82).
    //
    // ⏳ [K7 — câu III default, chờ anh ratify] Bộ vai = Drafter + Procurement + Admin
    // ("người có quyền tạo HĐ"): Drafter = người soạn HĐ, Procurement = Mua hàng chủ trì cầu
    // NCC, Admin = quản trị/UAT. Anh chốt khác thì SỬA ĐÚNG mảng `roleNames` dưới đây (1 chỗ),
    // KHÔNG đụng controller — máy BE không đổi theo câu trả lời, chỉ đổi ai thấy nút.
    //
    // Khuôn = `SeedKeHoachKyKetAccessAsync` (W1 S161): UPGRADE-ONLY (row có sẵn → NÂNG cờ
    // desired, KHÔNG hạ cờ nào, KHÔNG đụng Update/Delete), row chưa có (fresh DB) → tạo
    // Read+Create. CẤM SQL tay (#84): grant tay bị seeder/revoker lật ở restart kế (bài S159
    // Run #423 — 447/494 row rơi). Chạy SAU `RevokeTemporarilyHiddenModulesAsync` để thắng
    // revoke (revoker hiện KHÔNG còn nhánh `Contracts` từ S159, nhưng giữ thứ tự cho an toàn).
    // Thu hồi khi cần: xoá call trong `SeedAdminPermissionsAsync` (gate-K7 bắt bản đầu trỏ nhầm SeedAsync).
    private static async Task SeedContractCreateAccessAsync(
        ApplicationDbContext db, RoleManager<Role> roleManager, ILogger logger)
    {
        // CHỈ key root `Contracts` — đó là key mà policy `Contracts.Create` của endpoint đọc
        // (`MenuPermissionHandler` so khớp CHÍNH XÁC 1 key, KHÔNG kế thừa xuống `Ct_*`).
        // Cấp thêm cho `Ct_*` là cấp thừa: không endpoint nào gate bằng key con đó.
        const string key = MenuKeys.Contracts;
        var roleNames = new[] { AppRoles.Drafter, AppRoles.Procurement, AppRoles.Admin };

        var roles = await roleManager.Roles
            .Where(r => r.Name != null && roleNames.Contains(r.Name))
            .ToListAsync();
        if (roles.Count == 0)
        {
            logger.LogWarning("SeedContractCreateAccessAsync: skip — chưa seed role nào trong {Roles}.",
                string.Join("/", roleNames));
            return;
        }

        var roleIds = roles.Select(r => r.Id).ToList();
        var existing = (await db.Permissions
                .Where(p => p.MenuKey == key && roleIds.Contains(p.RoleId))
                .ToListAsync())
            .ToDictionary(p => p.RoleId);

        var added = 0;
        var upgraded = 0;
        foreach (var role in roles)
        {
            if (existing.TryGetValue(role.Id, out var row))
            {
                var changed = false;
                if (!row.CanRead) { row.CanRead = true; changed = true; }
                if (!row.CanCreate) { row.CanCreate = true; changed = true; }
                if (changed) upgraded++;
                continue;
            }

            db.Permissions.Add(new Permission
            {
                RoleId = role.Id,
                MenuKey = key,
                CanRead = true,
                CanCreate = true,
                CanUpdate = false,
                CanDelete = false,
            });
            added++;
        }

        if (added > 0 || upgraded > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation(
                "Seeded Contracts.Create access: {Added} added + {Upgraded} upgraded " +
                "(K7 cầu KHKK→HĐ — {Roles}; câu III default chờ ratify)",
                added, upgraded, string.Join("/", roleNames));
        }
    }

    // [S65 2026-06-16] Mở quyền XEM (Read-only) "Hồ sơ Nhân sự" cho MỌI role: anh chốt
    // public module Nhân sự (trọng tâm Hồ sơ NS) để user thường tra cứu hồ sơ nhân sự.
    // CHẠY SAU RevokeTemporarilyHiddenModulesAsync (SeedAsync) để THẮNG revoke: revoke
    // set mọi Hrm* = false cho non-Admin; method này nâng RIÊNG 2 key Hrm + Hrm_HoSo.
    //   - GIỮ ẨN: Dashboard NS (Hrm_Dashboard) + 6 catalog Hrm_Config* — vẫn bị revoke che.
    //   - Read-only: chỉ CanRead. Create/Update/Delete hồ sơ vẫn khóa qua policy
    //     Hrm_HoSo.{Create/Update/Delete} (EmployeesController) — non-Admin không có.
    //   - UPGRADE-ONLY (mirror Pe S57bis :2107): row đã tồn tại (revoke vừa set CanRead
    //     =false trên prod) → NÂNG CanRead=true. Row chưa có (DB mới) → tạo CanRead=true,
    //     cờ khác false. KHÔNG hạ + KHÔNG đụng Create/Update/Delete (không phá quyền admin).
    // Flip ẩn lại khi cần: xóa call ở SeedAsync — revoke sẽ tự che lại lần seed kế.
    private static async Task SeedAllRolesHrmProfileReadPermissionsAsync(
        ApplicationDbContext db, RoleManager<Role> roleManager, ILogger logger)
    {
        // Root nhóm "Nhân sự" (để menu group hiện chắc chắn) + leaf "Hồ sơ Nhân sự".
        // KHÔNG Hrm_Dashboard, KHÔNG Hrm_Config* (giữ ẩn theo scope anh chốt).
        var hrmKeys = new[] { MenuKeys.Hrm, MenuKeys.HrmHoSo };
        var roles = await roleManager.Roles.ToListAsync();

        var existingRows = (await db.Permissions
                .Where(p => hrmKeys.Contains(p.MenuKey))
                .ToListAsync())
            .ToDictionary(p => (p.RoleId, p.MenuKey));

        var added = 0;
        var upgraded = 0;
        foreach (var role in roles)
        {
            foreach (var key in hrmKeys)
            {
                if (existingRows.TryGetValue((role.Id, key), out var row))
                {
                    // Upgrade-only: nâng CanRead nếu đang false (revoke vừa set false).
                    if (!row.CanRead) { row.CanRead = true; upgraded++; }
                    continue;
                }

                db.Permissions.Add(new Permission
                {
                    RoleId = role.Id,
                    MenuKey = key,
                    CanRead = true,
                    CanCreate = false,
                    CanUpdate = false,
                    CanDelete = false,
                });
                added++;
            }
        }

        if (added > 0 || upgraded > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation(
                "Seeded all-roles HRM profile read perms: {Added} added + {Upgraded} upgraded " +
                "(Hrm + Hrm_HoSo CanRead — public Hồ sơ NS, S65)",
                added, upgraded);
        }
    }

    // [S69 2026-06-17] GOLIVE Văn phòng số: mở quyền XEM + TẠO (Read+Create) module Office
    // cho MỌI role — anh chốt "public văn phòng số cho all user eoffice". Self-service tạo
    // phiếu (đề xuất / đơn từ / đặt xe / ticket / đặt phòng) giống Pe (S57bis read+create).
    // CHẠY SAU RevokeTemporarilyHiddenModulesAsync (SeedAsync) để THẮNG revoke (mirror S65
    // HRM): revoke set mọi Off* = false cho non-Admin; method này nâng RIÊNG allow-list.
    //   - ALLOW (read+create, 16 key): Off root + Dashboard + DanhBa + PhongHop(View/Book) +
    //     DeXuat(List/Create/Inbox) + DonTu(Leave/Ot/Travel) + DatXe + ItTicket.
    //   - GIỮ ẨN (KHÔNG trong allow-list → revoke vẫn che cho non-Admin): Off_PhongHop_Manage
    //     (Admin CRUD phòng), Off_AttendanceReport (báo cáo admin — riêng tư), Off_ChamCong
    //     (re-parent Cá nhân — golive riêng). HRM (trừ Hồ sơ NS S65) + Personal VẪN ẩn.
    //   - Write thật (quản lý phòng/duyệt phiếu) vẫn khóa ở controller theo role; CanCreate chỉ
    //     mở menu + nút tạo self-service (API self-service = [Authorize] thường).
    //   - UPGRADE-ONLY (mirror Pe S57bis / HRM S65): row đã tồn tại (revoke vừa set false trên
    //     prod) → NÂNG CanRead+CanCreate=true. Row chưa có → tạo read+create=true, Update/Delete
    //     =false. KHÔNG hạ + KHÔNG đụng Update/Delete (không phá quyền admin đã chỉnh cao hơn).
    // Flip ẩn lại khi cần: xóa call ở SeedAsync — revoke sẽ tự che lại lần seed kế.
    private static async Task SeedAllRolesOfficeModulePermissionsAsync(
        ApplicationDbContext db, RoleManager<Role> roleManager, ILogger logger)
    {
        // Allow-list self-service Văn phòng số (KHÔNG Manage/AttendanceReport/ChamCong).
        var officeKeys = new[]
        {
            MenuKeys.Off, MenuKeys.OffDashboard, MenuKeys.OffDanhBa,
            MenuKeys.OffPhongHop, MenuKeys.OffPhongHopView, MenuKeys.OffPhongHopBook,
            MenuKeys.OffDeXuat, MenuKeys.OffDeXuatList, MenuKeys.OffDeXuatCreate, MenuKeys.OffDeXuatInbox,
            MenuKeys.OffDonTu, MenuKeys.OffDonTuLeave, MenuKeys.OffDonTuOt, MenuKeys.OffDonTuTravel,
            MenuKeys.OffDatXe, MenuKeys.OffItTicket,
        };
        var roles = await roleManager.Roles.ToListAsync();

        var existingRows = (await db.Permissions
                .Where(p => officeKeys.Contains(p.MenuKey))
                .ToListAsync())
            .ToDictionary(p => (p.RoleId, p.MenuKey));

        var added = 0;
        var upgraded = 0;
        foreach (var role in roles)
        {
            foreach (var key in officeKeys)
            {
                if (existingRows.TryGetValue((role.Id, key), out var row))
                {
                    // Upgrade-only: nâng CanRead/CanCreate nếu đang false (revoke vừa set false).
                    var changed = false;
                    if (!row.CanRead) { row.CanRead = true; changed = true; }
                    if (!row.CanCreate) { row.CanCreate = true; changed = true; }
                    if (changed) upgraded++;
                    continue;
                }

                db.Permissions.Add(new Permission
                {
                    RoleId = role.Id,
                    MenuKey = key,
                    CanRead = true,
                    CanCreate = true,
                    CanUpdate = false,
                    CanDelete = false,
                });
                added++;
            }
        }

        if (added > 0 || upgraded > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation(
                "Seeded all-roles Office module perms: {Added} added + {Upgraded} upgraded " +
                "(Văn phòng số read+create — golive S69)",
                added, upgraded);
        }
    }

    // [Plan CA S29 2026-05-22] Permission defaults cho role CatalogManager.
    // Strategy: full CRUD trên 9 menu key danh mục dùng chung:
    //   - Master (root group) + Suppliers + Projects + Departments
    //   - Catalogs (root group) + CatalogUnits + CatalogMaterials + CatalogServices + CatalogWorkItems
    // Idempotent: skip per-(role,menuKey) đã có row.
    private static async Task SeedCatalogManagerPermissionsAsync(
        ApplicationDbContext db, RoleManager<Role> roleManager, ILogger logger)
    {
        var role = await roleManager.FindByNameAsync(AppRoles.CatalogManager);
        if (role is null) return;

        var menuKeys = new[]
        {
            MenuKeys.Master,
            MenuKeys.Suppliers,
            MenuKeys.Projects,
            MenuKeys.Departments,
            MenuKeys.Catalogs,
            MenuKeys.CatalogUnits,
            MenuKeys.CatalogMaterials,
            MenuKeys.CatalogServices,
            MenuKeys.CatalogWorkItems,
        };

        var existing = await db.Permissions
            .Where(p => p.RoleId == role.Id && menuKeys.Contains(p.MenuKey))
            .Select(p => p.MenuKey)
            .ToListAsync();

        int added = 0;
        foreach (var menuKey in menuKeys)
        {
            if (existing.Contains(menuKey)) continue;
            db.Permissions.Add(new Permission
            {
                RoleId = role.Id,
                MenuKey = menuKey,
                CanRead = true,
                CanCreate = true,
                CanUpdate = true,
                CanDelete = true,
            });
            added++;
        }

        if (added > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation("Seeded {Count} CatalogManager permissions (9 menu keys)", added);
        }
    }

    // Permission defaults cho module Duyệt NCC (Pe_*). Strategy:
    //   - Drafter / DeptManager / Procurement / CostControl / Director / AuthorizedSigner / ProjectManager
    //     đều có Read + Create + Update trên CẢ 2 type (xem được, soạn / chỉnh).
    //   - Workflow admin (PeWf_*) chỉ Admin (đã grant qua MenuKeys.All) — không cấp non-admin.
    //   - Idempotent: skip per-(roleId × menuKey) đã có row.
    private static async Task SeedPurchaseEvaluationPermissionDefaultsAsync(
        ApplicationDbContext db, RoleManager<Role> roleManager, ILogger logger)
    {
        // Roles cần access PE module
        var roleNames = new[]
        {
            AppRoles.Drafter, AppRoles.DeptManager,
            AppRoles.Procurement, AppRoles.CostControl,
            AppRoles.ProjectManager,
            AppRoles.Director, AppRoles.AuthorizedSigner,
        };

        // Menu keys cần grant: PurchaseEvaluations root + per-type group + 4 leaf
        // ([Plan AA S24 t1] thêm WfView read-only matrix view).
        var menuKeys = new List<string> { MenuKeys.PurchaseEvaluations };
        foreach (var typeCode in MenuKeys.PurchaseEvaluationTypeCodes)
        {
            menuKeys.Add(MenuKeys.PurchaseEvaluationGroup(typeCode));
            menuKeys.Add(MenuKeys.PurchaseEvaluationWorkflowView(typeCode));
            menuKeys.Add(MenuKeys.PurchaseEvaluationList(typeCode));
            menuKeys.Add(MenuKeys.PurchaseEvaluationCreate(typeCode));
            menuKeys.Add(MenuKeys.PurchaseEvaluationPending(typeCode));
            // [S155 đợt 2] 2 mục menu mới ("Đã duyệt" / "Đã xóa") — mirror danh sách
            // `peKeys` trong `UpgradeReviewModulePermissionsForAllRolesAsync` (~:2092).
            menuKeys.Add(MenuKeys.PurchaseEvaluationApproved(typeCode));
            menuKeys.Add(MenuKeys.PurchaseEvaluationDeleted(typeCode));
        }

        int added = 0;
        foreach (var roleName in roleNames)
        {
            var role = await roleManager.FindByNameAsync(roleName);
            if (role is null) continue;

            var existing = await db.Permissions
                .Where(p => p.RoleId == role.Id && menuKeys.Contains(p.MenuKey))
                .Select(p => p.MenuKey)
                .ToListAsync();

            // Drafter có quyền Create + Update (soạn phiếu); Director chỉ Read +
            // Update (duyệt — xem + nhấn nút). Map default rõ ràng:
            bool canCreate = roleName is AppRoles.Drafter or AppRoles.DeptManager
                or AppRoles.Procurement;
            bool canUpdate = true;       // mọi role được transition (workflow check tách)
            bool canDelete = roleName == AppRoles.DeptManager;   // chỉ TPB xóa nháp

            foreach (var menuKey in menuKeys)
            {
                if (existing.Contains(menuKey)) continue;
                db.Permissions.Add(new Permission
                {
                    RoleId = role.Id,
                    MenuKey = menuKey,
                    CanRead = true,
                    CanCreate = canCreate,
                    CanUpdate = canUpdate,
                    CanDelete = canDelete,
                });
                added++;
            }
        }
        if (added > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation("Seeded {Count} PE permission defaults across {Roles} roles", added, roleNames.Length);
        }
    }

    // 9 departments từ QT-TP-NCC.docx — reference data, không phải demo.
    // Mỗi role-based check trên workflow tham chiếu phòng ban này.
    private static async Task SeedDepartmentsAsync(ApplicationDbContext db, ILogger logger)
    {
        var departments = new (string Code, string Name, string? Note)[]
        {
            ("PM",  "Ban Quản lý Dự án",            "PM/PD: Giám đốc Dự án, Giám đốc Thi công"),
            ("QS",  "Phòng Quantity Surveyor",      "QS công trường — tính khối lượng + soạn HĐ"),
            ("CCM", "Phòng Kiểm soát Chi phí",      "Cost Control Management — review HĐ trước khi BOD ký"),
            ("PRO", "Phòng Cung ứng",               "Procurement — NCC vật tư / thầu phụ"),
            ("FIN", "Phòng Tài chính",              "Financial — điều khoản thanh toán, bảo lãnh"),
            ("ACT", "Phòng Kế toán",                "Accounting — thuế, hóa đơn"),
            ("EQU", "Phòng Thiết bị",               "Equipment — thuê/mua máy móc"),
            ("HRA", "Phòng Nhân sự - Hành chính",   "HRA/ISO — đóng dấu HĐ sau BOD ký"),
            ("BOD", "Ban Giám đốc",                 "Board of Directors — ký duyệt HĐ"),
            ("IT",  "Phòng CNTT",                   "IT helpdesk — tiếp nhận + xử lý ticket CNTT (P11-D round-robin assign)"),
        };

        var existingCodes = await db.Departments.Select(d => d.Code).ToListAsync();
        var added = 0;
        foreach (var (code, name, note) in departments)
        {
            if (existingCodes.Contains(code)) continue;
            db.Departments.Add(new Department { Code = code, Name = name, Note = note });
            added++;
        }
        if (added > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation("Seeded {Count} departments", added);
        }
    }

    // P11-D (Wave 2) — gán 2 SAMPLE demo user vào Phòng CNTT (IT) làm IT staff
    // để round-robin auto-assign ticket có người nhận ngày 1. CHỈ đụng 2 sample
    // user (nv.cao + nv.truong — prefix nv.* = sample, KHÔNG phải 14 Solutions
    // real user). Idempotent: chỉ set DepartmentId nếu user CHƯA thuộc IT.
    //
    // ⚠️ PHẢI chạy SAU SeedDemoUsersAsync: method đó reconcile DepartmentId 2 user
    // này về PRO/CCM (hardcoded deptCode) mỗi lần boot. Chạy sau → override về IT.
    // End-state sau mỗi full SeedAsync = deterministic (IT). Infrastructure data
    // (NOT gated DemoSeed flag, gotcha #51).
    private static async Task SeedItDepartmentStaffAsync(
        ApplicationDbContext db, UserManager<User> userManager, ILogger logger)
    {
        var itDept = await db.Departments.FirstOrDefaultAsync(d => d.Code == "IT" && !d.IsDeleted);
        if (itDept is null)
        {
            logger.LogWarning("SeedItDepartmentStaffAsync: skip — phòng IT chưa seed (chạy SeedDepartmentsAsync trước).");
            return;
        }

        // 2 sample user (Drafter-only) chuyển sang IT staff. KHÔNG đụng real user.
        var sampleItStaffEmails = new[]
        {
            "nv.cao@solutions.com.vn",      // Cao Văn Long
            "nv.truong@solutions.com.vn",   // Trương Minh Quân
        };

        int assigned = 0;
        foreach (var email in sampleItStaffEmails)
        {
            var user = await userManager.FindByEmailAsync(email);
            if (user is null)
            {
                logger.LogWarning("SeedItDepartmentStaffAsync: sample user {Email} not found — skip.", email);
                continue;
            }
            if (user.DepartmentId == itDept.Id) continue;   // đã thuộc IT → idempotent skip
            user.DepartmentId = itDept.Id;
            user.UpdatedAt = DateTime.UtcNow;
            await userManager.UpdateAsync(user);
            assigned++;
        }

        if (assigned > 0)
            logger.LogInformation("SeedItDepartmentStaffAsync: gán {Count} sample user vào Phòng CNTT (IT staff).", assigned);
    }

    // Sample master data for UAT/demo — per-code idempotent (skip Code đã có,
    // add Code mới). Cho phép admin add/sửa supplier/project mà restart không
    // clobber, đồng thời cho phép expand seed list theo thời gian.
    private static async Task SeedDemoMasterDataAsync(ApplicationDbContext db, ILogger logger)
    {
        var now = DateTime.UtcNow;

        // 15 NCC covering 5 SupplierType (3 each)
        var suppliersToSeed = new[]
        {
            // NhaCungCap (5) — vật tư xây dựng
            new Supplier { Code = "NCC-VLXD", Name = "Công ty TNHH Vật liệu Xây dựng ABC", Type = SupplierType.NhaCungCap, TaxCode = "0100000001", Phone = "024 3888 1111", Email = "contact@vlxd-abc.vn", ContactPerson = "Nguyễn Văn An", Address = "123 Láng Hạ, Đống Đa, Hà Nội" },
            new Supplier { Code = "NCC-XIMANG", Name = "Công ty CP Xi măng Hà Tiên", Type = SupplierType.NhaCungCap, TaxCode = "0100000005", Phone = "028 3829 5555", Email = "sales@hatien.vn", ContactPerson = "Lê Minh Tuấn", Address = "82 Lê Lợi, Q1, TP.HCM" },
            new Supplier { Code = "NCC-THEP", Name = "Công ty CP Thép Hòa Phát", Type = SupplierType.NhaCungCap, TaxCode = "0100000006", Phone = "024 6266 4666", Email = "sales@hoaphat.com.vn", ContactPerson = "Trần Quốc Đạt", Address = "643 Phạm Văn Đồng, Bắc Từ Liêm, Hà Nội" },
            new Supplier { Code = "NCC-DIEN", Name = "Công ty TNHH Thiết bị Điện Cadivi", Type = SupplierType.NhaCungCap, TaxCode = "0100000007", Phone = "028 3925 9889", Email = "info@cadivi.com.vn", ContactPerson = "Hoàng Thị Lan", Address = "70 Nguyễn Tri Phương, Q5, TP.HCM" },
            new Supplier { Code = "NCC-NUOC", Name = "Công ty TNHH Vật tư Cấp thoát nước Tiền Phong", Type = SupplierType.NhaCungCap, TaxCode = "0100000008", Phone = "0225 3876 666", Email = "sales@tienphong-pvc.vn", ContactPerson = "Vũ Văn Bình", Address = "KCN Đình Vũ, Hải Phòng" },

            // NhaThauPhu (4) — thầu phụ thi công
            new Supplier { Code = "NTP-XD", Name = "Công ty CP Xây dựng Thăng Long", Type = SupplierType.NhaThauPhu, TaxCode = "0100000002", Phone = "024 3888 2222", Email = "info@thanglong-xd.vn", ContactPerson = "Trần Thị Bình", Address = "45 Nguyễn Chí Thanh, Đống Đa, Hà Nội" },
            new Supplier { Code = "NTP-COCONG", Name = "Công ty CP Xây dựng & Cơ khí Cô Công", Type = SupplierType.NhaThauPhu, TaxCode = "0100000009", Phone = "024 3776 8888", Email = "info@cocong.com.vn", ContactPerson = "Đỗ Mạnh Hùng", Address = "Khu CN Quang Minh, Mê Linh, Hà Nội" },
            new Supplier { Code = "NTP-MEPCO", Name = "Công ty TNHH MEP Hà Nội", Type = SupplierType.NhaThauPhu, TaxCode = "0100000010", Phone = "024 3556 9999", Email = "contact@mep-hn.vn", ContactPerson = "Nguyễn Thị Hồng", Address = "Cầu Giấy, Hà Nội", Note = "Chuyên thầu phụ M&E" },
            new Supplier { Code = "NTP-NEXTAGE", Name = "Công ty CP Hoàn thiện Nội thất Next Stage", Type = SupplierType.NhaThauPhu, TaxCode = "0100000011", Phone = "024 6688 7777", Email = "sales@nextstage.vn", ContactPerson = "Phạm Văn Khoa", Address = "Hà Đông, Hà Nội" },

            // ToDoi (3) — tổ đội nhân công
            new Supplier { Code = "TD-NEHOANG", Name = "Tổ đội Hoàng Nam", Type = SupplierType.ToDoi, Phone = "098 111 2233", ContactPerson = "Phạm Hoàng Nam", Address = "Cầu Giấy, Hà Nội", Note = "Tổ đội cốp pha 15 người" },
            new Supplier { Code = "TD-XAYTRAT", Name = "Tổ đội Xây trát Bắc Ninh", Type = SupplierType.ToDoi, Phone = "098 444 5566", ContactPerson = "Nguyễn Văn Khánh", Address = "Yên Phong, Bắc Ninh", Note = "Tổ đội xây trát 25 người" },
            new Supplier { Code = "TD-OPLAT", Name = "Tổ đội Ốp lát Hà Trang", Type = SupplierType.ToDoi, Phone = "097 222 8899", ContactPerson = "Trần Thị Hà", Address = "Hà Đông, Hà Nội", Note = "Tổ đội ốp lát + sơn bả 12 người" },

            // DonViDichVu (3) — dịch vụ
            new Supplier { Code = "DV-CLEAN", Name = "Công ty TNHH Vệ sinh Công nghiệp Clean Pro", Type = SupplierType.DonViDichVu, TaxCode = "0100000004", Phone = "024 3888 4444", Email = "sales@cleanpro.vn", Address = "Trung Hòa, Cầu Giấy, Hà Nội" },
            new Supplier { Code = "DV-VANCHUYEN", Name = "Công ty CP Vận chuyển Hồng Phát", Type = SupplierType.DonViDichVu, TaxCode = "0100000012", Phone = "024 3776 5555", Email = "info@hongphat-vc.vn", ContactPerson = "Lê Văn Dũng", Address = "Long Biên, Hà Nội", Note = "Vận chuyển vật tư + xe cẩu" },
            new Supplier { Code = "DV-BAOVE", Name = "Công ty Bảo vệ Long An", Type = SupplierType.DonViDichVu, TaxCode = "0100000013", Phone = "024 3556 6666", Email = "service@longan-bv.vn", Address = "Thanh Xuân, Hà Nội" },

            // ChuDauTu (3) — chủ đầu tư (bypass CCM)
            new Supplier { Code = "CDT-VIN", Name = "Tập đoàn Vingroup", Type = SupplierType.ChuDauTu, TaxCode = "0100109106", Phone = "024 3974 9999", Email = "contact@vingroup.net", Address = "7 Bằng Lăng 1, Vinhomes Riverside, Long Biên, Hà Nội", Note = "Chủ đầu tư - bypass CCM" },
            new Supplier { Code = "CDT-SUNGROUP", Name = "Tập đoàn Sun Group", Type = SupplierType.ChuDauTu, TaxCode = "0100000014", Phone = "024 3939 8888", Email = "contact@sungroup.com.vn", Address = "Hoàn Kiếm, Hà Nội", Note = "Chủ đầu tư - bypass CCM" },
            new Supplier { Code = "CDT-MASTERISE", Name = "Masterise Homes", Type = SupplierType.ChuDauTu, TaxCode = "0100000015", Phone = "028 3939 7777", Email = "contact@masterisehomes.com", Address = "Q2, TP.HCM", Note = "Chủ đầu tư - bypass CCM" },
        };

        var existingSupplierCodes = await db.Suppliers.Select(s => s.Code).ToListAsync();
        int addedSuppliers = 0;
        foreach (var s in suppliersToSeed)
        {
            if (existingSupplierCodes.Contains(s.Code)) continue;
            s.IsPublic = true;                 // Mig 64 (S113) — sample NCC đều có Code → công bố
            db.Suppliers.Add(s);
            addedSuppliers++;
        }
        if (addedSuppliers > 0) logger.LogInformation("Seeded {Count} demo suppliers", addedSuppliers);

        // 8 dự án (đa dạng quy mô + giai đoạn)
        var projectsToSeed = new[]
        {
            new Project { Code = "FLOCK01",     Name = "Dự án FLOCK 01 — Khu đô thị Mỹ Đình",       StartDate = now.AddMonths(-3),  EndDate = now.AddMonths(18), BudgetTotal = 120_000_000_000m, Note = "Dự án mẫu — demo" },
            new Project { Code = "SKYGARDEN",   Name = "Sky Garden Residence",                        StartDate = now.AddMonths(-6),  EndDate = now.AddMonths(12), BudgetTotal = 85_000_000_000m,  Note = "Dự án mẫu — demo" },
            new Project { Code = "INDUSTRIAL",  Name = "Nhà máy Công nghiệp Yên Phong",               StartDate = now.AddMonths(-1),  EndDate = now.AddMonths(9),  BudgetTotal = 45_000_000_000m,  Note = "Dự án mẫu — demo" },
            new Project { Code = "VHOMES-OP",   Name = "Vinhomes Ocean Park 3 — Khu Sapphire",        StartDate = now.AddMonths(-12), EndDate = now.AddMonths(24), BudgetTotal = 350_000_000_000m, Note = "Khu đô thị quy mô lớn" },
            new Project { Code = "ECOPARK-VL",  Name = "Ecopark Văn Lâm — Tổ hợp biệt thự",           StartDate = now.AddMonths(-4),  EndDate = now.AddMonths(20), BudgetTotal = 180_000_000_000m, Note = "Biệt thự + nhà phố" },
            new Project { Code = "BWTOWER",     Name = "Tòa nhà Văn phòng BW Tower — Cầu Giấy",       StartDate = now.AddMonths(2),   EndDate = now.AddMonths(28), BudgetTotal = 95_000_000_000m,  Note = "Văn phòng cho thuê 25 tầng" },
            new Project { Code = "WAREHOUSE-LB",Name = "Kho vận Long Biên — Phase 1",                 StartDate = now.AddMonths(-2),  EndDate = now.AddMonths(8),  BudgetTotal = 32_000_000_000m,  Note = "Kho lạnh + kho thường" },
            new Project { Code = "RESORT-PQ",   Name = "Resort 5* Phú Quốc — Bãi Trường",             StartDate = now.AddMonths(-8),  EndDate = now.AddMonths(16), BudgetTotal = 220_000_000_000m, Note = "Resort 250 phòng + biệt thự" },
        };

        var existingProjectCodes = await db.Projects.Select(p => p.Code).ToListAsync();
        int addedProjects = 0;
        foreach (var p in projectsToSeed)
        {
            if (existingProjectCodes.Contains(p.Code)) continue;
            db.Projects.Add(p);
            addedProjects++;
        }
        if (addedProjects > 0) logger.LogInformation("Seeded {Count} demo projects", addedProjects);

        if (addedSuppliers + addedProjects > 0)
            await db.SaveChangesAsync();
    }

    // S55 (2026-06-09) — Import master data thật của công ty (62 dự án + 71 hạng
    // mục công việc + 3 NCC) từ file Excel `scripts/master-import-data.generated.md`.
    // Data hardcode inline vì runtime KHÔNG đọc được scratch file đó.
    //
    // NOT gated DemoSeed:Disabled — đây là INFRASTRUCTURE master data thật (như
    // Roles/Departments/Catalogs), coexist với demo data qua per-code idempotent
    // (gotcha #51). Mirror pattern SeedDemoMasterDataAsync: load existing codes →
    // foreach skip-if-contains else Add → 1 SaveChanges.
    //
    // KNOWN collision: project "FLOCK01" cũng là demo project (SeedDemoMasterDataAsync)
    // → per-code idempotent SKIP nó (demo thắng). Chấp nhận được — FLOCK01 thật chỉ
    // có Code + Year. Không special-case.
    private static async Task SeedRealMasterDataAsync(ApplicationDbContext db, ILogger logger)
    {
        // ---------- PROJECTS (62) — (Code, Year, Name [=Code nếu blank], Package, Investor, Location) ----------
        var projects = new (string Code, int? Year, string? Name, string? Package, string? Investor, string? Location)[]
        {
            ("APVN01", 2023, null, null, null, null),
            ("AUS01", 2025, null, null, null, null),
            ("BVN01", 2025, null, null, null, null),
            ("CAL01", 2025, "Phân xưởng homecare 75 tấn/ngày và các hạng mục phụ trợ", null, "Công ty TNHH Calofic", "Lô C18-C24, Khu Công Nghiệp Hiệp Phước, Xã Hiệp Phước, Thành Phố Hồ Chí Minh"),
            ("CET01", 2023, null, null, null, null),
            ("CET02", 2023, null, null, null, null),
            ("EQU01", null, null, null, null, null),
            ("EVO01", null, null, null, null, null),
            ("FLEMING01", null, null, null, null, null),
            ("FLEMING01.24", null, null, null, null, null),
            ("FLEMING01.25", null, null, null, null, null),
            ("FLEMING03", 2023, null, null, null, null),
            ("FLOCK01", 2024, null, null, null, null),
            ("FLOCK01.25", 2024, null, null, null, null),
            ("FLOCK01.26", 2024, null, null, null, null),
            ("FLOCK02", 2024, null, null, null, null),
            ("FLOCK02.25", 2024, null, null, null, null),
            ("FLOCK03", 2026, null, null, null, null),
            ("FLOCK04", 2026, null, null, null, null),
            ("GCP01", null, null, null, null, null),
            ("GO01", 2023, null, null, null, null),
            ("GST01", 2024, null, null, null, null),
            ("ILD01", null, null, null, null, null),
            ("IML06", null, null, null, null, null),
            ("MASAN01", null, null, null, null, null),
            ("MIDEA01", null, "Nhà máy Midea", "Công trình xây dựng tầng lửng Nhà Máy 2 SDA Việt Nam Năm 2025", "Công Ty TNHH Midea Consumer Electric (Vietnam)", "Xưởng A2-1, A2-3, Lô A3, Đường NA2, Khu Công Nghiệp Thới Hòa, Phường Thới Hòa, Thành Phố Hồ Chí Minh"),
            ("NTBA01", 2023, null, null, null, null),
            ("NTBA01.25", 2025, null, null, null, null),
            ("OLAM01", null, null, null, null, null),
            ("OLAM02", null, null, null, null, null),
            ("PMH01", null, null, null, null, null),
            ("REDBULL01", 2021, null, null, null, null),
            ("REDBULL03", 2021, null, null, null, null),
            ("RMIT01", null, null, null, null, null),
            ("SAM01", 2024, "Xây dựng nhà kho SAMHWA-VH", "Thi công chính", "Công ty TNHH SAMHWA-VH CO., LTD", "Lô số b-13, đường d2, Phường Long Đức, Xã An Phước, Huyện Long Thành, Tỉnh Đồng Nai"),
            ("SEW01", 2023, null, null, null, null),
            ("SHEICO01", null, null, null, null, null),
            ("SHEICO01B", null, null, null, null, null),
            ("SHEICO01C", null, null, null, null, null),
            ("SHEICO03", null, null, null, null, null),
            ("SHEICO03.ELEVATOR", null, null, null, null, null),
            ("SHEICO03.MEP", null, null, null, null, null),
            ("SHEICO04", null, null, null, null, null),
            ("SHEICO04.14", null, null, null, null, null),
            ("SHEICO04.15ME", null, null, null, null, null),
            ("SHEICO04.16ME", null, null, null, null, null),
            ("SHEICO04.17ME", null, null, null, null, null),
            ("SHEICO04.9", null, null, null, null, null),
            ("SOL01", null, null, null, null, null),
            ("SOVI01", null, null, null, null, null),
            ("TBA01", null, null, null, null, null),
            ("TLB01", 2023, "Merlion Trelleborg", "Thiết Kế & Thi Công", "Công Ty TNHH Trelleborg Việt Nam", "Lô Số F2, Đường Số 2, Phường Phước Hòa, Thị Xã Phú Mỹ, Tỉnh Bà Rịa - Vũng Tàu"),
            ("TTC01", null, null, null, null, null),
            ("VLU04", null, null, null, null, null),
            ("VLU04.1", null, null, null, null, null),
            ("VP2024", null, null, null, null, null),
            ("VPS01", null, null, null, null, null),
            ("VRR01", null, null, null, null, null),
            ("WFW01", null, null, null, null, null),
            ("WPL01", null, null, null, null, null),
            ("WPL02", null, null, null, null, null),
            ("ZOTE01", 2025, "Zotefoams new manufacturing plant", "Building and building service", "Zotefoams PLC", "Changshin VJ2, Khu Công Nghiệp Lộc An - Bình Sơn, Xã Long Thành, Tỉnh Đồng Nai"),
        };

        var existingProjectCodes = await db.Projects.Select(p => p.Code).ToListAsync();
        int addedProjects = 0;
        foreach (var (code, year, name, package, investor, location) in projects)
        {
            if (existingProjectCodes.Contains(code)) continue;   // FLOCK01 collision → demo thắng, skip
            db.Projects.Add(new Project
            {
                Code = code,
                Name = string.IsNullOrWhiteSpace(name) ? code : name,
                Year = year,
                Package = package,
                Investor = investor,
                Location = location,
            });
            addedProjects++;
        }
        if (addedProjects > 0) logger.LogInformation("Seeded {Count} real projects (S55 import)", addedProjects);

        // ---------- WORKITEMS (71) — (Code, Category, Name) ----------
        // S59 (2026-06-11) anh Kiệt FDC chốt format theo file PMH: Mã CV = nhóm+STT
        // không pad (MAT-1 / SUB-1 / MEP-SUB-1 / MEP-EQU-1), Tên CV = "STT nhóm tên"
        // ("1 MEP Sub MEP (Full)" — "đúng kiểu vậy"). Đã UPDATE rename prod+Dev cùng
        // commit (scripts/s59-rename-workitems-pmh.sql) — giữ nguyên Id rows.
        // FE sort client numeric-aware (mã không pad → string-sort loạn "10"<"2").
        var workItems = new (string Code, string Category, string Name)[]
        {
            ("MAT-1", "Vật tư - Xây dựng", "1 Mat Bê tông"),
            ("MAT-2", "Vật tư - Xây dựng", "2 Mat Thép cây, Thép lưới hàn"),
            ("MAT-3", "Vật tư - Xây dựng", "3 Mat Cọc ly tâm"),
            ("MAT-4", "Vật tư - Xây dựng", "4 Mat Cống ly tâm"),
            ("MAT-5", "Vật tư - Xây dựng", "5 Mat Cừ tràm"),
            ("MAT-6", "Vật tư - Xây dựng", "6 Mat Kim loại ( rọ đầu cọc, V mạ kẽm, Vinox, bản mã…)"),
            ("MAT-7", "Vật tư - Xây dựng", "7 Mat Nắp gang"),
            ("MAT-8", "Vật tư - Xây dựng", "8 Mat Gạch xây, Xi măng, cát, đá, gạch trồng cỏ"),
            ("MAT-9", "Vật tư - Xây dựng", "9 Mat Gạch trồng cỏ, gạch trang trí…"),
            ("MAT-10", "Vật tư - Xây dựng", "10 Mat Gạch ốp lát"),
            ("MAT-11", "Vật tư - Xây dựng", "11 Mat Ván khuôn"),
            ("MAT-12", "Vật tư - Xây dựng", "12 Mat Sơn nước"),
            ("MAT-13", "Vật tư - Xây dựng", "13 Mat Phụ gia (sika, vinkem, Gps…)"),
            ("MAT-14", "Vật tư - Xây dựng", "14 Mat PVC, vải địa"),
            ("MAT-15", "Vật tư - Xây dựng", "15 Mat Nẹp inox, nẹp nhựa"),
            ("MAT-16", "Vật tư - Xây dựng", "16 Mat Khác"),
            ("SUB-1", "Thầu phụ - Xây dựng", "1 Sub Thầu phụ cọc ( cơ, robot)"),
            ("SUB-2", "Thầu phụ - Xây dựng", "2 Sub Thầu phụ cọc Khoan nhồi"),
            ("SUB-3", "Thầu phụ - Xây dựng", "3 Sub Thầu phụ cọc CDM"),
            ("SUB-4", "Thầu phụ - Xây dựng", "4 Sub Kết cấu thép"),
            ("SUB-5", "Thầu phụ - Xây dựng", "5 Sub Cáp dự ứng lực"),
            ("SUB-6", "Thầu phụ - Xây dựng", "6 Sub Panel"),
            ("SUB-7", "Thầu phụ - Xây dựng", "7 Sub Nhôm kính, alu"),
            ("SUB-8", "Thầu phụ - Xây dựng", "8 Sub Cửa thép, Cửa cuốn, sectional door"),
            ("SUB-9", "Thầu phụ - Xây dựng", "9 Sub Cửa gỗ"),
            ("SUB-10", "Thầu phụ - Xây dựng", "10 Sub Cơ khí hoàn thiện lan can, cầu thang"),
            ("SUB-11", "Thầu phụ - Xây dựng", "11 Sub Dây cứu sinh"),
            ("SUB-12", "Thầu phụ - Xây dựng", "12 Sub Nội thất"),
            ("SUB-13", "Thầu phụ - Xây dựng", "13 Sub Vách trần thạch cao"),
            ("SUB-14", "Thầu phụ - Xây dựng", "14 Sub Vách ngăn di động"),
            ("SUB-15", "Thầu phụ - Xây dựng", "15 Sub Vách ngăn vệ sinh"),
            ("SUB-16", "Thầu phụ - Xây dựng", "16 Sub Granit"),
            ("SUB-17", "Thầu phụ - Xây dựng", "17 Sub Hoàn thiện sàn thảm, vinyl"),
            ("SUB-18", "Thầu phụ - Xây dựng", "18 Sub Chống mối"),
            ("SUB-19", "Thầu phụ - Xây dựng", "19 Sub Chống thấm"),
            ("SUB-20", "Thầu phụ - Xây dựng", "20 Sub Sơn PU, Epoxy, liquid"),
            ("SUB-21", "Thầu phụ - Xây dựng", "21 Sub Dock Leveler."),
            ("SUB-22", "Thầu phụ - Xây dựng", "22 Sub Cẩu trục"),
            ("SUB-23", "Thầu phụ - Xây dựng", "23 Sub Trạm cân"),
            ("SUB-24", "Thầu phụ - Xây dựng", "24 Sub Thang máy, thang hàng, …"),
            ("SUB-25", "Thầu phụ - Xây dựng", "25 Sub Sàn nâng."),
            ("SUB-26", "Thầu phụ - Xây dựng", "26 Sub Landscape"),
            ("SUB-27", "Thầu phụ - Xây dựng", "27 Sub Cổng xếp."),
            ("SUB-28", "Thầu phụ - Xây dựng", "28 Sub Logo, Bảng hiệu."),
            ("SUB-29", "Thầu phụ - Xây dựng", "29 Sub TP thi công Màng nhựa"),
            ("SUB-30", "Thầu phụ - Xây dựng", "30 Sub Khác"),
            ("MEP-SUB-1", "MEP", "1 MEP Sub MEP (Full)"),
            ("MEP-SUB-2", "MEP", "2 MEP Sub Hệ thống trung thế"),
            ("MEP-SUB-3", "MEP", "3 MEP Sub PCCC (Phòng cháy chữa cháy)."),
            ("MEP-SUB-4", "MEP", "4 MEP Sub Hệ thống điện, điện nhẹ"),
            ("MEP-SUB-5", "MEP", "5 MEP Sub Hệ thống HVAC, Utility (Thông gió & Điều hòa không khí & Phù trợ)."),
            ("MEP-SUB-6", "MEP", "6 MEP Sub Hệ thống cấp thoát nước"),
            ("MEP-SUB-7", "MEP", "7 MEP Sub Hệ thống xử lý nước thải"),
            ("MEP-SUB-8", "MEP", "8 MEP Sub Hệ thống solar"),
            ("MEP-SUB-9", "MEP", "9 MEP Sub Khác"),
            ("MEP-EQU-1", "Thiết bị", "1 MEP _ Equ Máy biến áp"),
            ("MEP-EQU-2", "Thiết bị", "2 MEP _ Equ Tủ điện"),
            ("MEP-EQU-3", "Thiết bị", "3 MEP _ Equ Tủ trung thế."),
            ("MEP-EQU-4", "Thiết bị", "4 MEP _ Equ Busway"),
            ("MEP-EQU-5", "Thiết bị", "5 MEP _ Equ Cáp điện"),
            ("MEP-EQU-6", "Thiết bị", "6 MEP _ Equ Thiết bị Utility (Máy nén khí, Hút bụi hút mùi, Boiler,...)"),
            ("MEP-EQU-7", "Thiết bị", "7 MEP _ Equ Máy lạnh"),
            ("MEP-EQU-8", "Thiết bị", "8 MEP _ Equ Ống thép, inox"),
            ("MEP-EQU-9", "Thiết bị", "9 MEP _ Equ Thang máng cáp"),
            ("MEP-EQU-10", "Thiết bị", "10 MEP _ Equ Ống PVC, HDPE"),
            ("MEP-EQU-11", "Thiết bị", "11 MEP _ Equ Thiết bị báo cháy"),
            ("MEP-EQU-12", "Thiết bị", "12 MEP _ Equ Đèn (đèn chiếu sáng, đèn năng lượng, đèn exit….)"),
            ("MEP-EQU-13", "Thiết bị", "13 MEP _ Equ Thiết bị chữa cháy (Sprinkler, Chữa cháy khí, FM200,…)"),
            ("MEP-EQU-14", "Thiết bị", "14 MEP _ Equ Bơm (bơm PCCC, Bơm nước…)"),
            ("MEP-EQU-15", "Thiết bị", "15 MEP _ Equ Bồn nước, Thiết bị vệ sinh"),
            ("MEP-EQU-16", "Thiết bị", "16 MEP _ Equ Khác"),
        };

        var existingWorkItemCodes = await db.WorkItems.Select(w => w.Code).ToListAsync();
        int addedWorkItems = 0;
        foreach (var (code, category, name) in workItems)
        {
            if (existingWorkItemCodes.Contains(code)) continue;
            db.WorkItems.Add(new WorkItem { Code = code, Name = name, Category = category, IsActive = true });
            addedWorkItems++;
        }
        if (addedWorkItems > 0) logger.LogInformation("Seeded {Count} real work items (S55 import)", addedWorkItems);

        // ---------- 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[]
        {
            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,
                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 = "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,
                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" },
        };

        // 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 filledSuppliers = 0;
        foreach (var s in suppliers)
        {
            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 (!e.IsPublic) { e.IsPublic = true; changed = true; }   // Mig 64 (S113) — 4 NCC real live, luôn công bố
                if (changed) filledSuppliers++;
                continue;
            }
            s.IsPublic = true;                 // Mig 64 (S113) — 4 NCC real đều có Code → công bố
            db.Suppliers.Add(s);
            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 + filledSuppliers > 0)
            await db.SaveChangesAsync();
    }

    // [K1 — S164, Mig 70] Danh mục Hợp đồng theo quy trình SOL-CCM-SP-002.v01
    // (bảng 5.1 mục A1..C3 = 85 dòng + mục 5.2 CEO 1 dòng = 86 row). Mỗi dòng gắn 1
    // trong 8 NHÓM DUYỆT N1..N8 — nhóm quyết định workflow KHKK nào áp cho phiếu (K2/K3).
    //
    // NOT gated DemoSeed:Disabled — đây là INFRASTRUCTURE master data thật (mirror
    // SeedRealMasterDataAsync, xem note :2893 gotcha #51), phải có mặt prod ngay deploy đầu.
    //
    // Idempotent PER-CODE + `IgnoreQueryFilters()` — né #75/#76 (resurrect): row đã
    // xoá-mềm VẪN tính là "đã tồn tại" ⇒ KHÔNG re-create mỗi restart. Cố ý LÀM ĐÚNG
    // HƠN khuôn :2970 (khuôn dò `db.Projects.Select(p => p.Code)` KHÔNG IgnoreQueryFilters
    // ⇒ row soft-deleted vô hình → hồi sinh mỗi lần boot).
    //
    // SignerRole + InitialsChain = TEXT denorm theo nhóm (QĐ-4 spec S164: TEXT in trên
    // khối ký bản cứng GĐ4 ⟂ ApprovalWorkflowLevel.ApproverUserId = người duyệt THẬT
    // trên hệ thống). Lấy từ lookup 8-nhóm rồi ghi vào TỪNG row (denorm per-row) —
    // chép tay cùng chuỗi 86 lần dễ lệch ký tự.
    private static async Task SeedContractCatalogAsync(ApplicationDbContext db, ILogger logger)
    {
        // 8 nhóm duyệt — (Ký chính, Ký nháy Ô1→Ô4). Ô "N/A" trong bảng gốc đã BỎ.
        var signByGroup = new Dictionary<int, (string SignerRole, string InitialsChain)>
        {
            [1] = ("TP.CCM", "TBP.CCM → NV.CCM → TBP.EQU → NV.EQU"),
            [2] = ("TP.CCM", "TBP.CCM → PD/PM → SM → QS"),
            [3] = ("TP.CCM", "TBP.CCM → TPB/TBP → TBP/NV.PB"),
            [4] = ("TP.PRO", "TBP.PRO → NV.PRO"),
            [5] = ("TP.PRO", "TP.CCM → TBP.PRO → PD/PM/SM → QS"),
            [6] = ("TP.PRO", "TP.CCM → TBP.PRO → PD/PM/SM → MEP SM"),
            [7] = ("PD/PM", "TP.CCM → SM → QS"),
            [8] = ("CEO", "TP.CCM → TP.PRO"),
        };

        // (Code, TenVi, TenEn, GroupCode, ApprovalGroup 1-8, SortOrder = STT trong section)
        // Nguồn: transcribe 7 ảnh SOL-CCM-SP-002.v01 trang 5-9 (OG-6 owner soát).
        var entries = new (string Code, string TenVi, string TenEn, string GroupCode, int ApprovalGroup, int SortOrder)[]
        {
            // ----- A1 · Thiết bị, máy móc, vật tư phụ (5) → N1 -----
            ("A1-01", "Thuê thiết bị/máy móc/dụng cụ thi công/thiết bị an toàn lao động/thiết bị văn phòng (máy photo, máy in)", "Rental of construction equipment, machinery, and tools; occupational safety equipment; and office equipment (photocopiers, printers)", "A1", 1, 1),
            ("A1-02", "Thuê container văn phòng, văn phòng BCH CT, nhà vệ sinh, nhà bảo vệ", "Rental of office containers, site offices, toilets, and guardhouses", "A1", 1, 2),
            ("A1-03", "Cung cấp vật tư phụ phục vụ công tác tạm/tiện ích công trường", "Supply of materials for temporary works and site utilities", "A1", 1, 3),
            ("A1-04", "Cung cấp vật tư phụ thi công: nylon bảo dưỡng, Sika, khoan cấy, …", "Supply of auxiliary construction materials: curing nylon sheets, Sika products, chemical anchors, etc", "A1", 1, 4),
            ("A1-05", "Cung cấp vật tư biện pháp: sika, lưới mắt cáo, thanh trương nở, …", "Supply of method-related materials: Sika products, wire mesh, expansion bars, etc", "A1", 1, 5),

            // ----- A2 · Kết cấu, hạ tầng, phá dỡ (1) → N2 -----
            ("A2-01", "Thầu phụ hạ tầng & phá dỡ", "Infrastructure and demolition subcontracting works", "A2", 2, 1),

            // ----- A3 · Dịch vụ hỗ trợ thi công (9) → N2 -----
            ("A3-01", "Dịch vụ ca cẩu", "Crane rental services", "A3", 2, 1),
            ("A3-02", "Dịch vụ vận chuyển", "Transportation services", "A3", 2, 2),
            ("A3-03", "Trắc đạc", "Surveying services", "A3", 2, 3),
            ("A3-04", "Thí nghiệm", "Testing services", "A3", 2, 4),
            ("A3-05", "Bảo vệ", "Security services", "A3", 2, 5),
            ("A3-06", "Dịch vụ giấy phép, tư vấn, thiết kế (kể cả dịch vụ tư vấn MEP)", "Licensing, consultancy, and design services (including MEP consultancy services)", "A3", 2, 6),
            ("A3-07", "Điện tạm", "Temporary power supply", "A3", 2, 7),
            ("A3-08", "Cấp và thoát nước tạm", "Temporary water supply and drainage", "A3", 2, 8),
            ("A3-09", "Cung cấp tiện ích công trường: tủ hồ sơ, bàn ghế, internet, văn phòng phẩm, thiết bị bảo hộ lao động (PPE), nước uống, dịch vụ y tế, bảng biểu an toàn, …", "Supply of site utilities: filing cabinets, desks and chairs, internet, stationery, personal protective equipment (PPE), drinking water, medical services, safety signage, etc.", "A3", 2, 9),

            // ----- A4 · Hoạt động văn phòng (5) → N3 -----
            ("A4-01", "Thuê văn phòng", "Office leasing", "A4", 3, 1),
            ("A4-02", "Văn phòng phẩm", "Stationery", "A4", 3, 2),
            ("A4-03", "Bảo hiểm cháy nổ văn phòng", "Office fire and explosion insurance", "A4", 3, 3),
            ("A4-04", "Thiết bị máy tính, phần mềm", "Computer equipment and software", "A4", 3, 4),
            ("A4-05", "Các hợp đồng khác liên quan đến hoạt động văn phòng", "Other contracts related to office operations", "A4", 3, 5),

            // ----- B1 · Vật tư (23) → N4 -----
            ("B1-01", "Đá", "Stone", "B1", 4, 1),
            ("B1-02", "Cát", "Sand", "B1", 4, 2),
            ("B1-03", "Cừ tràm", "Mangrove piles", "B1", 4, 3),
            ("B1-04", "Bê tông", "Concrete", "B1", 4, 4),
            ("B1-05", "Xi măng", "Cement", "B1", 4, 5),
            ("B1-06", "Thép", "Steel", "B1", 4, 6),
            ("B1-07", "Ván phủ phim", "Film-faced plywood", "B1", 4, 7),
            ("B1-08", "Gạch ốp lát", "Floor and wall tiles", "B1", 4, 8),
            ("B1-09", "Gạch xây", "Bricks", "B1", 4, 9),
            ("B1-10", "Nylon", "Nylon sheets", "B1", 4, 10),
            ("B1-11", "Vữa grouting", "Grouting", "B1", 4, 11),
            ("B1-12", "Chất tăng cứng bề mặt", "Hardener", "B1", 4, 12),
            ("B1-13", "Sơn nước", "Emulsion paint", "B1", 4, 13),
            ("B1-14", "Sơn dầu", "Oil-based paint", "B1", 4, 14),
            ("B1-15", "Bột matic", "Matic", "B1", 4, 15),
            ("B1-16", "Vật liệu hoàn thiện sàn", "Floor finishing materials", "B1", 4, 16),
            ("B1-17", "Thanh cản nước", "Waterbar", "B1", 4, 17),
            ("B1-18", "Vật liệu chống thấm", "Waterproofing", "B1", 4, 18),
            ("B1-19", "Đá granite", "Granite stone", "B1", 4, 19),
            ("B1-20", "Thiết bị vệ sinh", "Sanitary equipment", "B1", 4, 20),
            ("B1-21", "Vật liệu hoàn thiện", "Finishing materials", "B1", 4, 21),
            ("B1-22", "Vật tư, thiết bị MEP (cáp điện, tủ điện, máy móc của hệ MEP)", "MEP materials and equipment (electrical cables, panels, machinery for MEP systems)", "B1", 4, 22),
            ("B1-23", "Vật tư chính khác", "Other main materials", "B1", 4, 23),

            // ----- B2 · Thầu phụ xây dựng (14) → N5 -----
            // ⚠️ Ảnh gốc đánh số 1-13 nhưng TRÙNG số "7" ×2 (kim loại + cảnh quan);
            // lead transcribe đánh lại 01-14 ⇒ B2 = 14 dòng (KHÔNG phải 13). Giữ AS-IS.
            ("B2-01", "Ép cọc, cừ Larsen", "Pile works and sheet pile", "B2", 5, 1),
            ("B2-02", "Trần thạch cao", "Gypsum ceiling works", "B2", 5, 2),
            ("B2-03", "Đá granite", "Granite", "B2", 5, 3),
            ("B2-04", "Kết cấu thép", "Steel structure", "B2", 5, 4),
            ("B2-05", "Cửa đi, cửa sổ", "Doors and windows", "B2", 5, 5),
            ("B2-06", "Cửa chống cháy, thoát hiểm", "Fire-rated and emergency exit doors", "B2", 5, 6),
            ("B2-07", "Công tác kim loại", "Metal", "B2", 5, 7),                    // ảnh: số 7 thứ nhất
            ("B2-08", "Cảnh quan", "Landscaping", "B2", 5, 8),                      // ảnh: số 7 thứ hai ⚠️
            ("B2-09", "Công tác kính", "Glazing", "B2", 5, 9),                      // ảnh: số 8
            ("B2-10", "Rèm cửa", "Curtain", "B2", 5, 10),                           // ảnh: 9
            ("B2-11", "Nội thất", "Interior", "B2", 5, 11),                         // ảnh: 10
            ("B2-12", "Chống thấm", "Waterproofing", "B2", 5, 12),                  // ảnh: 11
            ("B2-13", "Hoàn thiện sàn nâng", "Raised floor finishing", "B2", 5, 13),// ảnh: 12
            ("B2-14", "Thầu phụ xây dựng khác", "Other civil subcontractors", "B2", 5, 14), // ảnh: 13

            // ----- B3 · Thầu phụ MEP (15) → N6 -----
            ("B3-01", "Hệ thống điện trung thế", "Medium-voltage power systems", "B3", 6, 1),
            ("B3-02", "Máy phát điện", "Generator systems", "B3", 6, 2),
            ("B3-03", "Hệ thống điện hạ thế và chiếu sáng", "Low-voltage power and lighting systems", "B3", 6, 3),
            ("B3-04", "Hệ thống điện nhẹ ELV", "Extra Low Voltage (ELV) systems", "B3", 6, 4),
            ("B3-05", "Hệ thống lạnh (Máy lạnh, Chiller, Cooling Tower, Process cooling...)", "Cooling systems (air conditioning, chillers, cooling towers, process cooling, etc.)", "B3", 6, 5),
            ("B3-06", "Hệ thống thông gió", "Ventilation systems", "B3", 6, 6),
            ("B3-07", "Hệ thống cấp thoát nước", "Water supply and drainage systems", "B3", 6, 7),
            ("B3-08", "Hệ thống xử lý nước", "Water treatment systems", "B3", 6, 8),
            ("B3-09", "Hệ thống chữa cháy", "Firefighting systems", "B3", 6, 9),
            ("B3-10", "Hệ thống báo cháy", "Fire alarm systems", "B3", 6, 10),
            ("B3-11", "Hệ thống chống sét", "Lightning protection systems", "B3", 6, 11),
            ("B3-12", "Hệ thống Gas", "Gas systems", "B3", 6, 12),
            ("B3-13", "Hệ thống khí nén", "Compressed air systems", "B3", 6, 13),
            ("B3-14", "Thiết bị bếp", "Kitchen equipment systems", "B3", 6, 14),
            ("B3-15", "Thầu phụ MEP khác", "Other MEP subcontractors", "B3", 6, 15),

            // ----- C1 · Kết cấu, công tác tạm (6) → N7 -----
            ("C1-01", "Nhân công công tác tạm, vệ sinh công nghiệp", "Labor for temporary works and industrial cleaning", "C1", 7, 1),
            ("C1-02", "Nhân công khoán đội, công nhật", "Construction teams and daily workers", "C1", 7, 2),
            ("C1-03", "Nhân công thầu phụ", "Subcontractor labor", "C1", 7, 3),
            ("C1-04", "Thầu phụ trọn gói", "Lump-sum subcontractors", "C1", 7, 4),
            ("C1-05", "Thầu phụ giàn giáo", "Scaffolding subcontractors", "C1", 7, 5),
            ("C1-06", "Cơ giới, ca máy", "Machinery and equipment shifts", "C1", 7, 6),

            // ----- C2 · Xây tô, sơn nước, hoàn thiện, ốp lát (3) → N7 -----
            ("C2-01", "Thi công sơn nước", "Painting", "C2", 7, 1),
            ("C2-02", "Thi công công tác hoàn thiện (xây tô, ốp lát)", "Finishing works (masonry, plastering, tiling)", "C2", 7, 2),
            ("C2-03", "Thi công defect, bó vỉa, xoa nền, …", "Defect rectification works, kerb works, floor finishing/polishing, etc.", "C2", 7, 3),

            // ----- C3 · Khác (4) → N7 -----
            ("C3-01", "Thuê nhà ở cho BCH CT", "House rental", "C3", 7, 1),
            ("C3-02", "Bảo hiểm tai nạn 24/24", "24/7 accident insurance", "C3", 7, 2),
            ("C3-03", "Ban quản lý khu công nghiệp", "Industrial Park Management Board", "C3", 7, 3),
            ("C3-04", "Mua bán điện ngoài mục đích sinh hoạt với Công ty Điện lực", "Electricity supply agreement for non-residential use with EVN", "C3", 7, 4),

            // ----- 5.2 · CEO (1) → N8 -----
            ("CEO-01", "Tất cả Hợp đồng chưa Ủy quyền", "All Contracts Not Subject to Authorization", "CEO", 8, 1),
        };

        var existingCodes = await db.ContractCatalogEntries
            .IgnoreQueryFilters()          // né #75/#76 — row soft-deleted vẫn chặn re-create
            .Select(x => x.Code)
            .ToListAsync();

        var added = 0;
        foreach (var (code, tenVi, tenEn, groupCode, approvalGroup, sortOrder) in entries)
        {
            if (existingCodes.Contains(code)) continue;
            var sign = signByGroup[approvalGroup];
            db.ContractCatalogEntries.Add(new ContractCatalogEntry
            {
                Code = code,
                TenVi = tenVi,
                TenEn = tenEn,
                GroupCode = groupCode,
                ApprovalGroup = approvalGroup,
                SignerRole = sign.SignerRole,
                InitialsChain = sign.InitialsChain,
                NduqNote = null,      // [OG-3] ảnh KHÔNG có số NĐUQ per-dòng → nhập tay sau
                IsActive = true,
                SortOrder = sortOrder,
            });
            added++;
        }

        if (added > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation("Seeded {Count} contract catalog entries (S164 SP-002)", added);
        }
    }

    private static async Task SeedContractTemplatesAsync(ApplicationDbContext db, ILogger logger)
    {
        // Chỉ IsActive=true nếu file thực tế tồn tại trong wwwroot/templates/.
        // 3 file .doc (FO-002.02/03/06) chưa convert sẽ IsActive=false.
        var templates = new (string FormCode, string Name, ContractType? Type, string FileName, string Format, string? Description)[]
        {
            ("SOL-CCM-FO-002.01", "Bảng kiểm tra hợp đồng", null,
                "SOL-CCM-FO-002.01.v01 Bang kiem tra hop dong.docx", "docx",
                "Checklist duyệt HĐ theo 4 bộ phận: Đề xuất / Cung ứng / CCM / Giám đốc"),
            ("SOL-CCM-FO-002.02", "Hợp đồng trọn gói — Nhân công + Vật tư", ContractType.HopDongThauPhu,
                "SOL-CCM-FO-002.02.v01 Hop dong tron goi nhan cong, vat tu.docx", "docx",
                "Template HĐ thầu phụ trọn gói (cần convert .doc → .docx qua Word COM)"),
            ("SOL-CCM-FO-002.03", "Hợp đồng trọn gói — Nhân công + Thiết bị", ContractType.HopDongThauPhu,
                "SOL-CCM-FO-002.03.v01 Hop dong trong goi nhan cong, thiet bi.docx", "docx",
                "Template HĐ thầu phụ trọn gói (cần convert .doc → .docx qua Word COM)"),
            ("SOL-CCM-FO-002.04", "Điều kiện chung hợp đồng trọn gói", null,
                "SOL-CCM-FO-002.04.v01 Dieu kien chung hop dong tron goi.docx", "docx",
                "Appendix điều khoản chung đính kèm HĐ trọn gói"),
            ("SOL-CCM-FO-002.05", "Hợp đồng Giao khoán", ContractType.HopDongGiaoKhoan,
                "SOL-CCM-FO-002.05.v01 Hop dong Giao khoan.docx", "docx",
                "Template HĐ giao khoán với Tổ đội / Nhân công"),
            ("SOL-CCM-FO-002.06", "Hợp đồng mua bán", ContractType.HopDongMuaBan,
                "SOL-CCM-FO-002.06.v01 Hop dong mua ban.docx", "docx",
                "Template HĐ mua bán NCC (cần convert .doc → .docx qua Word COM)"),
            ("SOL-CCM-FO-002.07", "Đơn đặt hàng (PO)", null,
                "SOL-CCM-FO-002.07.V01 Don dat hang.xlsx", "xlsx",
                "Template PO Excel — 3 sheet: so sánh giá, PO chính, PO NLT"),
            ("SOL-CCM-RG-001", "Quy định mã số hợp đồng", null,
                "SOL-CCM-RG-001.v02 QD ma so hop dong.docx", "docx",
                "Regulation mã HĐ + PO — reference doc"),
        };

        var existingCodes = await db.ContractTemplates.Select(t => t.FormCode).ToListAsync();
        var wwwroot = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot");
        var added = 0;
        foreach (var (formCode, name, type, fileName, format, desc) in templates)
        {
            if (existingCodes.Contains(formCode)) continue;

            var templatePath = $"templates/{fileName}";
            var absPath = Path.Combine(wwwroot, "templates", fileName);
            var fileExists = File.Exists(absPath);

            db.ContractTemplates.Add(new ContractTemplate
            {
                FormCode = formCode,
                Name = name,
                ContractType = type,
                FileName = fileName,
                StoragePath = templatePath,
                Format = format,
                Description = desc,
                IsActive = fileExists,
            });
            added++;
        }
        if (added > 0)
        {
            await db.SaveChangesAsync();
            logger.LogInformation("Seeded {Count} contract templates (active file check)", added);
        }
    }

    // Plan B G-H1 (Mig 34 — S33 2026-05-26) — seed EmployeeProfile cho mọi
    // user @solutions.com.vn (30 demo + admin). Idempotent — skip nếu đã có
    // EmployeeProfile. Sequential code NV/{Year}/0001..N. Field nhạy cảm
    // (CMND/BHXH/Bank) PLACEHOLDER masked — bro UAT update real qua FE Hồ sơ
    // NS Page (Task 5).
    //
    // Cũng seed EmployeeCodeSequence "NV/{Year}" với LastSeq=N → production
    // gen tiếp từ N+1 (mirror PE/Contract CodeGen pattern).
    //
    // NOT gated DemoSeed:Disabled flag vì EmployeeProfile là INFRASTRUCTURE
    // data (1-1 với User), không phải DEMO content như sample HĐ/PE. Gotcha
    // #51 lesson (S29 Plan B): infra seed phải OUT of flag gate.
    private static async Task SeedDemoEmployeeProfilesAsync(
        ApplicationDbContext db, UserManager<User> userManager, ILogger logger)
    {
        if (await db.EmployeeProfiles.AnyAsync())
        {
            logger.LogInformation("SeedDemoEmployeeProfilesAsync: skip — đã có EmployeeProfile.");
            return;
        }

        var users = await userManager.Users
            .Where(u => u.Email != null && u.Email.EndsWith("@solutions.com.vn"))
            .OrderBy(u => u.Email)
            .ToListAsync();

        if (users.Count == 0)
        {
            logger.LogWarning("SeedDemoEmployeeProfilesAsync: no users @solutions.com.vn found — run SeedDemoUsersAsync first.");
            return;
        }

        var rng = new Random(20260526);                     // Deterministic seed for reproducibility
        var year = DateTime.UtcNow.Year;
        var seq = 0;
        foreach (var u in users)
        {
            seq++;
            var birthYear = 1970 + rng.Next(0, 26);          // 1970-1995 → 30-55 tuổi
            var hireYear = 2020 + (seq % 6);                  // 2020-2025
            var profile = new EmployeeProfile
            {
                Id = Guid.NewGuid(),
                UserId = u.Id,
                EmployeeCode = $"NV/{year}/{seq:D4}",
                EmployeeStatus = SolutionErp.Domain.Hrm.EmployeeStatus.Active,
                Gender = (seq % 2 == 0) ? SolutionErp.Domain.Hrm.Gender.Female : SolutionErp.Domain.Hrm.Gender.Male,
                MaritalStatus = (seq % 3 == 0) ? SolutionErp.Domain.Hrm.MaritalStatus.Single : SolutionErp.Domain.Hrm.MaritalStatus.Married,
                EmployeeType = SolutionErp.Domain.Hrm.EmployeeType.FullTime,
                DateOfBirth = new DateOnly(birthYear, rng.Next(1, 13), rng.Next(1, 28)),
                BirthPlace = "Hà Nội",
                Hometown = "Hà Nội",
                Phone = $"09{rng.Next(10000000, 99999999)}",
                PersonalEmail = u.Email,
                Nationality = "Việt Nam",
                Ethnicity = "Kinh",
                IdCardNumber = $"001099{seq:D6}",            // PLACEHOLDER masked
                IdCardIssueDate = new DateOnly(2020, 1, 15),
                IdCardIssuePlace = "Cục CS QLHC về TTXH",
                TaxCode = $"8{seq:D9}",
                SocialInsuranceNumber = $"BHXH{seq:D8}",
                PermanentAddressText = "Hà Nội (cập nhật qua Hồ sơ NS)",
                HireDate = new DateOnly(hireYear, ((seq % 12) + 1), 1),
                EmergencyContactName = "Người thân — cập nhật sau",
                EmergencyContactPhone = $"09{rng.Next(10000000, 99999999)}",
                Qualification = (seq % 4 == 0) ? "Thạc sĩ" : "Đại học",
                WorkLocation = (seq % 3 == 0) ? "Công trường" : "Văn phòng",
                BankAccount = $"19030000{seq:D4}",
                BankName = "Techcombank",
                BankBranch = "Hà Nội",
                BaseSalary = 15_000_000m + (seq % 10) * 1_000_000m,
                AnnualLeaveDays = 12m,
                RemainingLeaveDays = 12m - (seq % 5),
                SocialInsuranceStartDate = new DateOnly(hireYear, ((seq % 12) + 1), 1),
                IsCommunistParty = false,
                IsYouthUnion = (seq % 4 == 0),
                IsTradeUnion = true,
                CreatedAt = DateTime.UtcNow,
            };
            db.EmployeeProfiles.Add(profile);
        }

        db.EmployeeCodeSequences.Add(new EmployeeCodeSequence
        {
            Prefix = $"NV/{year}",
            LastSeq = seq,
            UpdatedAt = DateTime.UtcNow,
        });

        await db.SaveChangesAsync();
        logger.LogInformation(
            "SeedDemoEmployeeProfilesAsync: seeded {Count} profiles + 1 sequence row NV/{Year} LastSeq={Seq}",
            seq, year, seq);
    }

    // Phase 10.2 G-H2 (Mig 35 — S34 2026-05-27) — Cấu hình HRM 4 catalog seed sample.
    // INFRASTRUCTURE seed (NOT gated DemoSeed flag — gotcha #51 lesson).
    // Workflow Apps Phase 10.3 (G-O4 LeaveRequest, G-P1 chấm công OT) reference
    // 4 catalog active rows ngày 1 → admin có thể tinh chỉnh sau qua Designer UI.
    private static async Task SeedHrmConfigsAsync(ApplicationDbContext db, ILogger logger)
    {
        if (await db.LeaveTypes.AnyAsync()
            && await db.Holidays.AnyAsync()
            && await db.ShiftPatterns.AnyAsync()
            && await db.OtPolicies.AnyAsync()
            && await db.Vehicles.AnyAsync()
            && await db.Drivers.AnyAsync())
        {
            logger.LogInformation("SeedHrmConfigsAsync: skip — đã có 6 catalog.");
            return;
        }

        // 5 LeaveType — Luật Lao động VN 2019 reference.
        if (!await db.LeaveTypes.AnyAsync())
        {
            db.LeaveTypes.AddRange(
                new LeaveType
                {
                    Code = "ANNUAL", Name = "Phép năm", DaysPerYear = 12m, IsPaid = true,
                    RequiresAttachment = false,
                    Description = "Phép năm tiêu chuẩn. Luật quy định tối thiểu 12 ngày/năm cho lao động làm việc đủ 12 tháng.",
                },
                new LeaveType
                {
                    Code = "SICK", Name = "Phép ốm", DaysPerYear = 30m, IsPaid = true,
                    RequiresAttachment = true,
                    Description = "Nghỉ ốm có giấy bác sĩ. BHXH chi trả 75% lương.",
                },
                new LeaveType
                {
                    Code = "MATERNITY", Name = "Phép thai sản", DaysPerYear = 180m, IsPaid = true,
                    RequiresAttachment = true,
                    Description = "Phép thai sản 6 tháng (180 ngày) cho lao động nữ. BHXH chi trả 100%.",
                },
                new LeaveType
                {
                    Code = "COMPASSIONATE", Name = "Phép việc riêng", DaysPerYear = 3m, IsPaid = true,
                    RequiresAttachment = false,
                    Description = "Cưới hỏi / hiếu / việc gia đình quan trọng. Luật quy định 1-3 ngày tùy lý do.",
                },
                new LeaveType
                {
                    Code = "UNPAID", Name = "Phép không lương", DaysPerYear = 0m, IsPaid = false,
                    RequiresAttachment = false,
                    Description = "Nghỉ không lương — không giới hạn, cần BOD duyệt.",
                });
        }

        // 10 Holiday VN 2026 (placeholder Tết Nguyên đán date — admin update khi có lịch chính thức).
        if (!await db.Holidays.AnyAsync())
        {
            db.Holidays.AddRange(
                new Holiday { Year = 2026, Date = new DateOnly(2026, 1, 1), Name = "Tết Dương lịch", IsRecurring = true },
                // Tết Nguyên đán 2026 (Bính Ngọ) — 17/2 mùng 1 Âm — 5 ngày 16-20/2
                new Holiday { Year = 2026, Date = new DateOnly(2026, 2, 16), Name = "Tết Nguyên đán (30 Tết)", IsRecurring = false },
                new Holiday { Year = 2026, Date = new DateOnly(2026, 2, 17), Name = "Tết Nguyên đán (Mùng 1)", IsRecurring = false },
                new Holiday { Year = 2026, Date = new DateOnly(2026, 2, 18), Name = "Tết Nguyên đán (Mùng 2)", IsRecurring = false },
                new Holiday { Year = 2026, Date = new DateOnly(2026, 2, 19), Name = "Tết Nguyên đán (Mùng 3)", IsRecurring = false },
                new Holiday { Year = 2026, Date = new DateOnly(2026, 2, 20), Name = "Tết Nguyên đán (Mùng 4)", IsRecurring = false },
                // Giỗ tổ Hùng Vương 10/3 Âm — 2026 placeholder 14/4
                new Holiday { Year = 2026, Date = new DateOnly(2026, 4, 14), Name = "Giỗ tổ Hùng Vương", IsRecurring = false },
                new Holiday { Year = 2026, Date = new DateOnly(2026, 4, 30), Name = "Giải phóng miền Nam", IsRecurring = true },
                new Holiday { Year = 2026, Date = new DateOnly(2026, 5, 1), Name = "Quốc tế Lao động", IsRecurring = true },
                new Holiday { Year = 2026, Date = new DateOnly(2026, 9, 2), Name = "Quốc khánh", IsRecurring = true });
        }

        // 3 ShiftPattern — hành chính + 2 ca xoay.
        if (!await db.ShiftPatterns.AnyAsync())
        {
            db.ShiftPatterns.AddRange(
                new ShiftPattern
                {
                    Code = "HC", Name = "Hành chính",
                    StartTime = new TimeOnly(8, 0), EndTime = new TimeOnly(17, 0),
                    BreakMinutes = 60, WorkDays = "Mon,Tue,Wed,Thu,Fri",
                    Description = "Ca hành chính chuẩn 8-17h, nghỉ trưa 60 phút, T2-T6.",
                },
                new ShiftPattern
                {
                    Code = "CA1", Name = "Ca sáng",
                    StartTime = new TimeOnly(6, 0), EndTime = new TimeOnly(14, 0),
                    BreakMinutes = 30, WorkDays = "Mon,Tue,Wed,Thu,Fri,Sat",
                    Description = "Ca sáng 6-14h, nghỉ giữa ca 30 phút, T2-T7.",
                },
                new ShiftPattern
                {
                    Code = "CA2", Name = "Ca chiều",
                    StartTime = new TimeOnly(14, 0), EndTime = new TimeOnly(22, 0),
                    BreakMinutes = 30, WorkDays = "Mon,Tue,Wed,Thu,Fri,Sat",
                    Description = "Ca chiều 14-22h, nghỉ giữa ca 30 phút, T2-T7.",
                });
        }

        // 1 OtPolicy default — Luật Lao động VN cap.
        if (!await db.OtPolicies.AnyAsync())
        {
            db.OtPolicies.Add(new OtPolicy
            {
                Code = "STANDARD",
                Name = "Chính sách OT chuẩn",
                MultiplierWeekday = 1.5m,
                MultiplierWeekend = 2.0m,
                MultiplierHoliday = 3.0m,
                MaxHoursPerDay = 4,
                MaxHoursPerMonth = 40,
                MaxHoursPerYear = 200,
                Description = "Hệ số 1.5x/2.0x/3.0x theo Luật Lao động VN 2019. Max 4h/ngày, 40h/tháng, 200h/năm.",
            });
        }

        // 2 Vehicle sample (Mig 44 P11-C — reference VehicleBooking dropdown ngày 1).
        if (!await db.Vehicles.AnyAsync())
        {
            db.Vehicles.AddRange(
                new Vehicle
                {
                    Code = "XE-01", Name = "Toyota Innova", LicensePlate = "30A-12345",
                    SeatCount = 7,
                    Description = "Xe 7 chỗ đưa đón công tác nội thành.",
                },
                new Vehicle
                {
                    Code = "XE-02", Name = "Ford Transit", LicensePlate = "30A-67890",
                    SeatCount = 16,
                    Description = "Xe 16 chỗ đưa đón đoàn / công tác tỉnh.",
                });
        }

        // 2 Driver sample (Mig 44 P11-C — reference VehicleBooking dropdown ngày 1).
        if (!await db.Drivers.AnyAsync())
        {
            db.Drivers.AddRange(
                new Driver
                {
                    Code = "TX-01", Name = "Nguyễn Văn Tài", PhoneNumber = "0901234567",
                    LicenseNumber = "012345678901", LicenseClass = "D",
                    Description = "Tài xế xe 7-16 chỗ, GPLX hạng D.",
                },
                new Driver
                {
                    Code = "TX-02", Name = "Trần Văn Lái", PhoneNumber = "0907654321",
                    LicenseNumber = "098765432109", LicenseClass = "E",
                    Description = "Tài xế xe khách lớn, GPLX hạng E.",
                });
        }

        await db.SaveChangesAsync();
        logger.LogInformation("SeedHrmConfigsAsync: seeded 5 LeaveTypes + 10 Holidays 2026 + 3 ShiftPatterns + 1 OtPolicy + 2 Vehicles + 2 Drivers.");
    }

    // Plan G-O2 (Mig 36 — S36 2026-05-28). 4 sample MeetingRoom seed.
    // Infrastructure data (NOT gated DemoSeed flag, gotcha #51) — Booking calendar
    // cần dropdown phòng ngày 1. Admin có thể edit/delete/disable qua FE Manage page.
    // Idempotent: skip nếu đã có row.
    private static async Task SeedMeetingRoomsAsync(ApplicationDbContext db, ILogger logger)
    {
        if (await db.MeetingRooms.AnyAsync())
        {
            logger.LogInformation("SeedMeetingRoomsAsync: skip — đã có MeetingRoom.");
            return;
        }

        db.MeetingRooms.AddRange(
            new MeetingRoom
            {
                Code = "PH-A", Name = "Phòng họp lớn",
                Capacity = 20, Location = "Tầng 5, Toà A",
                Equipment = "Máy chiếu, Bảng trắng, Wifi, Webcam 4K",
                IsActive = true,
            },
            new MeetingRoom
            {
                Code = "PH-B", Name = "Phòng họp nhỏ",
                Capacity = 8, Location = "Tầng 3, Toà A",
                Equipment = "TV 55 inch, Bảng trắng",
                IsActive = true,
            },
            new MeetingRoom
            {
                Code = "PHG-501", Name = "Phòng Giám đốc",
                Capacity = 6, Location = "Tầng 5, Toà A",
                Equipment = "TV 65 inch, Bàn họp dài, Tủ tài liệu",
                IsActive = true,
            },
            new MeetingRoom
            {
                Code = "ONL-1", Name = "Trực tuyến Zoom",
                Capacity = 50, Location = "Online",
                Equipment = "License Zoom Pro, link cố định",
                IsActive = true,
            });
        await db.SaveChangesAsync();
        logger.LogInformation("SeedMeetingRoomsAsync: seeded 4 sample meeting rooms (PH-A/PH-B/PHG-501/ONL-1).");
    }
}
