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

162 lines
4.3 KiB
Go

package repository
import (
"context"
"database/sql"
"time"
)
// Invoice represents a financial invoice
type Invoice struct {
ID string `json:"id"`
CustomerID string `json:"customer_id"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
Status string `json:"status"`
DueDate *time.Time `json:"due_date"`
PaidAt *time.Time `json:"paid_at"`
CreatedAt time.Time `json:"created_at"`
}
// Expense represents a business expense
type Expense struct {
ID string `json:"id"`
Category string `json:"category"`
Description string `json:"description"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
Date time.Time `json:"date"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
}
// Account represents a ledger account
type Account struct {
Code string `json:"code"`
Name string `json:"name"`
Type string `json:"type"`
Balance float64 `json:"balance"`
}
// FinanceRepository handles all finance-related database operations
type FinanceRepository struct {
db *sql.DB
}
func NewFinanceRepository(db *sql.DB) *FinanceRepository {
return &FinanceRepository{db: db}
}
// ListInvoices returns all invoices
func (r *FinanceRepository) ListInvoices(ctx context.Context) ([]Invoice, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT id, customer_id, amount, currency, status, due_date, paid_at, created_at
FROM boc_invoices
ORDER BY created_at DESC
LIMIT 100
`)
if err != nil {
return nil, err
}
defer rows.Close()
var invoices []Invoice
for rows.Next() {
var i Invoice
if err := rows.Scan(&i.ID, &i.CustomerID, &i.Amount, &i.Currency, &i.Status, &i.DueDate, &i.PaidAt, &i.CreatedAt); err != nil {
continue
}
invoices = append(invoices, i)
}
return invoices, rows.Err()
}
// ListExpenses returns all expenses
func (r *FinanceRepository) ListExpenses(ctx context.Context) ([]Expense, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT id, category, description, amount, currency, date, status, created_at
FROM boc_expenses
ORDER BY date DESC
LIMIT 100
`)
if err != nil {
return nil, err
}
defer rows.Close()
var expenses []Expense
for rows.Next() {
var e Expense
if err := rows.Scan(&e.ID, &e.Category, &e.Description, &e.Amount, &e.Currency, &e.Date, &e.Status, &e.CreatedAt); err != nil {
continue
}
expenses = append(expenses, e)
}
return expenses, rows.Err()
}
// CreateExpense creates a new expense
func (r *FinanceRepository) CreateExpense(ctx context.Context, e *Expense) error {
return r.db.QueryRowContext(ctx, `
INSERT INTO boc_expenses (category, description, amount, currency, date, status)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, created_at
`, e.Category, e.Description, e.Amount, e.Currency, e.Date, e.Status).Scan(&e.ID, &e.CreatedAt)
}
// GetCashFlow returns income, outstanding, and expenses for the last month
func (r *FinanceRepository) GetCashFlow(ctx context.Context) (income, outstanding, expenses float64, err error) {
err = r.db.QueryRowContext(ctx, `
SELECT COALESCE(SUM(amount), 0)
FROM boc_invoices
WHERE status = 'paid' AND paid_at >= NOW() - INTERVAL '1 month'
`).Scan(&income)
if err != nil {
return 0, 0, 0, err
}
err = r.db.QueryRowContext(ctx, `
SELECT COALESCE(SUM(amount), 0)
FROM boc_invoices
WHERE status = 'sent'
`).Scan(&outstanding)
if err != nil {
return 0, 0, 0, err
}
err = r.db.QueryRowContext(ctx, `
SELECT COALESCE(SUM(amount), 0)
FROM boc_expenses
WHERE status = 'approved' AND created_at >= NOW() - INTERVAL '1 month'
`).Scan(&expenses)
if err != nil {
return 0, 0, 0, err
}
return income, outstanding, expenses, nil
}
// GetAccounts returns all ledger accounts
func (r *FinanceRepository) GetAccounts(ctx context.Context) ([]Account, error) {
// This should ideally query aamos-ledger, but for now return from local cache
rows, err := r.db.QueryContext(ctx, `
SELECT code, name, type, COALESCE(balance, 0)
FROM boc_accounts
ORDER BY code
`)
if err != nil {
return nil, err
}
defer rows.Close()
var accounts []Account
for rows.Next() {
var a Account
if err := rows.Scan(&a.Code, &a.Name, &a.Type, &a.Balance); err != nil {
continue
}
accounts = append(accounts, a)
}
return accounts, rows.Err()
}