@hed-hog/finance 0.0.261 → 0.0.266

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/dto/update-finance-scenario-settings.dto.d.ts +7 -0
  2. package/dist/dto/update-finance-scenario-settings.dto.d.ts.map +1 -0
  3. package/dist/dto/update-finance-scenario-settings.dto.js +39 -0
  4. package/dist/dto/update-finance-scenario-settings.dto.js.map +1 -0
  5. package/dist/finance-data.controller.d.ts +61 -7
  6. package/dist/finance-data.controller.d.ts.map +1 -1
  7. package/dist/finance-data.controller.js +23 -3
  8. package/dist/finance-data.controller.js.map +1 -1
  9. package/dist/finance.service.d.ts +79 -9
  10. package/dist/finance.service.d.ts.map +1 -1
  11. package/dist/finance.service.js +471 -70
  12. package/dist/finance.service.js.map +1 -1
  13. package/hedhog/data/route.yaml +9 -0
  14. package/hedhog/data/setting_group.yaml +152 -0
  15. package/hedhog/frontend/app/_lib/use-finance-data.ts.ejs +31 -3
  16. package/hedhog/frontend/app/planning/cash-flow-forecast/page.tsx.ejs +38 -7
  17. package/hedhog/frontend/app/planning/receivables-calendar/page.tsx.ejs +3 -1
  18. package/hedhog/frontend/app/planning/scenarios/page.tsx.ejs +74 -4
  19. package/hedhog/frontend/app/reports/actual-vs-forecast/page.tsx.ejs +361 -0
  20. package/hedhog/frontend/app/reports/aging-default/page.tsx.ejs +368 -0
  21. package/hedhog/frontend/app/reports/cash-position/page.tsx.ejs +432 -0
  22. package/hedhog/frontend/messages/en.json +182 -0
  23. package/hedhog/frontend/messages/pt.json +182 -0
  24. package/hedhog/query/triggers-period-close.sql +361 -0
  25. package/package.json +8 -8
  26. package/src/dto/update-finance-scenario-settings.dto.ts +21 -0
  27. package/src/finance-data.controller.ts +18 -3
  28. package/src/finance.service.ts +781 -79
@@ -7,6 +7,15 @@
7
7
  - where:
8
8
  slug: admin-finance
9
9
 
10
+ - url: /finance/scenarios/:cenario
11
+ method: PATCH
12
+ relations:
13
+ role:
14
+ - where:
15
+ slug: admin
16
+ - where:
17
+ slug: admin-finance
18
+
10
19
  - url: /finance/accounts-payable/installments
11
20
  method: GET
12
21
  relations:
