@hed-hog/finance 0.0.278 → 0.0.285
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/dist/dto/finance-report-query.dto.d.ts +16 -0
- package/dist/dto/finance-report-query.dto.d.ts.map +1 -0
- package/dist/dto/finance-report-query.dto.js +59 -0
- package/dist/dto/finance-report-query.dto.js.map +1 -0
- package/dist/finance-reports.controller.d.ts +71 -0
- package/dist/finance-reports.controller.d.ts.map +1 -0
- package/dist/finance-reports.controller.js +61 -0
- package/dist/finance-reports.controller.js.map +1 -0
- package/dist/finance.module.d.ts.map +1 -1
- package/dist/finance.module.js +2 -0
- package/dist/finance.module.js.map +1 -1
- package/dist/finance.service.d.ts +93 -0
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +456 -0
- package/dist/finance.service.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/hedhog/data/menu.yaml +46 -0
- package/hedhog/data/route.yaml +27 -0
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +158 -125
- package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +102 -88
- package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +113 -89
- 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/_lib/use-finance-reports.ts.ejs +233 -0
- package/hedhog/frontend/app/reports/overview-results/page.tsx.ejs +355 -0
- package/hedhog/frontend/app/reports/top-customers/page.tsx.ejs +427 -0
- package/hedhog/frontend/app/reports/top-operational-expenses/page.tsx.ejs +433 -0
- package/hedhog/frontend/messages/en.json +179 -0
- package/hedhog/frontend/messages/pt.json +179 -0
- package/package.json +7 -7
- package/src/dto/finance-report-query.dto.ts +49 -0
- package/src/finance-reports.controller.ts +28 -0
- package/src/finance.module.ts +2 -0
- package/src/finance.service.ts +645 -10
- package/src/index.ts +1 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Page, PageHeader } from '@/components/entity-list';
|
|
4
|
+
import { Badge } from '@/components/ui/badge';
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardDescription,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
} from '@/components/ui/card';
|
|
12
|
+
import { Input } from '@/components/ui/input';
|
|
13
|
+
import { Label } from '@/components/ui/label';
|
|
14
|
+
import { Money } from '@/components/ui/money';
|
|
15
|
+
import {
|
|
16
|
+
Select,
|
|
17
|
+
SelectContent,
|
|
18
|
+
SelectItem,
|
|
19
|
+
SelectTrigger,
|
|
20
|
+
SelectValue,
|
|
21
|
+
} from '@/components/ui/select';
|
|
22
|
+
import {
|
|
23
|
+
Table,
|
|
24
|
+
TableBody,
|
|
25
|
+
TableCell,
|
|
26
|
+
TableHead,
|
|
27
|
+
TableHeader,
|
|
28
|
+
TableRow,
|
|
29
|
+
} from '@/components/ui/table';
|
|
30
|
+
import { Target, TrendingDown, TrendingUp } from 'lucide-react';
|
|
31
|
+
import { useLocale, useTranslations } from 'next-intl';
|
|
32
|
+
import { useMemo, useState } from 'react';
|
|
33
|
+
import type { TooltipProps } from 'recharts';
|
|
34
|
+
import {
|
|
35
|
+
CartesianGrid,
|
|
36
|
+
Legend,
|
|
37
|
+
Line,
|
|
38
|
+
LineChart,
|
|
39
|
+
ResponsiveContainer,
|
|
40
|
+
Tooltip,
|
|
41
|
+
XAxis,
|
|
42
|
+
YAxis,
|
|
43
|
+
} from 'recharts';
|
|
44
|
+
import {
|
|
45
|
+
formatReportBucketLabel,
|
|
46
|
+
getDefaultDateRange,
|
|
47
|
+
type GroupBy,
|
|
48
|
+
useOverviewResultsReport,
|
|
49
|
+
} from '../_lib/use-finance-reports';
|
|
50
|
+
|
|
51
|
+
const LINE_COLORS = {
|
|
52
|
+
faturamento: '#2563eb',
|
|
53
|
+
despesasEmprestimos: '#ea580c',
|
|
54
|
+
diferenca: '#16a34a',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const currencyFormatter = new Intl.NumberFormat('pt-BR', {
|
|
58
|
+
style: 'currency',
|
|
59
|
+
currency: 'BRL',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function renderSolidTooltip({
|
|
63
|
+
active,
|
|
64
|
+
payload,
|
|
65
|
+
label,
|
|
66
|
+
}: TooltipProps<number, string>) {
|
|
67
|
+
if (!active || !payload?.length) return null;
|
|
68
|
+
return (
|
|
69
|
+
<div className="rounded-lg border bg-popover px-3 py-2 shadow-xl text-popover-foreground text-sm">
|
|
70
|
+
<p className="mb-1 font-semibold text-popover-foreground">{label}</p>
|
|
71
|
+
{payload.map((entry) => (
|
|
72
|
+
<div key={entry.dataKey} className="flex items-center gap-2 py-0.5">
|
|
73
|
+
<span
|
|
74
|
+
className="inline-block h-2.5 w-2.5 rounded-sm shrink-0"
|
|
75
|
+
style={{ background: entry.color }}
|
|
76
|
+
/>
|
|
77
|
+
<span className="text-muted-foreground">{entry.name}:</span>
|
|
78
|
+
<span className="font-semibold text-popover-foreground">
|
|
79
|
+
{typeof entry.value === 'number'
|
|
80
|
+
? currencyFormatter.format(entry.value)
|
|
81
|
+
: entry.value}
|
|
82
|
+
</span>
|
|
83
|
+
</div>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default function OverviewResultsReportPage() {
|
|
90
|
+
const t = useTranslations('finance.OverviewResultsReportPage');
|
|
91
|
+
const locale = useLocale();
|
|
92
|
+
const defaults = getDefaultDateRange();
|
|
93
|
+
const [from, setFrom] = useState(defaults.from);
|
|
94
|
+
const [to, setTo] = useState(defaults.to);
|
|
95
|
+
const [groupBy, setGroupBy] = useState<GroupBy>('year');
|
|
96
|
+
const { data } = useOverviewResultsReport({
|
|
97
|
+
from,
|
|
98
|
+
to,
|
|
99
|
+
groupBy,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const rows = useMemo(
|
|
103
|
+
() =>
|
|
104
|
+
data.rows.map((row) => ({
|
|
105
|
+
...row,
|
|
106
|
+
label: formatReportBucketLabel(row.period, groupBy, locale),
|
|
107
|
+
})),
|
|
108
|
+
[data.rows, groupBy, locale]
|
|
109
|
+
);
|
|
110
|
+
const totals = data.totals;
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<Page>
|
|
114
|
+
<PageHeader
|
|
115
|
+
title={t('header.title')}
|
|
116
|
+
description={t('header.description')}
|
|
117
|
+
breadcrumbs={[
|
|
118
|
+
{ label: t('breadcrumbs.home'), href: '/' },
|
|
119
|
+
{ label: t('breadcrumbs.finance'), href: '/finance' },
|
|
120
|
+
{ label: t('breadcrumbs.current') },
|
|
121
|
+
]}
|
|
122
|
+
/>
|
|
123
|
+
|
|
124
|
+
<Card>
|
|
125
|
+
<CardHeader>
|
|
126
|
+
<CardTitle>{t('filters.title')}</CardTitle>
|
|
127
|
+
<CardDescription>{t('filters.description')}</CardDescription>
|
|
128
|
+
</CardHeader>
|
|
129
|
+
<CardContent className="grid gap-4 md:grid-cols-3">
|
|
130
|
+
<div className="space-y-2">
|
|
131
|
+
<Label htmlFor="from">{t('filters.from')}</Label>
|
|
132
|
+
<Input
|
|
133
|
+
id="from"
|
|
134
|
+
type="date"
|
|
135
|
+
value={from}
|
|
136
|
+
onChange={(event) => setFrom(event.target.value)}
|
|
137
|
+
max={to}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
<div className="space-y-2">
|
|
141
|
+
<Label htmlFor="to">{t('filters.to')}</Label>
|
|
142
|
+
<Input
|
|
143
|
+
id="to"
|
|
144
|
+
type="date"
|
|
145
|
+
value={to}
|
|
146
|
+
onChange={(event) => setTo(event.target.value)}
|
|
147
|
+
min={from}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="space-y-2">
|
|
151
|
+
<Label>{t('filters.groupBy')}</Label>
|
|
152
|
+
<Select
|
|
153
|
+
value={groupBy}
|
|
154
|
+
onValueChange={(value) => setGroupBy(value as GroupBy)}
|
|
155
|
+
>
|
|
156
|
+
<SelectTrigger>
|
|
157
|
+
<SelectValue />
|
|
158
|
+
</SelectTrigger>
|
|
159
|
+
<SelectContent>
|
|
160
|
+
<SelectItem value="day">{t('groupBy.day')}</SelectItem>
|
|
161
|
+
<SelectItem value="week">{t('groupBy.week')}</SelectItem>
|
|
162
|
+
<SelectItem value="month">{t('groupBy.month')}</SelectItem>
|
|
163
|
+
<SelectItem value="year">{t('groupBy.year')}</SelectItem>
|
|
164
|
+
</SelectContent>
|
|
165
|
+
</Select>
|
|
166
|
+
</div>
|
|
167
|
+
</CardContent>
|
|
168
|
+
</Card>
|
|
169
|
+
|
|
170
|
+
<div className="grid gap-3 md:grid-cols-4">
|
|
171
|
+
<Card>
|
|
172
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1 pt-3 px-4">
|
|
173
|
+
<CardTitle className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
|
174
|
+
{t('cards.revenue')}
|
|
175
|
+
</CardTitle>
|
|
176
|
+
<div className="rounded-full bg-blue-500/10 p-1.5">
|
|
177
|
+
<TrendingUp className="h-4 w-4 text-blue-600" />
|
|
178
|
+
</div>
|
|
179
|
+
</CardHeader>
|
|
180
|
+
<CardContent className="pb-3 pt-0 px-4">
|
|
181
|
+
<div className="text-xl font-bold">
|
|
182
|
+
<Money value={totals.faturamento} />
|
|
183
|
+
</div>
|
|
184
|
+
</CardContent>
|
|
185
|
+
</Card>
|
|
186
|
+
|
|
187
|
+
<Card>
|
|
188
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1 pt-3 px-4">
|
|
189
|
+
<CardTitle className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
|
190
|
+
{t('cards.expensesAndLoans')}
|
|
191
|
+
</CardTitle>
|
|
192
|
+
<div className="rounded-full bg-orange-500/10 p-1.5">
|
|
193
|
+
<TrendingDown className="h-4 w-4 text-orange-600" />
|
|
194
|
+
</div>
|
|
195
|
+
</CardHeader>
|
|
196
|
+
<CardContent className="pb-3 pt-0 px-4">
|
|
197
|
+
<div className="text-xl font-bold">
|
|
198
|
+
<Money value={totals.despesasEmprestimos} />
|
|
199
|
+
</div>
|
|
200
|
+
</CardContent>
|
|
201
|
+
</Card>
|
|
202
|
+
|
|
203
|
+
<Card>
|
|
204
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1 pt-3 px-4">
|
|
205
|
+
<CardTitle className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
|
206
|
+
{t('cards.resultDifference')}
|
|
207
|
+
</CardTitle>
|
|
208
|
+
<div
|
|
209
|
+
className={`rounded-full p-1.5 ${totals.diferenca >= 0 ? 'bg-green-500/10' : 'bg-red-500/10'}`}
|
|
210
|
+
>
|
|
211
|
+
{totals.diferenca >= 0 ? (
|
|
212
|
+
<TrendingUp className="h-4 w-4 text-green-600" />
|
|
213
|
+
) : (
|
|
214
|
+
<TrendingDown className="h-4 w-4 text-red-600" />
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
</CardHeader>
|
|
218
|
+
<CardContent className="pb-3 pt-0 px-4">
|
|
219
|
+
<div
|
|
220
|
+
className={`text-xl font-bold ${totals.diferenca >= 0 ? 'text-green-600' : 'text-red-600'}`}
|
|
221
|
+
>
|
|
222
|
+
<Money value={totals.diferenca} showSign />
|
|
223
|
+
</div>
|
|
224
|
+
</CardContent>
|
|
225
|
+
</Card>
|
|
226
|
+
|
|
227
|
+
<Card>
|
|
228
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1 pt-3 px-4">
|
|
229
|
+
<CardTitle className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
|
230
|
+
{t('cards.margin')}
|
|
231
|
+
</CardTitle>
|
|
232
|
+
<div className="rounded-full bg-violet-500/10 p-1.5">
|
|
233
|
+
<Target className="h-4 w-4 text-violet-600" />
|
|
234
|
+
</div>
|
|
235
|
+
</CardHeader>
|
|
236
|
+
<CardContent className="pb-3 pt-0 px-4">
|
|
237
|
+
<div className="text-xl font-bold">{totals.margem.toFixed(1)}%</div>
|
|
238
|
+
<Badge
|
|
239
|
+
className="mt-1"
|
|
240
|
+
variant={totals.margem >= 0 ? 'default' : 'destructive'}
|
|
241
|
+
>
|
|
242
|
+
{totals.margem >= 0 ? t('status.positive') : t('status.negative')}
|
|
243
|
+
</Badge>
|
|
244
|
+
</CardContent>
|
|
245
|
+
</Card>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<Card>
|
|
249
|
+
<CardHeader>
|
|
250
|
+
<CardTitle>{t('chart.title')}</CardTitle>
|
|
251
|
+
<CardDescription>{t('chart.description')}</CardDescription>
|
|
252
|
+
</CardHeader>
|
|
253
|
+
<CardContent>
|
|
254
|
+
<ResponsiveContainer width="100%" height={360}>
|
|
255
|
+
<LineChart data={rows}>
|
|
256
|
+
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
|
257
|
+
<XAxis dataKey="label" tick={{ fontSize: 12 }} />
|
|
258
|
+
<YAxis
|
|
259
|
+
tick={{ fontSize: 12 }}
|
|
260
|
+
tickFormatter={(value) => `${(value / 1000).toFixed(0)}k`}
|
|
261
|
+
/>
|
|
262
|
+
<Tooltip content={renderSolidTooltip} />
|
|
263
|
+
<Legend />
|
|
264
|
+
<Line
|
|
265
|
+
type="monotone"
|
|
266
|
+
dataKey="faturamento"
|
|
267
|
+
name={t('chart.revenueLabel')}
|
|
268
|
+
stroke={LINE_COLORS.faturamento}
|
|
269
|
+
strokeWidth={3}
|
|
270
|
+
dot={false}
|
|
271
|
+
/>
|
|
272
|
+
<Line
|
|
273
|
+
type="monotone"
|
|
274
|
+
dataKey="despesasEmprestimos"
|
|
275
|
+
name={t('chart.expensesLabel')}
|
|
276
|
+
stroke={LINE_COLORS.despesasEmprestimos}
|
|
277
|
+
strokeWidth={3}
|
|
278
|
+
dot={false}
|
|
279
|
+
/>
|
|
280
|
+
<Line
|
|
281
|
+
type="monotone"
|
|
282
|
+
dataKey="diferenca"
|
|
283
|
+
name={t('chart.differenceLabel')}
|
|
284
|
+
stroke={LINE_COLORS.diferenca}
|
|
285
|
+
strokeWidth={2}
|
|
286
|
+
strokeDasharray="6 4"
|
|
287
|
+
dot={false}
|
|
288
|
+
/>
|
|
289
|
+
</LineChart>
|
|
290
|
+
</ResponsiveContainer>
|
|
291
|
+
</CardContent>
|
|
292
|
+
</Card>
|
|
293
|
+
|
|
294
|
+
<Card>
|
|
295
|
+
<CardHeader>
|
|
296
|
+
<CardTitle>{t('table.title')}</CardTitle>
|
|
297
|
+
<CardDescription>{t('table.description')}</CardDescription>
|
|
298
|
+
</CardHeader>
|
|
299
|
+
<CardContent>
|
|
300
|
+
<Table>
|
|
301
|
+
<TableHeader>
|
|
302
|
+
<TableRow>
|
|
303
|
+
<TableHead>{t('table.headers.period')}</TableHead>
|
|
304
|
+
<TableHead className="text-right">
|
|
305
|
+
{t('table.headers.revenue')}
|
|
306
|
+
</TableHead>
|
|
307
|
+
<TableHead className="text-right">
|
|
308
|
+
{t('table.headers.expensesAndLoans')}
|
|
309
|
+
</TableHead>
|
|
310
|
+
<TableHead className="text-right">
|
|
311
|
+
{t('table.headers.investorContribution')}
|
|
312
|
+
</TableHead>
|
|
313
|
+
<TableHead className="text-right">
|
|
314
|
+
{t('table.headers.difference')}
|
|
315
|
+
</TableHead>
|
|
316
|
+
</TableRow>
|
|
317
|
+
</TableHeader>
|
|
318
|
+
<TableBody>
|
|
319
|
+
{rows.length === 0 ? (
|
|
320
|
+
<TableRow>
|
|
321
|
+
<TableCell
|
|
322
|
+
colSpan={5}
|
|
323
|
+
className="text-muted-foreground text-center"
|
|
324
|
+
>
|
|
325
|
+
{t('table.empty')}
|
|
326
|
+
</TableCell>
|
|
327
|
+
</TableRow>
|
|
328
|
+
) : (
|
|
329
|
+
rows.map((row) => (
|
|
330
|
+
<TableRow key={row.period}>
|
|
331
|
+
<TableCell>{row.label}</TableCell>
|
|
332
|
+
<TableCell className="text-right">
|
|
333
|
+
<Money value={row.faturamento} />
|
|
334
|
+
</TableCell>
|
|
335
|
+
<TableCell className="text-right">
|
|
336
|
+
<Money value={row.despesasEmprestimos} />
|
|
337
|
+
</TableCell>
|
|
338
|
+
<TableCell className="text-right">
|
|
339
|
+
<Money value={row.aporteInvestidor} />
|
|
340
|
+
</TableCell>
|
|
341
|
+
<TableCell
|
|
342
|
+
className={`text-right font-semibold ${row.diferenca >= 0 ? 'text-green-600' : 'text-red-600'}`}
|
|
343
|
+
>
|
|
344
|
+
<Money value={row.diferenca} showSign />
|
|
345
|
+
</TableCell>
|
|
346
|
+
</TableRow>
|
|
347
|
+
))
|
|
348
|
+
)}
|
|
349
|
+
</TableBody>
|
|
350
|
+
</Table>
|
|
351
|
+
</CardContent>
|
|
352
|
+
</Card>
|
|
353
|
+
</Page>
|
|
354
|
+
);
|
|
355
|
+
}
|