@hed-hog/finance 0.0.278 → 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 +65 -29
- 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 +5 -5
package/README.md
CHANGED
|
@@ -133,10 +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
|
-
<<<<<<< HEAD
|
|
137
|
-
=======
|
|
138
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 |
|
|
139
|
-
>>>>>>> d51b9b5865563d7f3890f049742d5c20ae326ff9
|
|
140
137
|
| GET | /finance/statements/export | Sim | Exporta extratos em CSV | Query: `bank_account_id` (obrigatório), `search` | CSV para download | 400 BadRequest, 401 |
|
|
141
138
|
| POST | /finance/statements/import | Sim | Importa extrato bancário | Multipart file + Body: `bank_account_id` (obrigatório) | Importação realizada | 400 BadRequest, 401 |
|
|
142
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 |
|
|
@@ -248,6 +245,18 @@ O módulo financeiro gerencia o ciclo completo da gestão financeira corporativa
|
|
|
248
245
|
}
|
|
249
246
|
```
|
|
250
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
|
+
|
|
251
260
|
### CreateBankStatementAdjustmentDto
|
|
252
261
|
|
|
253
262
|
```ts
|
|
@@ -260,24 +269,20 @@ O módulo financeiro gerencia o ciclo completo da gestão financeira corporativa
|
|
|
260
269
|
}
|
|
261
270
|
```
|
|
262
271
|
|
|
263
|
-
###
|
|
272
|
+
### CreateCostCenterDto
|
|
264
273
|
|
|
265
274
|
```ts
|
|
266
275
|
{
|
|
267
|
-
|
|
268
|
-
destination_account_id: number; // obrigatório
|
|
269
|
-
date: string; // obrigatório
|
|
270
|
-
amount: number; // obrigatório, >= 0.01
|
|
271
|
-
description?: string;
|
|
276
|
+
name: string; // obrigatório
|
|
272
277
|
}
|
|
273
278
|
```
|
|
274
279
|
|
|
275
|
-
###
|
|
280
|
+
### UpdateCostCenterDto
|
|
276
281
|
|
|
277
282
|
```ts
|
|
278
283
|
{
|
|
279
|
-
name
|
|
280
|
-
|
|
284
|
+
name?: string;
|
|
285
|
+
status?: 'active' | 'inactive';
|
|
281
286
|
}
|
|
282
287
|
```
|
|
283
288
|
|
|
@@ -311,23 +316,6 @@ O módulo financeiro gerencia o ciclo completo da gestão financeira corporativa
|
|
|
311
316
|
}
|
|
312
317
|
```
|
|
313
318
|
|
|
314
|
-
### CreateCostCenterDto
|
|
315
|
-
|
|
316
|
-
```ts
|
|
317
|
-
{
|
|
318
|
-
name: string; // obrigatório
|
|
319
|
-
}
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
### UpdateCostCenterDto
|
|
323
|
-
|
|
324
|
-
```ts
|
|
325
|
-
{
|
|
326
|
-
name?: string;
|
|
327
|
-
status?: 'active' | 'inactive';
|
|
328
|
-
}
|
|
329
|
-
```
|
|
330
|
-
|
|
331
319
|
### SendCollectionDto
|
|
332
320
|
|
|
333
321
|
```ts
|
|
@@ -375,6 +363,38 @@ O módulo financeiro gerencia o ciclo completo da gestão financeira corporativa
|
|
|
375
363
|
}
|
|
376
364
|
```
|
|
377
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
|
+
|
|
378
398
|
---
|
|
379
399
|
|
|
380
400
|
## 6. Erros comuns
|
|
@@ -754,6 +774,22 @@ Form-data:
|
|
|
754
774
|
- bank_account_id: 10
|
|
755
775
|
```
|
|
756
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
|
+
|
|
757
793
|
### Estornar liquidação
|
|
758
794
|
|
|
759
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
|
+
);
|