@@ -0,0 +1,152 @@
1
+ - slug: financeiro
2
+ icon: chart-line
3
+ name:
4
+ en: Financial
5
+ pt: Financeiro
6
+ description:
7
+ en: Forecast and scenario settings for finance planning.
8
+ pt: Configurações de projeção e cenários para o planejamento financeiro.
9
+ relations:
10
+ setting:
11
+ - slug: finance-default-scenario
12
+ type: string
13
+ component: combobox
14
+ name:
15
+ en: Default Scenario
16
+ pt: Cenário Padrão
17
+ description:
18
+ en: Default scenario used in cash flow forecast screens.
19
+ pt: Cenário padrão utilizado nas telas de previsão de fluxo de caixa.
20
+ value: base
21
+ user_override: false
22
+ relations:
23
+ setting_list:
24
+ - value: base
25
+ order: 0
26
+ - value: pessimista
27
+ order: 1
28
+ - value: otimista
29
+ order: 2
30
+ - slug: finance-default-horizon-days
31
+ type: number
32
+ component: combobox
33
+ name:
34
+ en: Default Forecast Horizon (Days)
35
+ pt: Horizonte Padrão da Previsão (Dias)
36
+ description:
37
+ en: Default forecast period in days.
38
+ pt: Período padrão da previsão em dias.
39
+ value: 90
40
+ user_override: false
41
+ relations:
42
+ setting_list:
43
+ - value: 30
44
+ order: 0
45
+ - value: 60
46
+ order: 1
47
+ - value: 90
48
+ order: 2
49
+ - value: 180
50
+ order: 3
51
+ - value: 365
52
+ order: 4
53
+ - slug: finance-scenario-base-average-delay-days
54
+ type: number
55
+ component: input-number
56
+ name:
57
+ en: Base Scenario Average Delay (Days)
58
+ pt: Atraso Médio no Cenário Base (Dias)
59
+ description:
60
+ en: Average receivable delay in days for base scenario.
61
+ pt: Atraso médio de recebimento em dias para o cenário base.
62
+ value: 5
63
+ user_override: false
64
+ - slug: finance-scenario-base-default-rate-percent
65
+ type: number
66
+ component: input-number
67
+ name:
68
+ en: Base Scenario Default Rate (%)
69
+ pt: Taxa de Inadimplência no Cenário Base (%)
70
+ description:
71
+ en: Expected default rate for base scenario.
72
+ pt: Taxa esperada de inadimplência para o cenário base.
73
+ value: 3
74
+ user_override: false
75
+ - slug: finance-scenario-base-revenue-growth-percent
76
+ type: number
77
+ component: input-number
78
+ name:
79
+ en: Base Scenario Revenue Growth (%)
80
+ pt: Crescimento de Receita no Cenário Base (%)
81
+ description:
82
+ en: Expected revenue growth for base scenario.
83
+ pt: Crescimento esperado da receita para o cenário base.
84
+ value: 5
85
+ user_override: false
86
+ - slug: finance-scenario-pessimistic-average-delay-days
87
+ type: number
88
+ component: input-number
89
+ name:
90
+ en: Pessimistic Scenario Average Delay (Days)
91
+ pt: Atraso Médio no Cenário Pessimista (Dias)
92
+ description:
93
+ en: Average receivable delay in days for pessimistic scenario.
94
+ pt: Atraso médio de recebimento em dias para o cenário pessimista.
95
+ value: 12
96
+ user_override: false
97
+ - slug: finance-scenario-pessimistic-default-rate-percent
98
+ type: number
99
+ component: input-number
100
+ name:
101
+ en: Pessimistic Scenario Default Rate (%)
102
+ pt: Taxa de Inadimplência no Cenário Pessimista (%)
103
+ description:
104
+ en: Expected default rate for pessimistic scenario.
105
+ pt: Taxa esperada de inadimplência para o cenário pessimista.
106
+ value: 6
107
+ user_override: false
108
+ - slug: finance-scenario-pessimistic-revenue-growth-percent
109
+ type: number
110
+ component: input-number
111
+ name:
112
+ en: Pessimistic Scenario Revenue Growth (%)
113
+ pt: Crescimento de Receita no Cenário Pessimista (%)
114
+ description:
115
+ en: Expected revenue growth for pessimistic scenario.
116
+ pt: Crescimento esperado da receita para o cenário pessimista.
117
+ value: -5
118
+ user_override: false
119
+ - slug: finance-scenario-optimistic-average-delay-days
120
+ type: number
121
+ component: input-number
122
+ name:
123
+ en: Optimistic Scenario Average Delay (Days)
124
+ pt: Atraso Médio no Cenário Otimista (Dias)
125
+ description:
126
+ en: Average receivable delay in days for optimistic scenario.
127
+ pt: Atraso médio de recebimento em dias para o cenário otimista.
128
+ value: 2
129
+ user_override: false
130
+ - slug: finance-scenario-optimistic-default-rate-percent
131
+ type: number
132
+ component: input-number
133
+ name:
134
+ en: Optimistic Scenario Default Rate (%)
135
+ pt: Taxa de Inadimplência no Cenário Otimista (%)
136
+ description:
137
+ en: Expected default rate for optimistic scenario.
138
+ pt: Taxa esperada de inadimplência para o cenário otimista.
139
+ value: 1.5
140
+ user_override: false
141
+ - slug: finance-scenario-optimistic-revenue-growth-percent
142
+ type: number
143
+ component: input-number
144
+ name:
145
+ en: Optimistic Scenario Revenue Growth (%)
146
+ pt: Crescimento de Receita no Cenário Otimista (%)
147
+ description:
148
+ en: Expected revenue growth for optimistic scenario.
149
+ pt: Crescimento esperado da receita para o cenário otimista.
150
+ value: 10
151
+ user_override: false
152
+
@@ -3,6 +3,8 @@
3
3
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
4
4
 
