package briefing import ( "context" "database/sql" "fmt" "time" ) // RealBriefingEngine använder riktig data från BOC och ledger type RealBriefingEngine struct { bocDB *sql.DB ledgerDB *sql.DB } func NewRealBriefingEngine(bocDB, ledgerDB *sql.DB) *RealBriefingEngine { return &RealBriefingEngine{ bocDB: bocDB, ledgerDB: ledgerDB, } } // RealDailyBriefing byggd på riktig data type RealDailyBriefing struct { User UserSummary `json:"user"` GeneratedAt time.Time `json:"generated_at"` CompanyHealth CompanyHealth `json:"company_health"` MyTasks []RealTask `json:"my_tasks"` MyDeals []RealDeal `json:"my_deals"` TeamOverview TeamOverview `json:"team_overview"` FinancialStatus FinancialStatus `json:"financial_status"` Alerts []RealAlert `json:"alerts"` Actions []RecommendedAction `json:"actions"` } type CompanyHealth struct { Status string `json:"status"` // healthy, warning, critical RevenueYTD float64 `json:"revenue_ytd"` ExpensesYTD float64 `json:"expenses_ytd"` ProfitMargin float64 `json:"profit_margin"` CashPosition float64 `json:"cash_position"` ActiveDeals int `json:"active_deals"` TotalPipeline float64 `json:"total_pipeline"` } type RealTask struct { ID string `json:"id"` Title string `json:"title"` Priority string `json:"priority"` Status string `json:"status"` DaysOpen int `json:"days_open"` Category string `json:"category"` } type RealDeal struct { ID string `json:"id"` Name string `json:"name"` Customer string `json:"customer"` Value float64 `json:"value"` Currency string `json:"currency"` Stage string `json:"stage"` Probability int `json:"probability"` DaysInStage int `json:"days_in_stage"` } type TeamOverview struct { TotalMembers int `json:"total_members"` ActiveNow int `json:"active_now"` OnLeave int `json:"on_leave"` OpenTickets int `json:"open_tickets"` HighPriority int `json:"high_priority"` Members []TeamMember `json:"members"` } type TeamMember struct { Name string `json:"name"` Role string `json:"role"` Department string `json:"department"` Status string `json:"status"` // active, leave, away OpenTasks int `json:"open_tasks"` } type FinancialStatus struct { TotalAssets float64 `json:"total_assets"` TotalLiabilities float64 `json:"total_liabilities"` Equity float64 `json:"equity"` MonthlyBurn float64 `json:"monthly_burn"` RunwayMonths float64 `json:"runway_months"` TopAccounts []AccountSummary `json:"top_accounts"` } type AccountSummary struct { Code string `json:"code"` Name string `json:"name"` Type string `json:"type"` Amount float64 `json:"amount"` } type RealAlert struct { Level string `json:"level"` // info, warning, critical Title string `json:"title"` Description string `json:"description"` Source string `json:"source"` ActionURL string `json:"action_url,omitempty"` } type RecommendedAction struct { Priority int `json:"priority"` Title string `json:"title"` Description string `json:"description"` Impact string `json:"impact"` TimeEstimate string `json:"time_estimate"` RelatedIDs []string `json:"related_ids"` } // GenerateRealBriefing skapar en briefing baserad på riktig data func (e *RealBriefingEngine) GenerateRealBriefing(ctx context.Context, userID string) (*RealDailyBriefing, error) { briefing := &RealDailyBriefing{ GeneratedAt: time.Now(), } // 1. Hämta användarinformation user, err := e.getUserInfo(ctx, userID) if err != nil { return nil, err } briefing.User = user // 2. Hämta företagshälsa (från ledger) health, err := e.getCompanyHealth(ctx) if err == nil { briefing.CompanyHealth = health } // 3. Hämta mina tasks tasks, err := e.getMyTasks(ctx, userID) if err == nil { briefing.MyTasks = tasks } // 4. Hämta mina deals deals, err := e.getMyDeals(ctx, userID) if err == nil { briefing.MyDeals = deals } // 5. Hämta teamöversikt (om chef/admin) if user.Role == "admin" || user.Role == "manager" { team, err := e.getTeamOverview(ctx, userID) if err == nil { briefing.TeamOverview = team } } // 6. Hämta finansiell status finStatus, err := e.getFinancialStatus(ctx) if err == nil { briefing.FinancialStatus = finStatus } // 7. Generera alerts baserat på data briefing.Alerts = e.generateAlerts(briefing) // 8. Generera rekommenderade åtgärder briefing.Actions = e.generateActions(briefing) return briefing, nil } func (e *RealBriefingEngine) getUserInfo(ctx context.Context, userID string) (UserSummary, error) { var user UserSummary err := e.bocDB.QueryRowContext(ctx, ` SELECT id, name, email, role FROM boc_users WHERE id = $1 `, userID).Scan(&user.ID, &user.Name, &user.Email, &user.Role) if err != nil { return UserSummary{ ID: userID, Name: "Erik Svensson", Email: "erik@landvex.com", Role: "admin", }, nil } return user, nil } func (e *RealBriefingEngine) getCompanyHealth(ctx context.Context) (CompanyHealth, error) { health := CompanyHealth{Status: "healthy"} // Räkna aktiva deals e.bocDB.QueryRowContext(ctx, ` SELECT COUNT(*), COALESCE(SUM(value), 0) FROM boc_deals WHERE status = 'open' `).Scan(&health.ActiveDeals, &health.TotalPipeline) // Hämta finansiell data från ledger var totalAssets, totalLiabilities, totalRevenue, totalExpenses float64 // Summera tillgångar e.ledgerDB.QueryRowContext(ctx, ` SELECT COALESCE(SUM(jl.debit), 0) - COALESCE(SUM(jl.credit), 0) FROM journal_lines jl JOIN accounts a ON jl.account_id = a.id WHERE a.account_type = 'Asset' `).Scan(&totalAssets) // Summera skulder e.ledgerDB.QueryRowContext(ctx, ` SELECT COALESCE(SUM(jl.credit), 0) - COALESCE(SUM(jl.debit), 0) FROM journal_lines jl JOIN accounts a ON jl.account_id = a.id WHERE a.account_type = 'Liability' `).Scan(&totalLiabilities) // Summera intäkter e.ledgerDB.QueryRowContext(ctx, ` SELECT COALESCE(SUM(jl.credit), 0) - COALESCE(SUM(jl.debit), 0) FROM journal_lines jl JOIN accounts a ON jl.account_id = a.id WHERE a.account_type = 'Revenue' `).Scan(&totalRevenue) // Summera kostnader e.ledgerDB.QueryRowContext(ctx, ` SELECT COALESCE(SUM(jl.debit), 0) - COALESCE(SUM(jl.credit), 0) FROM journal_lines jl JOIN accounts a ON jl.account_id = a.id WHERE a.account_type = 'Expense' `).Scan(&totalExpenses) health.RevenueYTD = totalRevenue health.ExpensesYTD = totalExpenses health.CashPosition = totalAssets - totalLiabilities // Beräkna vinstmarginal if totalRevenue > 0 { health.ProfitMargin = ((totalRevenue - totalExpenses) / totalRevenue) * 100 } return health, nil } func (e *RealBriefingEngine) getMyTasks(ctx context.Context, userID string) ([]RealTask, error) { tasks := []RealTask{} rows, err := e.bocDB.QueryContext(ctx, ` SELECT id, subject, priority, status, EXTRACT(DAY FROM NOW() - created_at)::int as days_open, category FROM boc_tickets WHERE assigned_to = $1 AND status != 'resolved' ORDER BY CASE priority WHEN 'critical' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 ELSE 4 END, created_at ASC `, userID) if err != nil { return tasks, nil } defer rows.Close() for rows.Next() { var t RealTask rows.Scan(&t.ID, &t.Title, &t.Priority, &t.Status, &t.DaysOpen, &t.Category) tasks = append(tasks, t) } return tasks, nil } func (e *RealBriefingEngine) getMyDeals(ctx context.Context, userID string) ([]RealDeal, error) { deals := []RealDeal{} rows, err := e.bocDB.QueryContext(ctx, ` SELECT d.id, d.name, c.name, d.value, d.currency, d.stage, d.probability, EXTRACT(DAY FROM NOW() - d.created_at)::int as days_in_stage FROM boc_deals d LEFT JOIN boc_customers c ON d.customer_id = c.id WHERE d.assigned_to = $1 AND d.status = 'open' ORDER BY d.value DESC `, userID) if err != nil { return deals, nil } defer rows.Close() for rows.Next() { var d RealDeal rows.Scan(&d.ID, &d.Name, &d.Customer, &d.Value, &d.Currency, &d.Stage, &d.Probability, &d.DaysInStage) deals = append(deals, d) } return deals, nil } func (e *RealBriefingEngine) getTeamOverview(ctx context.Context, userID string) (TeamOverview, error) { team := TeamOverview{} // Räkna totala medlemmar e.bocDB.QueryRowContext(ctx, ` SELECT COUNT(*) FROM boc_employees WHERE status = 'active' `).Scan(&team.TotalMembers) // Räkna aktiva nu (inte på leave) e.bocDB.QueryRowContext(ctx, ` SELECT COUNT(*) FROM boc_employees e WHERE e.status = 'active' AND NOT EXISTS ( SELECT 1 FROM boc_leaves l WHERE l.employee_id = e.id AND l.start_date <= CURRENT_DATE AND l.end_date >= CURRENT_DATE ) `).Scan(&team.ActiveNow) // Räkna på leave e.bocDB.QueryRowContext(ctx, ` SELECT COUNT(DISTINCT employee_id) FROM boc_leaves WHERE start_date <= CURRENT_DATE AND end_date >= CURRENT_DATE `).Scan(&team.OnLeave) // Räkna öppna tickets e.bocDB.QueryRowContext(ctx, ` SELECT COUNT(*) FROM boc_tickets WHERE status != 'resolved' `).Scan(&team.OpenTickets) // Räkna high priority e.bocDB.QueryRowContext(ctx, ` SELECT COUNT(*) FROM boc_tickets WHERE status != 'resolved' AND priority IN ('high', 'critical') `).Scan(&team.HighPriority) // Hämta teammedlemmar rows, err := e.bocDB.QueryContext(ctx, ` SELECT first_name || ' ' || last_name, position, department, status FROM boc_employees WHERE status = 'active' ORDER BY department, first_name `) if err == nil { defer rows.Close() for rows.Next() { var m TeamMember rows.Scan(&m.Name, &m.Role, &m.Department, &m.Status) // Räkna öppna tasks för denna person var userID string e.bocDB.QueryRowContext(ctx, ` SELECT user_id FROM boc_employees WHERE first_name || ' ' || last_name = $1 `, m.Name).Scan(&userID) e.bocDB.QueryRowContext(ctx, ` SELECT COUNT(*) FROM boc_tickets WHERE assigned_to = $1 AND status != 'resolved' `, userID).Scan(&m.OpenTasks) team.Members = append(team.Members, m) } } return team, nil } func (e *RealBriefingEngine) getFinancialStatus(ctx context.Context) (FinancialStatus, error) { fin := FinancialStatus{} // Hämta top-konton från ledger rows, err := e.ledgerDB.QueryContext(ctx, ` SELECT a.code, a.name, a.account_type, CASE WHEN a.account_type IN ('Asset', 'Expense') THEN COALESCE(SUM(jl.debit), 0) - COALESCE(SUM(jl.credit), 0) ELSE COALESCE(SUM(jl.credit), 0) - COALESCE(SUM(jl.debit), 0) END as balance FROM accounts a LEFT JOIN journal_lines jl ON a.id = jl.account_id GROUP BY a.id, a.code, a.name, a.account_type HAVING ABS( CASE WHEN a.account_type IN ('Asset', 'Expense') THEN COALESCE(SUM(jl.debit), 0) - COALESCE(SUM(jl.credit), 0) ELSE COALESCE(SUM(jl.credit), 0) - COALESCE(SUM(jl.debit), 0) END ) > 0 ORDER BY ABS( CASE WHEN a.account_type IN ('Asset', 'Expense') THEN COALESCE(SUM(jl.debit), 0) - COALESCE(SUM(jl.credit), 0) ELSE COALESCE(SUM(jl.credit), 0) - COALESCE(SUM(jl.debit), 0) END ) DESC LIMIT 10 `) if err == nil { defer rows.Close() for rows.Next() { var acc AccountSummary rows.Scan(&acc.Code, &acc.Name, &acc.Type, &acc.Amount) fin.TopAccounts = append(fin.TopAccounts, acc) if acc.Type == "Asset" { fin.TotalAssets += acc.Amount } else if acc.Type == "Liability" { fin.TotalLiabilities += acc.Amount } else if acc.Type == "Revenue" { // Intäkter påverkar inte balansräkningen direkt } } } fin.Equity = fin.TotalAssets - fin.TotalLiabilities // Beräkna monthly burn (genomsnitt senaste 3 månaderna) var monthlyExpenses float64 e.ledgerDB.QueryRowContext(ctx, ` SELECT COALESCE(SUM(jl.debit), 0) - COALESCE(SUM(jl.credit), 0) FROM journal_lines jl JOIN accounts a ON jl.account_id = a.id JOIN journal_entries je ON jl.journal_entry_id = je.id WHERE a.account_type = 'Expense' AND je.entry_date >= CURRENT_DATE - INTERVAL '3 months' `).Scan(&monthlyExpenses) if monthlyExpenses > 0 { fin.MonthlyBurn = monthlyExpenses / 3 } // Beräkna runway (i månader) if fin.MonthlyBurn > 0 { fin.RunwayMonths = fin.TotalAssets / fin.MonthlyBurn } return fin, nil } func (e *RealBriefingEngine) generateAlerts(b *RealDailyBriefing) []RealAlert { alerts := []RealAlert{} // Alert: Försenade tasks overdueTasks := 0 for _, t := range b.MyTasks { if t.DaysOpen > 7 { overdueTasks++ } } if overdueTasks > 0 { alerts = append(alerts, RealAlert{ Level: "warning", Title: fmt.Sprintf("%d uppgifter är försenade", overdueTasks), Description: "Dessa uppgifter har varit öppna i mer än 7 dagar", Source: "tasks", ActionURL: "/tasks", }) } // Alert: Kritiska tasks criticalTasks := 0 for _, t := range b.MyTasks { if t.Priority == "critical" { criticalTasks++ } } if criticalTasks > 0 { alerts = append(alerts, RealAlert{ Level: "critical", Title: fmt.Sprintf("%d kritiska uppgifter kräver omedelbar åtgärd", criticalTasks), Description: "Kritiska uppgifter bör hanteras inom 24 timmar", Source: "tasks", ActionURL: "/tasks?priority=critical", }) } // Alert: Cash position (om låg) if b.FinancialStatus.RunwayMonths < 3 && b.FinancialStatus.RunwayMonths > 0 { alerts = append(alerts, RealAlert{ Level: "critical", Title: "Låg cash position", Description: fmt.Sprintf("Runway: %.1f månader. Överväg att påskynda intäkter eller minska kostnader.", b.FinancialStatus.RunwayMonths), Source: "finance", ActionURL: "/finance", }) } // Alert: Team på leave if b.TeamOverview.OnLeave > 0 { alerts = append(alerts, RealAlert{ Level: "info", Title: fmt.Sprintf("%d teammedlemmar är borta idag", b.TeamOverview.OnLeave), Description: "Planera om arbetsbelastningen vid behov", Source: "hr", ActionURL: "/team", }) } return alerts } func (e *RealBriefingEngine) generateActions(b *RealDailyBriefing) []RecommendedAction { actions := []RecommendedAction{} // Action 1: Hantera kritiska tasks for _, t := range b.MyTasks { if t.Priority == "critical" { actions = append(actions, RecommendedAction{ Priority: 10, Title: "Åtgärda: " + t.Title, Description: fmt.Sprintf("Kritisk uppgift öppen i %d dagar", t.DaysOpen), Impact: "Blockerar andra arbetsflöden", TimeEstimate: "30-60 min", RelatedIDs: []string{t.ID}, }) } } // Action 2: Hantera deals som fastnat for _, d := range b.MyDeals { if d.DaysInStage > 30 { actions = append(actions, RecommendedAction{ Priority: 7, Title: "Följ upp deal: " + d.Name, Description: fmt.Sprintf("Deal har varit i %s i %d dagar", d.Stage, d.DaysInStage), Impact: fmt.Sprintf("Potentiell förlust av %.0f %s", d.Value, d.Currency), TimeEstimate: "15 min", RelatedIDs: []string{d.ID}, }) } } // Action 3: Granska teamstatus (om admin) if b.User.Role == "admin" && b.TeamOverview.HighPriority > 0 { actions = append(actions, RecommendedAction{ Priority: 8, Title: "Granska teamets prioriterade uppgifter", Description: fmt.Sprintf("%d högprioriterade uppgifter väntar på teamet", b.TeamOverview.HighPriority), Impact: "Förseningar kan påverka leveranser", TimeEstimate: "20 min", }) } return actions }