package training import ( "context" "math" "github.com/google/uuid" ) // Escalation level thresholds (days overdue) const ( EscalationThresholdL1 = 7 // Reminder to user EscalationThresholdL2 = 14 // Notify team lead EscalationThresholdL3 = 30 // Notify management EscalationThresholdL4 = 45 // Notify compliance officer ) // EscalationLabels maps levels to human-readable labels var EscalationLabels = map[int]string{ 0: "Keine Eskalation", 1: "Erinnerung an Mitarbeiter", 2: "Benachrichtigung Teamleitung", 3: "Benachrichtigung Management", 4: "Benachrichtigung Compliance Officer", } // CheckEscalations checks all overdue assignments and escalates as needed func CheckEscalations(ctx context.Context, store *Store, tenantID uuid.UUID) ([]EscalationResult, error) { overdueAssignments, err := store.ListOverdueAssignments(ctx, tenantID) if err != nil { return nil, err } var results []EscalationResult now := timeNow().UTC() for _, assignment := range overdueAssignments { daysOverdue := int(math.Floor(now.Sub(assignment.Deadline).Hours() / 24)) if daysOverdue < 0 { continue } // Determine new escalation level newLevel := 0 if daysOverdue >= EscalationThresholdL4 { newLevel = 4 } else if daysOverdue >= EscalationThresholdL3 { newLevel = 3 } else if daysOverdue >= EscalationThresholdL2 { newLevel = 2 } else if daysOverdue >= EscalationThresholdL1 { newLevel = 1 } // Only escalate if the level has increased if newLevel <= assignment.EscalationLevel { continue } previousLevel := assignment.EscalationLevel // Update the assignment nowTime := now _, err := store.pool.Exec(ctx, ` UPDATE training_assignments SET escalation_level = $2, last_escalation_at = $3, status = 'overdue', updated_at = $3 WHERE id = $1 `, assignment.ID, newLevel, nowTime) if err != nil { return nil, err } // Log the escalation assignmentID := assignment.ID store.LogAction(ctx, &AuditLogEntry{ TenantID: tenantID, UserID: &assignment.UserID, Action: AuditActionEscalated, EntityType: AuditEntityAssignment, EntityID: &assignmentID, Details: map[string]interface{}{ "previous_level": previousLevel, "new_level": newLevel, "days_overdue": daysOverdue, "label": EscalationLabels[newLevel], }, }) results = append(results, EscalationResult{ AssignmentID: assignment.ID, UserID: assignment.UserID, UserName: assignment.UserName, UserEmail: assignment.UserEmail, ModuleTitle: assignment.ModuleTitle, PreviousLevel: previousLevel, NewLevel: newLevel, DaysOverdue: daysOverdue, EscalationLabel: EscalationLabels[newLevel], }) } if results == nil { results = []EscalationResult{} } return results, nil } // GetOverdueDeadlines returns all overdue assignments with deadline info func GetOverdueDeadlines(ctx context.Context, store *Store, tenantID uuid.UUID) ([]DeadlineInfo, error) { rows, err := store.pool.Query(ctx, ` SELECT ta.id, m.module_code, m.title, ta.user_id, ta.user_name, ta.deadline, ta.status, EXTRACT(DAY FROM (NOW() - ta.deadline))::INT AS days_overdue FROM training_assignments ta JOIN training_modules m ON m.id = ta.module_id WHERE ta.tenant_id = $1 AND ta.status IN ('pending', 'in_progress', 'overdue') AND ta.deadline < NOW() ORDER BY ta.deadline ASC `, tenantID) if err != nil { return nil, err } defer rows.Close() var deadlines []DeadlineInfo for rows.Next() { var d DeadlineInfo var status string err := rows.Scan( &d.AssignmentID, &d.ModuleCode, &d.ModuleTitle, &d.UserID, &d.UserName, &d.Deadline, &status, &d.DaysLeft, ) if err != nil { return nil, err } d.Status = AssignmentStatus(status) d.DaysLeft = -d.DaysLeft // Negative means overdue deadlines = append(deadlines, d) } if deadlines == nil { deadlines = []DeadlineInfo{} } return deadlines, nil } // VerifyCertificate verifies a certificate by checking the assignment status func VerifyCertificate(ctx context.Context, store *Store, certificateID uuid.UUID) (bool, *TrainingAssignment, error) { // Find assignment with this certificate var assignmentID uuid.UUID err := store.pool.QueryRow(ctx, "SELECT id FROM training_assignments WHERE certificate_id = $1", certificateID).Scan(&assignmentID) if err != nil { return false, nil, err } assignment, err := store.GetAssignment(ctx, assignmentID) if err != nil { return false, nil, err } if assignment == nil { return false, nil, nil } return assignment.Status == AssignmentStatusCompleted, assignment, nil }