@hed-hog/finance 0.0.276 → 0.0.279
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.
- package/README.md +66 -26
- package/hedhog/data/menu.yaml +46 -0
- package/hedhog/frontend/app/reports/_lib/report-aggregations.ts.ejs +275 -0
- package/hedhog/frontend/app/reports/_lib/report-mocks.ts.ejs +186 -0
- package/hedhog/frontend/app/reports/overview-results/page.tsx.ejs +337 -0
- package/hedhog/frontend/app/reports/top-customers/page.tsx.ejs +310 -0
- package/hedhog/frontend/app/reports/top-operational-expenses/page.tsx.ejs +318 -0
- package/hedhog/frontend/messages/en.json +148 -0
- package/hedhog/frontend/messages/pt.json +148 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -133,6 +133,7 @@ O módulo financeiro gerencia o ciclo completo da gestão financeira corporativa
|
|
|
133
133
|
|--------|-------------------------------|------|-------------------------------|-------------------------------------------------------------------------------------------------------|------------------------|----------------------|
|
|
134
134
|
| GET | /finance/statements | Sim | Lista extratos bancários | Query: `bank_account_id`, `search` | Lista de extratos | 400 BadRequest, 401 |
|
|
135
135
|
| GET | /finance/bank-reconciliation/summary | Sim | Resumo da conciliação bancária | Query: `bank_account_id` (obrigatório) | Resumo da conciliação | 400 BadRequest, 401 |
|
|
136
|
+
| POST | /finance/bank-reconciliations | Sim | Cria conciliação bancária | Body: `{ title_id: number; installment_id: number; bank_statement_line_id: number; payment_channel?: string; description?: string }` | Conciliação criada | 400 Validação, 401 |
|
|
136
137
|
| GET | /finance/statements/export | Sim | Exporta extratos em CSV | Query: `bank_account_id` (obrigatório), `search` | CSV para download | 400 BadRequest, 401 |
|
|
137
138
|
| POST | /finance/statements/import | Sim | Importa extrato bancário | Multipart file + Body: `bank_account_id` (obrigatório) | Importação realizada | 400 BadRequest, 401 |
|
|
138
139
|
| POST | /finance/statements/adjustments | Sim | Cria ajuste em extrato bancário | Body: `{ bank_account_id: number; amount: number; date?: string; type: string; description?: string }` | Ajuste criado | 400 Validação, 401 |
|
|
@@ -244,6 +245,18 @@ O módulo financeiro gerencia o ciclo completo da gestão financeira corporativa
|
|
|
244
245
|
}
|
|
245
246
|
```
|
|
246
247
|
|
|
248
|
+
### CreateBankReconciliationDto
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
{
|
|
252
|
+
title_id: number; // obrigatório
|
|
253
|
+
installment_id: number; // obrigatório
|
|
254
|
+
bank_statement_line_id: number; // obrigatório
|
|
255
|
+
payment_channel?: string;
|
|
256
|
+
description?: string;
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
247
260
|
### CreateBankStatementAdjustmentDto
|
|
248
261
|
|
|
249
262
|
```ts
|
|
@@ -256,24 +269,20 @@ O módulo financeiro gerencia o ciclo completo da gestão financeira corporativa
|
|
|
256
269
|
}
|
|
257
270
|
```
|
|
258
271
|
|
|
259
|
-
###
|
|
272
|
+
### CreateCostCenterDto
|
|
260
273
|
|
|
261
274
|
```ts
|
|
262
275
|
{
|
|
263
|
-
|
|
264
|
-
destination_account_id: number; // obrigatório
|
|
265
|
-
date: string; // obrigatório
|
|
266
|
-
amount: number; // obrigatório, >= 0.01
|
|
267
|
-
description?: string;
|
|
276
|
+
name: string; // obrigatório
|
|
268
277
|
}
|
|
269
278
|
```
|
|
270
279
|
|
|
271
|
-
###
|
|
280
|
+
### UpdateCostCenterDto
|
|
272
281
|
|
|
273
282
|
```ts
|
|
274
283
|
{
|
|
275
|
-
name
|
|
276
|
-
|
|
284
|
+
name?: string;
|
|
285
|
+
status?: 'active' | 'inactive';
|
|
277
286
|
}
|
|
278
287
|
```
|
|
279
288
|
|
|
@@ -307,23 +316,6 @@ O módulo financeiro gerencia o ciclo completo da gestão financeira corporativa
|
|
|
307
316
|
}
|
|
308
317
|
```
|
|
309
318
|
|
|
310
|
-
### CreateCostCenterDto
|
|
311
|
-
|
|
312
|
-
```ts
|
|
313
|
-
{
|
|
314
|
-
name: string; // obrigatório
|
|
315
|
-
}
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
### UpdateCostCenterDto
|
|
319
|
-
|
|
320
|
-
```ts
|
|
321
|
-
{
|
|
322
|
-
name?: string;
|
|
323
|
-
status?: 'active' | 'inactive';
|
|
324
|
-
}
|
|
325
|
-
```
|
|
326
|
-
|
|
327
319
|
### SendCollectionDto
|
|
328
320
|
|
|
329
321
|
```ts
|
|
@@ -371,6 +363,38 @@ O módulo financeiro gerencia o ciclo completo da gestão financeira corporativa
|
|
|
371
363
|
}
|
|
372
364
|
```
|
|
373
365
|
|
|
366
|
+
### CreateTransferDto
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
{
|
|
370
|
+
source_account_id: number; // obrigatório
|
|
371
|
+
destination_account_id: number; // obrigatório
|
|
372
|
+
date: string; // obrigatório
|
|
373
|
+
amount: number; // obrigatório, >= 0.01
|
|
374
|
+
description?: string;
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### CreateFinanceTagDto
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
{
|
|
382
|
+
name: string; // obrigatório, string não vazia
|
|
383
|
+
color?: string; // opcional, cor hexadecimal válida
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### UpdateFinanceScenarioSettingsDto
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
{
|
|
391
|
+
atrasoMedio: number;
|
|
392
|
+
taxaInadimplencia: number;
|
|
393
|
+
crescimentoReceita: number;
|
|
394
|
+
setAsDefault?: boolean;
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
374
398
|
---
|
|
375
399
|
|
|
376
400
|
## 6. Erros comuns
|
|
@@ -750,6 +774,22 @@ Form-data:
|
|
|
750
774
|
- bank_account_id: 10
|
|
751
775
|
```
|
|
752
776
|
|
|
777
|
+
### Criar conciliação bancária
|
|
778
|
+
|
|
779
|
+
```http
|
|
780
|
+
POST /finance/bank-reconciliations
|
|
781
|
+
Authorization: Bearer <token>
|
|
782
|
+
Content-Type: application/json
|
|
783
|
+
|
|
784
|
+
{
|
|
785
|
+
"title_id": 123,
|
|
786
|
+
"installment_id": 456,
|
|
787
|
+
"bank_statement_line_id": 789,
|
|
788
|
+
"payment_channel": "PIX",
|
|
789
|
+
"description": "Conciliação automática"
|
|
790
|
+
}
|
|
791
|
+
```
|
|
792
|
+
|
|
753
793
|
### Estornar liquidação
|
|
754
794
|
|
|
755
795
|
```http
|
package/hedhog/data/menu.yaml
CHANGED
|
@@ -322,6 +322,52 @@
|
|
|
322
322
|
- where:
|
|
323
323
|
slug: admin-finance
|
|
324
324
|
|
|
325
|
+
- menu_id:
|
|
326
|
+
where:
|
|
327
|
+
slug: /finance/reports
|
|
328
|
+
icon: chart-no-axes-combined
|
|
329
|
+
url: /finance/reports/overview-results
|
|
330
|
+
name:
|
|
331
|
+
en: Results Overview
|
|
332
|
+
pt: Visão Geral dos Resultados
|
|
333
|
+
slug: /finance/reports/overview-results
|
|
334
|
+
relations:
|
|
335
|
+
role:
|
|
336
|
+
- where:
|
|
337
|
+
slug: admin
|
|
338
|
+
- where:
|
|
339
|
+
slug: admin-finance
|
|
340
|
+
- menu_id:
|
|
341
|
+
where:
|
|
342
|
+
slug: /finance/reports
|
|
343
|
+
icon: chart-bar-big
|
|
344
|
+
url: /finance/reports/top-customers
|
|
345
|
+
name:
|
|
346
|
+
en: Top Customers
|
|
347
|
+
pt: Principais Clientes
|
|
348
|
+
slug: /finance/reports/top-customers
|
|
349
|
+
relations:
|
|
350
|
+
role:
|
|
351
|
+
- where:
|
|
352
|
+
slug: admin
|
|
353
|
+
- where:
|
|
354
|
+
slug: admin-finance
|
|
355
|
+
- menu_id:
|
|
356
|
+
where:
|
|
357
|
+
slug: /finance/reports
|
|
358
|
+
icon: chart-pie
|
|
359
|
+
url: /finance/reports/top-operational-expenses
|
|
360
|
+
name:
|
|
361
|
+
en: Top Operational Expenses
|
|
362
|
+
pt: Principais Gastos Operacionais
|
|
363
|
+
slug: /finance/reports/top-operational-expenses
|
|
364
|
+
relations:
|
|
365
|
+
role:
|
|
366
|
+
- where:
|
|
367
|
+
slug: admin
|
|
368
|
+
- where:
|
|
369
|
+
slug: admin-finance
|
|
370
|
+
|
|
325
371
|
- menu_id:
|
|
326
372
|
where:
|
|
327
373
|
slug: /finance
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import {
|
|
2
|
+
customerRevenueMock,
|
|
3
|
+
financialResultsMock,
|
|
4
|
+
operationalExpensesMock,
|
|
5
|
+
type GroupBy,
|
|
6
|
+
} from './report-mocks';
|
|
7
|
+
|
|
8
|
+
const dateFormatter = new Intl.DateTimeFormat('pt-BR');
|
|
9
|
+
|
|
10
|
+
function toDate(value: string) {
|
|
11
|
+
return new Date(`${value}T00:00:00`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isWithinRange(date: string, from: string, to: string) {
|
|
15
|
+
return date >= from && date <= to;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function startOfWeek(date: Date) {
|
|
19
|
+
const current = new Date(date);
|
|
20
|
+
const day = current.getDay();
|
|
21
|
+
const diff = day === 0 ? -6 : 1 - day;
|
|
22
|
+
current.setDate(current.getDate() + diff);
|
|
23
|
+
current.setHours(0, 0, 0, 0);
|
|
24
|
+
|
|
25
|
+
return current;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getIsoWeek(date: Date) {
|
|
29
|
+
const current = new Date(
|
|
30
|
+
Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
|
|
31
|
+
);
|
|
32
|
+
const dayNum = current.getUTCDay() || 7;
|
|
33
|
+
current.setUTCDate(current.getUTCDate() + 4 - dayNum);
|
|
34
|
+
const yearStart = new Date(Date.UTC(current.getUTCFullYear(), 0, 1));
|
|
35
|
+
|
|
36
|
+
return Math.ceil(
|
|
37
|
+
((current.getTime() - yearStart.getTime()) / 86400000 + 1) / 7
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getBucketKey(date: string, groupBy: GroupBy) {
|
|
42
|
+
const d = toDate(date);
|
|
43
|
+
|
|
44
|
+
if (groupBy === 'day') {
|
|
45
|
+
return date;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (groupBy === 'week') {
|
|
49
|
+
const weekStart = startOfWeek(d);
|
|
50
|
+
const isoWeek = getIsoWeek(weekStart);
|
|
51
|
+
return `${weekStart.getFullYear()}-W${String(isoWeek).padStart(2, '0')}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (groupBy === 'month') {
|
|
55
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return String(d.getFullYear());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function formatBucketLabel(bucket: string, groupBy: GroupBy) {
|
|
62
|
+
if (groupBy === 'day') {
|
|
63
|
+
return dateFormatter.format(toDate(bucket));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (groupBy === 'week') {
|
|
67
|
+
const [year, week] = bucket.split('-W');
|
|
68
|
+
return `Sem ${Number(week)}/${year}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (groupBy === 'month') {
|
|
72
|
+
const [year, month] = bucket.split('-');
|
|
73
|
+
return `${month}/${year}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return bucket;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sortBuckets(a: string, b: string, groupBy: GroupBy) {
|
|
80
|
+
if (groupBy === 'year' || groupBy === 'month' || groupBy === 'day') {
|
|
81
|
+
return a.localeCompare(b);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const [aYear, aWeek] = a.split('-W');
|
|
85
|
+
const [bYear, bWeek] = b.split('-W');
|
|
86
|
+
const yearDiff = Number(aYear) - Number(bYear);
|
|
87
|
+
|
|
88
|
+
if (yearDiff !== 0) {
|
|
89
|
+
return yearDiff;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return Number(aWeek) - Number(bWeek);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function aggregateOverview(params: {
|
|
96
|
+
from: string;
|
|
97
|
+
to: string;
|
|
98
|
+
groupBy: GroupBy;
|
|
99
|
+
}) {
|
|
100
|
+
const { from, to, groupBy } = params;
|
|
101
|
+
const grouped = new Map<
|
|
102
|
+
string,
|
|
103
|
+
{
|
|
104
|
+
period: string;
|
|
105
|
+
label: string;
|
|
106
|
+
faturamento: number;
|
|
107
|
+
despesasEmprestimos: number;
|
|
108
|
+
diferenca: number;
|
|
109
|
+
aporteInvestidor: number;
|
|
110
|
+
emprestimoBanco: number;
|
|
111
|
+
despesas: number;
|
|
112
|
+
}
|
|
113
|
+
>();
|
|
114
|
+
|
|
115
|
+
for (const item of financialResultsMock) {
|
|
116
|
+
if (!isWithinRange(item.date, from, to)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const bucket = getBucketKey(item.date, groupBy);
|
|
121
|
+
const current = grouped.get(bucket) || {
|
|
122
|
+
period: bucket,
|
|
123
|
+
label: formatBucketLabel(bucket, groupBy),
|
|
124
|
+
faturamento: 0,
|
|
125
|
+
despesasEmprestimos: 0,
|
|
126
|
+
diferenca: 0,
|
|
127
|
+
aporteInvestidor: 0,
|
|
128
|
+
emprestimoBanco: 0,
|
|
129
|
+
despesas: 0,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
current.faturamento += item.faturamento;
|
|
133
|
+
current.aporteInvestidor += item.aporteInvestidor;
|
|
134
|
+
current.emprestimoBanco += item.emprestimoBanco;
|
|
135
|
+
current.despesas += item.despesas;
|
|
136
|
+
current.despesasEmprestimos += item.despesas + item.emprestimoBanco;
|
|
137
|
+
current.diferenca = current.faturamento - current.despesasEmprestimos;
|
|
138
|
+
|
|
139
|
+
grouped.set(bucket, current);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return Array.from(grouped.values()).sort((a, b) =>
|
|
143
|
+
sortBuckets(a.period, b.period, groupBy)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function aggregateTopCustomers(params: {
|
|
148
|
+
from: string;
|
|
149
|
+
to: string;
|
|
150
|
+
groupBy: GroupBy;
|
|
151
|
+
topN?: number;
|
|
152
|
+
}) {
|
|
153
|
+
const { from, to, groupBy, topN = 20 } = params;
|
|
154
|
+
|
|
155
|
+
const byCustomer = new Map<string, number>();
|
|
156
|
+
const byBucket = new Map<string, number>();
|
|
157
|
+
|
|
158
|
+
for (const item of customerRevenueMock) {
|
|
159
|
+
if (!isWithinRange(item.date, from, to)) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const current = byCustomer.get(item.customer) || 0;
|
|
164
|
+
byCustomer.set(item.customer, current + item.value);
|
|
165
|
+
|
|
166
|
+
const bucket = getBucketKey(item.date, groupBy);
|
|
167
|
+
const currentBucket = byBucket.get(bucket) || 0;
|
|
168
|
+
byBucket.set(bucket, currentBucket + item.value);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const sorted = Array.from(byCustomer.entries())
|
|
172
|
+
.map(([customer, value]) => ({ customer, value }))
|
|
173
|
+
.sort((a, b) => b.value - a.value);
|
|
174
|
+
|
|
175
|
+
const topCustomers = sorted.slice(0, topN);
|
|
176
|
+
const total = sorted.reduce((acc, row) => acc + row.value, 0);
|
|
177
|
+
const top5 = topCustomers
|
|
178
|
+
.slice(0, 5)
|
|
179
|
+
.reduce((acc, row) => acc + row.value, 0);
|
|
180
|
+
|
|
181
|
+
const pieData = topCustomers.slice(0, 9);
|
|
182
|
+
const othersValue = sorted.slice(9).reduce((acc, row) => acc + row.value, 0);
|
|
183
|
+
|
|
184
|
+
if (othersValue > 0) {
|
|
185
|
+
pieData.push({ customer: 'Outros', value: othersValue });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const groupedPeriods = Array.from(byBucket.entries())
|
|
189
|
+
.map(([period, value]) => ({
|
|
190
|
+
period,
|
|
191
|
+
label: formatBucketLabel(period, groupBy),
|
|
192
|
+
value,
|
|
193
|
+
}))
|
|
194
|
+
.sort((a, b) => sortBuckets(a.period, b.period, groupBy));
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
total,
|
|
198
|
+
top5,
|
|
199
|
+
top5Percent: total > 0 ? (top5 / total) * 100 : 0,
|
|
200
|
+
topCustomers,
|
|
201
|
+
pieData,
|
|
202
|
+
groupedPeriods,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function aggregateOperationalExpenses(params: {
|
|
207
|
+
from: string;
|
|
208
|
+
to: string;
|
|
209
|
+
groupBy: GroupBy;
|
|
210
|
+
topN?: number;
|
|
211
|
+
}) {
|
|
212
|
+
const { from, to, groupBy, topN = 20 } = params;
|
|
213
|
+
|
|
214
|
+
const byCategory = new Map<
|
|
215
|
+
string,
|
|
216
|
+
{ category: string; costCenter: string; value: number }
|
|
217
|
+
>();
|
|
218
|
+
const byCostCenter = new Map<string, number>();
|
|
219
|
+
const byBucket = new Map<string, number>();
|
|
220
|
+
|
|
221
|
+
for (const item of operationalExpensesMock) {
|
|
222
|
+
if (!isWithinRange(item.date, from, to)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const currentCategory = byCategory.get(item.category) || {
|
|
227
|
+
category: item.category,
|
|
228
|
+
costCenter: item.costCenter,
|
|
229
|
+
value: 0,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
currentCategory.value += item.value;
|
|
233
|
+
byCategory.set(item.category, currentCategory);
|
|
234
|
+
|
|
235
|
+
const currentCenter = byCostCenter.get(item.costCenter) || 0;
|
|
236
|
+
byCostCenter.set(item.costCenter, currentCenter + item.value);
|
|
237
|
+
|
|
238
|
+
const bucket = getBucketKey(item.date, groupBy);
|
|
239
|
+
const currentBucket = byBucket.get(bucket) || 0;
|
|
240
|
+
byBucket.set(bucket, currentBucket + item.value);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const sorted = Array.from(byCategory.values()).sort(
|
|
244
|
+
(a, b) => b.value - a.value
|
|
245
|
+
);
|
|
246
|
+
const topExpenses = sorted.slice(0, topN);
|
|
247
|
+
const total = sorted.reduce((acc, item) => acc + item.value, 0);
|
|
248
|
+
|
|
249
|
+
const pieData = Array.from(byCostCenter.entries()).map(([name, value]) => ({
|
|
250
|
+
name,
|
|
251
|
+
value,
|
|
252
|
+
}));
|
|
253
|
+
|
|
254
|
+
const groupedPeriods = Array.from(byBucket.entries())
|
|
255
|
+
.map(([period, value]) => ({
|
|
256
|
+
period,
|
|
257
|
+
label: formatBucketLabel(period, groupBy),
|
|
258
|
+
value,
|
|
259
|
+
}))
|
|
260
|
+
.sort((a, b) => sortBuckets(a.period, b.period, groupBy));
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
total,
|
|
264
|
+
topExpenses,
|
|
265
|
+
pieData,
|
|
266
|
+
groupedPeriods,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function getDefaultDateRange() {
|
|
271
|
+
return {
|
|
272
|
+
from: '2021-01-01',
|
|
273
|
+
to: '2026-12-31',
|
|
274
|
+
};
|
|
275
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
export type GroupBy = 'day' | 'week' | 'month' | 'year';
|
|
2
|
+
|
|
3
|
+
export interface FinancialResultEntry {
|
|
4
|
+
date: string;
|
|
5
|
+
faturamento: number;
|
|
6
|
+
emprestimoBanco: number;
|
|
7
|
+
aporteInvestidor: number;
|
|
8
|
+
despesas: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CustomerRevenueEntry {
|
|
12
|
+
date: string;
|
|
13
|
+
customer: string;
|
|
14
|
+
value: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface OperationalExpenseEntry {
|
|
18
|
+
date: string;
|
|
19
|
+
category: string;
|
|
20
|
+
costCenter: string;
|
|
21
|
+
value: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const customerNames = [
|
|
25
|
+
'VALE',
|
|
26
|
+
'CALIAN',
|
|
27
|
+
'ALURA',
|
|
28
|
+
'FIAP',
|
|
29
|
+
'UDEMY',
|
|
30
|
+
'HIREWORK',
|
|
31
|
+
'PAULO DE TARSO',
|
|
32
|
+
'BRCLICK',
|
|
33
|
+
'DISTRITO',
|
|
34
|
+
'COINBIT CLUB',
|
|
35
|
+
'IT GLOBAL',
|
|
36
|
+
'INFINITY LEGACY',
|
|
37
|
+
'XFX',
|
|
38
|
+
'CLASS',
|
|
39
|
+
'IMPACTA',
|
|
40
|
+
'AFX',
|
|
41
|
+
'SOLARIUM',
|
|
42
|
+
'ADVANCED RESOURCES',
|
|
43
|
+
'BMX SCHOOL',
|
|
44
|
+
'CODE NATION',
|
|
45
|
+
'NEXT LEVEL',
|
|
46
|
+
'MEGA TECH',
|
|
47
|
+
'EDU PRO',
|
|
48
|
+
'CONECTA BR',
|
|
49
|
+
'SABER MAIS',
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const topCustomerMultipliers: Record<string, number> = {
|
|
53
|
+
VALE: 4.5,
|
|
54
|
+
CALIAN: 2.4,
|
|
55
|
+
ALURA: 1.65,
|
|
56
|
+
FIAP: 1.45,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const expenseCategories = [
|
|
60
|
+
{ category: 'Folha de Pagamento', costCenter: 'Pessoas', weight: 4.6 },
|
|
61
|
+
{ category: 'Marketing', costCenter: 'Comercial', weight: 3.4 },
|
|
62
|
+
{ category: 'Tecnologia SaaS', costCenter: 'TI', weight: 3.1 },
|
|
63
|
+
{ category: 'Infraestrutura Cloud', costCenter: 'TI', weight: 2.8 },
|
|
64
|
+
{
|
|
65
|
+
category: 'Jurídico e Compliance',
|
|
66
|
+
costCenter: 'Administrativo',
|
|
67
|
+
weight: 2.1,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
category: 'Facilities e Escritório',
|
|
71
|
+
costCenter: 'Administrativo',
|
|
72
|
+
weight: 1.9,
|
|
73
|
+
},
|
|
74
|
+
{ category: 'Treinamentos', costCenter: 'Pessoas', weight: 1.7 },
|
|
75
|
+
{ category: 'Viagens e Eventos', costCenter: 'Comercial', weight: 1.5 },
|
|
76
|
+
{ category: 'Suporte ao Cliente', costCenter: 'Operações', weight: 2.0 },
|
|
77
|
+
{
|
|
78
|
+
category: 'Ferramentas Financeiras',
|
|
79
|
+
costCenter: 'Financeiro',
|
|
80
|
+
weight: 1.3,
|
|
81
|
+
},
|
|
82
|
+
{ category: 'Consultorias', costCenter: 'Operações', weight: 1.2 },
|
|
83
|
+
{ category: 'Seguros', costCenter: 'Financeiro', weight: 0.9 },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
function clamp(value: number, min: number, max: number) {
|
|
87
|
+
return Math.max(min, Math.min(max, value));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function monthSeed(year: number, monthIndex: number) {
|
|
91
|
+
return year * 17 + monthIndex * 31;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildMonthlyDates() {
|
|
95
|
+
const dates: string[] = [];
|
|
96
|
+
|
|
97
|
+
for (let year = 2021; year <= 2026; year += 1) {
|
|
98
|
+
for (let month = 1; month <= 12; month += 1) {
|
|
99
|
+
dates.push(`${year}-${String(month).padStart(2, '0')}-01`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return dates;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const monthlyDates = buildMonthlyDates();
|
|
107
|
+
|
|
108
|
+
export const financialResultsMock: FinancialResultEntry[] = monthlyDates.map(
|
|
109
|
+
(date, index) => {
|
|
110
|
+
const d = new Date(`${date}T00:00:00`);
|
|
111
|
+
const year = d.getFullYear();
|
|
112
|
+
const month = d.getMonth();
|
|
113
|
+
const growthFactor = 1 + (year - 2021) * 0.08;
|
|
114
|
+
const seasonality = Math.sin((month / 12) * Math.PI * 2) * 0.18;
|
|
115
|
+
const trend = 1 + index * 0.006;
|
|
116
|
+
const seed = monthSeed(year, month);
|
|
117
|
+
|
|
118
|
+
const faturamento = Math.round(
|
|
119
|
+
(260000 + seasonality * 70000 + (seed % 13) * 3500) * trend * growthFactor
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const despesas = Math.round(
|
|
123
|
+
(190000 +
|
|
124
|
+
Math.cos((month / 12) * Math.PI * 2) * 45000 +
|
|
125
|
+
(seed % 7) * 4200) *
|
|
126
|
+
(0.93 + (year - 2021) * 0.035)
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const emprestimoBanco =
|
|
130
|
+
month % 6 === 1 ? Math.round(22000 + (seed % 9) * 1800) : 0;
|
|
131
|
+
|
|
132
|
+
const aporteInvestidor =
|
|
133
|
+
year === 2023 && (month === 2 || month === 8)
|
|
134
|
+
? 175000
|
|
135
|
+
: year === 2024 && month === 1
|
|
136
|
+
? 50000
|
|
137
|
+
: year === 2025 && month === 4
|
|
138
|
+
? 52000
|
|
139
|
+
: 0;
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
date,
|
|
143
|
+
faturamento: clamp(faturamento, 120000, 530000),
|
|
144
|
+
emprestimoBanco,
|
|
145
|
+
aporteInvestidor,
|
|
146
|
+
despesas: clamp(despesas, 130000, 430000),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
export const customerRevenueMock: CustomerRevenueEntry[] = monthlyDates.flatMap(
|
|
152
|
+
(date, monthIndex) => {
|
|
153
|
+
const dateObj = new Date(`${date}T00:00:00`);
|
|
154
|
+
const seasonal =
|
|
155
|
+
1 + Math.sin((dateObj.getMonth() / 12) * Math.PI * 2) * 0.08;
|
|
156
|
+
|
|
157
|
+
return customerNames.map((customer, index) => {
|
|
158
|
+
const base = Math.max(1800, 46000 - index * 1850);
|
|
159
|
+
const priorityBoost = topCustomerMultipliers[customer] || 1;
|
|
160
|
+
const variation = 1 + ((monthIndex + index * 3) % 9) * 0.023;
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
date,
|
|
164
|
+
customer,
|
|
165
|
+
value: Math.round(base * priorityBoost * seasonal * variation),
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
export const operationalExpensesMock: OperationalExpenseEntry[] =
|
|
172
|
+
monthlyDates.flatMap((date, monthIndex) =>
|
|
173
|
+
expenseCategories.map((entry, categoryIndex) => {
|
|
174
|
+
const monthPulse = 1 + ((monthIndex + categoryIndex) % 7) * 0.04;
|
|
175
|
+
const quarterEffects = [1.04, 0.97, 1.1, 0.99] as const;
|
|
176
|
+
const quarterEffect =
|
|
177
|
+
quarterEffects[Math.floor((monthIndex % 12) / 3)] ?? 1;
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
date,
|
|
181
|
+
category: entry.category,
|
|
182
|
+
costCenter: entry.costCenter,
|
|
183
|
+
value: Math.round(14000 * entry.weight * monthPulse * quarterEffect),
|
|
184
|
+
};
|
|
185
|
+
})
|
|
186
|
+
);
|