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,265 @@
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Manager hanterar multi-tenancy för hela BOC-systemet
|
||||
type Manager struct {
|
||||
db *sql.DB
|
||||
cache map[string]*Tenant
|
||||
cacheMu sync.RWMutex
|
||||
cacheTTL time.Duration
|
||||
}
|
||||
|
||||
// Tenant representerar en verksamhet i systemet
|
||||
type Tenant struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Domain string `json:"domain"`
|
||||
OrgNumber string `json:"org_number,omitempty"` // Svenskt org.nr
|
||||
VatNumber string `json:"vat_number,omitempty"` // Momsregistreringsnummer
|
||||
Address string `json:"address,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Currency string `json:"currency"` // SEK, EUR, USD
|
||||
Language string `json:"language"` // sv, en, etc.
|
||||
Timezone string `json:"timezone"`
|
||||
FiscalYear string `json:"fiscal_year"` // 2026-01-01
|
||||
Status string `json:"status"` // active, suspended, archived
|
||||
ParentID *string `json:"parent_id,omitempty"` // För koncernstruktur
|
||||
LedgerDB string `json:"ledger_db,omitempty"` // Separat ledger-databas
|
||||
Settings Settings `json:"settings"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Settings innehåller tenant-specifika konfigurationer
|
||||
type Settings struct {
|
||||
AccountingStandard string `json:"accounting_standard"` // BAS2024, etc.
|
||||
VatRate float64 `json:"vat_rate"`
|
||||
PayrollProvider string `json:"payroll_provider,omitempty"`
|
||||
BankAccounts []BankAccount `json:"bank_accounts,omitempty"`
|
||||
Integrations map[string]string `json:"integrations,omitempty"`
|
||||
Features []string `json:"features,omitempty"` // Vilka moduler som är aktiva
|
||||
}
|
||||
|
||||
// BankAccount representerar ett bankkonto för en tenant
|
||||
type BankAccount struct {
|
||||
Name string `json:"name"`
|
||||
AccountNo string `json:"account_no"`
|
||||
BIC string `json:"bic,omitempty"`
|
||||
IBAN string `json:"iban,omitempty"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
// NewManager skapar en ny tenant manager
|
||||
func NewManager(db *sql.DB) *Manager {
|
||||
return &Manager{
|
||||
db: db,
|
||||
cache: make(map[string]*Tenant),
|
||||
cacheTTL: 5 * time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
// GetTenant hämtar en tenant baserat på ID
|
||||
func (m *Manager) GetTenant(ctx context.Context, tenantID string) (*Tenant, error) {
|
||||
// Kolla cache först
|
||||
m.cacheMu.RLock()
|
||||
if tenant, ok := m.cache[tenantID]; ok {
|
||||
m.cacheMu.RUnlock()
|
||||
return tenant, nil
|
||||
}
|
||||
m.cacheMu.RUnlock()
|
||||
|
||||
// Hämta från databas
|
||||
var t Tenant
|
||||
var settingsJSON []byte
|
||||
var parentID sql.NullString
|
||||
var fiscalYearStart sql.NullTime
|
||||
var orgNumber, vatNumber, address, phone, email, ledgerDB sql.NullString
|
||||
|
||||
err := m.db.QueryRowContext(ctx, `
|
||||
SELECT id, name, slug, domain, org_number, vat_number, address, phone, email,
|
||||
currency, language, timezone, fiscal_year_start, status, parent_id,
|
||||
ledger_db, settings, created_at, updated_at
|
||||
FROM boc_tenants
|
||||
WHERE id = $1
|
||||
`, tenantID).Scan(
|
||||
&t.ID, &t.Name, &t.Slug, &t.Domain, &orgNumber,
|
||||
&vatNumber, &address, &phone, &email,
|
||||
&t.Currency, &t.Language, &t.Timezone, &fiscalYearStart,
|
||||
&t.Status, &parentID, &ledgerDB, &settingsJSON,
|
||||
&t.CreatedAt, &t.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("tenant not found: %s", tenantID)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get tenant: %w", err)
|
||||
}
|
||||
|
||||
if orgNumber.Valid {
|
||||
t.OrgNumber = orgNumber.String
|
||||
}
|
||||
if vatNumber.Valid {
|
||||
t.VatNumber = vatNumber.String
|
||||
}
|
||||
if address.Valid {
|
||||
t.Address = address.String
|
||||
}
|
||||
if phone.Valid {
|
||||
t.Phone = phone.String
|
||||
}
|
||||
if email.Valid {
|
||||
t.Email = email.String
|
||||
}
|
||||
if ledgerDB.Valid {
|
||||
t.LedgerDB = ledgerDB.String
|
||||
}
|
||||
|
||||
if fiscalYearStart.Valid {
|
||||
t.FiscalYear = fiscalYearStart.Time.Format("2006-01-02")
|
||||
}
|
||||
|
||||
if parentID.Valid {
|
||||
t.ParentID = &parentID.String
|
||||
}
|
||||
|
||||
// Parse settings JSON
|
||||
if len(settingsJSON) > 0 {
|
||||
// TODO: Parse settings
|
||||
}
|
||||
|
||||
// Uppdatera cache
|
||||
m.cacheMu.Lock()
|
||||
m.cache[tenantID] = &t
|
||||
m.cacheMu.Unlock()
|
||||
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
// GetTenantBySlug hämtar tenant baserat på slug
|
||||
func (m *Manager) GetTenantBySlug(ctx context.Context, slug string) (*Tenant, error) {
|
||||
var tenantID string
|
||||
err := m.db.QueryRowContext(ctx, `
|
||||
SELECT id FROM boc_tenants WHERE slug = $1 AND status = 'active'
|
||||
`, slug).Scan(&tenantID)
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("tenant not found: %s", slug)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m.GetTenant(ctx, tenantID)
|
||||
}
|
||||
|
||||
// ListTenants listar alla aktiva tenants
|
||||
func (m *Manager) ListTenants(ctx context.Context) ([]*Tenant, error) {
|
||||
rows, err := m.db.QueryContext(ctx, `
|
||||
SELECT id, name, slug, domain, status, currency, language, created_at
|
||||
FROM boc_tenants
|
||||
WHERE status = 'active'
|
||||
ORDER BY name
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tenants []*Tenant
|
||||
for rows.Next() {
|
||||
var t Tenant
|
||||
rows.Scan(&t.ID, &t.Name, &t.Slug, &t.Domain, &t.Status, &t.Currency, &t.Language, &t.CreatedAt)
|
||||
tenants = append(tenants, &t)
|
||||
}
|
||||
|
||||
return tenants, nil
|
||||
}
|
||||
|
||||
// ListChildTenants listar dotterbolag för en koncern
|
||||
func (m *Manager) ListChildTenants(ctx context.Context, parentID string) ([]*Tenant, error) {
|
||||
rows, err := m.db.QueryContext(ctx, `
|
||||
SELECT id, name, slug, domain, status, currency, language, created_at
|
||||
FROM boc_tenants
|
||||
WHERE parent_id = $1 AND status = 'active'
|
||||
ORDER BY name
|
||||
`, parentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tenants []*Tenant
|
||||
for rows.Next() {
|
||||
var t Tenant
|
||||
rows.Scan(&t.ID, &t.Name, &t.Slug, &t.Domain, &t.Status, &t.Currency, &t.Language, &t.CreatedAt)
|
||||
tenants = append(tenants, &t)
|
||||
}
|
||||
|
||||
return tenants, nil
|
||||
}
|
||||
|
||||
// CreateTenant skapar en ny verksamhet
|
||||
func (m *Manager) CreateTenant(ctx context.Context, tenant *Tenant) error {
|
||||
_, err := m.db.ExecContext(ctx, `
|
||||
INSERT INTO boc_tenants (
|
||||
id, name, slug, domain, org_number, vat_number, address, phone, email,
|
||||
currency, language, timezone, fiscal_year_start, status, parent_id, ledger_db
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
||||
`,
|
||||
tenant.ID, tenant.Name, tenant.Slug, tenant.Domain, tenant.OrgNumber,
|
||||
tenant.VatNumber, tenant.Address, tenant.Phone, tenant.Email,
|
||||
tenant.Currency, tenant.Language, tenant.Timezone, tenant.FiscalYear,
|
||||
tenant.Status, tenant.ParentID, tenant.LedgerDB,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetTenantLedgerDB returnerar ledger-databas för en tenant
|
||||
func (m *Manager) GetTenantLedgerDB(ctx context.Context, tenantID string) (string, error) {
|
||||
tenant, err := m.GetTenant(ctx, tenantID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Om tenant har egen ledger-databas, använd den
|
||||
if tenant.LedgerDB != "" {
|
||||
return tenant.LedgerDB, nil
|
||||
}
|
||||
|
||||
// Annars använd default
|
||||
return "postgres://postgres:postgres@localhost:5432/aamos_ledger?sslmode=disable", nil
|
||||
}
|
||||
|
||||
// IsFeatureEnabled kollar om en modul är aktiverad för tenant
|
||||
func (m *Manager) IsFeatureEnabled(ctx context.Context, tenantID string, feature string) bool {
|
||||
tenant, err := m.GetTenant(ctx, tenantID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, f := range tenant.Settings.Features {
|
||||
if f == feature {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// InvalidateCache rensar cache för en tenant
|
||||
func (m *Manager) InvalidateCache(tenantID string) {
|
||||
m.cacheMu.Lock()
|
||||
delete(m.cache, tenantID)
|
||||
m.cacheMu.Unlock()
|
||||
}
|
||||
Reference in New Issue
Block a user