// ============================================================ // GoodBooks ERP — Skill Management Module // DTO : SkillMatrix DTOs (multiple classes — one file) // Tables : MSKILLMATRIX (E3), MSKILL (S3), MSKILLPRODUCT (M1), // MEMPLOYEESKILL (E1), MSKILLLEVEL (S4), // MJOBROLESKILL (M3), MROUTINGSKILL (M2), // MEMPLOYEE (external), MJOBROLE (external) // Updated: Aligned to new DDL schema v1.0 // // ASSEMBLY NOTE: // SkillMatrixViewDTO is assembled in BLL memory — it is NOT // returned by a single SQL query. The BLL calls multiple QB // queries and stitches the result together. // ============================================================ namespace SkillManagementDAL.DTO.SkillMatrix { // ── MSKILLMATRIX config DTO ────────────────────────────────── /// /// Full config record for one matrix definition. /// Maps directly to MSKILLMATRIX (E3). /// Used by the matrix setup/admin screen. /// Was missing entirely from old DTO file. /// public class SkillMatrixConfigDTO { // Primary key public int ConfigId { get; set; } // Business columns (MSKILLMATRIX) public string MatrixCode { get; set; } = string.Empty; // MSKILLMATRIX.MATRIXCODE UK per TENANTID public string MatrixTitle { get; set; } = string.Empty; // MSKILLMATRIX.MATRIXTITLE public int DomainId { get; set; } = -1; // MSKILLMATRIX.DOMAINID -1=all domains public string? TargetDept { get; set; } // MSKILLMATRIX.TARGETDEPT NULL=all public string? TargetRoleIds { get; set; } // MSKILLMATRIX.TARGETROLEIDS JSON int array e.g. [10,11] public int ProductFamilyId { get; set; } = -1; // MSKILLMATRIX.PRODUCTFAMILYID -1=not scoped public string? ModelIds { get; set; } // MSKILLMATRIX.MODELIDS JSON int array public string IncludeSkillIds { get; set; } = string.Empty; // MSKILLMATRIX.INCLUDESKILLIDS JSON ordered int array — mandatory public byte ShowSummaryRow { get; set; } = 1; // MSKILLMATRIX.SHOWSUMMARYROW 0=No,1=Yes public byte ShowMinLevel { get; set; } = 1; // MSKILLMATRIX.SHOWMINLEVEL 0=No,1=Yes public string? UpdateFrequency { get; set; } // MSKILLMATRIX.UPDATEFREQUENCY e.g. 'Monthly','Quarterly' public byte IsActive { get; set; } = 1; // MSKILLMATRIX.ISACTIVE 0=No,1=Yes // Standard fields (MSKILLMATRIX) public short Version { get; set; } public byte Status { get; set; } = 1; // 0=Pending,1=Active,2=Deleted,3=Amended,4=Inactive,5=Archived public short SortOrder { get; set; } = 9999; public int CreatedById { get; set; } public DateTime? CreatedOn { get; set; } public int ModifiedById { get; set; } public DateTime? ModifiedOn { get; set; } public byte SourceType { get; set; } = 5; public int TenantId { get; set; } // Joined display fields public string? DomainName { get; set; } // MSKILLDOMAIN.DOMAINNAME (when DomainId != -1) } // ── View DTO — assembled in BLL ────────────────────────────── /// /// Complete matrix view assembled in BLL memory from multiple /// queries. Not returned by a single SQL query. /// public class SkillMatrixViewDTO { // Config context — from MSKILLMATRIX (E3) public int ConfigId { get; set; } public string MatrixCode { get; set; } = string.Empty; // Added — needed to identify the matrix public string MatrixTitle { get; set; } = string.Empty; public int? UpdateFrequency { get; set; } public byte ShowSummaryRow { get; set; } // 0=No, 1=Yes public byte ShowMinLevel { get; set; } // 0=No, 1=Yes // Scope descriptor — computed from MSKILLMATRIX fields // Values: "General" | "ProductFamily" | "Model" // Maps to MSKILL.SKILLSCOPE: 0=General, 1=ProductFamily, 2=Model public string Scope { get; set; } = string.Empty; // Optional scope context — from MSKILLMATRIX public string? TargetDept { get; set; } // MSKILLMATRIX.TARGETDEPT public int ProductFamilyId { get; set; } = -1; // MSKILLMATRIX.PRODUCTFAMILYID public List Columns { get; set; } = new(); public List Rows { get; set; } = new(); public List Summary { get; set; } = new(); } // ── Column header DTO ──────────────────────────────────────── /// /// One column in the matrix header. Sourced from MSKILL (S3) /// and MSKILLPRODUCT (M1) for product/model scoped skills. /// /// ColumnKey pattern (from DDL COLUMN KEY PATTERN): /// SKILLSCOPE=0 General → "{SkillId}_0" /// SKILLSCOPE=1 ProductFamily → "{SkillId}_{ProductFamilyId}" /// SKILLSCOPE=2 Model → "{SkillId}_{ModelId}" /// /// MinLevelRequired source: /// Loaded from MJOBROLESKILL.MINLEVNO (job role minimum) /// OR MROUTINGSKILL.MINLEVELREQUIRED (routing op minimum) /// depending on matrix scope. See SkillMatrixQB comments. /// public class SkillMatrixColumnDTO { public int SkillId { get; set; } public string SkillName { get; set; } = string.Empty; // MSKILL.SKILLNAME public string? SkillCode { get; set; } // MSKILL.SKILLCODE public byte SkillScope { get; set; } // 0=General,1=ProductFamily,2=Model public byte IsSpecialChar { get; set; } // 0/1 star indicator public short MatrixSeqNo { get; set; } // column ordering public byte LevelCount { get; set; } // 1-4 levels public byte MinLevelRequired { get; set; } // from job/routing logic (not SQL yet) public string? ScopeLabel { get; set; } // e.g. "General", "Model X200" public int ProductFamilyId { get; set; } = -1; // -1 = not applicable public int ModelId { get; set; } = -1; // -1 = not applicable public string ColumnKey { get; set; } = string.Empty; // "{SkillId}_0" / "{SkillId}_{PFId}" / "{SkillId}_{ModelId}" public int GroupId { get; set; } = -1; // FIXED: safe default for NULL groups public string? GroupName { get; set; } // MSKILLGROUP.GROUPNAME } // ── Employee row DTO ───────────────────────────────────────── /// One employee row — cells keyed by ColumnKey. public class SkillMatrixRowDTO { public int EmployeeId { get; set; } public string EmployeeName { get; set; } = string.Empty; public string? Department { get; set; } public string? RoleName { get; set; } public string? Thumbnail { get; set; } public Dictionary Cells { get; set; } = new(); } // ── Cell DTO ───────────────────────────────────────────────── /// /// One cell — one employee x one skill(xscope) intersection. /// Sourced from MEMPLOYEESKILL (E1) and MSKILLLEVEL (S4). /// NULL ProfileId means the employee has no profile row for /// this skill yet (CurrentLevNo defaults to 0 = NotAssessed). /// public class SkillMatrixCellDTO { public int? ProfileId { get; set; } // MEMPLOYEESKILL.PROFILEID NULL = not assessed yet public byte CurrentLevNo { get; set; } // MEMPLOYEESKILL.CURRENTLEVNO 0=NotAssessed,1-4 public byte ProfileStatus { get; set; } // MEMPLOYEESKILL.PROFILESTATUS 0=NotAssessed,1=Current,2=TrainingPlan,3=Expired public DateOnly? AssessedDt { get; set; } // MEMPLOYEESKILL.ASSESSEDDT public DateOnly? TrainingPlanDate { get; set; } // MEMPLOYEESKILL.TRAININGPLANDATE public DateOnly? ActualTrainDt { get; set; } // MEMPLOYEESKILL.ACTUALTRAINDT public DateOnly? NextReviewDt { get; set; } // MEMPLOYEESKILL.NEXTREVIEWDT (added — for expired review highlighting) public string? LevelLabel { get; set; } // MSKILLLEVEL.LEVELLABEL at SLNO = CurrentLevNo public byte MinLevelRequired { get; set; } // From column — MJOBROLESKILL.MINLEVNO public bool MeetsMinLevel { get; set; } // Computed in BLL: CurrentLevNo >= MinLevelRequired } // ── Summary row DTO ────────────────────────────────────────── /// Summary row — qualified count per column. public class SkillMatrixSummaryDTO { public string ColumnKey { get; set; } = string.Empty; // Matches SkillMatrixColumnDTO.ColumnKey public int QualifiedCount { get; set; } // Employees meeting MinLevelRequired for this column public int TotalEmployees { get; set; } // Total employees in the matrix } // ── Internal: product scope ────────────────────────────────── /// /// Internal DTO — product/model scope rows from MSKILLPRODUCT (M1). /// Used when building columns for SKILLSCOPE=1 or SKILLSCOPE=2 skills. /// ProductFamilyName and ModelName resolved via BLL or QB join. /// public class SkillProductScopeDTO { public int SkillId { get; set; } // MSKILLPRODUCT.SKILLID public short SlNo { get; set; } // MSKILLPRODUCT.SLNO (added — sequence within skill) public int ProductFamilyId { get; set; } = -1; // MSKILLPRODUCT.PRODUCTFAMILYID -1=not applicable public int ModelId { get; set; } = -1; // MSKILLPRODUCT.MODELID -1=not applicable public string? ProductFamilyName { get; set; } // MPRODUCTFAMILY.FAMILYNAME (from join in QB) public string? ModelName { get; set; } // MMODEL.MODELNAME (from join in QB) } // ── Internal: employee row loader ──────────────────────────── /// /// Internal DTO — employee rows loaded for a matrix. /// Sourced from MEMPLOYEE (external) with optional MJOBROLE join. /// public class SkillMatrixEmployeeDTO { public int EmployeeId { get; set; } // MEMPLOYEE.EMPLOYEEID public string EmployeeName { get; set; } = string.Empty; // MEMPLOYEE.EMPLOYEENAME public string? Department { get; set; } // MEMPLOYEE.DEPARTMENT public int RoleId { get; set; } = -1; // MEMPLOYEE.ROLEID (added — needed for role-based min level lookup) public string? RoleName { get; set; } // MJOBROLE.ROLENAME public string? Thumbnail { get; set; } // MEMPLOYEE.THUMBNAIL } // ── Internal: profile cell loader ──────────────────────────── /// /// Internal DTO — profile cell data loaded from MEMPLOYEESKILL (E1). /// One row per (EmployeeId, SkillId, ProductFamilyId, ModelId). /// Matched to matrix columns by ColumnKey in BLL. /// public class SkillMatrixProfileCellDTO { public int EmployeeId { get; set; } public int SkillId { get; set; } public int ProductFamilyId { get; set; } = -1; public int ModelId { get; set; } = -1; // Must be nullable because missing profile rows are allowed public int? ProfileId { get; set; } public byte CurrentLevNo { get; set; } public byte ProfileStatus { get; set; } // IMPORTANT: // Dapper cannot automatically convert SQL DATETIME to DateOnly. // Use DateTime? here, or add a custom TypeHandler. public DateTime? AssessedDt { get; set; } public DateTime? TrainingPlanDate { get; set; } public DateTime? ActualTrainDt { get; set; } public DateTime? NextReviewDt { get; set; } public string? LevelLabel { get; set; } } // ── Internal: min level per skill ──────────────────────────── /// /// Internal DTO — minimum required level per skill. /// Sourced from MJOBROLESKILL.MINLEVNO via GET_MIN_LEVELS query. /// /// IMPORTANT: GET_MIN_LEVELS uses MIN(MINLEVNO) aggregated across /// ALL roles for each skill. This returns the lowest bar across /// all job roles. If the matrix is role-scoped via /// MSKILLMATRIX.TARGETROLEIDS, the QB query must be called with /// @RoleIds filter to get the role-specific minimum. /// See SkillMatrixQB.GET_MIN_LEVELS_BY_ROLES for scoped variant. /// public class SkillMinLevelDTO { public int SkillId { get; set; } // MJOBROLESKILL.SKILLID public byte MinLevNo { get; set; } // MIN(MJOBROLESKILL.MINLEVNO) across target roles } }