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
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
package ledger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
// RobustClient connects directly to aamos-ledger database
|
||||
// and provides a stable API for BOC
|
||||
type RobustClient struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NewRobustClient creates a client connected to ledger DB
|
||||
func NewRobustClient(dbURL string) (*RobustClient, error) {
|
||||
db, err := sql.Open("postgres", dbURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to ledger DB: %w", err)
|
||||
}
|
||||
|
||||
db.SetMaxOpenConns(10)
|
||||
db.SetMaxIdleConns(5)
|
||||
db.SetConnMaxLifetime(5 * time.Minute)
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("failed to ping ledger DB: %w", err)
|
||||
}
|
||||
|
||||
return &RobustClient{db: db}, nil
|
||||
}
|
||||
|
||||
// LedgerAccount represents a BAS account
|
||||
type LedgerAccount struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
AccountType string `json:"account_type"`
|
||||
Balance float64 `json:"balance"`
|
||||
}
|
||||
|
||||
// GetAccounts returns all BAS accounts with balances
|
||||
func (c *RobustClient) GetAccounts(ctx context.Context) ([]LedgerAccount, error) {
|
||||
rows, err := c.db.QueryContext(ctx, `
|
||||
SELECT a.code, a.name, a.account_type,
|
||||
COALESCE(SUM(CASE WHEN jl.debit IS NOT NULL THEN jl.debit ELSE 0 END) -
|
||||
SUM(CASE WHEN jl.credit IS NOT NULL THEN jl.credit ELSE 0 END), 0) 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
|
||||
ORDER BY a.code
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query accounts: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var accounts []LedgerAccount
|
||||
for rows.Next() {
|
||||
var a LedgerAccount
|
||||
if err := rows.Scan(&a.Code, &a.Name, &a.AccountType, &a.Balance); err != nil {
|
||||
return nil, fmt.Errorf("scan account: %w", err)
|
||||
}
|
||||
accounts = append(accounts, a)
|
||||
}
|
||||
|
||||
return accounts, rows.Err()
|
||||
}
|
||||
|
||||
// BalanceSheet represents a balance sheet report
|
||||
type BalanceSheet struct {
|
||||
Assets []LedgerAccount `json:"assets"`
|
||||
Liabilities []LedgerAccount `json:"liabilities"`
|
||||
Equity []LedgerAccount `json:"equity"`
|
||||
TotalAssets float64 `json:"total_assets"`
|
||||
TotalLiabilities float64 `json:"total_liabilities"`
|
||||
TotalEquity float64 `json:"total_equity"`
|
||||
Period string `json:"period"`
|
||||
}
|
||||
|
||||
// GetBalanceSheet returns assets, liabilities, equity for a period
|
||||
func (c *RobustClient) GetBalanceSheet(ctx context.Context, period string) (*BalanceSheet, error) {
|
||||
if period == "" {
|
||||
period = time.Now().Format("2006-01")
|
||||
}
|
||||
|
||||
rows, err := c.db.QueryContext(ctx, `
|
||||
SELECT a.code, a.name, a.account_type,
|
||||
COALESCE(SUM(CASE WHEN jl.debit IS NOT NULL THEN jl.debit ELSE 0 END) -
|
||||
SUM(CASE WHEN jl.credit IS NOT NULL THEN jl.credit ELSE 0 END), 0) as balance
|
||||
FROM accounts a
|
||||
LEFT JOIN journal_lines jl ON a.id = jl.account_id
|
||||
LEFT JOIN journal_entries je ON jl.journal_entry_id = je.id AND je.period = $1 AND je.status = 'posted'
|
||||
GROUP BY a.id, a.code, a.name, a.account_type
|
||||
ORDER BY a.code
|
||||
`, period)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query balance sheet: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
bs := &BalanceSheet{Period: period}
|
||||
for rows.Next() {
|
||||
var a LedgerAccount
|
||||
if err := rows.Scan(&a.Code, &a.Name, &a.AccountType, &a.Balance); err != nil {
|
||||
return nil, fmt.Errorf("scan account: %w", err)
|
||||
}
|
||||
|
||||
switch a.AccountType {
|
||||
case "Asset":
|
||||
bs.Assets = append(bs.Assets, a)
|
||||
bs.TotalAssets += a.Balance
|
||||
case "Liability":
|
||||
bs.Liabilities = append(bs.Liabilities, a)
|
||||
bs.TotalLiabilities += a.Balance
|
||||
case "Equity":
|
||||
bs.Equity = append(bs.Equity, a)
|
||||
bs.TotalEquity += a.Balance
|
||||
}
|
||||
}
|
||||
|
||||
return bs, rows.Err()
|
||||
}
|
||||
|
||||
// IncomeStatement represents a P&L report
|
||||
type IncomeStatement struct {
|
||||
Revenues []LedgerAccount `json:"revenues"`
|
||||
Expenses []LedgerAccount `json:"expenses"`
|
||||
TotalRevenue float64 `json:"total_revenue"`
|
||||
TotalExpense float64 `json:"total_expense"`
|
||||
NetIncome float64 `json:"net_income"`
|
||||
Period string `json:"period"`
|
||||
}
|
||||
|
||||
// GetIncomeStatement returns P&L for a period
|
||||
func (c *RobustClient) GetIncomeStatement(ctx context.Context, period string) (*IncomeStatement, error) {
|
||||
if period == "" {
|
||||
period = time.Now().Format("2006-01")
|
||||
}
|
||||
|
||||
rows, err := c.db.QueryContext(ctx, `
|
||||
SELECT a.code, a.name, a.account_type,
|
||||
COALESCE(SUM(CASE WHEN jl.debit IS NOT NULL THEN jl.debit ELSE 0 END) -
|
||||
SUM(CASE WHEN jl.credit IS NOT NULL THEN jl.credit ELSE 0 END), 0) as balance
|
||||
FROM accounts a
|
||||
LEFT JOIN journal_lines jl ON a.id = jl.account_id
|
||||
LEFT JOIN journal_entries je ON jl.journal_entry_id = je.id AND je.period = $1 AND je.status = 'posted'
|
||||
WHERE a.account_type IN ('Revenue', 'Expense')
|
||||
GROUP BY a.id, a.code, a.name, a.account_type
|
||||
ORDER BY a.code
|
||||
`, period)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query income statement: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
is := &IncomeStatement{Period: period}
|
||||
for rows.Next() {
|
||||
var a LedgerAccount
|
||||
if err := rows.Scan(&a.Code, &a.Name, &a.AccountType, &a.Balance); err != nil {
|
||||
return nil, fmt.Errorf("scan account: %w", err)
|
||||
}
|
||||
|
||||
switch a.AccountType {
|
||||
case "Revenue":
|
||||
is.Revenues = append(is.Revenues, a)
|
||||
is.TotalRevenue += a.Balance
|
||||
case "Expense":
|
||||
is.Expenses = append(is.Expenses, a)
|
||||
is.TotalExpense += a.Balance
|
||||
}
|
||||
}
|
||||
|
||||
is.NetIncome = is.TotalRevenue - is.TotalExpense
|
||||
return is, rows.Err()
|
||||
}
|
||||
|
||||
// MomsReport represents Swedish VAT report
|
||||
type MomsReport struct {
|
||||
MomsIn float64 `json:"moms_in"`
|
||||
MomsUt float64 `json:"moms_ut"`
|
||||
MomsAttBetala float64 `json:"moms_att_betala"`
|
||||
Period string `json:"period"`
|
||||
}
|
||||
|
||||
// GetMomsReport returns VAT report for a period
|
||||
func (c *RobustClient) GetMomsReport(ctx context.Context, period string) (*MomsReport, error) {
|
||||
if period == "" {
|
||||
period = time.Now().Format("2006-01")
|
||||
}
|
||||
|
||||
var report MomsReport
|
||||
report.Period = period
|
||||
|
||||
// Moms in (utgående moms från försäljning)
|
||||
err := c.db.QueryRowContext(ctx, `
|
||||
SELECT COALESCE(SUM(jl.credit), 0)
|
||||
FROM journal_lines jl
|
||||
JOIN journal_entries je ON jl.journal_entry_id = je.id
|
||||
JOIN accounts a ON jl.account_id = a.id
|
||||
WHERE je.period = $1 AND je.status = 'posted'
|
||||
AND a.code LIKE '26%'
|
||||
`, period).Scan(&report.MomsUt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query moms ut: %w", err)
|
||||
}
|
||||
|
||||
// Moms att betala (förenklad - i verkligheten mer komplex)
|
||||
report.MomsAttBetala = report.MomsUt - report.MomsIn
|
||||
|
||||
return &report, nil
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
func (c *RobustClient) Close() error {
|
||||
return c.db.Close()
|
||||
}
|
||||
Reference in New Issue
Block a user