feat: BOC ledger-integration frontend + backend fixes
- Uppdatera robust_client.go med rätt tabellnamn (boc_chart_of_accounts, boc_journal_lines, boc_journal_entries) - Uppdatera kolumnnamn (entry_id istället för journal_entry_id, account_code istället för code) - Hantera både stora och små bokstäver för account_type - Lägg till ledgerApi i frontend med BalanceSheet, IncomeStatement, MomsReport - Uppdatera DashboardPage med Ledger KPI-kort - Lägg till LEDGER_DB_URL i docker-compose.yml - Uppdatera Dockerfile till golang:1.25-alpine
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
# BOC Backend Dockerfile
|
# BOC Backend Dockerfile
|
||||||
# Multi-stage build for minimal image
|
# Multi-stage build for minimal image
|
||||||
|
|
||||||
FROM golang:1.22-alpine AS builder
|
FROM golang:1.25-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
BIN
Binary file not shown.
@@ -26,7 +26,7 @@ func Load() *Config {
|
|||||||
return &Config{
|
return &Config{
|
||||||
Port: getEnv("PORT", "9092"),
|
Port: getEnv("PORT", "9092"),
|
||||||
DBURL: getEnv("DB_URL", "postgres://boc:boc@localhost:5432/boc?sslmode=disable"),
|
DBURL: getEnv("DB_URL", "postgres://boc:boc@localhost:5432/boc?sslmode=disable"),
|
||||||
LedgerDBURL: getEnv("LEDGER_DB_URL", "postgres://postgres:postgres@localhost:5432/aamos_ledger?sslmode=disable"),
|
LedgerDBURL: getEnv("LEDGER_DB_URL", "postgres://wavult_admin:efG15aKjqgu7uotZoAiLTRBtBDMoXITxIe9Hi6EB@platform-identity-core.cvi0qcksmsfj.eu-north-1.rds.amazonaws.com:5432/amos?sslmode=disable"),
|
||||||
JWTSecret: getEnv("JWT_SECRET", "w+Qkf/CoDda3Ba7vZLKokrGHiwUV5Ak/3tiBmFAvRC8="),
|
JWTSecret: getEnv("JWT_SECRET", "w+Qkf/CoDda3Ba7vZLKokrGHiwUV5Ak/3tiBmFAvRC8="),
|
||||||
AMOSBaseURL: getEnv("AMOS_BASE_URL", "http://localhost:9000"),
|
AMOSBaseURL: getEnv("AMOS_BASE_URL", "http://localhost:9000"),
|
||||||
CORSOrigins: splitComma(getEnv("CORS_ORIGINS", "http://localhost:3000")),
|
CORSOrigins: splitComma(getEnv("CORS_ORIGINS", "http://localhost:3000")),
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"boc/ledger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ledgerDBURL = getEnv("LEDGER_DB_URL", "postgres://wavult_admin:efG15aKjqgu7uotZoAiLTRBtBDMoXITxIe9Hi6EB@platform-identity-core.cvi0qcksmsfj.eu-north-1.rds.amazonaws.com:5432/amos?sslmode=disable")
|
||||||
|
|
||||||
|
// LedgerV2Handler uses direct DB connection for reliable data
|
||||||
|
type LedgerV2Handler struct {
|
||||||
|
client *ledger.RobustClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLedgerV2Handler creates a handler with DB connection
|
||||||
|
func NewLedgerV2Handler() (*LedgerV2Handler, error) {
|
||||||
|
client, err := ledger.NewRobustClient(ledgerDBURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &LedgerV2Handler{client: client}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAccounts returns all BAS accounts
|
||||||
|
func (h *LedgerV2Handler) GetAccounts(w http.ResponseWriter, r *http.Request) {
|
||||||
|
accounts, err := h.client.GetAccounts(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"accounts": accounts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBalanceSheet returns balance sheet for a period
|
||||||
|
func (h *LedgerV2Handler) GetBalanceSheet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
period := r.URL.Query().Get("period")
|
||||||
|
if period == "" {
|
||||||
|
period = "2026-01"
|
||||||
|
}
|
||||||
|
|
||||||
|
bs, err := h.client.GetBalanceSheet(r.Context(), period)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(bs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIncomeStatement returns P&L for a period
|
||||||
|
func (h *LedgerV2Handler) GetIncomeStatement(w http.ResponseWriter, r *http.Request) {
|
||||||
|
period := r.URL.Query().Get("period")
|
||||||
|
if period == "" {
|
||||||
|
period = "2026-01"
|
||||||
|
}
|
||||||
|
|
||||||
|
is, err := h.client.GetIncomeStatement(r.Context(), period)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(is)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMomsReport returns VAT report for a period
|
||||||
|
func (h *LedgerV2Handler) GetMomsReport(w http.ResponseWriter, r *http.Request) {
|
||||||
|
period := r.URL.Query().Get("period")
|
||||||
|
if period == "" {
|
||||||
|
period = "2026-01"
|
||||||
|
}
|
||||||
|
|
||||||
|
report, err := h.client.GetMomsReport(r.Context(), period)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(report)
|
||||||
|
}
|
||||||
@@ -44,13 +44,13 @@ type LedgerAccount struct {
|
|||||||
// GetAccounts returns all BAS accounts with balances
|
// GetAccounts returns all BAS accounts with balances
|
||||||
func (c *RobustClient) GetAccounts(ctx context.Context) ([]LedgerAccount, error) {
|
func (c *RobustClient) GetAccounts(ctx context.Context) ([]LedgerAccount, error) {
|
||||||
rows, err := c.db.QueryContext(ctx, `
|
rows, err := c.db.QueryContext(ctx, `
|
||||||
SELECT a.code, a.name, a.account_type,
|
SELECT a.account_code, a.name, a.account_type,
|
||||||
COALESCE(SUM(CASE WHEN jl.debit IS NOT NULL THEN jl.debit ELSE 0 END) -
|
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
|
SUM(CASE WHEN jl.credit IS NOT NULL THEN jl.credit ELSE 0 END), 0) as balance
|
||||||
FROM accounts a
|
FROM boc_chart_of_accounts a
|
||||||
LEFT JOIN journal_lines jl ON a.id = jl.account_id
|
LEFT JOIN boc_journal_lines jl ON a.id = jl.account_id
|
||||||
GROUP BY a.id, a.code, a.name, a.account_type
|
GROUP BY a.id, a.account_code, a.name, a.account_type
|
||||||
ORDER BY a.code
|
ORDER BY a.account_code
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("query accounts: %w", err)
|
return nil, fmt.Errorf("query accounts: %w", err)
|
||||||
@@ -87,14 +87,14 @@ func (c *RobustClient) GetBalanceSheet(ctx context.Context, period string) (*Bal
|
|||||||
}
|
}
|
||||||
|
|
||||||
rows, err := c.db.QueryContext(ctx, `
|
rows, err := c.db.QueryContext(ctx, `
|
||||||
SELECT a.code, a.name, a.account_type,
|
SELECT a.account_code, a.name, a.account_type,
|
||||||
COALESCE(SUM(CASE WHEN jl.debit IS NOT NULL THEN jl.debit ELSE 0 END) -
|
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
|
SUM(CASE WHEN jl.credit IS NOT NULL THEN jl.credit ELSE 0 END), 0) as balance
|
||||||
FROM accounts a
|
FROM boc_chart_of_accounts a
|
||||||
LEFT JOIN journal_lines jl ON a.id = jl.account_id
|
LEFT JOIN boc_journal_lines jl ON a.id = jl.account_id
|
||||||
LEFT JOIN journal_entries je ON jl.journal_entry_id = je.id AND je.entry_date >= $1::date AND je.entry_date < ($1::date + INTERVAL '1 month')
|
LEFT JOIN boc_journal_entries je ON jl.entry_id = je.id AND je.entry_date >= $1::date AND je.entry_date < ($1::date + INTERVAL '1 month')
|
||||||
GROUP BY a.id, a.code, a.name, a.account_type
|
GROUP BY a.id, a.account_code, a.name, a.account_type
|
||||||
ORDER BY a.code
|
ORDER BY a.account_code
|
||||||
`, period+"-01")
|
`, period+"-01")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("query balance sheet: %w", err)
|
return nil, fmt.Errorf("query balance sheet: %w", err)
|
||||||
@@ -109,13 +109,13 @@ func (c *RobustClient) GetBalanceSheet(ctx context.Context, period string) (*Bal
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch a.AccountType {
|
switch a.AccountType {
|
||||||
case "Asset":
|
case "Asset", "asset":
|
||||||
bs.Assets = append(bs.Assets, a)
|
bs.Assets = append(bs.Assets, a)
|
||||||
bs.TotalAssets += a.Balance
|
bs.TotalAssets += a.Balance
|
||||||
case "Liability":
|
case "Liability", "liability":
|
||||||
bs.Liabilities = append(bs.Liabilities, a)
|
bs.Liabilities = append(bs.Liabilities, a)
|
||||||
bs.TotalLiabilities += a.Balance
|
bs.TotalLiabilities += a.Balance
|
||||||
case "Equity":
|
case "Equity", "equity":
|
||||||
bs.Equity = append(bs.Equity, a)
|
bs.Equity = append(bs.Equity, a)
|
||||||
bs.TotalEquity += a.Balance
|
bs.TotalEquity += a.Balance
|
||||||
}
|
}
|
||||||
@@ -141,15 +141,15 @@ func (c *RobustClient) GetIncomeStatement(ctx context.Context, period string) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
rows, err := c.db.QueryContext(ctx, `
|
rows, err := c.db.QueryContext(ctx, `
|
||||||
SELECT a.code, a.name, a.account_type,
|
SELECT a.account_code, a.name, a.account_type,
|
||||||
COALESCE(SUM(CASE WHEN jl.debit IS NOT NULL THEN jl.debit ELSE 0 END) -
|
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
|
SUM(CASE WHEN jl.credit IS NOT NULL THEN jl.credit ELSE 0 END), 0) as balance
|
||||||
FROM accounts a
|
FROM boc_chart_of_accounts a
|
||||||
LEFT JOIN journal_lines jl ON a.id = jl.account_id
|
LEFT JOIN boc_journal_lines jl ON a.id = jl.account_id
|
||||||
LEFT JOIN journal_entries je ON jl.journal_entry_id = je.id AND je.entry_date >= $1::date AND je.entry_date < ($1::date + INTERVAL '1 month')
|
LEFT JOIN boc_journal_entries je ON jl.entry_id = je.id AND je.entry_date >= $1::date AND je.entry_date < ($1::date + INTERVAL '1 month')
|
||||||
WHERE a.account_type IN ('Revenue', 'Expense')
|
WHERE a.account_type IN ('Revenue', 'Expense')
|
||||||
GROUP BY a.id, a.code, a.name, a.account_type
|
GROUP BY a.id, a.account_code, a.name, a.account_type
|
||||||
ORDER BY a.code
|
ORDER BY a.account_code
|
||||||
`, period+"-01")
|
`, period+"-01")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("query income statement: %w", err)
|
return nil, fmt.Errorf("query income statement: %w", err)
|
||||||
@@ -164,10 +164,10 @@ func (c *RobustClient) GetIncomeStatement(ctx context.Context, period string) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch a.AccountType {
|
switch a.AccountType {
|
||||||
case "Revenue":
|
case "Revenue", "revenue":
|
||||||
is.Revenues = append(is.Revenues, a)
|
is.Revenues = append(is.Revenues, a)
|
||||||
is.TotalRevenue += a.Balance
|
is.TotalRevenue += a.Balance
|
||||||
case "Expense":
|
case "Expense", "expense":
|
||||||
is.Expenses = append(is.Expenses, a)
|
is.Expenses = append(is.Expenses, a)
|
||||||
is.TotalExpense += a.Balance
|
is.TotalExpense += a.Balance
|
||||||
}
|
}
|
||||||
@@ -197,11 +197,11 @@ func (c *RobustClient) GetMomsReport(ctx context.Context, period string) (*MomsR
|
|||||||
// Moms in (utgående moms från försäljning)
|
// Moms in (utgående moms från försäljning)
|
||||||
err := c.db.QueryRowContext(ctx, `
|
err := c.db.QueryRowContext(ctx, `
|
||||||
SELECT COALESCE(SUM(jl.credit), 0)
|
SELECT COALESCE(SUM(jl.credit), 0)
|
||||||
FROM journal_lines jl
|
FROM boc_journal_lines jl
|
||||||
JOIN journal_entries je ON jl.journal_entry_id = je.id
|
JOIN boc_journal_entries je ON jl.entry_id = je.id
|
||||||
JOIN accounts a ON jl.account_id = a.id
|
JOIN boc_chart_of_accounts a ON jl.account_id = a.id
|
||||||
WHERE je.entry_date >= $1::date AND je.entry_date < ($1::date + INTERVAL '1 month')
|
WHERE je.entry_date >= $1::date AND je.entry_date < ($1::date + INTERVAL '1 month')
|
||||||
AND a.code LIKE '26%'
|
AND a.account_code LIKE '26%'
|
||||||
`, period+"-01").Scan(&report.MomsUt)
|
`, period+"-01").Scan(&report.MomsUt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("query moms ut: %w", err)
|
return nil, fmt.Errorf("query moms ut: %w", err)
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ services:
|
|||||||
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET must be set}
|
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET must be set}
|
||||||
AMOS_BASE_URL: "http://172.17.0.1:3250"
|
AMOS_BASE_URL: "http://172.17.0.1:3250"
|
||||||
LEDGER_URL: "http://172.17.0.1:3250"
|
LEDGER_URL: "http://172.17.0.1:3250"
|
||||||
|
LEDGER_DB_URL: "postgres://boc:boc_secret_2026@postgres:5432/boc?sslmode=disable"
|
||||||
MIGRATIONS_DIR: "./db/migrations"
|
MIGRATIONS_DIR: "./db/migrations"
|
||||||
REDIS_URL: "redis://redis:6379"
|
REDIS_URL: "redis://redis:6379"
|
||||||
RESEND_API_KEY: ${RESEND_API_KEY:-}
|
RESEND_API_KEY: ${RESEND_API_KEY:-}
|
||||||
|
|||||||
-1
File diff suppressed because one or more lines are too long
Vendored
+455
File diff suppressed because one or more lines are too long
Vendored
-455
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -8,8 +8,8 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||||
<script type="module" crossorigin src="/assets/index-CbMx-xOV.js"></script>
|
<script type="module" crossorigin src="/assets/index-BtifZyOD.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-B0Irvkd7.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-oHhMI01Z.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -104,6 +104,40 @@ export const financeApi = {
|
|||||||
cashflow: () => api.get<{ cashflow: unknown[] }>('/finance/cashflow'),
|
cashflow: () => api.get<{ cashflow: unknown[] }>('/finance/cashflow'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ledger (aamos-ledger integration)
|
||||||
|
export interface LedgerAccount {
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
account_type: string
|
||||||
|
balance: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BalanceSheet {
|
||||||
|
assets: LedgerAccount[]
|
||||||
|
liabilities: LedgerAccount[]
|
||||||
|
equity: LedgerAccount[]
|
||||||
|
total_assets: number
|
||||||
|
total_liabilities: number
|
||||||
|
total_equity: number
|
||||||
|
period: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IncomeStatement {
|
||||||
|
revenues: LedgerAccount[]
|
||||||
|
expenses: LedgerAccount[]
|
||||||
|
total_revenue: number
|
||||||
|
total_expense: number
|
||||||
|
net_income: number
|
||||||
|
period: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ledgerApi = {
|
||||||
|
accounts: () => api.get<{ accounts: LedgerAccount[] }>('/ledger/accounts'),
|
||||||
|
balanceSheet: (period?: string) => api.get<BalanceSheet>(`/ledger/balance-sheet?period=${period || ''}`),
|
||||||
|
incomeStatement: (period?: string) => api.get<IncomeStatement>(`/ledger/income-statement?period=${period || ''}`),
|
||||||
|
momsReport: (period?: string) => api.get<unknown>(`/ledger/moms?period=${period || ''}`),
|
||||||
|
}
|
||||||
|
|
||||||
// HR
|
// HR
|
||||||
export const hrApi = {
|
export const hrApi = {
|
||||||
employees: () => api.get<{ employees: unknown[] }>('/hr/employees'),
|
employees: () => api.get<{ employees: unknown[] }>('/hr/employees'),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from '@/components/ui/Table'
|
} from '@/components/ui/Table'
|
||||||
import { Badge } from '@/components/ui/Badge'
|
import { Badge } from '@/components/ui/Badge'
|
||||||
import { formatCurrency, formatDate } from '@/lib/utils'
|
import { formatCurrency, formatDate } from '@/lib/utils'
|
||||||
import { financeApi, salesApi, crmApi } from '@/lib/api'
|
import { financeApi, salesApi, crmApi, ledgerApi } from '@/lib/api'
|
||||||
import {
|
import {
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Users,
|
Users,
|
||||||
@@ -61,16 +61,24 @@ export function DashboardPage() {
|
|||||||
const [deals, setDeals] = useState<Deal[]>([])
|
const [deals, setDeals] = useState<Deal[]>([])
|
||||||
const [dealTotal, setDealTotal] = useState(0)
|
const [dealTotal, setDealTotal] = useState(0)
|
||||||
|
|
||||||
|
// Ledger state
|
||||||
|
const [ledgerAssets, setLedgerAssets] = useState(0)
|
||||||
|
const [ledgerLiabilities, setLedgerLiabilities] = useState(0)
|
||||||
|
const [ledgerEquity, setLedgerEquity] = useState(0)
|
||||||
|
const [netIncome, setNetIncome] = useState(0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError('')
|
setError('')
|
||||||
try {
|
try {
|
||||||
const [balanceRes, mrrRes, customersRes, dealsRes] = await Promise.all([
|
const [balanceRes, mrrRes, customersRes, dealsRes, ledgerBs, ledgerIs] = await Promise.all([
|
||||||
financeApi.balance(),
|
financeApi.balance(),
|
||||||
salesApi.mrr(),
|
salesApi.mrr(),
|
||||||
crmApi.customers(),
|
crmApi.customers(),
|
||||||
salesApi.deals(),
|
salesApi.deals(),
|
||||||
|
ledgerApi.balanceSheet('2026-01').catch(() => null),
|
||||||
|
ledgerApi.incomeStatement('2026-01').catch(() => null),
|
||||||
])
|
])
|
||||||
|
|
||||||
const b = balanceRes as { total_assets?: number }
|
const b = balanceRes as { total_assets?: number }
|
||||||
@@ -84,6 +92,16 @@ export function DashboardPage() {
|
|||||||
const d = dealsRes as { deals: Deal[]; total: number }
|
const d = dealsRes as { deals: Deal[]; total: number }
|
||||||
setDeals(d.deals || [])
|
setDeals(d.deals || [])
|
||||||
setDealTotal(d.total || 0)
|
setDealTotal(d.total || 0)
|
||||||
|
|
||||||
|
// Ledger data
|
||||||
|
if (ledgerBs) {
|
||||||
|
setLedgerAssets(ledgerBs.total_assets || 0)
|
||||||
|
setLedgerLiabilities(ledgerBs.total_liabilities || 0)
|
||||||
|
setLedgerEquity(ledgerBs.total_equity || 0)
|
||||||
|
}
|
||||||
|
if (ledgerIs) {
|
||||||
|
setNetIncome(ledgerIs.net_income || 0)
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to load dashboard data')
|
setError(err instanceof Error ? err.message : 'Failed to load dashboard data')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -125,6 +143,37 @@ export function DashboardPage() {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const ledgerKpis = [
|
||||||
|
{
|
||||||
|
label: 'Ledger Assets',
|
||||||
|
value: formatCurrency(ledgerAssets),
|
||||||
|
change: 0,
|
||||||
|
changeLabel: 'per balansräkning',
|
||||||
|
icon: <DollarSign size={18} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Ledger Liabilities',
|
||||||
|
value: formatCurrency(ledgerLiabilities),
|
||||||
|
change: 0,
|
||||||
|
changeLabel: 'per balansräkning',
|
||||||
|
icon: <TrendingUp size={18} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Net Income',
|
||||||
|
value: formatCurrency(netIncome),
|
||||||
|
change: 0,
|
||||||
|
changeLabel: 'per resultaträkning',
|
||||||
|
icon: <TrendingUp size={18} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Equity',
|
||||||
|
value: formatCurrency(ledgerEquity),
|
||||||
|
change: 0,
|
||||||
|
changeLabel: 'per balansräkning',
|
||||||
|
icon: <DollarSign size={18} />,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-10">
|
<div className="space-y-10">
|
||||||
@@ -159,6 +208,16 @@ export function DashboardPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Ledger KPI Cards */}
|
||||||
|
<div className="mt-8">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">Ledger (Bokföring)</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-5">
|
||||||
|
{ledgerKpis.map((kpi, i) => (
|
||||||
|
<KPICard key={kpi.label} {...kpi} index={i} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Infrastructure Health — Grafana Integration */}
|
{/* Infrastructure Health — Grafana Integration */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
|
|||||||
Reference in New Issue
Block a user