- 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>
114 lines
3.5 KiB
TypeScript
114 lines
3.5 KiB
TypeScript
/**
|
|
* 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,
|
|
}
|
|
}
|