@hed-hog/finance 0.0.3 → 0.0.224
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/hedhog/data/menu.yaml +18 -23
- 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 +265 -0
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +388 -0
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +364 -0
- package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +388 -0
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +385 -0
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +364 -0
- package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +274 -0
- package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +401 -0
- package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +266 -0
- package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +244 -0
- package/hedhog/frontend/app/page.tsx.ejs +313 -15
- package/hedhog/frontend/app/planning/cash-flow-forecast/page.tsx.ejs +285 -0
- package/hedhog/frontend/app/planning/receivables-calendar/page.tsx.ejs +195 -0
- package/hedhog/frontend/app/planning/scenarios/page.tsx.ejs +321 -0
- package/hedhog/query/constraints.sql +169 -0
- package/package.json +4 -4
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Badge } from '@/components/ui/badge';
|
|
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 { Checkbox } from '@/components/ui/checkbox';
|
|
13
|
+
import {
|
|
14
|
+
Dialog,
|
|
15
|
+
DialogContent,
|
|
16
|
+
DialogDescription,
|
|
17
|
+
DialogFooter,
|
|
18
|
+
DialogHeader,
|
|
19
|
+
DialogTitle,
|
|
20
|
+
DialogTrigger,
|
|
21
|
+
} from '@/components/ui/dialog';
|
|
22
|
+
import { Input } from '@/components/ui/input';
|
|
23
|
+
import { Label } from '@/components/ui/label';
|
|
24
|
+
import { Money } from '@/components/ui/money';
|
|
25
|
+
import { PageHeader } from '@/components/ui/page-header';
|
|
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 { useState } from 'react';
|
|
37
|
+
import { formatarData, formatarMoeda } from '../../_lib/formatters';
|
|
38
|
+
import { useFinanceData } from '../../_lib/use-finance-data';
|
|
39
|
+
|
|
40
|
+
function CriarAjusteDialog() {
|
|
41
|
+
return (
|
|
42
|
+
<Dialog>
|
|
43
|
+
<DialogTrigger asChild>
|
|
44
|
+
<Button variant="outline" size="sm">
|
|
45
|
+
<DollarSign className="mr-2 h-4 w-4" />
|
|
46
|
+
Criar Ajuste
|
|
47
|
+
</Button>
|
|
48
|
+
</DialogTrigger>
|
|
49
|
+
<DialogContent>
|
|
50
|
+
<DialogHeader>
|
|
51
|
+
<DialogTitle>Criar Ajuste</DialogTitle>
|
|
52
|
+
<DialogDescription>
|
|
53
|
+
Registre um ajuste para tarifas ou diferenças.
|
|
54
|
+
</DialogDescription>
|
|
55
|
+
</DialogHeader>
|
|
56
|
+
<div className="space-y-4">
|
|
57
|
+
<div className="space-y-2">
|
|
58
|
+
<Label htmlFor="tipo">Tipo de Ajuste</Label>
|
|
59
|
+
<Select>
|
|
60
|
+
<SelectTrigger>
|
|
61
|
+
<SelectValue placeholder="Selecione..." />
|
|
62
|
+
</SelectTrigger>
|
|
63
|
+
<SelectContent>
|
|
64
|
+
<SelectItem value="tarifa">Tarifa Bancária</SelectItem>
|
|
65
|
+
<SelectItem value="juros">Juros</SelectItem>
|
|
66
|
+
<SelectItem value="diferenca">Diferença de Valor</SelectItem>
|
|
67
|
+
<SelectItem value="outro">Outro</SelectItem>
|
|
68
|
+
</SelectContent>
|
|
69
|
+
</Select>
|
|
70
|
+
</div>
|
|
71
|
+
<div className="space-y-2">
|
|
72
|
+
<Label htmlFor="valor">Valor</Label>
|
|
73
|
+
<Input id="valor" type="number" placeholder="0,00" />
|
|
74
|
+
</div>
|
|
75
|
+
<div className="space-y-2">
|
|
76
|
+
<Label htmlFor="descricao">Descrição</Label>
|
|
77
|
+
<Input id="descricao" placeholder="Descrição do ajuste..." />
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<DialogFooter>
|
|
81
|
+
<Button variant="outline">Cancelar</Button>
|
|
82
|
+
<Button>Criar Ajuste</Button>
|
|
83
|
+
</DialogFooter>
|
|
84
|
+
</DialogContent>
|
|
85
|
+
</Dialog>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default function ConciliacaoPage() {
|
|
90
|
+
const { data } = useFinanceData();
|
|
91
|
+
const { extratos, titulosPagar, titulosReceber, contasBancarias, pessoas } =
|
|
92
|
+
data;
|
|
93
|
+
|
|
94
|
+
const getPessoaById = (id?: string) => pessoas.find((p) => p.id === id);
|
|
95
|
+
|
|
96
|
+
const extratosPendentes = extratos.filter(
|
|
97
|
+
(e) => e.statusConciliacao === 'pendente'
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const titulosAbertos = [
|
|
101
|
+
...titulosPagar.flatMap((t: any) =>
|
|
102
|
+
t.parcelas
|
|
103
|
+
.filter((p: any) => p.status === 'aberto' || p.status === 'vencido')
|
|
104
|
+
.map((p: any) => ({
|
|
105
|
+
id: `pagar-${t.id}-${p.id}`,
|
|
106
|
+
tipo: 'pagar' as const,
|
|
107
|
+
documento: t.documento,
|
|
108
|
+
pessoa: getPessoaById(t.fornecedorId)?.nome || '',
|
|
109
|
+
vencimento: p.vencimento,
|
|
110
|
+
valor: p.valor,
|
|
111
|
+
}))
|
|
112
|
+
),
|
|
113
|
+
...titulosReceber.flatMap((t: any) =>
|
|
114
|
+
t.parcelas
|
|
115
|
+
.filter((p: any) => p.status === 'aberto' || p.status === 'vencido')
|
|
116
|
+
.map((p: any) => ({
|
|
117
|
+
id: `receber-${t.id}-${p.id}`,
|
|
118
|
+
tipo: 'receber' as const,
|
|
119
|
+
documento: t.documento,
|
|
120
|
+
pessoa: getPessoaById(t.clienteId)?.nome || '',
|
|
121
|
+
vencimento: p.vencimento,
|
|
122
|
+
valor: p.valor,
|
|
123
|
+
}))
|
|
124
|
+
),
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
const [contaFilter, setContaFilter] = useState<string>('1');
|
|
128
|
+
const [selectedExtrato, setSelectedExtrato] = useState<string | null>(null);
|
|
129
|
+
const [selectedTitulo, setSelectedTitulo] = useState<string | null>(null);
|
|
130
|
+
|
|
131
|
+
const extratosFiltered = extratosPendentes.filter(
|
|
132
|
+
(e) => e.contaBancariaId === contaFilter
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const totalConciliado = extratos.filter(
|
|
136
|
+
(e) =>
|
|
137
|
+
e.contaBancariaId === contaFilter && e.statusConciliacao === 'conciliado'
|
|
138
|
+
).length;
|
|
139
|
+
const totalExtratoConta = extratos.filter(
|
|
140
|
+
(e) => e.contaBancariaId === contaFilter
|
|
141
|
+
).length;
|
|
142
|
+
const percentualConciliado =
|
|
143
|
+
totalExtratoConta > 0
|
|
144
|
+
? Math.round((totalConciliado / totalExtratoConta) * 100)
|
|
145
|
+
: 0;
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className="space-y-6">
|
|
149
|
+
<PageHeader
|
|
150
|
+
title="Conciliação Bancária"
|
|
151
|
+
description="Concilie transações do extrato com títulos"
|
|
152
|
+
breadcrumbs={[
|
|
153
|
+
{ label: 'Caixa e Bancos', href: '/finance/cash-and-banks/bank-accounts' },
|
|
154
|
+
{ label: 'Conciliação Bancária' },
|
|
155
|
+
]}
|
|
156
|
+
/>
|
|
157
|
+
|
|
158
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
159
|
+
<Select value={contaFilter} onValueChange={setContaFilter}>
|
|
160
|
+
<SelectTrigger className="w-[280px]">
|
|
161
|
+
<SelectValue placeholder="Selecione a conta..." />
|
|
162
|
+
</SelectTrigger>
|
|
163
|
+
<SelectContent>
|
|
164
|
+
{contasBancarias.map((conta) => (
|
|
165
|
+
<SelectItem key={conta.id} value={conta.id}>
|
|
166
|
+
{conta.banco} - {conta.descricao}
|
|
167
|
+
</SelectItem>
|
|
168
|
+
))}
|
|
169
|
+
</SelectContent>
|
|
170
|
+
</Select>
|
|
171
|
+
<div className="flex gap-2">
|
|
172
|
+
<CriarAjusteDialog />
|
|
173
|
+
<Button disabled={!selectedExtrato || !selectedTitulo}>
|
|
174
|
+
<Link2 className="mr-2 h-4 w-4" />
|
|
175
|
+
Conciliar Selecionados
|
|
176
|
+
</Button>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div className="grid gap-4 md:grid-cols-4">
|
|
181
|
+
<Card>
|
|
182
|
+
<CardHeader className="pb-2">
|
|
183
|
+
<CardTitle className="text-sm font-medium">Conciliado</CardTitle>
|
|
184
|
+
</CardHeader>
|
|
185
|
+
<CardContent>
|
|
186
|
+
<div className="text-2xl font-bold">{percentualConciliado}%</div>
|
|
187
|
+
<Progress value={percentualConciliado} className="mt-2" />
|
|
188
|
+
</CardContent>
|
|
189
|
+
</Card>
|
|
190
|
+
<Card>
|
|
191
|
+
<CardHeader className="pb-2">
|
|
192
|
+
<CardTitle className="text-sm font-medium">Pendentes</CardTitle>
|
|
193
|
+
</CardHeader>
|
|
194
|
+
<CardContent>
|
|
195
|
+
<div className="text-2xl font-bold">{extratosFiltered.length}</div>
|
|
196
|
+
<p className="text-xs text-muted-foreground">transações</p>
|
|
197
|
+
</CardContent>
|
|
198
|
+
</Card>
|
|
199
|
+
<Card>
|
|
200
|
+
<CardHeader className="pb-2">
|
|
201
|
+
<CardTitle className="text-sm font-medium">Divergências</CardTitle>
|
|
202
|
+
</CardHeader>
|
|
203
|
+
<CardContent>
|
|
204
|
+
<div className="text-2xl font-bold text-orange-600">0</div>
|
|
205
|
+
<p className="text-xs text-muted-foreground">a resolver</p>
|
|
206
|
+
</CardContent>
|
|
207
|
+
</Card>
|
|
208
|
+
<Card>
|
|
209
|
+
<CardHeader className="pb-2">
|
|
210
|
+
<CardTitle className="text-sm font-medium">Diferença</CardTitle>
|
|
211
|
+
</CardHeader>
|
|
212
|
+
<CardContent>
|
|
213
|
+
<div className="text-2xl font-bold">
|
|
214
|
+
<Money value={2230.5} />
|
|
215
|
+
</div>
|
|
216
|
+
<p className="text-xs text-muted-foreground">não conciliado</p>
|
|
217
|
+
</CardContent>
|
|
218
|
+
</Card>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<div className="grid gap-6 lg:grid-cols-2">
|
|
222
|
+
{/* Coluna Esquerda - Extrato */}
|
|
223
|
+
<Card>
|
|
224
|
+
<CardHeader>
|
|
225
|
+
<CardTitle className="flex items-center gap-2">
|
|
226
|
+
<RefreshCw className="h-4 w-4" />
|
|
227
|
+
Transações do Extrato
|
|
228
|
+
</CardTitle>
|
|
229
|
+
<CardDescription>
|
|
230
|
+
Selecione uma transação para conciliar
|
|
231
|
+
</CardDescription>
|
|
232
|
+
</CardHeader>
|
|
233
|
+
<CardContent>
|
|
234
|
+
<div className="space-y-2">
|
|
235
|
+
{extratosFiltered.length > 0 ? (
|
|
236
|
+
extratosFiltered.map((extrato) => (
|
|
237
|
+
<div
|
|
238
|
+
key={extrato.id}
|
|
239
|
+
className={`flex items-center gap-3 rounded-lg border p-3 cursor-pointer transition-colors ${
|
|
240
|
+
selectedExtrato === extrato.id
|
|
241
|
+
? 'border-primary bg-primary/5'
|
|
242
|
+
: 'hover:bg-muted/50'
|
|
243
|
+
}`}
|
|
244
|
+
onClick={() =>
|
|
245
|
+
setSelectedExtrato(
|
|
246
|
+
selectedExtrato === extrato.id ? null : extrato.id
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
>
|
|
250
|
+
<Checkbox
|
|
251
|
+
checked={selectedExtrato === extrato.id}
|
|
252
|
+
onCheckedChange={() =>
|
|
253
|
+
setSelectedExtrato(
|
|
254
|
+
selectedExtrato === extrato.id ? null : extrato.id
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
/>
|
|
258
|
+
<div className="flex-1">
|
|
259
|
+
<div className="flex items-center justify-between">
|
|
260
|
+
<span className="text-sm font-medium">
|
|
261
|
+
{extrato.descricao}
|
|
262
|
+
</span>
|
|
263
|
+
<span
|
|
264
|
+
className={`font-semibold ${
|
|
265
|
+
extrato.tipo === 'entrada'
|
|
266
|
+
? 'text-green-600'
|
|
267
|
+
: 'text-red-600'
|
|
268
|
+
}`}
|
|
269
|
+
>
|
|
270
|
+
{extrato.tipo === 'saida' && '-'}
|
|
271
|
+
{formatarMoeda(extrato.valor)}
|
|
272
|
+
</span>
|
|
273
|
+
</div>
|
|
274
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
275
|
+
<span>{formatarData(extrato.data)}</span>
|
|
276
|
+
<StatusBadge
|
|
277
|
+
status={extrato.statusConciliacao}
|
|
278
|
+
type="conciliacao"
|
|
279
|
+
/>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
))
|
|
284
|
+
) : (
|
|
285
|
+
<div className="flex flex-col items-center justify-center py-8 text-center">
|
|
286
|
+
<Check className="h-12 w-12 text-green-500" />
|
|
287
|
+
<p className="mt-2 font-medium">Tudo conciliado!</p>
|
|
288
|
+
<p className="text-sm text-muted-foreground">
|
|
289
|
+
Não há transações pendentes.
|
|
290
|
+
</p>
|
|
291
|
+
</div>
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
</CardContent>
|
|
295
|
+
</Card>
|
|
296
|
+
|
|
297
|
+
{/* Coluna Direita - Títulos */}
|
|
298
|
+
<Card>
|
|
299
|
+
<CardHeader>
|
|
300
|
+
<CardTitle className="flex items-center gap-2">
|
|
301
|
+
<DollarSign className="h-4 w-4" />
|
|
302
|
+
Títulos em Aberto
|
|
303
|
+
</CardTitle>
|
|
304
|
+
<CardDescription>Selecione um título para vincular</CardDescription>
|
|
305
|
+
</CardHeader>
|
|
306
|
+
<CardContent>
|
|
307
|
+
<div className="space-y-2 max-h-[400px] overflow-y-auto">
|
|
308
|
+
{titulosAbertos.map((titulo) => (
|
|
309
|
+
<div
|
|
310
|
+
key={titulo.id}
|
|
311
|
+
className={`flex items-center gap-3 rounded-lg border p-3 cursor-pointer transition-colors ${
|
|
312
|
+
selectedTitulo === titulo.id
|
|
313
|
+
? 'border-primary bg-primary/5'
|
|
314
|
+
: 'hover:bg-muted/50'
|
|
315
|
+
}`}
|
|
316
|
+
onClick={() =>
|
|
317
|
+
setSelectedTitulo(
|
|
318
|
+
selectedTitulo === titulo.id ? null : titulo.id
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
>
|
|
322
|
+
<Checkbox
|
|
323
|
+
checked={selectedTitulo === titulo.id}
|
|
324
|
+
onCheckedChange={() =>
|
|
325
|
+
setSelectedTitulo(
|
|
326
|
+
selectedTitulo === titulo.id ? null : titulo.id
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
/>
|
|
330
|
+
<div className="flex-1">
|
|
331
|
+
<div className="flex items-center justify-between">
|
|
332
|
+
<div className="flex items-center gap-2">
|
|
333
|
+
<span className="text-sm font-medium">
|
|
334
|
+
{titulo.documento}
|
|
335
|
+
</span>
|
|
336
|
+
<Badge
|
|
337
|
+
variant="outline"
|
|
338
|
+
className={
|
|
339
|
+
titulo.tipo === 'pagar'
|
|
340
|
+
? 'border-red-200 text-red-700'
|
|
341
|
+
: 'border-green-200 text-green-700'
|
|
342
|
+
}
|
|
343
|
+
>
|
|
344
|
+
{titulo.tipo === 'pagar' ? 'Pagar' : 'Receber'}
|
|
345
|
+
</Badge>
|
|
346
|
+
</div>
|
|
347
|
+
<span className="font-semibold">
|
|
348
|
+
{formatarMoeda(titulo.valor)}
|
|
349
|
+
</span>
|
|
350
|
+
</div>
|
|
351
|
+
<div className="text-xs text-muted-foreground">
|
|
352
|
+
{titulo.pessoa} | Venc: {formatarData(titulo.vencimento)}
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
))}
|
|
357
|
+
</div>
|
|
358
|
+
</CardContent>
|
|
359
|
+
</Card>
|
|
360
|
+
</div>
|
|
361
|
+
|
|
362
|
+
{selectedExtrato && selectedTitulo && (
|
|
363
|
+
<Card className="border-primary">
|
|
364
|
+
<CardHeader>
|
|
365
|
+
<CardTitle className="flex items-center gap-2 text-primary">
|
|
366
|
+
<Link2 className="h-4 w-4" />
|
|
367
|
+
Pronto para Conciliar
|
|
368
|
+
</CardTitle>
|
|
369
|
+
</CardHeader>
|
|
370
|
+
<CardContent>
|
|
371
|
+
<div className="flex items-center justify-between">
|
|
372
|
+
<div>
|
|
373
|
+
<p className="text-sm text-muted-foreground">
|
|
374
|
+
Transação e título selecionados. Clique em "Conciliar" para
|
|
375
|
+
vincular.
|
|
376
|
+
</p>
|
|
377
|
+
</div>
|
|
378
|
+
<div className="flex gap-2">
|
|
379
|
+
<Button
|
|
380
|
+
variant="outline"
|
|
381
|
+
onClick={() => {
|
|
382
|
+
setSelectedExtrato(null);
|
|
383
|
+
setSelectedTitulo(null);
|
|
384
|
+
}}
|
|
385
|
+
>
|
|
386
|
+
Cancelar
|
|
387
|
+
</Button>
|
|
388
|
+
<Button>
|
|
389
|
+
<Check className="mr-2 h-4 w-4" />
|
|
390
|
+
Conciliar
|
|
391
|
+
</Button>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
</CardContent>
|
|
395
|
+
</Card>
|
|
396
|
+
)}
|
|
397
|
+
</div>
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
} from '@/components/ui/card';
|
|
11
|
+
import {
|
|
12
|
+
Dialog,
|
|
13
|
+
DialogContent,
|
|
14
|
+
DialogDescription,
|
|
15
|
+
DialogFooter,
|
|
16
|
+
DialogHeader,
|
|
17
|
+
DialogTitle,
|
|
18
|
+
DialogTrigger,
|
|
19
|
+
} from '@/components/ui/dialog';
|
|
20
|
+
import { FilterBar } from '@/components/ui/filter-bar';
|
|
21
|
+
import { Input } from '@/components/ui/input';
|
|
22
|
+
import { Label } from '@/components/ui/label';
|
|
23
|
+
import { Money } from '@/components/ui/money';
|
|
24
|
+
import { PageHeader } from '@/components/ui/page-header';
|
|
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 { useState } from 'react';
|
|
43
|
+
import { formatarData } from '../../_lib/formatters';
|
|
44
|
+
import { useFinanceData } from '../../_lib/use-finance-data';
|
|
45
|
+
|
|
46
|
+
function ImportarExtratoDialog({
|
|
47
|
+
contasBancarias,
|
|
48
|
+
}: {
|
|
49
|
+
contasBancarias: any[];
|
|
50
|
+
}) {
|
|
51
|
+
return (
|
|
52
|
+
<Dialog>
|
|
53
|
+
<DialogTrigger asChild>
|
|
54
|
+
<Button>
|
|
55
|
+
<Upload className="mr-2 h-4 w-4" />
|
|
56
|
+
Importar
|
|
57
|
+
</Button>
|
|
58
|
+
</DialogTrigger>
|
|
59
|
+
<DialogContent>
|
|
60
|
+
<DialogHeader>
|
|
61
|
+
<DialogTitle>Importar Extrato</DialogTitle>
|
|
62
|
+
<DialogDescription>
|
|
63
|
+
Selecione um arquivo OFX ou CSV para importar.
|
|
64
|
+
</DialogDescription>
|
|
65
|
+
</DialogHeader>
|
|
66
|
+
<div className="space-y-4">
|
|
67
|
+
<div className="space-y-2">
|
|
68
|
+
<Label htmlFor="conta">Conta Bancária</Label>
|
|
69
|
+
<Select>
|
|
70
|
+
<SelectTrigger>
|
|
71
|
+
<SelectValue placeholder="Selecione a conta..." />
|
|
72
|
+
</SelectTrigger>
|
|
73
|
+
<SelectContent>
|
|
74
|
+
{contasBancarias.map((conta: any) => (
|
|
75
|
+
<SelectItem key={conta.id} value={conta.id}>
|
|
76
|
+
{conta.banco} - {conta.descricao}
|
|
77
|
+
</SelectItem>
|
|
78
|
+
))}
|
|
79
|
+
</SelectContent>
|
|
80
|
+
</Select>
|
|
81
|
+
</div>
|
|
82
|
+
<div className="space-y-2">
|
|
83
|
+
<Label htmlFor="arquivo">Arquivo</Label>
|
|
84
|
+
<Input id="arquivo" type="file" accept=".ofx,.csv" />
|
|
85
|
+
<p className="text-xs text-muted-foreground">
|
|
86
|
+
Formatos aceitos: OFX, CSV
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<DialogFooter>
|
|
91
|
+
<Button variant="outline">Cancelar</Button>
|
|
92
|
+
<Button>Importar</Button>
|
|
93
|
+
</DialogFooter>
|
|
94
|
+
</DialogContent>
|
|
95
|
+
</Dialog>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default function ExtratosPage() {
|
|
100
|
+
const { data } = useFinanceData();
|
|
101
|
+
const { extratos, contasBancarias } = data;
|
|
102
|
+
|
|
103
|
+
const [contaFilter, setContaFilter] = useState<string>('1');
|
|
104
|
+
const [search, setSearch] = useState('');
|
|
105
|
+
|
|
106
|
+
const filteredExtratos = extratos.filter((extrato) => {
|
|
107
|
+
const matchesConta = extrato.contaBancariaId === contaFilter;
|
|
108
|
+
const matchesSearch = extrato.descricao
|
|
109
|
+
.toLowerCase()
|
|
110
|
+
.includes(search.toLowerCase());
|
|
111
|
+
return matchesConta && matchesSearch;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const conta = contasBancarias.find((item) => item.id === contaFilter);
|
|
115
|
+
const totalEntradas = filteredExtratos
|
|
116
|
+
.filter((e) => e.tipo === 'entrada')
|
|
117
|
+
.reduce((acc, e) => acc + e.valor, 0);
|
|
118
|
+
const totalSaidas = filteredExtratos
|
|
119
|
+
.filter((e) => e.tipo === 'saida')
|
|
120
|
+
.reduce((acc, e) => acc + e.valor, 0);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div className="space-y-6">
|
|
124
|
+
<PageHeader
|
|
125
|
+
title="Extratos"
|
|
126
|
+
description="Visualize e importe extratos bancários"
|
|
127
|
+
breadcrumbs={[
|
|
128
|
+
{ label: 'Caixa e Bancos', href: '/finance/cash-and-banks/bank-accounts' },
|
|
129
|
+
{ label: 'Extratos' },
|
|
130
|
+
]}
|
|
131
|
+
actions={
|
|
132
|
+
<div className="flex gap-2">
|
|
133
|
+
<Button variant="outline">
|
|
134
|
+
<Download className="mr-2 h-4 w-4" />
|
|
135
|
+
Exportar
|
|
136
|
+
</Button>
|
|
137
|
+
<ImportarExtratoDialog contasBancarias={contasBancarias} />
|
|
138
|
+
</div>
|
|
139
|
+
}
|
|
140
|
+
/>
|
|
141
|
+
|
|
142
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
|
143
|
+
<Select value={contaFilter} onValueChange={setContaFilter}>
|
|
144
|
+
<SelectTrigger className="w-[280px]">
|
|
145
|
+
<SelectValue placeholder="Selecione a conta..." />
|
|
146
|
+
</SelectTrigger>
|
|
147
|
+
<SelectContent>
|
|
148
|
+
{contasBancarias.map((conta) => (
|
|
149
|
+
<SelectItem key={conta.id} value={conta.id}>
|
|
150
|
+
{conta.banco} - {conta.descricao}
|
|
151
|
+
</SelectItem>
|
|
152
|
+
))}
|
|
153
|
+
</SelectContent>
|
|
154
|
+
</Select>
|
|
155
|
+
<div className="flex-1">
|
|
156
|
+
<FilterBar
|
|
157
|
+
searchPlaceholder="Buscar na descrição..."
|
|
158
|
+
searchValue={search}
|
|
159
|
+
onSearchChange={setSearch}
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div className="grid gap-4 md:grid-cols-3">
|
|
165
|
+
<Card>
|
|
166
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
167
|
+
<CardTitle className="text-sm font-medium">Entradas</CardTitle>
|
|
168
|
+
<ArrowUpRight className="h-4 w-4 text-green-500" />
|
|
169
|
+
</CardHeader>
|
|
170
|
+
<CardContent>
|
|
171
|
+
<div className="text-2xl font-bold text-green-600">
|
|
172
|
+
<Money value={totalEntradas} />
|
|
173
|
+
</div>
|
|
174
|
+
</CardContent>
|
|
175
|
+
</Card>
|
|
176
|
+
<Card>
|
|
177
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
178
|
+
<CardTitle className="text-sm font-medium">Saídas</CardTitle>
|
|
179
|
+
<ArrowDownRight className="h-4 w-4 text-red-500" />
|
|
180
|
+
</CardHeader>
|
|
181
|
+
<CardContent>
|
|
182
|
+
<div className="text-2xl font-bold text-red-600">
|
|
183
|
+
<Money value={totalSaidas} />
|
|
184
|
+
</div>
|
|
185
|
+
</CardContent>
|
|
186
|
+
</Card>
|
|
187
|
+
<Card>
|
|
188
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
189
|
+
<CardTitle className="text-sm font-medium">
|
|
190
|
+
Saldo da Conta
|
|
191
|
+
</CardTitle>
|
|
192
|
+
</CardHeader>
|
|
193
|
+
<CardContent>
|
|
194
|
+
<div className="text-2xl font-bold">
|
|
195
|
+
<Money value={conta?.saldoAtual || 0} />
|
|
196
|
+
</div>
|
|
197
|
+
<p className="text-xs text-muted-foreground">{conta?.descricao}</p>
|
|
198
|
+
</CardContent>
|
|
199
|
+
</Card>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<Card>
|
|
203
|
+
<CardHeader>
|
|
204
|
+
<CardTitle>Movimentações</CardTitle>
|
|
205
|
+
<CardDescription>
|
|
206
|
+
{filteredExtratos.length} transações encontradas
|
|
207
|
+
</CardDescription>
|
|
208
|
+
</CardHeader>
|
|
209
|
+
<CardContent>
|
|
210
|
+
<Table>
|
|
211
|
+
<TableHeader>
|
|
212
|
+
<TableRow>
|
|
213
|
+
<TableHead>Data</TableHead>
|
|
214
|
+
<TableHead>Descrição</TableHead>
|
|
215
|
+
<TableHead className="text-right">Valor</TableHead>
|
|
216
|
+
<TableHead>Tipo</TableHead>
|
|
217
|
+
<TableHead>Conciliação</TableHead>
|
|
218
|
+
</TableRow>
|
|
219
|
+
</TableHeader>
|
|
220
|
+
<TableBody>
|
|
221
|
+
{filteredExtratos.map((extrato) => (
|
|
222
|
+
<TableRow key={extrato.id}>
|
|
223
|
+
<TableCell>{formatarData(extrato.data)}</TableCell>
|
|
224
|
+
<TableCell>{extrato.descricao}</TableCell>
|
|
225
|
+
<TableCell className="text-right">
|
|
226
|
+
<span
|
|
227
|
+
className={
|
|
228
|
+
extrato.tipo === 'entrada'
|
|
229
|
+
? 'text-green-600'
|
|
230
|
+
: 'text-red-600'
|
|
231
|
+
}
|
|
232
|
+
>
|
|
233
|
+
{extrato.tipo === 'saida' && '-'}
|
|
234
|
+
<Money value={extrato.valor} />
|
|
235
|
+
</span>
|
|
236
|
+
</TableCell>
|
|
237
|
+
<TableCell>
|
|
238
|
+
{extrato.tipo === 'entrada' ? (
|
|
239
|
+
<span className="flex items-center gap-1 text-green-600">
|
|
240
|
+
<ArrowUpRight className="h-4 w-4" />
|
|
241
|
+
Entrada
|
|
242
|
+
</span>
|
|
243
|
+
) : (
|
|
244
|
+
<span className="flex items-center gap-1 text-red-600">
|
|
245
|
+
<ArrowDownRight className="h-4 w-4" />
|
|
246
|
+
Saída
|
|
247
|
+
</span>
|
|
248
|
+
)}
|
|
249
|
+
</TableCell>
|
|
250
|
+
<TableCell>
|
|
251
|
+
<StatusBadge
|
|
252
|
+
status={extrato.statusConciliacao}
|
|
253
|
+
type="conciliacao"
|
|
254
|
+
/>
|
|
255
|
+
</TableCell>
|
|
256
|
+
</TableRow>
|
|
257
|
+
))}
|
|
258
|
+
</TableBody>
|
|
259
|
+
</Table>
|
|
260
|
+
</CardContent>
|
|
261
|
+
</Card>
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|