feat: Add Academy, Whistleblower, Incidents, Vendor, DSB, SSO, Reporting, Multi-Tenant and Industry backends
Go handlers, models, stores and migrations for all SDK modules. Updates developer portal navigation and BYOEH page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
97
ai-compliance-sdk/internal/reporting/models.go
Normal file
97
ai-compliance-sdk/internal/reporting/models.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package reporting
|
||||
|
||||
import "time"
|
||||
|
||||
type ExecutiveReport struct {
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
ComplianceScore int `json:"compliance_score"` // 0-100 overall score
|
||||
|
||||
// Module summaries
|
||||
DSGVO DSGVOSummary `json:"dsgvo"`
|
||||
Vendors VendorSummary `json:"vendors"`
|
||||
Incidents IncidentSummary `json:"incidents"`
|
||||
Whistleblower WhistleblowerSummary `json:"whistleblower"`
|
||||
Academy AcademySummary `json:"academy"`
|
||||
|
||||
// Cross-module metrics
|
||||
RiskOverview RiskOverview `json:"risk_overview"`
|
||||
UpcomingDeadlines []Deadline `json:"upcoming_deadlines"`
|
||||
RecentActivity []ActivityEntry `json:"recent_activity"`
|
||||
}
|
||||
|
||||
type DSGVOSummary struct {
|
||||
ProcessingActivities int `json:"processing_activities"`
|
||||
ActiveProcessings int `json:"active_processings"`
|
||||
TOMsImplemented int `json:"toms_implemented"`
|
||||
TOMsPlanned int `json:"toms_planned"`
|
||||
TOMsTotal int `json:"toms_total"`
|
||||
CompletionPercent int `json:"completion_percent"` // TOMsImplemented / total * 100
|
||||
OpenDSRs int `json:"open_dsrs"`
|
||||
OverdueDSRs int `json:"overdue_dsrs"`
|
||||
DSFAsCompleted int `json:"dsfas_completed"`
|
||||
RetentionPolicies int `json:"retention_policies"`
|
||||
}
|
||||
|
||||
type VendorSummary struct {
|
||||
TotalVendors int `json:"total_vendors"`
|
||||
ActiveVendors int `json:"active_vendors"`
|
||||
ByRiskLevel map[string]int `json:"by_risk_level"`
|
||||
PendingReviews int `json:"pending_reviews"`
|
||||
ExpiredContracts int `json:"expired_contracts"`
|
||||
}
|
||||
|
||||
type IncidentSummary struct {
|
||||
TotalIncidents int `json:"total_incidents"`
|
||||
OpenIncidents int `json:"open_incidents"`
|
||||
CriticalIncidents int `json:"critical_incidents"`
|
||||
NotificationsPending int `json:"notifications_pending"`
|
||||
AvgResolutionHours float64 `json:"avg_resolution_hours"`
|
||||
}
|
||||
|
||||
type WhistleblowerSummary struct {
|
||||
TotalReports int `json:"total_reports"`
|
||||
OpenReports int `json:"open_reports"`
|
||||
OverdueAcknowledgments int `json:"overdue_acknowledgments"`
|
||||
OverdueFeedbacks int `json:"overdue_feedbacks"`
|
||||
AvgResolutionDays float64 `json:"avg_resolution_days"`
|
||||
}
|
||||
|
||||
type AcademySummary struct {
|
||||
TotalCourses int `json:"total_courses"`
|
||||
TotalEnrollments int `json:"total_enrollments"`
|
||||
CompletionRate float64 `json:"completion_rate"` // 0-100
|
||||
OverdueCount int `json:"overdue_count"`
|
||||
AvgCompletionDays float64 `json:"avg_completion_days"`
|
||||
}
|
||||
|
||||
type RiskOverview struct {
|
||||
OverallLevel string `json:"overall_level"` // LOW, MEDIUM, HIGH, CRITICAL
|
||||
ModuleRisks []ModuleRisk `json:"module_risks"`
|
||||
OpenFindings int `json:"open_findings"`
|
||||
CriticalFindings int `json:"critical_findings"`
|
||||
}
|
||||
|
||||
type ModuleRisk struct {
|
||||
Module string `json:"module"`
|
||||
Level string `json:"level"` // LOW, MEDIUM, HIGH, CRITICAL
|
||||
Score int `json:"score"` // 0-100
|
||||
Issues int `json:"issues"`
|
||||
}
|
||||
|
||||
type Deadline struct {
|
||||
Module string `json:"module"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
DueDate time.Time `json:"due_date"`
|
||||
DaysLeft int `json:"days_left"`
|
||||
Severity string `json:"severity"` // INFO, WARNING, URGENT, OVERDUE
|
||||
}
|
||||
|
||||
type ActivityEntry struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Module string `json:"module"`
|
||||
Action string `json:"action"`
|
||||
Description string `json:"description"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
}
|
||||
520
ai-compliance-sdk/internal/reporting/store.go
Normal file
520
ai-compliance-sdk/internal/reporting/store.go
Normal file
@@ -0,0 +1,520 @@
|
||||
package reporting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/academy"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/dsgvo"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/incidents"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/vendor"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/whistleblower"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
pool *pgxpool.Pool
|
||||
dsgvoStore *dsgvo.Store
|
||||
vendorStore *vendor.Store
|
||||
incidentStore *incidents.Store
|
||||
whistleStore *whistleblower.Store
|
||||
academyStore *academy.Store
|
||||
}
|
||||
|
||||
func NewStore(pool *pgxpool.Pool, ds *dsgvo.Store, vs *vendor.Store, is *incidents.Store, ws *whistleblower.Store, as *academy.Store) *Store {
|
||||
return &Store{
|
||||
pool: pool,
|
||||
dsgvoStore: ds,
|
||||
vendorStore: vs,
|
||||
incidentStore: is,
|
||||
whistleStore: ws,
|
||||
academyStore: as,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) GenerateReport(ctx context.Context, tenantID uuid.UUID) (*ExecutiveReport, error) {
|
||||
report := &ExecutiveReport{
|
||||
GeneratedAt: time.Now().UTC(),
|
||||
TenantID: tenantID.String(),
|
||||
}
|
||||
|
||||
tid := tenantID.String()
|
||||
|
||||
// 1. Gather DSGVO stats
|
||||
dsgvoStats, err := s.dsgvoStore.GetStats(ctx, tenantID)
|
||||
if err == nil && dsgvoStats != nil {
|
||||
total := dsgvoStats.TOMsImplemented + dsgvoStats.TOMsPlanned
|
||||
pct := 0
|
||||
if total > 0 {
|
||||
pct = int(math.Round(float64(dsgvoStats.TOMsImplemented) / float64(total) * 100))
|
||||
}
|
||||
report.DSGVO = DSGVOSummary{
|
||||
ProcessingActivities: dsgvoStats.ProcessingActivities,
|
||||
ActiveProcessings: dsgvoStats.ActiveProcessings,
|
||||
TOMsImplemented: dsgvoStats.TOMsImplemented,
|
||||
TOMsPlanned: dsgvoStats.TOMsPlanned,
|
||||
TOMsTotal: total,
|
||||
CompletionPercent: pct,
|
||||
OpenDSRs: dsgvoStats.OpenDSRs,
|
||||
OverdueDSRs: dsgvoStats.OverdueDSRs,
|
||||
DSFAsCompleted: dsgvoStats.DSFAsCompleted,
|
||||
RetentionPolicies: dsgvoStats.RetentionPolicies,
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Gather vendor stats
|
||||
vendorStats, err := s.vendorStore.GetVendorStats(ctx, tid)
|
||||
if err == nil && vendorStats != nil {
|
||||
active := 0
|
||||
if v, ok := vendorStats.ByStatus["ACTIVE"]; ok {
|
||||
active = v
|
||||
}
|
||||
report.Vendors = VendorSummary{
|
||||
TotalVendors: vendorStats.TotalVendors,
|
||||
ActiveVendors: active,
|
||||
ByRiskLevel: vendorStats.ByRiskLevel,
|
||||
PendingReviews: vendorStats.PendingReviews,
|
||||
ExpiredContracts: vendorStats.ExpiredContracts,
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Gather incident stats
|
||||
incidentStats, err := s.incidentStore.GetStatistics(ctx, tenantID)
|
||||
if err == nil && incidentStats != nil {
|
||||
critical := 0
|
||||
if v, ok := incidentStats.BySeverity["CRITICAL"]; ok {
|
||||
critical = v
|
||||
}
|
||||
report.Incidents = IncidentSummary{
|
||||
TotalIncidents: incidentStats.TotalIncidents,
|
||||
OpenIncidents: incidentStats.OpenIncidents,
|
||||
CriticalIncidents: critical,
|
||||
NotificationsPending: incidentStats.NotificationsPending,
|
||||
AvgResolutionHours: incidentStats.AvgResolutionHours,
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Gather whistleblower stats
|
||||
whistleStats, err := s.whistleStore.GetStatistics(ctx, tenantID)
|
||||
if err == nil && whistleStats != nil {
|
||||
openReports := 0
|
||||
for status, count := range whistleStats.ByStatus {
|
||||
if status != "CLOSED" && status != "ARCHIVED" {
|
||||
openReports += count
|
||||
}
|
||||
}
|
||||
report.Whistleblower = WhistleblowerSummary{
|
||||
TotalReports: whistleStats.TotalReports,
|
||||
OpenReports: openReports,
|
||||
OverdueAcknowledgments: whistleStats.OverdueAcknowledgments,
|
||||
OverdueFeedbacks: whistleStats.OverdueFeedbacks,
|
||||
AvgResolutionDays: whistleStats.AvgResolutionDays,
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Gather academy stats
|
||||
academyStats, err := s.academyStore.GetStatistics(ctx, tenantID)
|
||||
if err == nil && academyStats != nil {
|
||||
report.Academy = AcademySummary{
|
||||
TotalCourses: academyStats.TotalCourses,
|
||||
TotalEnrollments: academyStats.TotalEnrollments,
|
||||
CompletionRate: academyStats.CompletionRate,
|
||||
OverdueCount: academyStats.OverdueCount,
|
||||
AvgCompletionDays: academyStats.AvgCompletionDays,
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Calculate risk overview
|
||||
report.RiskOverview = s.calculateRiskOverview(report)
|
||||
|
||||
// 7. Calculate compliance score (0-100)
|
||||
report.ComplianceScore = s.calculateComplianceScore(report)
|
||||
|
||||
// 8. Gather upcoming deadlines from DB
|
||||
report.UpcomingDeadlines = s.getUpcomingDeadlines(ctx, tenantID)
|
||||
|
||||
// 9. Gather recent activity from DB
|
||||
report.RecentActivity = s.getRecentActivity(ctx, tenantID)
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func (s *Store) calculateRiskOverview(report *ExecutiveReport) RiskOverview {
|
||||
modules := []ModuleRisk{}
|
||||
|
||||
// DSGVO risk based on overdue DSRs and missing TOMs
|
||||
dsgvoScore := 100
|
||||
dsgvoIssues := report.DSGVO.OverdueDSRs + report.DSGVO.TOMsPlanned
|
||||
if report.DSGVO.OverdueDSRs > 0 {
|
||||
dsgvoScore -= report.DSGVO.OverdueDSRs * 15
|
||||
}
|
||||
if report.DSGVO.TOMsTotal > 0 {
|
||||
dsgvoScore = int(math.Round(float64(report.DSGVO.CompletionPercent)))
|
||||
}
|
||||
if dsgvoScore < 0 {
|
||||
dsgvoScore = 0
|
||||
}
|
||||
modules = append(modules, ModuleRisk{Module: "DSGVO", Level: riskLevel(dsgvoScore), Score: dsgvoScore, Issues: dsgvoIssues})
|
||||
|
||||
// Vendor risk based on high-risk vendors and pending reviews
|
||||
vendorScore := 100
|
||||
vendorIssues := report.Vendors.PendingReviews + report.Vendors.ExpiredContracts
|
||||
highRisk := 0
|
||||
if v, ok := report.Vendors.ByRiskLevel["HIGH"]; ok {
|
||||
highRisk += v
|
||||
}
|
||||
if v, ok := report.Vendors.ByRiskLevel["CRITICAL"]; ok {
|
||||
highRisk += v
|
||||
}
|
||||
if report.Vendors.TotalVendors > 0 {
|
||||
vendorScore = 100 - int(math.Round(float64(highRisk)/float64(report.Vendors.TotalVendors)*100))
|
||||
}
|
||||
vendorScore -= report.Vendors.PendingReviews * 5
|
||||
vendorScore -= report.Vendors.ExpiredContracts * 10
|
||||
if vendorScore < 0 {
|
||||
vendorScore = 0
|
||||
}
|
||||
modules = append(modules, ModuleRisk{Module: "Vendors", Level: riskLevel(vendorScore), Score: vendorScore, Issues: vendorIssues})
|
||||
|
||||
// Incident risk
|
||||
incidentScore := 100
|
||||
incidentIssues := report.Incidents.OpenIncidents
|
||||
incidentScore -= report.Incidents.CriticalIncidents * 20
|
||||
incidentScore -= report.Incidents.OpenIncidents * 5
|
||||
incidentScore -= report.Incidents.NotificationsPending * 15
|
||||
if incidentScore < 0 {
|
||||
incidentScore = 0
|
||||
}
|
||||
modules = append(modules, ModuleRisk{Module: "Incidents", Level: riskLevel(incidentScore), Score: incidentScore, Issues: incidentIssues})
|
||||
|
||||
// Whistleblower compliance
|
||||
whistleScore := 100
|
||||
whistleIssues := report.Whistleblower.OverdueAcknowledgments + report.Whistleblower.OverdueFeedbacks
|
||||
whistleScore -= report.Whistleblower.OverdueAcknowledgments * 20
|
||||
whistleScore -= report.Whistleblower.OverdueFeedbacks * 10
|
||||
if whistleScore < 0 {
|
||||
whistleScore = 0
|
||||
}
|
||||
modules = append(modules, ModuleRisk{Module: "Whistleblower", Level: riskLevel(whistleScore), Score: whistleScore, Issues: whistleIssues})
|
||||
|
||||
// Academy compliance
|
||||
academyScore := int(math.Round(report.Academy.CompletionRate))
|
||||
academyIssues := report.Academy.OverdueCount
|
||||
modules = append(modules, ModuleRisk{Module: "Academy", Level: riskLevel(academyScore), Score: academyScore, Issues: academyIssues})
|
||||
|
||||
// Overall score is the average across modules
|
||||
totalScore := 0
|
||||
for _, m := range modules {
|
||||
totalScore += m.Score
|
||||
}
|
||||
if len(modules) > 0 {
|
||||
totalScore = totalScore / len(modules)
|
||||
}
|
||||
|
||||
totalFindings := 0
|
||||
criticalFindings := 0
|
||||
for _, m := range modules {
|
||||
totalFindings += m.Issues
|
||||
if m.Level == "CRITICAL" {
|
||||
criticalFindings += m.Issues
|
||||
}
|
||||
}
|
||||
|
||||
return RiskOverview{
|
||||
OverallLevel: riskLevel(totalScore),
|
||||
ModuleRisks: modules,
|
||||
OpenFindings: totalFindings,
|
||||
CriticalFindings: criticalFindings,
|
||||
}
|
||||
}
|
||||
|
||||
func riskLevel(score int) string {
|
||||
switch {
|
||||
case score >= 75:
|
||||
return "LOW"
|
||||
case score >= 50:
|
||||
return "MEDIUM"
|
||||
case score >= 25:
|
||||
return "HIGH"
|
||||
default:
|
||||
return "CRITICAL"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) calculateComplianceScore(report *ExecutiveReport) int {
|
||||
scores := []int{}
|
||||
weights := []int{}
|
||||
|
||||
// DSGVO: weight 30 (most important)
|
||||
if report.DSGVO.TOMsTotal > 0 {
|
||||
scores = append(scores, report.DSGVO.CompletionPercent)
|
||||
} else {
|
||||
scores = append(scores, 0)
|
||||
}
|
||||
weights = append(weights, 30)
|
||||
|
||||
// Vendor compliance: weight 20
|
||||
vendorScore := 100
|
||||
if report.Vendors.TotalVendors > 0 {
|
||||
vendorScore -= report.Vendors.PendingReviews * 10
|
||||
vendorScore -= report.Vendors.ExpiredContracts * 15
|
||||
}
|
||||
if vendorScore < 0 {
|
||||
vendorScore = 0
|
||||
}
|
||||
scores = append(scores, vendorScore)
|
||||
weights = append(weights, 20)
|
||||
|
||||
// Incident handling: weight 20
|
||||
incidentScore := 100
|
||||
incidentScore -= report.Incidents.OpenIncidents * 10
|
||||
incidentScore -= report.Incidents.NotificationsPending * 20
|
||||
if incidentScore < 0 {
|
||||
incidentScore = 0
|
||||
}
|
||||
scores = append(scores, incidentScore)
|
||||
weights = append(weights, 20)
|
||||
|
||||
// Whistleblower: weight 15
|
||||
whistleScore := 100
|
||||
whistleScore -= report.Whistleblower.OverdueAcknowledgments * 25
|
||||
whistleScore -= report.Whistleblower.OverdueFeedbacks * 15
|
||||
if whistleScore < 0 {
|
||||
whistleScore = 0
|
||||
}
|
||||
scores = append(scores, whistleScore)
|
||||
weights = append(weights, 15)
|
||||
|
||||
// Academy: weight 15
|
||||
academyScore := int(math.Round(report.Academy.CompletionRate))
|
||||
scores = append(scores, academyScore)
|
||||
weights = append(weights, 15)
|
||||
|
||||
totalWeight := 0
|
||||
weightedSum := 0
|
||||
for i, sc := range scores {
|
||||
weightedSum += sc * weights[i]
|
||||
totalWeight += weights[i]
|
||||
}
|
||||
if totalWeight == 0 {
|
||||
return 0
|
||||
}
|
||||
return int(math.Round(float64(weightedSum) / float64(totalWeight)))
|
||||
}
|
||||
|
||||
func (s *Store) getUpcomingDeadlines(ctx context.Context, tenantID uuid.UUID) []Deadline {
|
||||
deadlines := []Deadline{}
|
||||
now := time.Now().UTC()
|
||||
|
||||
// Vendor reviews due
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT name, next_review_date FROM vendor_vendors
|
||||
WHERE tenant_id = $1 AND next_review_date IS NOT NULL
|
||||
ORDER BY next_review_date ASC LIMIT 10
|
||||
`, tenantID)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var dueDate time.Time
|
||||
if err := rows.Scan(&name, &dueDate); err != nil {
|
||||
continue
|
||||
}
|
||||
daysLeft := int(dueDate.Sub(now).Hours() / 24)
|
||||
severity := "INFO"
|
||||
if daysLeft < 0 {
|
||||
severity = "OVERDUE"
|
||||
} else if daysLeft <= 7 {
|
||||
severity = "URGENT"
|
||||
} else if daysLeft <= 30 {
|
||||
severity = "WARNING"
|
||||
}
|
||||
deadlines = append(deadlines, Deadline{
|
||||
Module: "Vendors",
|
||||
Type: "REVIEW",
|
||||
Description: "Vendor-Review: " + name,
|
||||
DueDate: dueDate,
|
||||
DaysLeft: daysLeft,
|
||||
Severity: severity,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Contract expirations
|
||||
rows2, err := s.pool.Query(ctx, `
|
||||
SELECT vv.name, vc.expiration_date, vc.document_type FROM vendor_contracts vc
|
||||
JOIN vendor_vendors vv ON vc.vendor_id = vv.id
|
||||
WHERE vc.tenant_id = $1 AND vc.expiration_date IS NOT NULL
|
||||
ORDER BY vc.expiration_date ASC LIMIT 10
|
||||
`, tenantID)
|
||||
if err == nil {
|
||||
defer rows2.Close()
|
||||
for rows2.Next() {
|
||||
var name, docType string
|
||||
var dueDate time.Time
|
||||
if err := rows2.Scan(&name, &dueDate, &docType); err != nil {
|
||||
continue
|
||||
}
|
||||
daysLeft := int(dueDate.Sub(now).Hours() / 24)
|
||||
severity := "INFO"
|
||||
if daysLeft < 0 {
|
||||
severity = "OVERDUE"
|
||||
} else if daysLeft <= 14 {
|
||||
severity = "URGENT"
|
||||
} else if daysLeft <= 60 {
|
||||
severity = "WARNING"
|
||||
}
|
||||
deadlines = append(deadlines, Deadline{
|
||||
Module: "Contracts",
|
||||
Type: "EXPIRATION",
|
||||
Description: docType + " läuft ab: " + name,
|
||||
DueDate: dueDate,
|
||||
DaysLeft: daysLeft,
|
||||
Severity: severity,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// DSR deadlines (overdue)
|
||||
rows3, err := s.pool.Query(ctx, `
|
||||
SELECT request_type, deadline FROM dsgvo_dsr_requests
|
||||
WHERE tenant_id = $1 AND status NOT IN ('COMPLETED', 'REJECTED')
|
||||
AND deadline IS NOT NULL
|
||||
ORDER BY deadline ASC LIMIT 10
|
||||
`, tenantID)
|
||||
if err == nil {
|
||||
defer rows3.Close()
|
||||
for rows3.Next() {
|
||||
var reqType string
|
||||
var dueDate time.Time
|
||||
if err := rows3.Scan(&reqType, &dueDate); err != nil {
|
||||
continue
|
||||
}
|
||||
daysLeft := int(dueDate.Sub(now).Hours() / 24)
|
||||
severity := "INFO"
|
||||
if daysLeft < 0 {
|
||||
severity = "OVERDUE"
|
||||
} else if daysLeft <= 3 {
|
||||
severity = "URGENT"
|
||||
} else if daysLeft <= 14 {
|
||||
severity = "WARNING"
|
||||
}
|
||||
deadlines = append(deadlines, Deadline{
|
||||
Module: "DSR",
|
||||
Type: "RESPONSE",
|
||||
Description: "Betroffenenrecht: " + reqType,
|
||||
DueDate: dueDate,
|
||||
DaysLeft: daysLeft,
|
||||
Severity: severity,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by due date ascending
|
||||
sort.Slice(deadlines, func(i, j int) bool {
|
||||
return deadlines[i].DueDate.Before(deadlines[j].DueDate)
|
||||
})
|
||||
|
||||
// Limit to top 15
|
||||
if len(deadlines) > 15 {
|
||||
deadlines = deadlines[:15]
|
||||
}
|
||||
|
||||
return deadlines
|
||||
}
|
||||
|
||||
func (s *Store) getRecentActivity(ctx context.Context, tenantID uuid.UUID) []ActivityEntry {
|
||||
activities := []ActivityEntry{}
|
||||
|
||||
// Recent vendors created/updated
|
||||
rows, _ := s.pool.Query(ctx, `
|
||||
SELECT name, created_at, 'CREATED' as action FROM vendor_vendors
|
||||
WHERE tenant_id = $1 AND created_at > NOW() - INTERVAL '30 days'
|
||||
UNION ALL
|
||||
SELECT name, updated_at, 'UPDATED' FROM vendor_vendors
|
||||
WHERE tenant_id = $1 AND updated_at > created_at AND updated_at > NOW() - INTERVAL '30 days'
|
||||
ORDER BY 2 DESC LIMIT 5
|
||||
`, tenantID)
|
||||
if rows != nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var name, action string
|
||||
var ts time.Time
|
||||
if err := rows.Scan(&name, &ts, &action); err != nil {
|
||||
continue
|
||||
}
|
||||
desc := "Vendor "
|
||||
if action == "CREATED" {
|
||||
desc += "angelegt: "
|
||||
} else {
|
||||
desc += "aktualisiert: "
|
||||
}
|
||||
activities = append(activities, ActivityEntry{
|
||||
Timestamp: ts,
|
||||
Module: "Vendors",
|
||||
Action: action,
|
||||
Description: desc + name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Recent incidents
|
||||
rows2, _ := s.pool.Query(ctx, `
|
||||
SELECT title, created_at, severity FROM incidents
|
||||
WHERE tenant_id = $1 AND created_at > NOW() - INTERVAL '30 days'
|
||||
ORDER BY created_at DESC LIMIT 5
|
||||
`, tenantID)
|
||||
if rows2 != nil {
|
||||
defer rows2.Close()
|
||||
for rows2.Next() {
|
||||
var title, severity string
|
||||
var ts time.Time
|
||||
if err := rows2.Scan(&title, &ts, &severity); err != nil {
|
||||
continue
|
||||
}
|
||||
activities = append(activities, ActivityEntry{
|
||||
Timestamp: ts,
|
||||
Module: "Incidents",
|
||||
Action: "CREATED",
|
||||
Description: "Datenpanne (" + severity + "): " + title,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Recent whistleblower reports (admin view)
|
||||
rows3, _ := s.pool.Query(ctx, `
|
||||
SELECT category, created_at FROM whistleblower_reports
|
||||
WHERE tenant_id = $1 AND created_at > NOW() - INTERVAL '30 days'
|
||||
ORDER BY created_at DESC LIMIT 5
|
||||
`, tenantID)
|
||||
if rows3 != nil {
|
||||
defer rows3.Close()
|
||||
for rows3.Next() {
|
||||
var category string
|
||||
var ts time.Time
|
||||
if err := rows3.Scan(&category, &ts); err != nil {
|
||||
continue
|
||||
}
|
||||
activities = append(activities, ActivityEntry{
|
||||
Timestamp: ts,
|
||||
Module: "Whistleblower",
|
||||
Action: "REPORT",
|
||||
Description: "Neue Meldung: " + category,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by timestamp descending (most recent first)
|
||||
sort.Slice(activities, func(i, j int) bool {
|
||||
return activities[i].Timestamp.After(activities[j].Timestamp)
|
||||
})
|
||||
|
||||
if len(activities) > 20 {
|
||||
activities = activities[:20]
|
||||
}
|
||||
|
||||
return activities
|
||||
}
|
||||
Reference in New Issue
Block a user