package training import ( "context" "github.com/google/uuid" ) // ComputeRequiredModules returns all required training modules for a user // based on their assigned roles. Deduplicates modules across roles. func ComputeRequiredModules(ctx context.Context, store *Store, tenantID uuid.UUID, roleCodes []string) ([]TrainingModule, error) { seen := make(map[uuid.UUID]bool) var modules []TrainingModule for _, role := range roleCodes { entries, err := store.GetMatrixForRole(ctx, tenantID, role) if err != nil { return nil, err } for _, entry := range entries { if seen[entry.ModuleID] { continue } seen[entry.ModuleID] = true module, err := store.GetModule(ctx, entry.ModuleID) if err != nil { return nil, err } if module != nil && module.IsActive { modules = append(modules, *module) } } } if modules == nil { modules = []TrainingModule{} } return modules, nil } // GetComplianceGaps finds modules that are required but not completed for a user func GetComplianceGaps(ctx context.Context, store *Store, tenantID uuid.UUID, userID uuid.UUID, roleCodes []string) ([]ComplianceGap, error) { var gaps []ComplianceGap for _, role := range roleCodes { entries, err := store.GetMatrixForRole(ctx, tenantID, role) if err != nil { return nil, err } for _, entry := range entries { // Check if there's an active, completed assignment for this module assignments, _, err := store.ListAssignments(ctx, tenantID, &AssignmentFilters{ ModuleID: &entry.ModuleID, UserID: &userID, Limit: 1, }) if err != nil { return nil, err } gap := ComplianceGap{ ModuleID: entry.ModuleID, ModuleCode: entry.ModuleCode, ModuleTitle: entry.ModuleTitle, RoleCode: role, IsMandatory: entry.IsMandatory, } // Determine regulation area from module module, err := store.GetModule(ctx, entry.ModuleID) if err != nil { return nil, err } if module != nil { gap.RegulationArea = module.RegulationArea } if len(assignments) == 0 { gap.Status = "missing" gaps = append(gaps, gap) } else { a := assignments[0] gap.AssignmentID = &a.ID gap.Deadline = &a.Deadline switch a.Status { case AssignmentStatusCompleted: // No gap continue case AssignmentStatusOverdue, AssignmentStatusExpired: gap.Status = string(a.Status) gaps = append(gaps, gap) default: // Check if overdue if a.Deadline.Before(timeNow()) { gap.Status = "overdue" gaps = append(gaps, gap) } } } } } if gaps == nil { gaps = []ComplianceGap{} } return gaps, nil } // BuildMatrixResponse builds the full CTM response grouped by role func BuildMatrixResponse(entries []TrainingMatrixEntry) *MatrixResponse { resp := &MatrixResponse{ Entries: make(map[string][]TrainingMatrixEntry), Roles: RoleLabels, } for _, entry := range entries { resp.Entries[entry.RoleCode] = append(resp.Entries[entry.RoleCode], entry) } return resp }