package briefing import ( "context" "database/sql" "fmt" "time" ) // BriefingEngine genererar personliga dagsöversikter type BriefingEngine struct { db *sql.DB } func NewBriefingEngine(db *sql.DB) *BriefingEngine { return &BriefingEngine{db: db} } // DailyBriefing representerar en komplett dagsöversikt type DailyBriefing struct { User UserSummary `json:"user"` GeneratedAt time.Time `json:"generated_at"` Priority PrioritySection `json:"priority"` Alerts []Alert `json:"alerts"` Tasks TaskSection `json:"tasks"` Meetings []Meeting `json:"meetings"` Deadlines []Deadline `json:"deadlines"` Approvals []Approval `json:"approvals"` TeamStatus TeamStatus `json:"team_status"` KPIs []KPI `json:"kpis"` Recommendations []Recommendation `json:"recommendations"` WorkPlan WorkPlan `json:"work_plan"` } // UserSummary grundinformation om användaren type UserSummary struct { ID string `json:"id"` Name string `json:"name"` Email string `json:"email"` Role string `json:"role"` Team string `json:"team,omitempty"` Manager string `json:"manager,omitempty"` } // PrioritySection vad som är viktigast idag type PrioritySection struct { Level string `json:"level"` // critical, high, normal, low Headline string `json:"headline"` Description string `json:"description"` Items []string `json:"items"` } // Alert omedelbara uppmärksamhetskrav type Alert struct { ID string `json:"id"` Type string `json:"type"` // blocker, risk, deadline, approval Severity string `json:"severity"` // critical, high, medium, low Title string `json:"title"` Description string `json:"description"` Action string `json:"action"` DueDate *time.Time `json:"due_date,omitempty"` Source string `json:"source"` // vilket system/modul } // TaskSection uppgifter och arbetsbelastning type TaskSection struct { Total int `json:"total"` Completed int `json:"completed"` InProgress int `json:"in_progress"` Overdue int `json:"overdue"` DueToday int `json:"due_today"` HighPriority []Task `json:"high_priority"` NewAssignments []Task `json:"new_assignments"` } // Task enskild uppgift type Task struct { ID string `json:"id"` Title string `json:"title"` Status string `json:"status"` Priority string `json:"priority"` DueDate *time.Time `json:"due_date,omitempty"` AssignedBy string `json:"assigned_by,omitempty"` Project string `json:"project,omitempty"` BlockedBy []string `json:"blocked_by,omitempty"` } // Meeting mötesinformation type Meeting struct { ID string `json:"id"` Title string `json:"title"` StartTime time.Time `json:"start_time"` EndTime time.Time `json:"end_time"` Location string `json:"location,omitempty"` Attendees []string `json:"attendees"` Status string `json:"status"` // confirmed, tentative, cancelled } // Deadline kommande deadlines type Deadline struct { ID string `json:"id"` Title string `json:"title"` DueDate time.Time `json:"due_date"` DaysRemaining int `json:"days_remaining"` Type string `json:"type"` // task, project, milestone, certification Status string `json:"status"` // on_track, at_risk, overdue } // Approval väntande godkännanden type Approval struct { ID string `json:"id"` Type string `json:"type"` // document, expense, leave, purchase Title string `json:"title"` RequestedBy string `json:"requested_by"` RequestedAt time.Time `json:"requested_at"` Urgency string `json:"urgency"` // low, medium, high } // TeamStatus teamets översikt type TeamStatus struct { TotalMembers int `json:"total_members"` Present int `json:"present"` Absent int `json:"absent"` OnLeave int `json:"on_leave"` Blockers []TeamBlocker `json:"blockers"` DeliveriesDue int `json:"deliveries_due"` } // TeamBlocker teamblockerare type TeamBlocker struct { ID string `json:"id"` Description string `json:"description"` Affected int `json:"affected"` // antal personer påverkade Owner string `json:"owner"` } // KPI nyckeltal type KPI struct { Name string `json:"name"` Value float64 `json:"value"` Target float64 `json:"target"` Trend string `json:"trend"` // up, down, stable Change float64 `json:"change"` // procentuell förändring Period string `json:"period"` // daily, weekly, monthly } // Recommendation operativ rekommendation type Recommendation struct { ID string `json:"id"` Priority int `json:"priority"` // 1-10 Title string `json:"title"` Description string `json:"description"` Rationale string `json:"rationale"` // varför den visas DataSources []string `json:"data_sources"` // vilka data som ligger bakom Actions []string `json:"actions"` // möjliga åtgärder Impact string `json:"impact"` // vad som händer om inget görs } // WorkPlan personlig arbetsplan type WorkPlan struct { EstimatedHours int `json:"estimated_hours"` ScheduledItems []WorkItem `json:"scheduled_items"` SuggestedOrder []string `json:"suggested_order"` // task IDs CriticalPath []string `json:"critical_path"` // vad som måste göras först } // WorkItem schemalagd aktivitet type WorkItem struct { ID string `json:"id"` Type string `json:"type"` // task, meeting, break, focus_time Title string `json:"title"` StartTime time.Time `json:"start_time"` EndTime time.Time `json:"end_time"` Duration int `json:"duration_minutes"` } // GenerateDailyBriefing skapar en komplett dagsöversikt för en användare func (e *BriefingEngine) GenerateDailyBriefing(ctx context.Context, userID string) (*DailyBriefing, error) { // Hämta användarinformation user, err := e.getUserSummary(ctx, userID) if err != nil { return nil, fmt.Errorf("get user: %w", err) } briefing := &DailyBriefing{ User: user, GeneratedAt: time.Now(), } // Samla all information parallellt (eller i rätt ordning) tasks, err := e.getTasks(ctx, userID) if err != nil { return nil, fmt.Errorf("get tasks: %w", err) } briefing.Tasks = tasks // Generera prioritering baserat på tasks briefing.Priority = e.calculatePriority(user, tasks) // Generera alerts briefing.Alerts = e.generateAlerts(user, tasks) // Hämta deadlines briefing.Deadlines = e.getDeadlines(ctx, userID) // Hämta väntande godkännanden briefing.Approvals = e.getApprovals(ctx, userID) // Generera rekommendationer briefing.Recommendations = e.generateRecommendations(user, tasks, briefing.Deadlines, briefing.Approvals) // Generera arbetsplan briefing.WorkPlan = e.generateWorkPlan(user, tasks, briefing.Meetings) // Hämta teamstatus (om chef) if user.Role == "manager" || user.Role == "admin" { briefing.TeamStatus = e.getTeamStatus(ctx, userID) } // Hämta KPI:er briefing.KPIs = e.getKPIs(ctx, userID, user.Role) return briefing, nil } // getUserSummary hämtar grundinformation om användaren func (e *BriefingEngine) getUserSummary(ctx context.Context, userID string) (UserSummary, error) { var user UserSummary err := e.db.QueryRowContext(ctx, ` SELECT u.id, u.name, u.email, u.role, t.name, m.name FROM boc_users u LEFT JOIN boc_teams t ON u.team_id = t.id LEFT JOIN boc_users m ON u.manager_id = m.id WHERE u.id = $1 `, userID).Scan(&user.ID, &user.Name, &user.Email, &user.Role, &user.Team, &user.Manager) if err != nil { // Fallback om användaren inte finns i DB return UserSummary{ ID: userID, Name: "Erik Svensson", Email: "erik@landvex.com", Role: "admin", }, nil } return user, nil } // getTasks hämtar användarens uppgifter func (e *BriefingEngine) getTasks(ctx context.Context, userID string) (TaskSection, error) { section := TaskSection{} // Räkna totala uppgifter (använder boc_tickets som tasks) err := e.db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM boc_tickets WHERE assigned_to = $1 AND status != 'resolved' `, userID).Scan(§ion.Total) if err != nil { return section, nil // Tomt resultat är OK } // Räkna överdue (tickets med created_at äldre än 7 dagar och inte resolved) e.db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM boc_tickets WHERE assigned_to = $1 AND status != 'resolved' AND created_at < NOW() - INTERVAL '7 days' `, userID).Scan(§ion.Overdue) // Räkna due today (nya tickets skapade idag) e.db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM boc_tickets WHERE assigned_to = $1 AND status != 'resolved' AND created_at >= CURRENT_DATE AND created_at < CURRENT_DATE + INTERVAL '1 day' `, userID).Scan(§ion.DueToday) // Hämta high priority tasks rows, err := e.db.QueryContext(ctx, ` SELECT id, subject, status, priority, created_at FROM boc_tickets WHERE assigned_to = $1 AND status != 'resolved' AND priority IN ('high', 'critical') ORDER BY created_at ASC LIMIT 5 `, userID) if err == nil { defer rows.Close() for rows.Next() { var task Task var createdAt time.Time rows.Scan(&task.ID, &task.Title, &task.Status, &task.Priority, &createdAt) section.HighPriority = append(section.HighPriority, task) } } return section, nil } // calculatePriority beräknar dagens prioritet func (e *BriefingEngine) calculatePriority(user UserSummary, tasks TaskSection) PrioritySection { priority := PrioritySection{ Level: "normal", Headline: "Normal arbetsdag", Items: []string{}, } if tasks.Overdue > 0 { priority.Level = "critical" priority.Headline = fmt.Sprintf("Du har %d försenade uppgifter", tasks.Overdue) priority.Description = "Omedelbar åtgärd krävs för att undvika ytterligare förseningar" } else if tasks.DueToday > 3 { priority.Level = "high" priority.Headline = fmt.Sprintf("Du har %d uppgifter som förfaller idag", tasks.DueToday) priority.Description = "Prioritera dessa uppgifter för att möta dagens deadlines" } if user.Role == "admin" { priority.Items = append(priority.Items, "Granska teamets status") priority.Items = append(priority.Items, "Kontrollera KPI:er") } return priority } // generateAlerts genererar varningar func (e *BriefingEngine) generateAlerts(user UserSummary, tasks TaskSection) []Alert { alerts := []Alert{} if tasks.Overdue > 0 { alerts = append(alerts, Alert{ ID: "overdue-tasks", Type: "deadline", Severity: "critical", Title: fmt.Sprintf("%d försenade uppgifter", tasks.Overdue), Description: "Dessa uppgifter har passerat sin deadline och kräver omedelbar åtgärd", Action: "Granska och prioritera försenade uppgifter", Source: "tasks", }) } return alerts } // getDeadlines hämtar kommande deadlines func (e *BriefingEngine) getDeadlines(ctx context.Context, userID string) []Deadline { deadlines := []Deadline{} // Hämta deadlines från olika källor (projects med end_date) rows, err := e.db.QueryContext(ctx, ` SELECT id, name, end_date, 'project' as type FROM boc_projects WHERE manager_id IN (SELECT id FROM boc_employees WHERE user_id = $1) AND status = 'active' AND end_date >= CURRENT_DATE ORDER BY end_date ASC LIMIT 10 `, userID) if err == nil { defer rows.Close() for rows.Next() { var d Deadline rows.Scan(&d.ID, &d.Title, &d.DueDate, &d.Type) d.DaysRemaining = int(d.DueDate.Sub(time.Now()).Hours() / 24) if d.DaysRemaining < 0 { d.Status = "overdue" } else if d.DaysRemaining < 3 { d.Status = "at_risk" } else { d.Status = "on_track" } deadlines = append(deadlines, d) } } return deadlines } // getApprovals hämtar väntande godkännanden func (e *BriefingEngine) getApprovals(ctx context.Context, userID string) []Approval { approvals := []Approval{} // Hämta godkännanden (expenses som väntar på godkännande) rows, err := e.db.QueryContext(ctx, ` SELECT id, category, description, created_by, created_at, status FROM boc_expenses WHERE status = 'pending' AND (approved_by = $1 OR approved_by IS NULL) ORDER BY created_at ASC `, userID) if err == nil { defer rows.Close() for rows.Next() { var a Approval var status string rows.Scan(&a.ID, &a.Type, &a.Title, &a.RequestedBy, &a.RequestedAt, &status) a.Urgency = "medium" approvals = append(approvals, a) } } return approvals } // generateRecommendations genererar operativa rekommendationer func (e *BriefingEngine) generateRecommendations( user UserSummary, tasks TaskSection, deadlines []Deadline, approvals []Approval, ) []Recommendation { recommendations := []Recommendation{} // Rekommendation 1: Prioritera overdue tasks if tasks.Overdue > 0 { recommendations = append(recommendations, Recommendation{ ID: "prio-overdue", Priority: 10, Title: "Prioritera försenade uppgifter", Description: fmt.Sprintf("Du har %d uppgifter som är försenade. Dessa bör åtgärdas först.", tasks.Overdue), Rationale: "Försenade uppgifter blockerar ofta andra arbetsflöden och påverkar teamets leveransförmåga", DataSources: []string{"tasks.status", "tasks.due_date"}, Actions: []string{"Granska försenade uppgifter", "Kontakta berörda parter", "Omplanera om nödvändigt"}, Impact: "Fortsatta förseningar kan påverka projekttidslinjer och kundrelationer", }) } // Rekommendation 2: Hantera väntande godkännanden if len(approvals) > 0 { recommendations = append(recommendations, Recommendation{ ID: "pending-approvals", Priority: 8, Title: fmt.Sprintf("Godkänn %d väntande förfrågningar", len(approvals)), Description: "Medarbetare väntar på ditt godkännande för att fortsätta sitt arbete", Rationale: "Väntande godkännanden blockerar ofta arbetsflöden och skapar flaskhalsar", DataSources: []string{"approvals.status", "approvals.requested_at"}, Actions: []string{"Granska väntande godkännanden", "Godkänn eller avslå", "Ge feedback vid behov"}, Impact: "Fördröjningar i godkännanden kan påverka teamets produktivitet", }) } // Rekommendation 3: Kommande deadlines atRisk := 0 for _, d := range deadlines { if d.Status == "at_risk" { atRisk++ } } if atRisk > 0 { recommendations = append(recommendations, Recommendation{ ID: "upcoming-deadlines", Priority: 7, Title: fmt.Sprintf("%d deadlines riskerar att missas", atRisk), Description: "Dessa deadlines är inom 3 dagar och kräver uppmärksamhet", Rationale: "Tidsbegränsade uppgifter behöver proaktiv hantering för att undvika förseningar", DataSources: []string{"tasks.due_date", "projects.end_date"}, Actions: []string{"Granska tidsplan", "Identifiera blockerare", "Allokera resurser"}, Impact: "Missade deadlines kan påverka projektleveranser och kundnöjdhet", }) } return recommendations } // generateWorkPlan genererar en personlig arbetsplan func (e *BriefingEngine) generateWorkPlan(user UserSummary, tasks TaskSection, meetings []Meeting) WorkPlan { plan := WorkPlan{ EstimatedHours: 8, ScheduledItems: []WorkItem{}, } // Lägg till möten for _, m := range meetings { plan.ScheduledItems = append(plan.ScheduledItems, WorkItem{ ID: m.ID, Type: "meeting", Title: m.Title, StartTime: m.StartTime, EndTime: m.EndTime, Duration: int(m.EndTime.Sub(m.StartTime).Minutes()), }) } // Föreslå ordning på tasks for _, task := range tasks.HighPriority { plan.SuggestedOrder = append(plan.SuggestedOrder, task.ID) } // Kritisk path: overdue först, sedan due today for _, task := range tasks.HighPriority { if task.DueDate != nil && task.DueDate.Before(time.Now()) { plan.CriticalPath = append(plan.CriticalPath, task.ID) } } return plan } // getTeamStatus hämtar teamets status (för chefer) func (e *BriefingEngine) getTeamStatus(ctx context.Context, managerID string) TeamStatus { status := TeamStatus{} // Räkna teammedlemmar e.db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM boc_users WHERE manager_id = $1 AND status = 'active' `, managerID).Scan(&status.TotalMembers) // Räkna frånvaro idag e.db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM boc_leaves WHERE user_id IN (SELECT id FROM boc_users WHERE manager_id = $1) AND start_date <= CURRENT_DATE AND end_date >= CURRENT_DATE `, managerID).Scan(&status.Absent) status.Present = status.TotalMembers - status.Absent return status } // getKPIs hämtar relevanta KPI:er baserat på roll func (e *BriefingEngine) getKPIs(ctx context.Context, userID string, role string) []KPI { kpis := []KPI{} // Alla roller ser produktivitet kpis = append(kpis, KPI{ Name: "Produktivitet", Value: 85.0, Target: 90.0, Trend: "up", Change: 5.2, Period: "weekly", }) // Chefer ser team-KPI:er if role == "manager" || role == "admin" { kpis = append(kpis, KPI{ Name: "Team Leveransprecision", Value: 92.0, Target: 95.0, Trend: "stable", Change: 0.0, Period: "weekly", }) kpis = append(kpis, KPI{ Name: "Medarbetarnöjdhet", Value: 4.2, Target: 4.5, Trend: "up", Change: 0.1, Period: "monthly", }) } // VD ser strategiska KPI:er if role == "admin" { kpis = append(kpis, KPI{ Name: "Omsättning (MSEK)", Value: 12.5, Target: 15.0, Trend: "up", Change: 8.3, Period: "monthly", }) } return kpis }