@hed-hog/finance 0.0.223 → 0.0.225
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/dist/dto/create-financial-title.dto.d.ts +19 -0
- package/dist/dto/create-financial-title.dto.d.ts.map +1 -0
- package/dist/dto/create-financial-title.dto.js +128 -0
- package/dist/dto/create-financial-title.dto.js.map +1 -0
- package/dist/finance.controller.d.ts +276 -0
- package/dist/finance.controller.d.ts.map +1 -0
- package/dist/finance.controller.js +110 -0
- package/dist/finance.controller.js.map +1 -0
- package/dist/finance.module.d.ts.map +1 -1
- package/dist/finance.module.js +8 -3
- package/dist/finance.module.js.map +1 -1
- package/dist/finance.service.d.ts +295 -0
- package/dist/finance.service.d.ts.map +1 -0
- package/dist/finance.service.js +416 -0
- package/dist/finance.service.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/hedhog/data/menu.yaml +72 -25
- package/hedhog/data/route.yaml +55 -1
- package/hedhog/frontend/app/_lib/formatters.ts.ejs +20 -0
- package/hedhog/frontend/app/_lib/use-finance-data.ts.ejs +87 -0
- package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +290 -0
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +410 -0
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +388 -0
- package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +423 -0
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +411 -0
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +385 -0
- package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +296 -0
- package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +427 -0
- package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +273 -0
- package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +253 -0
- package/hedhog/frontend/app/page.tsx.ejs +338 -17
- package/hedhog/frontend/app/planning/cash-flow-forecast/page.tsx.ejs +298 -0
- package/hedhog/frontend/app/planning/receivables-calendar/page.tsx.ejs +225 -0
- package/hedhog/frontend/app/planning/scenarios/page.tsx.ejs +338 -0
- package/hedhog/frontend/messages/en.json +776 -0
- package/hedhog/frontend/messages/pt.json +776 -0
- package/hedhog/query/constraints.sql +169 -0
- package/package.json +3 -3
- package/src/dto/create-financial-title.dto.ts +142 -0
- package/src/finance.controller.ts +89 -0
- package/src/finance.module.ts +8 -3
- package/src/finance.service.ts +529 -0
- package/src/index.ts +4 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Page, PageHeader } from '@/components/entity-list';
|
|
4
|
+
import { Badge } from '@/components/ui/badge';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import {
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardDescription,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle,
|
|
12
|
+
} from '@/components/ui/card';
|
|
13
|
+
import { Checkbox } from '@/components/ui/checkbox';
|
|
14
|
+
import {
|
|
15
|
+
Dialog,
|
|
16
|
+
DialogContent,
|
|
17
|
+
DialogDescription,
|
|
18
|
+
DialogFooter,
|
|
19
|
+
DialogHeader,
|
|
20
|
+
DialogTitle,
|
|
21
|
+
DialogTrigger,
|
|
22
|
+
} from '@/components/ui/dialog';
|
|
23
|
+
import { Input } from '@/components/ui/input';
|
|
24
|
+
import { Label } from '@/components/ui/label';
|
|
25
|
+
import { Money } from '@/components/ui/money';
|
|
26
|
+
import { Progress } from '@/components/ui/progress';
|
|
27
|
+
import {
|
|
28
|
+
Select,
|
|
29
|
+
SelectContent,
|
|
30
|
+
SelectItem,
|
|
31
|
+
SelectTrigger,
|
|
32
|
+
SelectValue,
|
|
33
|
+
} from '@/components/ui/select';
|
|
34
|
+
import { StatusBadge } from '@/components/ui/status-badge';
|
|
35
|
+
import { Check, DollarSign, Link2, RefreshCw } from 'lucide-react';
|
|
36
|
+
import { useTranslations } from 'next-intl';
|
|
37
|
+
import { useState } from 'react';
|
|
38
|
+
import { formatarData, formatarMoeda } from '../../_lib/formatters';
|
|
39
|
+
import { useFinanceData } from '../../_lib/use-finance-data';
|
|
40
|
+
|
|
41
|
+
function CriarAjusteDialog({ t }: { t: ReturnType<typeof useTranslations> }) {
|
|
42
|
+
return (
|
|
43
|
+
<Dialog>
|
|
44
|
+
<DialogTrigger asChild>
|
|
45
|
+
<Button variant="outline" size="sm">
|
|
46
|
+
<DollarSign className="mr-2 h-4 w-4" />
|
|
47
|
+
{t('adjustment.action')}
|
|
48
|
+
</Button>
|
|
49
|
+
</DialogTrigger>
|
|
50
|
+
<DialogContent>
|
|
51
|
+
<DialogHeader>
|
|
52
|
+
<DialogTitle>{t('adjustment.title')}</DialogTitle>
|
|
53
|
+
<DialogDescription>{t('adjustment.description')}</DialogDescription>
|
|
54
|
+
</DialogHeader>
|
|
55
|
+
<div className="space-y-4">
|
|
56
|
+
<div className="space-y-2">
|
|
57
|
+
<Label htmlFor="tipo">{t('adjustment.type')}</Label>
|
|
58
|
+
<Select>
|
|
59
|
+
<SelectTrigger>
|
|
60
|
+
<SelectValue placeholder={t('common.select')} />
|
|
61
|
+
</SelectTrigger>
|
|
62
|
+
<SelectContent>
|
|
63
|
+
<SelectItem value="tarifa">
|
|
64
|
+
{t('adjustment.types.bankFee')}
|
|
65
|
+
</SelectItem>
|
|
66
|
+
<SelectItem value="juros">
|
|
67
|
+
{t('adjustment.types.interest')}
|
|
68
|
+
</SelectItem>
|
|
69
|
+
<SelectItem value="diferenca">
|
|
70
|
+
{t('adjustment.types.valueDifference')}
|
|
71
|
+
</SelectItem>
|
|
72
|
+
<SelectItem value="outro">
|
|
73
|
+
{t('adjustment.types.other')}
|
|
74
|
+
</SelectItem>
|
|
75
|
+
</SelectContent>
|
|
76
|
+
</Select>
|
|
77
|
+
</div>
|
|
78
|
+
<div className="space-y-2">
|
|
79
|
+
<Label htmlFor="valor">{t('adjustment.value')}</Label>
|
|
80
|
+
<Input id="valor" type="number" placeholder="0,00" />
|
|
81
|
+
</div>
|
|
82
|
+
<div className="space-y-2">
|
|
83
|
+
<Label htmlFor="descricao">{t('adjustment.details')}</Label>
|
|
84
|
+
<Input
|
|
85
|
+
id="descricao"
|
|
86
|
+
placeholder={t('adjustment.detailsPlaceholder')}
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<DialogFooter>
|
|
91
|
+
<Button variant="outline">{t('common.cancel')}</Button>
|
|
92
|
+
<Button>{t('adjustment.submit')}</Button>
|
|
93
|
+
</DialogFooter>
|
|
94
|
+
</DialogContent>
|
|
95
|
+
</Dialog>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default function ConciliacaoPage() {
|
|
100
|
+
const t = useTranslations('finance.BankReconciliationPage');
|
|
101
|
+
const { data } = useFinanceData();
|
|
102
|
+
const { extratos, titulosPagar, titulosReceber, contasBancarias, pessoas } =
|
|
103
|
+
data;
|
|
104
|
+
|
|
105
|
+
const getPessoaById = (id?: string) => pessoas.find((p) => p.id === id);
|
|
106
|
+
|
|
107
|
+
const extratosPendentes = extratos.filter(
|
|
108
|
+
(e) => e.statusConciliacao === 'pendente'
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const titulosAbertos = [
|
|
112
|
+
...titulosPagar.flatMap((t: any) =>
|
|
113
|
+
t.parcelas
|
|
114
|
+
.filter((p: any) => p.status === 'aberto' || p.status === 'vencido')
|
|
115
|
+
.map((p: any) => ({
|
|
116
|
+
id: `pagar-${t.id}-${p.id}`,
|
|
117
|
+
tipo: 'pagar' as const,
|
|
118
|
+
documento: t.documento,
|
|
119
|
+
pessoa: getPessoaById(t.fornecedorId)?.nome || '',
|
|
120
|
+
vencimento: p.vencimento,
|
|
121
|
+
valor: p.valor,
|
|
122
|
+
}))
|
|
123
|
+
),
|
|
124
|
+
...titulosReceber.flatMap((t: any) =>
|
|
125
|
+
t.parcelas
|
|
126
|
+
.filter((p: any) => p.status === 'aberto' || p.status === 'vencido')
|
|
127
|
+
.map((p: any) => ({
|
|
128
|
+
id: `receber-${t.id}-${p.id}`,
|
|
129
|
+
tipo: 'receber' as const,
|
|
130
|
+
documento: t.documento,
|
|
131
|
+
pessoa: getPessoaById(t.clienteId)?.nome || '',
|
|
132
|
+
vencimento: p.vencimento,
|
|
133
|
+
valor: p.valor,
|
|
134
|
+
}))
|
|
135
|
+
),
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
const [contaFilter, setContaFilter] = useState<string>('1');
|
|
139
|
+
const [selectedExtrato, setSelectedExtrato] = useState<string | null>(null);
|
|
140
|
+
const [selectedTitulo, setSelectedTitulo] = useState<string | null>(null);
|
|
141
|
+
|
|
142
|
+
const extratosFiltered = extratosPendentes.filter(
|
|
143
|
+
(e) => e.contaBancariaId === contaFilter
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const totalConciliado = extratos.filter(
|
|
147
|
+
(e) =>
|
|
148
|
+
e.contaBancariaId === contaFilter && e.statusConciliacao === 'conciliado'
|
|
149
|
+
).length;
|
|
150
|
+
const totalExtratoConta = extratos.filter(
|
|
151
|
+
(e) => e.contaBancariaId === contaFilter
|
|
152
|
+
).length;
|
|
153
|
+
const percentualConciliado =
|
|
154
|
+
totalExtratoConta > 0
|
|
155
|
+
? Math.round((totalConciliado / totalExtratoConta) * 100)
|
|
156
|
+
: 0;
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<Page>
|
|
160
|
+
<PageHeader
|
|
161
|
+
title={t('header.title')}
|
|
162
|
+
description={t('header.description')}
|
|
163
|
+
breadcrumbs={[
|
|
164
|
+
{ label: t('breadcrumbs.home'), href: '/' },
|
|
165
|
+
{ label: t('breadcrumbs.finance'), href: '/finance' },
|
|
166
|
+
{ label: t('breadcrumbs.current') },
|
|
167
|
+
]}
|
|
168
|
+
/>
|
|
169
|
+
|
|
170
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
171
|
+
<Select value={contaFilter} onValueChange={setContaFilter}>
|
|
172
|
+
<SelectTrigger className="w-[280px]">
|
|
173
|
+
<SelectValue placeholder={t('filters.selectAccount')} />
|
|
174
|
+
</SelectTrigger>
|
|
175
|
+
<SelectContent>
|
|
176
|
+
{contasBancarias.map((conta) => (
|
|
177
|
+
<SelectItem key={conta.id} value={conta.id}>
|
|
178
|
+
{conta.banco} - {conta.descricao}
|
|
179
|
+
</SelectItem>
|
|
180
|
+
))}
|
|
181
|
+
</SelectContent>
|
|
182
|
+
</Select>
|
|
183
|
+
<div className="flex gap-2">
|
|
184
|
+
<CriarAjusteDialog t={t} />
|
|
185
|
+
<Button disabled={!selectedExtrato || !selectedTitulo}>
|
|
186
|
+
<Link2 className="mr-2 h-4 w-4" />
|
|
187
|
+
{t('actions.reconcileSelected')}
|
|
188
|
+
</Button>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div className="grid gap-4 md:grid-cols-4">
|
|
193
|
+
<Card>
|
|
194
|
+
<CardHeader className="pb-2">
|
|
195
|
+
<CardTitle className="text-sm font-medium">
|
|
196
|
+
{t('cards.reconciled')}
|
|
197
|
+
</CardTitle>
|
|
198
|
+
</CardHeader>
|
|
199
|
+
<CardContent>
|
|
200
|
+
<div className="text-2xl font-bold">{percentualConciliado}%</div>
|
|
201
|
+
<Progress value={percentualConciliado} className="mt-2" />
|
|
202
|
+
</CardContent>
|
|
203
|
+
</Card>
|
|
204
|
+
<Card>
|
|
205
|
+
<CardHeader className="pb-2">
|
|
206
|
+
<CardTitle className="text-sm font-medium">
|
|
207
|
+
{t('cards.pending')}
|
|
208
|
+
</CardTitle>
|
|
209
|
+
</CardHeader>
|
|
210
|
+
<CardContent>
|
|
211
|
+
<div className="text-2xl font-bold">{extratosFiltered.length}</div>
|
|
212
|
+
<p className="text-xs text-muted-foreground">
|
|
213
|
+
{t('cards.transactions')}
|
|
214
|
+
</p>
|
|
215
|
+
</CardContent>
|
|
216
|
+
</Card>
|
|
217
|
+
<Card>
|
|
218
|
+
<CardHeader className="pb-2">
|
|
219
|
+
<CardTitle className="text-sm font-medium">
|
|
220
|
+
{t('cards.discrepancies')}
|
|
221
|
+
</CardTitle>
|
|
222
|
+
</CardHeader>
|
|
223
|
+
<CardContent>
|
|
224
|
+
<div className="text-2xl font-bold text-orange-600">0</div>
|
|
225
|
+
<p className="text-xs text-muted-foreground">
|
|
226
|
+
{t('cards.toResolve')}
|
|
227
|
+
</p>
|
|
228
|
+
</CardContent>
|
|
229
|
+
</Card>
|
|
230
|
+
<Card>
|
|
231
|
+
<CardHeader className="pb-2">
|
|
232
|
+
<CardTitle className="text-sm font-medium">
|
|
233
|
+
{t('cards.difference')}
|
|
234
|
+
</CardTitle>
|
|
235
|
+
</CardHeader>
|
|
236
|
+
<CardContent>
|
|
237
|
+
<div className="text-2xl font-bold">
|
|
238
|
+
<Money value={2230.5} />
|
|
239
|
+
</div>
|
|
240
|
+
<p className="text-xs text-muted-foreground">
|
|
241
|
+
{t('cards.notReconciled')}
|
|
242
|
+
</p>
|
|
243
|
+
</CardContent>
|
|
244
|
+
</Card>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div className="grid gap-6 lg:grid-cols-2">
|
|
248
|
+
<Card>
|
|
249
|
+
<CardHeader>
|
|
250
|
+
<CardTitle className="flex items-center gap-2">
|
|
251
|
+
<RefreshCw className="h-4 w-4" />
|
|
252
|
+
{t('statement.title')}
|
|
253
|
+
</CardTitle>
|
|
254
|
+
<CardDescription>{t('statement.description')}</CardDescription>
|
|
255
|
+
</CardHeader>
|
|
256
|
+
<CardContent>
|
|
257
|
+
<div className="space-y-2">
|
|
258
|
+
{extratosFiltered.length > 0 ? (
|
|
259
|
+
extratosFiltered.map((extrato) => (
|
|
260
|
+
<div
|
|
261
|
+
key={extrato.id}
|
|
262
|
+
className={`flex items-center gap-3 rounded-lg border p-3 cursor-pointer transition-colors ${
|
|
263
|
+
selectedExtrato === extrato.id
|
|
264
|
+
? 'border-primary bg-primary/5'
|
|
265
|
+
: 'hover:bg-muted/50'
|
|
266
|
+
}`}
|
|
267
|
+
onClick={() =>
|
|
268
|
+
setSelectedExtrato(
|
|
269
|
+
selectedExtrato === extrato.id ? null : extrato.id
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
>
|
|
273
|
+
<Checkbox
|
|
274
|
+
checked={selectedExtrato === extrato.id}
|
|
275
|
+
onCheckedChange={() =>
|
|
276
|
+
setSelectedExtrato(
|
|
277
|
+
selectedExtrato === extrato.id ? null : extrato.id
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
/>
|
|
281
|
+
<div className="flex-1">
|
|
282
|
+
<div className="flex items-center justify-between">
|
|
283
|
+
<span className="text-sm font-medium">
|
|
284
|
+
{extrato.descricao}
|
|
285
|
+
</span>
|
|
286
|
+
<span
|
|
287
|
+
className={`font-semibold ${
|
|
288
|
+
extrato.tipo === 'entrada'
|
|
289
|
+
? 'text-green-600'
|
|
290
|
+
: 'text-red-600'
|
|
291
|
+
}`}
|
|
292
|
+
>
|
|
293
|
+
{extrato.tipo === 'saida' && '-'}
|
|
294
|
+
{formatarMoeda(extrato.valor)}
|
|
295
|
+
</span>
|
|
296
|
+
</div>
|
|
297
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
298
|
+
<span>{formatarData(extrato.data)}</span>
|
|
299
|
+
<StatusBadge
|
|
300
|
+
status={extrato.statusConciliacao}
|
|
301
|
+
type="conciliacao"
|
|
302
|
+
/>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
))
|
|
307
|
+
) : (
|
|
308
|
+
<div className="flex flex-col items-center justify-center py-8 text-center">
|
|
309
|
+
<Check className="h-12 w-12 text-green-500" />
|
|
310
|
+
<p className="mt-2 font-medium">
|
|
311
|
+
{t('statement.emptyTitle')}
|
|
312
|
+
</p>
|
|
313
|
+
<p className="text-sm text-muted-foreground">
|
|
314
|
+
{t('statement.emptyDescription')}
|
|
315
|
+
</p>
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
</CardContent>
|
|
320
|
+
</Card>
|
|
321
|
+
|
|
322
|
+
<Card>
|
|
323
|
+
<CardHeader>
|
|
324
|
+
<CardTitle className="flex items-center gap-2">
|
|
325
|
+
<DollarSign className="h-4 w-4" />
|
|
326
|
+
{t('openTitles.title')}
|
|
327
|
+
</CardTitle>
|
|
328
|
+
<CardDescription>{t('openTitles.description')}</CardDescription>
|
|
329
|
+
</CardHeader>
|
|
330
|
+
<CardContent>
|
|
331
|
+
<div className="space-y-2 max-h-[400px] overflow-y-auto">
|
|
332
|
+
{titulosAbertos.map((titulo) => (
|
|
333
|
+
<div
|
|
334
|
+
key={titulo.id}
|
|
335
|
+
className={`flex items-center gap-3 rounded-lg border p-3 cursor-pointer transition-colors ${
|
|
336
|
+
selectedTitulo === titulo.id
|
|
337
|
+
? 'border-primary bg-primary/5'
|
|
338
|
+
: 'hover:bg-muted/50'
|
|
339
|
+
}`}
|
|
340
|
+
onClick={() =>
|
|
341
|
+
setSelectedTitulo(
|
|
342
|
+
selectedTitulo === titulo.id ? null : titulo.id
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
>
|
|
346
|
+
<Checkbox
|
|
347
|
+
checked={selectedTitulo === titulo.id}
|
|
348
|
+
onCheckedChange={() =>
|
|
349
|
+
setSelectedTitulo(
|
|
350
|
+
selectedTitulo === titulo.id ? null : titulo.id
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
/>
|
|
354
|
+
<div className="flex-1">
|
|
355
|
+
<div className="flex items-center justify-between">
|
|
356
|
+
<div className="flex items-center gap-2">
|
|
357
|
+
<span className="text-sm font-medium">
|
|
358
|
+
{titulo.documento}
|
|
359
|
+
</span>
|
|
360
|
+
<Badge
|
|
361
|
+
variant="outline"
|
|
362
|
+
className={
|
|
363
|
+
titulo.tipo === 'pagar'
|
|
364
|
+
? 'border-red-200 text-red-700'
|
|
365
|
+
: 'border-green-200 text-green-700'
|
|
366
|
+
}
|
|
367
|
+
>
|
|
368
|
+
{titulo.tipo === 'pagar'
|
|
369
|
+
? t('openTitles.pay')
|
|
370
|
+
: t('openTitles.receive')}
|
|
371
|
+
</Badge>
|
|
372
|
+
</div>
|
|
373
|
+
<span className="font-semibold">
|
|
374
|
+
{formatarMoeda(titulo.valor)}
|
|
375
|
+
</span>
|
|
376
|
+
</div>
|
|
377
|
+
<div className="text-xs text-muted-foreground">
|
|
378
|
+
{t('openTitles.personDue', {
|
|
379
|
+
person: titulo.pessoa,
|
|
380
|
+
dueDate: formatarData(titulo.vencimento),
|
|
381
|
+
})}
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
))}
|
|
386
|
+
</div>
|
|
387
|
+
</CardContent>
|
|
388
|
+
</Card>
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
{selectedExtrato && selectedTitulo && (
|
|
392
|
+
<Card className="border-primary">
|
|
393
|
+
<CardHeader>
|
|
394
|
+
<CardTitle className="flex items-center gap-2 text-primary">
|
|
395
|
+
<Link2 className="h-4 w-4" />
|
|
396
|
+
{t('ready.title')}
|
|
397
|
+
</CardTitle>
|
|
398
|
+
</CardHeader>
|
|
399
|
+
<CardContent>
|
|
400
|
+
<div className="flex items-center justify-between">
|
|
401
|
+
<div>
|
|
402
|
+
<p className="text-sm text-muted-foreground">
|
|
403
|
+
{t('ready.description')}
|
|
404
|
+
</p>
|
|
405
|
+
</div>
|
|
406
|
+
<div className="flex gap-2">
|
|
407
|
+
<Button
|
|
408
|
+
variant="outline"
|
|
409
|
+
onClick={() => {
|
|
410
|
+
setSelectedExtrato(null);
|
|
411
|
+
setSelectedTitulo(null);
|
|
412
|
+
}}
|
|
413
|
+
>
|
|
414
|
+
{t('common.cancel')}
|
|
415
|
+
</Button>
|
|
416
|
+
<Button>
|
|
417
|
+
<Check className="mr-2 h-4 w-4" />
|
|
418
|
+
{t('actions.reconcile')}
|
|
419
|
+
</Button>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
</CardContent>
|
|
423
|
+
</Card>
|
|
424
|
+
)}
|
|
425
|
+
</Page>
|
|
426
|
+
);
|
|
427
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Page, PageHeader } from '@/components/entity-list';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardDescription,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
} from '@/components/ui/card';
|
|
12
|
+
import {
|
|
13
|
+
Dialog,
|
|
14
|
+
DialogContent,
|
|
15
|
+
DialogDescription,
|
|
16
|
+
DialogFooter,
|
|
17
|
+
DialogHeader,
|
|
18
|
+
DialogTitle,
|
|
19
|
+
DialogTrigger,
|
|
20
|
+
} from '@/components/ui/dialog';
|
|
21
|
+
import { FilterBar } from '@/components/ui/filter-bar';
|
|
22
|
+
import { Input } from '@/components/ui/input';
|
|
23
|
+
import { Label } from '@/components/ui/label';
|
|
24
|
+
import { Money } from '@/components/ui/money';
|
|
25
|
+
import {
|
|
26
|
+
Select,
|
|
27
|
+
SelectContent,
|
|
28
|
+
SelectItem,
|
|
29
|
+
SelectTrigger,
|
|
30
|
+
SelectValue,
|
|
31
|
+
} from '@/components/ui/select';
|
|
32
|
+
import { StatusBadge } from '@/components/ui/status-badge';
|
|
33
|
+
import {
|
|
34
|
+
Table,
|
|
35
|
+
TableBody,
|
|
36
|
+
TableCell,
|
|
37
|
+
TableHead,
|
|
38
|
+
TableHeader,
|
|
39
|
+
TableRow,
|
|
40
|
+
} from '@/components/ui/table';
|
|
41
|
+
import { ArrowDownRight, ArrowUpRight, Download, Upload } from 'lucide-react';
|
|
42
|
+
import { useTranslations } from 'next-intl';
|
|
43
|
+
import { useState } from 'react';
|
|
44
|
+
import { formatarData } from '../../_lib/formatters';
|
|
45
|
+
import { useFinanceData } from '../../_lib/use-finance-data';
|
|
46
|
+
|
|
47
|
+
function ImportarExtratoDialog({
|
|
48
|
+
contasBancarias,
|
|
49
|
+
t,
|
|
50
|
+
}: {
|
|
51
|
+
contasBancarias: any[];
|
|
52
|
+
t: ReturnType<typeof useTranslations>;
|
|
53
|
+
}) {
|
|
54
|
+
return (
|
|
55
|
+
<Dialog>
|
|
56
|
+
<DialogTrigger asChild>
|
|
57
|
+
<Button>
|
|
58
|
+
<Upload className="mr-2 h-4 w-4" />
|
|
59
|
+
{t('importDialog.action')}
|
|
60
|
+
</Button>
|
|
61
|
+
</DialogTrigger>
|
|
62
|
+
<DialogContent>
|
|
63
|
+
<DialogHeader>
|
|
64
|
+
<DialogTitle>{t('importDialog.title')}</DialogTitle>
|
|
65
|
+
<DialogDescription>{t('importDialog.description')}</DialogDescription>
|
|
66
|
+
</DialogHeader>
|
|
67
|
+
<div className="space-y-4">
|
|
68
|
+
<div className="space-y-2">
|
|
69
|
+
<Label htmlFor="conta">{t('importDialog.bankAccount')}</Label>
|
|
70
|
+
<Select>
|
|
71
|
+
<SelectTrigger>
|
|
72
|
+
<SelectValue placeholder={t('importDialog.selectAccount')} />
|
|
73
|
+
</SelectTrigger>
|
|
74
|
+
<SelectContent>
|
|
75
|
+
{contasBancarias.map((conta: any) => (
|
|
76
|
+
<SelectItem key={conta.id} value={conta.id}>
|
|
77
|
+
{conta.banco} - {conta.descricao}
|
|
78
|
+
</SelectItem>
|
|
79
|
+
))}
|
|
80
|
+
</SelectContent>
|
|
81
|
+
</Select>
|
|
82
|
+
</div>
|
|
83
|
+
<div className="space-y-2">
|
|
84
|
+
<Label htmlFor="arquivo">{t('importDialog.file')}</Label>
|
|
85
|
+
<Input id="arquivo" type="file" accept=".ofx,.csv" />
|
|
86
|
+
<p className="text-xs text-muted-foreground">
|
|
87
|
+
{t('importDialog.acceptedFormats')}
|
|
88
|
+
</p>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<DialogFooter>
|
|
92
|
+
<Button variant="outline">{t('common.cancel')}</Button>
|
|
93
|
+
<Button>{t('importDialog.submit')}</Button>
|
|
94
|
+
</DialogFooter>
|
|
95
|
+
</DialogContent>
|
|
96
|
+
</Dialog>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default function ExtratosPage() {
|
|
101
|
+
const t = useTranslations('finance.StatementsPage');
|
|
102
|
+
const { data } = useFinanceData();
|
|
103
|
+
const { extratos, contasBancarias } = data;
|
|
104
|
+
|
|
105
|
+
const [contaFilter, setContaFilter] = useState<string>('1');
|
|
106
|
+
const [search, setSearch] = useState('');
|
|
107
|
+
|
|
108
|
+
const filteredExtratos = extratos.filter((extrato) => {
|
|
109
|
+
const matchesConta = extrato.contaBancariaId === contaFilter;
|
|
110
|
+
const matchesSearch = extrato.descricao
|
|
111
|
+
.toLowerCase()
|
|
112
|
+
.includes(search.toLowerCase());
|
|
113
|
+
return matchesConta && matchesSearch;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const conta = contasBancarias.find((item) => item.id === contaFilter);
|
|
117
|
+
const totalEntradas = filteredExtratos
|
|
118
|
+
.filter((e) => e.tipo === 'entrada')
|
|
119
|
+
.reduce((acc, e) => acc + e.valor, 0);
|
|
120
|
+
const totalSaidas = filteredExtratos
|
|
121
|
+
.filter((e) => e.tipo === 'saida')
|
|
122
|
+
.reduce((acc, e) => acc + e.valor, 0);
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<Page>
|
|
126
|
+
<PageHeader
|
|
127
|
+
title={t('header.title')}
|
|
128
|
+
description={t('header.description')}
|
|
129
|
+
breadcrumbs={[
|
|
130
|
+
{ label: t('breadcrumbs.home'), href: '/' },
|
|
131
|
+
{ label: t('breadcrumbs.finance'), href: '/finance' },
|
|
132
|
+
{ label: t('breadcrumbs.current') },
|
|
133
|
+
]}
|
|
134
|
+
actions={
|
|
135
|
+
<div className="flex gap-2">
|
|
136
|
+
<Button variant="outline">
|
|
137
|
+
<Download className="mr-2 h-4 w-4" />
|
|
138
|
+
{t('actions.export')}
|
|
139
|
+
</Button>
|
|
140
|
+
<ImportarExtratoDialog contasBancarias={contasBancarias} t={t} />
|
|
141
|
+
</div>
|
|
142
|
+
}
|
|
143
|
+
/>
|
|
144
|
+
|
|
145
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
|
146
|
+
<Select value={contaFilter} onValueChange={setContaFilter}>
|
|
147
|
+
<SelectTrigger className="w-[280px]">
|
|
148
|
+
<SelectValue placeholder={t('filters.selectAccount')} />
|
|
149
|
+
</SelectTrigger>
|
|
150
|
+
<SelectContent>
|
|
151
|
+
{contasBancarias.map((conta) => (
|
|
152
|
+
<SelectItem key={conta.id} value={conta.id}>
|
|
153
|
+
{conta.banco} - {conta.descricao}
|
|
154
|
+
</SelectItem>
|
|
155
|
+
))}
|
|
156
|
+
</SelectContent>
|
|
157
|
+
</Select>
|
|
158
|
+
<div className="flex-1">
|
|
159
|
+
<FilterBar
|
|
160
|
+
searchPlaceholder={t('filters.searchPlaceholder')}
|
|
161
|
+
searchValue={search}
|
|
162
|
+
onSearchChange={setSearch}
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div className="grid gap-4 md:grid-cols-3">
|
|
168
|
+
<Card>
|
|
169
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
170
|
+
<CardTitle className="text-sm font-medium">
|
|
171
|
+
{t('cards.inflows')}
|
|
172
|
+
</CardTitle>
|
|
173
|
+
<ArrowUpRight className="h-4 w-4 text-green-500" />
|
|
174
|
+
</CardHeader>
|
|
175
|
+
<CardContent>
|
|
176
|
+
<div className="text-2xl font-bold text-green-600">
|
|
177
|
+
<Money value={totalEntradas} />
|
|
178
|
+
</div>
|
|
179
|
+
</CardContent>
|
|
180
|
+
</Card>
|
|
181
|
+
<Card>
|
|
182
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
183
|
+
<CardTitle className="text-sm font-medium">
|
|
184
|
+
{t('cards.outflows')}
|
|
185
|
+
</CardTitle>
|
|
186
|
+
<ArrowDownRight className="h-4 w-4 text-red-500" />
|
|
187
|
+
</CardHeader>
|
|
188
|
+
<CardContent>
|
|
189
|
+
<div className="text-2xl font-bold text-red-600">
|
|
190
|
+
<Money value={totalSaidas} />
|
|
191
|
+
</div>
|
|
192
|
+
</CardContent>
|
|
193
|
+
</Card>
|
|
194
|
+
<Card>
|
|
195
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
196
|
+
<CardTitle className="text-sm font-medium">
|
|
197
|
+
{t('cards.accountBalance')}
|
|
198
|
+
</CardTitle>
|
|
199
|
+
</CardHeader>
|
|
200
|
+
<CardContent>
|
|
201
|
+
<div className="text-2xl font-bold">
|
|
202
|
+
<Money value={conta?.saldoAtual || 0} />
|
|
203
|
+
</div>
|
|
204
|
+
<p className="text-xs text-muted-foreground">{conta?.descricao}</p>
|
|
205
|
+
</CardContent>
|
|
206
|
+
</Card>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<Card>
|
|
210
|
+
<CardHeader>
|
|
211
|
+
<CardTitle>{t('table.title')}</CardTitle>
|
|
212
|
+
<CardDescription>
|
|
213
|
+
{t('table.foundTransactions', { count: filteredExtratos.length })}
|
|
214
|
+
</CardDescription>
|
|
215
|
+
</CardHeader>
|
|
216
|
+
<CardContent>
|
|
217
|
+
<Table>
|
|
218
|
+
<TableHeader>
|
|
219
|
+
<TableRow>
|
|
220
|
+
<TableHead>{t('table.headers.date')}</TableHead>
|
|
221
|
+
<TableHead>{t('table.headers.description')}</TableHead>
|
|
222
|
+
<TableHead className="text-right">
|
|
223
|
+
{t('table.headers.value')}
|
|
224
|
+
</TableHead>
|
|
225
|
+
<TableHead>{t('table.headers.type')}</TableHead>
|
|
226
|
+
<TableHead>{t('table.headers.reconciliation')}</TableHead>
|
|
227
|
+
</TableRow>
|
|
228
|
+
</TableHeader>
|
|
229
|
+
<TableBody>
|
|
230
|
+
{filteredExtratos.map((extrato) => (
|
|
231
|
+
<TableRow key={extrato.id}>
|
|
232
|
+
<TableCell>{formatarData(extrato.data)}</TableCell>
|
|
233
|
+
<TableCell>{extrato.descricao}</TableCell>
|
|
234
|
+
<TableCell className="text-right">
|
|
235
|
+
<span
|
|
236
|
+
className={
|
|
237
|
+
extrato.tipo === 'entrada'
|
|
238
|
+
? 'text-green-600'
|
|
239
|
+
: 'text-red-600'
|
|
240
|
+
}
|
|
241
|
+
>
|
|
242
|
+
{extrato.tipo === 'saida' && '-'}
|
|
243
|
+
<Money value={extrato.valor} />
|
|
244
|
+
</span>
|
|
245
|
+
</TableCell>
|
|
246
|
+
<TableCell>
|
|
247
|
+
{extrato.tipo === 'entrada' ? (
|
|
248
|
+
<span className="flex items-center gap-1 text-green-600">
|
|
249
|
+
<ArrowUpRight className="h-4 w-4" />
|
|
250
|
+
{t('types.inflow')}
|
|
251
|
+
</span>
|
|
252
|
+
) : (
|
|
253
|
+
<span className="flex items-center gap-1 text-red-600">
|
|
254
|
+
<ArrowDownRight className="h-4 w-4" />
|
|
255
|
+
{t('types.outflow')}
|
|
256
|
+
</span>
|
|
257
|
+
)}
|
|
258
|
+
</TableCell>
|
|
259
|
+
<TableCell>
|
|
260
|
+
<StatusBadge
|
|
261
|
+
status={extrato.statusConciliacao}
|
|
262
|
+
type="conciliacao"
|
|
263
|
+
/>
|
|
264
|
+
</TableCell>
|
|
265
|
+
</TableRow>
|
|
266
|
+
))}
|
|
267
|
+
</TableBody>
|
|
268
|
+
</Table>
|
|
269
|
+
</CardContent>
|
|
270
|
+
</Card>
|
|
271
|
+
</Page>
|
|
272
|
+
);
|
|
273
|
+
}
|