feat: add financial panel, compact services list, add Syncthing
- New /financial page with sales/expenses cards, monthly bar chart and expense distribution pie chart (Recharts) - New API endpoint GET /api/financial with queries on tblinvoices and tblexpenses - Compact services grid (2-col dots layout) in Monitor page - Add Syncthing to critical services monitoring - Add Financeiro nav link to Dashboard, Monitor and Financial headers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
22
api/routes/financial.ts
Normal file
22
api/routes/financial.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Financial API Route
|
||||
* GET /api/financial
|
||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||
*/
|
||||
import { Router } from 'express'
|
||||
import type { Request, Response } from 'express'
|
||||
import { getFinancialData } from '../services/financial.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const data = await getFinancialData()
|
||||
res.json(data)
|
||||
} catch (error) {
|
||||
console.error('Financial API error:', error)
|
||||
res.status(500).json({ error: 'Internal server error' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -13,6 +13,7 @@ import diagnosticRouter from './routes/diagnostic.js'
|
||||
import hetznerRouter from './routes/hetzner.js'
|
||||
import wpMonitorRouter from './routes/wp-monitor.js'
|
||||
import serverMetricsRouter from './routes/server-metrics.js'
|
||||
import financialRouter from './routes/financial.js'
|
||||
import { collectAllServerMetrics } from './services/server-metrics.js'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
@@ -41,6 +42,7 @@ app.use('/api/diagnostic', diagnosticRouter)
|
||||
app.use('/api/hetzner', hetznerRouter)
|
||||
app.use('/api/wp-monitor', wpMonitorRouter)
|
||||
app.use('/api/server-metrics', serverMetricsRouter)
|
||||
app.use('/api/financial', financialRouter)
|
||||
|
||||
// Serve static files in production
|
||||
if (isProduction) {
|
||||
|
||||
113
api/services/financial.ts
Normal file
113
api/services/financial.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Financial Queries Service
|
||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||
*/
|
||||
import db from '../db.js'
|
||||
import type { RowDataPacket } from 'mysql2'
|
||||
|
||||
export async function getFinancialData() {
|
||||
const [
|
||||
[vendasMes],
|
||||
[vendasAno],
|
||||
[despesasMes],
|
||||
[despesasAno],
|
||||
categorias,
|
||||
evolucaoReceitas,
|
||||
evolucaoDespesas,
|
||||
] = await Promise.all([
|
||||
// Vendas mes actual (facturas nao-draft, nao-canceladas)
|
||||
db.query<RowDataPacket[]>(`
|
||||
SELECT COALESCE(SUM(total), 0) as valor
|
||||
FROM tblinvoices
|
||||
WHERE YEAR(date) = YEAR(CURDATE()) AND MONTH(date) = MONTH(CURDATE())
|
||||
AND status NOT IN (1, 5)
|
||||
`),
|
||||
|
||||
// Vendas acumuladas ano
|
||||
db.query<RowDataPacket[]>(`
|
||||
SELECT COALESCE(SUM(total), 0) as valor
|
||||
FROM tblinvoices
|
||||
WHERE YEAR(date) = YEAR(CURDATE()) AND status NOT IN (1, 5)
|
||||
`),
|
||||
|
||||
// Despesas mes actual
|
||||
db.query<RowDataPacket[]>(`
|
||||
SELECT COALESCE(SUM(amount), 0) as valor
|
||||
FROM tblexpenses
|
||||
WHERE YEAR(date) = YEAR(CURDATE()) AND MONTH(date) = MONTH(CURDATE())
|
||||
`),
|
||||
|
||||
// Despesas acumuladas ano
|
||||
db.query<RowDataPacket[]>(`
|
||||
SELECT COALESCE(SUM(amount), 0) as valor
|
||||
FROM tblexpenses
|
||||
WHERE YEAR(date) = YEAR(CURDATE())
|
||||
`),
|
||||
|
||||
// Distribuicao por categoria
|
||||
db.query<RowDataPacket[]>(`
|
||||
SELECT ec.name as categoria, ROUND(SUM(e.amount), 2) as total
|
||||
FROM tblexpenses e
|
||||
LEFT JOIN tblexpenses_categories ec ON e.category = ec.id
|
||||
WHERE YEAR(e.date) = YEAR(CURDATE())
|
||||
GROUP BY e.category, ec.name
|
||||
ORDER BY total DESC
|
||||
`),
|
||||
|
||||
// Evolucao mensal receitas (ultimos 12 meses)
|
||||
db.query<RowDataPacket[]>(`
|
||||
SELECT DATE_FORMAT(date, '%Y-%m') as mes, ROUND(SUM(total), 2) as valor
|
||||
FROM tblinvoices
|
||||
WHERE status NOT IN (1, 5) AND date >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
|
||||
GROUP BY mes ORDER BY mes
|
||||
`),
|
||||
|
||||
// Evolucao mensal despesas (ultimos 12 meses)
|
||||
db.query<RowDataPacket[]>(`
|
||||
SELECT DATE_FORMAT(date, '%Y-%m') as mes, ROUND(SUM(amount), 2) as valor
|
||||
FROM tblexpenses
|
||||
WHERE date >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
|
||||
GROUP BY mes ORDER BY mes
|
||||
`),
|
||||
])
|
||||
|
||||
const vendas_mes = Math.round((vendasMes[0]?.valor || 0) * 100) / 100
|
||||
const vendas_ano = Math.round((vendasAno[0]?.valor || 0) * 100) / 100
|
||||
const despesas_mes = Math.round((despesasMes[0]?.valor || 0) * 100) / 100
|
||||
const despesas_ano = Math.round((despesasAno[0]?.valor || 0) * 100) / 100
|
||||
const lucro_mes = Math.round((vendas_mes - despesas_mes) * 100) / 100
|
||||
const lucro_ano = Math.round((vendas_ano - despesas_ano) * 100) / 100
|
||||
|
||||
// Build monthly evolution map
|
||||
const mesesMap = new Map<string, { receita: number; despesa: number }>()
|
||||
for (const r of evolucaoReceitas[0] as RowDataPacket[]) {
|
||||
mesesMap.set(r.mes, { receita: r.valor, despesa: 0 })
|
||||
}
|
||||
for (const d of evolucaoDespesas[0] as RowDataPacket[]) {
|
||||
const existing = mesesMap.get(d.mes) || { receita: 0, despesa: 0 }
|
||||
existing.despesa = d.valor
|
||||
mesesMap.set(d.mes, existing)
|
||||
}
|
||||
|
||||
const evolucao = Array.from(mesesMap.entries())
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([mes, vals]) => ({
|
||||
mes,
|
||||
receita: vals.receita,
|
||||
despesa: vals.despesa,
|
||||
}))
|
||||
|
||||
return {
|
||||
vendas_mes,
|
||||
vendas_ano,
|
||||
despesas_mes,
|
||||
despesas_ano,
|
||||
lucro_mes,
|
||||
lucro_ano,
|
||||
categorias: (categorias[0] as RowDataPacket[]).map(c => ({
|
||||
name: c.categoria || 'Sem categoria',
|
||||
value: c.total,
|
||||
})),
|
||||
evolucao,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user