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:
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