5
5
  type FinanceData = {
6
+ defaultScenario?: 'base' | 'pessimista' | 'otimista';
7
+ defaultHorizonDays?: number;
6
8
  kpis: {
7
9
  saldoCaixa: number;
8
10
  aPagar30dias: number;
@@ -36,7 +38,14 @@ type FinanceData = {
36
38
  } | null;
37
39
  };
38
40
 
41
+ type FinanceDataFilters = {
42
+ horizonteDias?: number;
43
+ cenario?: 'base' | 'pessimista' | 'otimista';
44
+ };
45
+
39
46
  const EMPTY_FINANCE_DATA: FinanceData = {
47
+ defaultScenario: 'base',
48
+ defaultHorizonDays: 90,
40
49
  kpis: {
41
50
  saldoCaixa: 0,
42
51
  aPagar30dias: 0,
@@ -67,17 +76,36 @@ const EMPTY_FINANCE_DATA: FinanceData = {
67
76
  periodoAberto: null,
68
77
  };
69
78
 
70
- export function useFinanceData() {
79
+ export function useFinanceData(filters?: FinanceDataFilters) {
71
80
  const { request } = useApp();
72
81
 
82
+ const horizonteDias = filters?.horizonteDias;
83
+ const cenario = filters?.cenario;
84
+ const query = new URLSearchParams();
85
+
86
+ if (typeof horizonteDias === 'number' && Number.isFinite(horizonteDias)) {
87
+ query.set('horizonte', String(horizonteDias));
88
+ }
89
+
90
+ if (cenario) {
91
+ query.set('cenario', cenario);
92
+ }
93
+
94
+ const querySuffix = query.toString();
95
+ const url = querySuffix ? `/finance/data?${querySuffix}` : '/finance/data';
96
+
73
97
  return useQuery<FinanceData>({
74
- queryKey: ['finance-data'],
98
+ queryKey: [
99
+ 'finance-data',
100
+ horizonteDias ?? 'default',
101
+ cenario ?? 'default',
102
+ ],
75
103
  staleTime: 0,
76
104
  refetchOnMount: 'always',
77
105
  queryFn: async () => {
78
106
  try {
79
107
  const response = await request({
80
- url: '/finance/data',
108
+ url,
81
109
  method: 'GET',
82
110
  });
83
111
 
@@ -26,7 +26,13 @@ import {
26
26
  TableRow,
27
27
  } from '@/components/ui/table';
28
28
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
29
- import { Download, TrendingDown, TrendingUp, Wallet } from 'lucide-react';
29
+ import {
30
+ Download,
31
+ Loader2,
32
+ TrendingDown,
33
+ TrendingUp,
34
+ Wallet,
35
+ } from 'lucide-react';
30
36
  import { useTranslations } from 'next-intl';
31
37
  import { useState } from 'react';
32
38
  import {
@@ -44,11 +50,20 @@ import { useFinanceData } from '../../_lib/use-finance-data';
44
50
 
45
51
  export default function FluxoCaixaPage() {
46
52
  const t = useTranslations('finance.CashFlowForecastPage');
47
- const { data } = useFinanceData();
48
- const { fluxoCaixaPrevisto, kpis, entradasPrevistas, saidasPrevistas } = data;
49
53
 
50
- const [horizonte, setHorizonte] = useState('90');
51
- const [cenario, setCenario] = useState('base');
54
+ const [horizonte, setHorizonte] = useState<string | null>(null);
55
+ const [cenario, setCenario] = useState<
56
+ 'base' | 'pessimista' | 'otimista' | null
57
+ >(null);
58
+
59
+ const { data, isFetching } = useFinanceData({
60
+ horizonteDias: horizonte ? Number(horizonte) : undefined,
61
+ cenario: cenario || undefined,
62
+ });
63
+
64
+ const { fluxoCaixaPrevisto, kpis, entradasPrevistas, saidasPrevistas } = data;
65
+ const selectedHorizon = horizonte || String(data.defaultHorizonDays || 90);
66
+ const selectedScenario = cenario || data.defaultScenario || 'base';
52
67
 
53
68
  const chartData = fluxoCaixaPrevisto.map((item) => ({
54
69
  ...item,
@@ -78,7 +93,11 @@ export default function FluxoCaixaPage() {
78
93
  />
79
94
 
80
95
  <div className="flex flex-col gap-4 sm:flex-row sm:items-center">
81
- <Select value={horizonte} onValueChange={setHorizonte}>
96
+ <Select
97
+ value={selectedHorizon}
98
+ onValueChange={setHorizonte}
99
+ disabled={isFetching}
100
+ >
82
101
  <SelectTrigger className="w-[180px]">
83
102
  <SelectValue placeholder={t('filters.horizon')} />
84
103
  </SelectTrigger>
@@ -90,7 +109,13 @@ export default function FluxoCaixaPage() {
90
109
  <SelectItem value="365">{t('filters.days365')}</SelectItem>
91
110
  </SelectContent>
92
111
  </Select>
93
- <Select value={cenario} onValueChange={setCenario}>
112
+ <Select
113
+ value={selectedScenario}
114
+ onValueChange={(value) =>
115
+ setCenario(value as 'base' | 'pessimista' | 'otimista')
116
+ }
117
+ disabled={isFetching}
118
+ >
94
119
  <SelectTrigger className="w-[180px]">
95
120
  <SelectValue placeholder={t('filters.scenario')} />
96
121
  </SelectTrigger>
@@ -104,6 +129,12 @@ export default function FluxoCaixaPage() {
104
129
  </SelectItem>
105
130
  </SelectContent>
106
131
  </Select>
132
+ {isFetching ? (
133
+ <div className="text-muted-foreground flex items-center gap-2 text-sm">
134
+ <Loader2 className="h-4 w-4 animate-spin" />
135
+ Atualizando dados...
136
+ </div>
137
+ ) : null}
107
138
  </div>
108
139
 
109
140
  <div className="grid gap-4 md:grid-cols-4">
@@ -46,6 +46,8 @@ export default function RecebiveisPage() {
46
46
  const totalBruto = recebiveis.reduce((acc, r) => acc + r.bruto, 0);
47
47
  const totalTaxas = recebiveis.reduce((acc, r) => acc + r.taxas, 0);
48
48
  const totalLiquido = recebiveis.reduce((acc, r) => acc + r.liquido, 0);
49
+ const taxasPercentual =
50
+ totalBruto > 0 ? ((totalTaxas / totalBruto) * 100).toFixed(2) : '0.00';
49
51
 
50
52
  return (
51
53
  <Page>
@@ -88,7 +90,7 @@ export default function RecebiveisPage() {
88
90
  </div>
89
91
  <p className="text-xs text-muted-foreground">
90
92
  {t('cards.percentOfGross', {
91
- percent: ((totalTaxas / totalBruto) * 100).toFixed(2),
93
+ percent: taxasPercentual,
92
94
  })}
93
95
  </p>
94
96
  </CardContent>
@@ -13,30 +13,58 @@ import {
13
13
  import { Label } from '@/components/ui/label';
14
14
  import { Money } from '@/components/ui/money';
15
15
  import { Slider } from '@/components/ui/slider';
16
+ import { useApp } from '@hed-hog/next-app-provider';
16
17
  import {
17
18
  Check,
18
19
  Lightbulb,
20
+ Loader2,
19
21
  Target,
20
22
  TrendingDown,
21
23
  TrendingUp,
22
24
  } from 'lucide-react';
23
25
  import { useTranslations } from 'next-intl';
24
- import { useState } from 'react';
26
+ import { useEffect, useState } from 'react';
25
27
  import { formatarMoeda } from '../../_lib/formatters';
26
28
  import { useFinanceData } from '../../_lib/use-finance-data';
27
29
 
28
30
  export default function CenariosPage() {
29
31
  const t = useTranslations('finance.ScenariosPage');
30
- const { data } = useFinanceData();
32
+ const { request, showToastHandler } = useApp();
33
+ const { data, isFetching, refetch } = useFinanceData();
31
34
  const { cenarios, kpis } = data;
32
35
 
33
- const [cenarioAtivo, setCenarioAtivo] = useState('1');
36
+ const [cenarioAtivo, setCenarioAtivo] = useState('base');
37
+ const [isSaving, setIsSaving] = useState(false);
34
38
  const [premissas, setPremissas] = useState({
35
39
  atrasoMedio: 5,
36
40
  taxaInadimplencia: 3,
37
41
  crescimentoReceita: 5,
38
42
  });
39
43
 
44
+ useEffect(() => {
45
+ if (!cenarios?.length) {
46
+ return;
47
+ }
48
+
49
+ const selected = cenarios.find((c) => c.id === cenarioAtivo);
50
+ if (selected) {
51
+ return;
52
+ }
53
+
54
+ const defaultScenario = cenarios.find((c) => c.padrao) || cenarios[0];
55
+
56
+ if (!defaultScenario) {
57
+ return;
58
+ }
59
+
60
+ setCenarioAtivo(defaultScenario.id);
61
+ setPremissas({
62
+ atrasoMedio: defaultScenario.atrasoMedio,
63
+ taxaInadimplencia: defaultScenario.taxaInadimplencia,
64
+ crescimentoReceita: defaultScenario.crescimentoReceita,
65
+ });
66
+ }, [cenarios, cenarioAtivo]);
67
+
40
68
  const cenarioSelecionado = cenarios.find((c) => c.id === cenarioAtivo);
41
69
 
42
70
  const impactoSaldo =
@@ -184,6 +212,7 @@ export default function CenariosPage() {
184
212
  </div>
185
213
  <Slider
186
214
  value={[premissas.atrasoMedio]}
215
+ disabled={isSaving || isFetching}
187
216
  onValueChange={([value]) =>
188
217
  setPremissas({
189
218
  ...premissas,
@@ -204,6 +233,7 @@ export default function CenariosPage() {
204
233
  </div>
205
234
  <Slider
206
235
  value={[premissas.taxaInadimplencia]}
236
+ disabled={isSaving || isFetching}
207
237
  onValueChange={([value]) =>
208
238
  setPremissas({
209
239
  ...premissas,
@@ -231,6 +261,7 @@ export default function CenariosPage() {
231
261
  </div>
232
262
  <Slider
233
263
  value={[premissas.crescimentoReceita + 10]} // Offset para permitir valores negativos
264
+ disabled={isSaving || isFetching}
234
265
  onValueChange={([value]) =>
235
266
  setPremissas({
236
267
  ...premissas,
@@ -247,6 +278,7 @@ export default function CenariosPage() {
247
278
  <div className="flex justify-end gap-2 pt-4">
248
279
  <Button
249
280
  variant="outline"
281
+ disabled={isSaving || isFetching || !cenarioSelecionado}
250
282
  onClick={() => {
251
283
  if (cenarioSelecionado) {
252
284
  setPremissas({
@@ -259,7 +291,45 @@ export default function CenariosPage() {
259
291
  >
260
292
  {t('assumptions.restore')}
261
293
  </Button>
262
- <Button>{t('assumptions.apply')}</Button>
294
+ <Button
295
+ disabled={isSaving || isFetching || !cenarioSelecionado}
296
+ onClick={async () => {
297
+ if (!cenarioSelecionado || isSaving) {
298
+ return;
299
+ }
300
+
301
+ try {
302
+ setIsSaving(true);
303
+
304
+ await request({
305
+ url: `/finance/scenarios/${cenarioSelecionado.id}`,
306
+ method: 'PATCH',
307
+ data: {
308
+ atrasoMedio: premissas.atrasoMedio,
309
+ taxaInadimplencia: premissas.taxaInadimplencia,
310
+ crescimentoReceita: premissas.crescimentoReceita,
311
+ setAsDefault: true,
312
+ },
313
+ });
314
+
315
+ await refetch();
316
+ showToastHandler?.('success', 'Cenário salvo com sucesso');
317
+ } catch {
318
+ showToastHandler?.('error', 'Erro ao salvar cenário');
319
+ } finally {
320
+ setIsSaving(false);
321
+ }
322
+ }}
323
+ >
324
+ {isSaving ? (
325
+ <>
326
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
327
+ {t('assumptions.apply')}
328
+ </>
329
+ ) : (
330
+ t('assumptions.apply')
331
+ )}
332
+ </Button>
263
333
  </div>
264
334
  </CardContent>
265
335
  </Card>