Files
Bernt e5623d2f84 feat: integrate Grafana dashboards into BOC DashboardPage
- Add Infrastructure Health section with CPU/Memory/Disk panels
- Add Service Status section with PM2/Docker panels
- Create GrafanaPanel component for iframe embedding
- Build passes successfully
2026-07-29 19:03:06 +00:00

563 lines
18 KiB
Go

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(&section.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(&section.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(&section.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
}