package handlers import ( "database/sql" "encoding/json" "fmt" "os" "time" "github.com/rs/zerolog/log" ) // AgentDataProvider hämtar riktig data från BOC för agenterna type AgentDataProvider struct { db *sql.DB ledgerDB *sql.DB amosURL string } // NewAgentDataProvider skapar en ny data provider func NewAgentDataProvider(db, ledgerDB *sql.DB) *AgentDataProvider { return &AgentDataProvider{ db: db, ledgerDB: ledgerDB, amosURL: os.Getenv("AMOS_BASE_URL"), } } // FinanceData innehåller finansiell data för Finance Agent type FinanceData struct { Balance *BalanceSnapshot `json:"balance,omitempty"` Income *IncomeSnapshot `json:"income,omitempty"` Cashflow *CashflowSummary `json:"cashflow,omitempty"` Invoices []InvoiceSummary `json:"invoices,omitempty"` Period string `json:"period"` GeneratedAt string `json:"generated_at"` } type BalanceSnapshot struct { TotalAssets float64 `json:"total_assets"` TotalLiabilities float64 `json:"total_liabilities"` TotalEquity float64 `json:"total_equity"` LiquidityRatio float64 `json:"liquidity_ratio"` } type IncomeSnapshot struct { Revenue float64 `json:"revenue"` Expenses float64 `json:"expenses"` NetIncome float64 `json:"net_income"` Margin float64 `json:"margin"` } type CashflowSummary struct { Inflow float64 `json:"inflow"` Outflow float64 `json:"outflow"` Net float64 `json:"net"` } type InvoiceSummary struct { ID string `json:"id"` Customer string `json:"customer"` Amount float64 `json:"amount"` Status string `json:"status"` DueDate string `json:"due_date"` DaysOverdue int `json:"days_overdue,omitempty"` } // CRMData innehåller CRM-data för CRM Agent type CRMData struct { CustomerCount int `json:"customer_count"` LeadCount int `json:"lead_count"` PipelineValue float64 `json:"pipeline_value"` RecentCustomers []CustomerBrief `json:"recent_customers,omitempty"` RecentLeads []LeadBrief `json:"recent_leads,omitempty"` GeneratedAt string `json:"generated_at"` } type CustomerBrief struct { ID string `json:"id"` Name string `json:"name"` Company string `json:"company"` Status string `json:"status"` } type LeadBrief struct { ID string `json:"id"` Name string `json:"name"` Source string `json:"source"` Status string `json:"status"` } // SalesData innehåller säljdata för Sales Agent type SalesData struct { TotalDeals int `json:"total_deals"` MRR float64 `json:"mrr"` ARR float64 `json:"arr"` PipelineStages []StageBrief `json:"pipeline_stages,omitempty"` RecentDeals []DealBrief `json:"recent_deals,omitempty"` GeneratedAt string `json:"generated_at"` } type StageBrief struct { Name string `json:"name"` Deals int `json:"deals"` Value float64 `json:"value"` } type DealBrief struct { ID string `json:"id"` Name string `json:"name"` Value float64 `json:"value"` Stage string `json:"stage"` } // HRData innehåller HR-data för HR Agent type HRData struct { EmployeeCount int `json:"employee_count"` ActiveLeaves int `json:"active_leaves"` Departments []DeptSummary `json:"departments,omitempty"` RecentHires []EmployeeBrief `json:"recent_hires,omitempty"` GeneratedAt string `json:"generated_at"` } type DeptSummary struct { Name string `json:"name"` Count int `json:"count"` } type EmployeeBrief struct { ID string `json:"id"` Name string `json:"name"` Department string `json:"department"` Status string `json:"status"` } // GetFinanceData hämtar finansiell data func (p *AgentDataProvider) GetFinanceData() (*FinanceData, error) { data := &FinanceData{ Period: time.Now().Format("2006-01"), GeneratedAt: time.Now().Format(time.RFC3339), } // Hämta balansräkning if p.ledgerDB != nil { var assets, liabilities, equity float64 err := p.ledgerDB.QueryRow(` SELECT COALESCE(SUM(CASE WHEN account_code < 2000 THEN balance ELSE 0 END), 0) as assets, COALESCE(SUM(CASE WHEN account_code >= 2000 AND account_code < 3000 THEN balance ELSE 0 END), 0) as liabilities, COALESCE(SUM(CASE WHEN account_code >= 3000 THEN balance ELSE 0 END), 0) as equity FROM account_balances WHERE period = $1 `, data.Period).Scan(&assets, &liabilities, &equity) if err == nil { data.Balance = &BalanceSnapshot{ TotalAssets: assets, TotalLiabilities: liabilities, TotalEquity: equity, LiquidityRatio: assets / liabilities * 100, } } // Hämta resultaträkning var revenue, expenses float64 err = p.ledgerDB.QueryRow(` SELECT COALESCE(SUM(CASE WHEN account_code >= 3000 AND account_code < 4000 THEN credit - debit ELSE 0 END), 0) as revenue, COALESCE(SUM(CASE WHEN account_code >= 4000 AND account_code < 8000 THEN debit - credit ELSE 0 END), 0) as expenses FROM journal_entries WHERE DATE_TRUNC('month', entry_date) = DATE_TRUNC('month', CURRENT_DATE) `).Scan(&revenue, &expenses) if err == nil { netIncome := revenue - expenses margin := 0.0 if revenue > 0 { margin = (netIncome / revenue) * 100 } data.Income = &IncomeSnapshot{ Revenue: revenue, Expenses: expenses, NetIncome: netIncome, Margin: margin, } } // Hämta fakturor rows, err := p.ledgerDB.Query(` SELECT id, customer_name, amount, status, due_date, CASE WHEN due_date < CURRENT_DATE AND status != 'paid' THEN EXTRACT(DAY FROM CURRENT_DATE - due_date)::int ELSE 0 END as days_overdue FROM invoices WHERE created_at > CURRENT_DATE - INTERVAL '90 days' ORDER BY created_at DESC LIMIT 10 `) if err == nil { defer rows.Close() for rows.Next() { var inv InvoiceSummary rows.Scan(&inv.ID, &inv.Customer, &inv.Amount, &inv.Status, &inv.DueDate, &inv.DaysOverdue) data.Invoices = append(data.Invoices, inv) } } } return data, nil } // GetCRMData hämtar CRM-data func (p *AgentDataProvider) GetCRMData() (*CRMData, error) { data := &CRMData{ GeneratedAt: time.Now().Format(time.RFC3339), } if p.db != nil { // Kundantal p.db.QueryRow("SELECT COUNT(*) FROM customers WHERE status = 'active'").Scan(&data.CustomerCount) // Leadantal p.db.QueryRow("SELECT COUNT(*) FROM leads WHERE status != 'converted'").Scan(&data.LeadCount) // Pipeline-värde p.db.QueryRow("SELECT COALESCE(SUM(value), 0) FROM pipeline_deals WHERE status = 'open'").Scan(&data.PipelineValue) // Senaste kunder rows, err := p.db.Query(` SELECT id, name, company, status FROM customers ORDER BY created_at DESC LIMIT 5 `) if err == nil { defer rows.Close() for rows.Next() { var c CustomerBrief rows.Scan(&c.ID, &c.Name, &c.Company, &c.Status) data.RecentCustomers = append(data.RecentCustomers, c) } } // Senaste leads rows, err = p.db.Query(` SELECT id, name, source, status FROM leads ORDER BY created_at DESC LIMIT 5 `) if err == nil { defer rows.Close() for rows.Next() { var l LeadBrief rows.Scan(&l.ID, &l.Name, &l.Source, &l.Status) data.RecentLeads = append(data.RecentLeads, l) } } } return data, nil } // GetSalesData hämtar säljdata func (p *AgentDataProvider) GetSalesData() (*SalesData, error) { data := &SalesData{ GeneratedAt: time.Now().Format(time.RFC3339), } if p.db != nil { // Totalt antal deals p.db.QueryRow("SELECT COUNT(*) FROM sales_deals").Scan(&data.TotalDeals) // MRR p.db.QueryRow("SELECT COALESCE(SUM(monthly_value), 0) FROM sales_deals WHERE status = 'active'").Scan(&data.MRR) // ARR data.ARR = data.MRR * 12 // Pipeline-stages rows, err := p.db.Query(` SELECT stage_name, COUNT(*), COALESCE(SUM(value), 0) FROM sales_deals WHERE status = 'open' GROUP BY stage_name ORDER BY stage_name `) if err == nil { defer rows.Close() for rows.Next() { var s StageBrief rows.Scan(&s.Name, &s.Deals, &s.Value) data.PipelineStages = append(data.PipelineStages, s) } } // Senaste deals rows, err = p.db.Query(` SELECT id, name, value, stage FROM sales_deals ORDER BY updated_at DESC LIMIT 5 `) if err == nil { defer rows.Close() for rows.Next() { var d DealBrief rows.Scan(&d.ID, &d.Name, &d.Value, &d.Stage) data.RecentDeals = append(data.RecentDeals, d) } } } return data, nil } // GetHRData hämtar HR-data func (p *AgentDataProvider) GetHRData() (*HRData, error) { data := &HRData{ GeneratedAt: time.Now().Format(time.RFC3339), } if p.db != nil { // Antal anställda p.db.QueryRow("SELECT COUNT(*) FROM employees WHERE status = 'active'").Scan(&data.EmployeeCount) // Aktiva ledigheter p.db.QueryRow(` SELECT COUNT(*) FROM leaves WHERE status = 'approved' AND start_date <= CURRENT_DATE AND end_date >= CURRENT_DATE `).Scan(&data.ActiveLeaves) // Avdelningar rows, err := p.db.Query(` SELECT department, COUNT(*) FROM employees WHERE status = 'active' GROUP BY department `) if err == nil { defer rows.Close() for rows.Next() { var d DeptSummary rows.Scan(&d.Name, &d.Count) data.Departments = append(data.Departments, d) } } // Senaste anställningar rows, err = p.db.Query(` SELECT id, first_name || ' ' || last_name as name, department, status FROM employees ORDER BY start_date DESC LIMIT 5 `) if err == nil { defer rows.Close() for rows.Next() { var e EmployeeBrief rows.Scan(&e.ID, &e.Name, &e.Department, &e.Status) data.RecentHires = append(data.RecentHires, e) } } } return data, nil } // GetDataForAgent hämtar data baserat på agent-typ func (p *AgentDataProvider) GetDataForAgent(rum string) (map[string]interface{}, error) { switch rum { case "finance": data, err := p.GetFinanceData() if err != nil { log.Warn().Err(err).Str("rum", rum).Msg("Failed to get finance data") return nil, err } return structToMap(data), nil case "crm": data, err := p.GetCRMData() if err != nil { log.Warn().Err(err).Str("rum", rum).Msg("Failed to get CRM data") return nil, err } return structToMap(data), nil case "sales": data, err := p.GetSalesData() if err != nil { log.Warn().Err(err).Str("rum", rum).Msg("Failed to get sales data") return nil, err } return structToMap(data), nil case "hr": data, err := p.GetHRData() if err != nil { log.Warn().Err(err).Str("rum", rum).Msg("Failed to get HR data") return nil, err } return structToMap(data), nil default: return map[string]interface{}{ "message": "No real-time data available for this agent type", "rum": rum, }, nil } } // structToMap konverterar en struct till map via JSON func structToMap(v interface{}) map[string]interface{} { var result map[string]interface{} b, _ := json.Marshal(v) json.Unmarshal(b, &result) return result } // FormatDataForPrompt formaterar data för Anthropic prompt func (p *AgentDataProvider) FormatDataForPrompt(rum string) string { data, err := p.GetDataForAgent(rum) if err != nil { return "" } jsonData, _ := json.MarshalIndent(data, "", " ") return fmt.Sprintf("\n\nAKTUELL DATA FRÅN SYSTEMET:\n%s\n\nAnvänd denna data för att ge exakta, datadrivna svar. Om data saknas för en specifik fråga, be användaren specificera.", string(jsonData)) }