@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.
@@ -0,0 +1,364 @@
1
+ 'use client';
2
+
3
+ import { Badge } from '@/components/ui/badge';
4
+ import { Button } from '@/components/ui/button';
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuSeparator,
10
+ DropdownMenuTrigger,
11
+ } from '@/components/ui/dropdown-menu';
12
+ import { FilterBar } from '@/components/ui/filter-bar';
13
+ import { Input } from '@/components/ui/input';
14
+ import { Label } from '@/components/ui/label';
15
+ import { Money } from '@/components/ui/money';
16
+ import { PageHeader } from '@/components/ui/page-header';
17
+ import {
18
+ Select,
19
+ SelectContent,
20
+ SelectItem,
21
+ SelectTrigger,
22
+ SelectValue,
23
+ } from '@/components/ui/select';
24
+ import {
25
+ Sheet,
26
+ SheetContent,
27
+ SheetDescription,
28
+ SheetHeader,
29
+ SheetTitle,
30
+ SheetTrigger,
31
+ } from '@/components/ui/sheet';
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 { Textarea } from '@/components/ui/textarea';
42
+ import {
43
+ Download,
44
+ Edit,
45
+ Eye,
46
+ MoreHorizontal,
47
+ Paperclip,
48
+ Plus,
49
+ Send,
50
+ } from 'lucide-react';
51
+ import Link from 'next/link';
52
+ import { useState } from 'react';
53
+ import { formatarData } from '../../_lib/formatters';
54
+ import { useFinanceData } from '../../_lib/use-finance-data';
55
+
56
+ function NovoTituloSheet({
57
+ pessoas,
58
+ categorias,
59
+ centrosCusto,
60
+ }: {
61
+ pessoas: any[];
62
+ categorias: any[];
63
+ centrosCusto: any[];
64
+ }) {
65
+ return (
66
+ <Sheet>
67
+ <SheetTrigger asChild>
68
+ <Button>
69
+ <Plus className="mr-2 h-4 w-4" />
70
+ Novo Título
71
+ </Button>
72
+ </SheetTrigger>
73
+ <SheetContent className="w-full sm:max-w-lg overflow-y-auto">
74
+ <SheetHeader>
75
+ <SheetTitle>Novo Título a Receber</SheetTitle>
76
+ <SheetDescription>
77
+ Preencha os dados para criar um novo título a receber.
78
+ </SheetDescription>
79
+ </SheetHeader>
80
+ <form className="mt-6 space-y-4">
81
+ <div className="grid gap-4">
82
+ <div className="space-y-2">
83
+ <Label htmlFor="documento">Documento</Label>
84
+ <Input id="documento" placeholder="FAT-00000" />
85
+ </div>
86
+ <div className="space-y-2">
87
+ <Label htmlFor="cliente">Cliente</Label>
88
+ <Select>
89
+ <SelectTrigger>
90
+ <SelectValue placeholder="Selecione..." />
91
+ </SelectTrigger>
92
+ <SelectContent>
93
+ {pessoas
94
+ .filter((p) => p.tipo === 'cliente' || p.tipo === 'ambos')
95
+ .map((p) => (
96
+ <SelectItem key={p.id} value={p.id}>
97
+ {p.nome}
98
+ </SelectItem>
99
+ ))}
100
+ </SelectContent>
101
+ </Select>
102
+ </div>
103
+ <div className="grid grid-cols-2 gap-4">
104
+ <div className="space-y-2">
105
+ <Label htmlFor="competencia">Competência</Label>
106
+ <Input id="competencia" type="month" />
107
+ </div>
108
+ <div className="space-y-2">
109
+ <Label htmlFor="vencimento">Vencimento</Label>
110
+ <Input id="vencimento" type="date" />
111
+ </div>
112
+ </div>
113
+ <div className="space-y-2">
114
+ <Label htmlFor="valor">Valor Total</Label>
115
+ <Input id="valor" type="number" placeholder="0,00" step="0.01" />
116
+ </div>
117
+ <div className="space-y-2">
118
+ <Label htmlFor="categoria">Categoria</Label>
119
+ <Select>
120
+ <SelectTrigger>
121
+ <SelectValue placeholder="Selecione..." />
122
+ </SelectTrigger>
123
+ <SelectContent>
124
+ {categorias
125
+ .filter((c) => c.natureza === 'receita')
126
+ .map((c) => (
127
+ <SelectItem key={c.id} value={c.id}>
128
+ {c.codigo} - {c.nome}
129
+ </SelectItem>
130
+ ))}
131
+ </SelectContent>
132
+ </Select>
133
+ </div>
134
+ <div className="space-y-2">
135
+ <Label htmlFor="centroCusto">Centro de Custo</Label>
136
+ <Select>
137
+ <SelectTrigger>
138
+ <SelectValue placeholder="Selecione..." />
139
+ </SelectTrigger>
140
+ <SelectContent>
141
+ {centrosCusto.map((c) => (
142
+ <SelectItem key={c.id} value={c.id}>
143
+ {c.codigo} - {c.nome}
144
+ </SelectItem>
145
+ ))}
146
+ </SelectContent>
147
+ </Select>
148
+ </div>
149
+ <div className="space-y-2">
150
+ <Label htmlFor="canal">Canal de Recebimento</Label>
151
+ <Select>
152
+ <SelectTrigger>
153
+ <SelectValue placeholder="Selecione..." />
154
+ </SelectTrigger>
155
+ <SelectContent>
156
+ <SelectItem value="boleto">Boleto</SelectItem>
157
+ <SelectItem value="pix">PIX</SelectItem>
158
+ <SelectItem value="cartao">Cartão</SelectItem>
159
+ <SelectItem value="transferencia">Transferência</SelectItem>
160
+ </SelectContent>
161
+ </Select>
162
+ </div>
163
+ <div className="space-y-2">
164
+ <Label htmlFor="descricao">Descrição</Label>
165
+ <Textarea id="descricao" placeholder="Descrição do título..." />
166
+ </div>
167
+ </div>
168
+ <div className="flex justify-end gap-2 pt-4">
169
+ <Button type="button" variant="outline">
170
+ Cancelar
171
+ </Button>
172
+ <Button type="submit">Salvar</Button>
173
+ </div>
174
+ </form>
175
+ </SheetContent>
176
+ </Sheet>
177
+ );
178
+ }
179
+
180
+ const canalBadge = {
181
+ boleto: { label: 'Boleto', className: 'bg-blue-100 text-blue-700' },
182
+ pix: { label: 'PIX', className: 'bg-green-100 text-green-700' },
183
+ cartao: { label: 'Cartão', className: 'bg-purple-100 text-purple-700' },
184
+ transferencia: {
185
+ label: 'Transferência',
186
+ className: 'bg-orange-100 text-orange-700',
187
+ },
188
+ };
189
+
190
+ export default function TitulosReceberPage() {
191
+ const { data } = useFinanceData();
192
+ const { titulosReceber, pessoas, categorias, centrosCusto } = data;
193
+
194
+ const getPessoaById = (id?: string) => pessoas.find((p) => p.id === id);
195
+
196
+ const [search, setSearch] = useState('');
197
+ const [statusFilter, setStatusFilter] = useState<string>('');
198
+
199
+ const filteredTitulos = titulosReceber.filter((titulo) => {
200
+ const matchesSearch =
201
+ titulo.documento.toLowerCase().includes(search.toLowerCase()) ||
202
+ getPessoaById(titulo.clienteId)
203
+ ?.nome.toLowerCase()
204
+ .includes(search.toLowerCase());
205
+
206
+ const matchesStatus = !statusFilter || titulo.status === statusFilter;
207
+
208
+ return matchesSearch && matchesStatus;
209
+ });
210
+
211
+ return (
212
+ <div className="space-y-6">
213
+ <PageHeader
214
+ title="Títulos a Receber"
215
+ description="Gerencie suas contas a receber"
216
+ breadcrumbs={[
217
+ { label: 'Contas a Receber', href: '/finance/accounts-receivable/installments' },
218
+ { label: 'Títulos e Parcelas' },
219
+ ]}
220
+ actions={
221
+ <NovoTituloSheet
222
+ pessoas={pessoas}
223
+ categorias={categorias}
224
+ centrosCusto={centrosCusto}
225
+ />
226
+ }
227
+ />
228
+
229
+ <FilterBar
230
+ searchPlaceholder="Buscar por documento ou cliente..."
231
+ searchValue={search}
232
+ onSearchChange={setSearch}
233
+ filters={[
234
+ {
235
+ id: 'status',
236
+ label: 'Status',
237
+ value: statusFilter,
238
+ onChange: setStatusFilter,
239
+ options: [
240
+ { value: 'all', label: 'Todos' },
241
+ { value: 'aberto', label: 'Aberto' },
242
+ { value: 'parcial', label: 'Parcial' },
243
+ { value: 'liquidado', label: 'Liquidado' },
244
+ { value: 'vencido', label: 'Vencido' },
245
+ { value: 'cancelado', label: 'Cancelado' },
246
+ ],
247
+ },
248
+ ]}
249
+ activeFilters={statusFilter && statusFilter !== 'all' ? 1 : 0}
250
+ onClearFilters={() => setStatusFilter('')}
251
+ />
252
+
253
+ <div className="rounded-md border">
254
+ <Table>
255
+ <TableHeader>
256
+ <TableRow>
257
+ <TableHead>Documento</TableHead>
258
+ <TableHead>Cliente</TableHead>
259
+ <TableHead>Competência</TableHead>
260
+ <TableHead>Vencimento</TableHead>
261
+ <TableHead className="text-right">Valor</TableHead>
262
+ <TableHead>Canal</TableHead>
263
+ <TableHead>Status</TableHead>
264
+ <TableHead className="w-[50px]" />
265
+ </TableRow>
266
+ </TableHeader>
267
+ <TableBody>
268
+ {filteredTitulos.map((titulo) => {
269
+ const cliente = getPessoaById(titulo.clienteId);
270
+ const canal =
271
+ canalBadge[titulo.canal as keyof typeof canalBadge] ||
272
+ canalBadge.transferencia;
273
+ const proximaParcela = titulo.parcelas.find(
274
+ (p: any) => p.status === 'aberto' || p.status === 'vencido'
275
+ );
276
+
277
+ return (
278
+ <TableRow key={titulo.id}>
279
+ <TableCell className="font-medium">
280
+ <Link
281
+ href={`/finance/accounts-receivable/installments/${titulo.id}`}
282
+ className="hover:underline"
283
+ >
284
+ {titulo.documento}
285
+ </Link>
286
+ {titulo.anexos.length > 0 && (
287
+ <Paperclip className="ml-1 inline h-3 w-3 text-muted-foreground" />
288
+ )}
289
+ </TableCell>
290
+ <TableCell>{cliente?.nome}</TableCell>
291
+ <TableCell>{titulo.competencia}</TableCell>
292
+ <TableCell>
293
+ {proximaParcela
294
+ ? formatarData(proximaParcela.vencimento)
295
+ : '-'}
296
+ </TableCell>
297
+ <TableCell className="text-right">
298
+ <Money value={titulo.valorTotal} />
299
+ </TableCell>
300
+ <TableCell>
301
+ <Badge className={canal.className} variant="outline">
302
+ {canal.label}
303
+ </Badge>
304
+ </TableCell>
305
+ <TableCell>
306
+ <StatusBadge status={titulo.status} />
307
+ </TableCell>
308
+ <TableCell>
309
+ <DropdownMenu>
310
+ <DropdownMenuTrigger asChild>
311
+ <Button variant="ghost" size="icon">
312
+ <MoreHorizontal className="h-4 w-4" />
313
+ <span className="sr-only">Ações</span>
314
+ </Button>
315
+ </DropdownMenuTrigger>
316
+ <DropdownMenuContent align="end">
317
+ <DropdownMenuItem asChild>
318
+ <Link href={`/finance/accounts-receivable/installments/${titulo.id}`}>
319
+ <Eye className="mr-2 h-4 w-4" />
320
+ Ver Detalhes
321
+ </Link>
322
+ </DropdownMenuItem>
323
+ <DropdownMenuItem>
324
+ <Edit className="mr-2 h-4 w-4" />
325
+ Editar
326
+ </DropdownMenuItem>
327
+ <DropdownMenuSeparator />
328
+ <DropdownMenuItem>
329
+ <Download className="mr-2 h-4 w-4" />
330
+ Registrar Recebimento
331
+ </DropdownMenuItem>
332
+ <DropdownMenuItem>
333
+ <Send className="mr-2 h-4 w-4" />
334
+ Enviar Cobrança
335
+ </DropdownMenuItem>
336
+ </DropdownMenuContent>
337
+ </DropdownMenu>
338
+ </TableCell>
339
+ </TableRow>
340
+ );
341
+ })}
342
+ </TableBody>
343
+ </Table>
344
+ </div>
345
+
346
+ <div className="flex items-center justify-between">
347
+ <p className="text-sm text-muted-foreground">
348
+ Mostrando {filteredTitulos.length} de {titulosReceber.length}{' '}
349
+ registros
350
+ </p>
351
+ <div className="flex items-center gap-2">
352
+ <Button variant="outline" size="sm" disabled>
353
+ Anterior
354
+ </Button>
355
+ <Button variant="outline" size="sm" disabled>
356
+ Próximo
357
+ </Button>
358
+ </div>
359
+ </div>
360
+ </div>
361
+ );
362
+ }
363
+
364
+
@@ -0,0 +1,274 @@
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 { Input } from '@/components/ui/input';
13
+ import { Label } from '@/components/ui/label';
14
+ import { Money } from '@/components/ui/money';
15
+ import { PageHeader } from '@/components/ui/page-header';
16
+ import {
17
+ Select,
18
+ SelectContent,
19
+ SelectItem,
20
+ SelectTrigger,
21
+ SelectValue,
22
+ } from '@/components/ui/select';
23
+ import {
24
+ Sheet,
25
+ SheetContent,
26
+ SheetDescription,
27
+ SheetHeader,
28
+ SheetTitle,
29
+ SheetTrigger,
30
+ } from '@/components/ui/sheet';
31
+ import {
32
+ Building2,
33
+ Eye,
34
+ Landmark,
35
+ PiggyBank,
36
+ Plus,
37
+ RefreshCw,
38
+ TrendingUp,
39
+ Upload,
40
+ Wallet,
41
+ } from 'lucide-react';
42
+ import Link from 'next/link';
43
+ import { useFinanceData } from '../../_lib/use-finance-data';
44
+
45
+ const tipoConfig = {
46
+ corrente: { label: 'Conta Corrente', icon: Building2 },
47
+ poupanca: { label: 'Poupança', icon: PiggyBank },
48
+ investimento: { label: 'Investimento', icon: TrendingUp },
49
+ caixa: { label: 'Caixa Físico', icon: Wallet },
50
+ };
51
+
52
+ function NovaContaSheet() {
53
+ return (
54
+ <Sheet>
55
+ <SheetTrigger asChild>
56
+ <Button>
57
+ <Plus className="mr-2 h-4 w-4" />
58
+ Nova Conta
59
+ </Button>
60
+ </SheetTrigger>
61
+ <SheetContent className="w-full sm:max-w-lg">
62
+ <SheetHeader>
63
+ <SheetTitle>Nova Conta Bancária</SheetTitle>
64
+ <SheetDescription>Cadastre uma nova conta bancária.</SheetDescription>
65
+ </SheetHeader>
66
+ <form className="mt-6 space-y-4">
67
+ <div className="grid gap-4">
68
+ <div className="space-y-2">
69
+ <Label htmlFor="banco">Banco</Label>
70
+ <Input id="banco" placeholder="Nome do banco" />
71
+ </div>
72
+ <div className="grid grid-cols-2 gap-4">
73
+ <div className="space-y-2">
74
+ <Label htmlFor="agencia">Agência</Label>
75
+ <Input id="agencia" placeholder="0000" />
76
+ </div>
77
+ <div className="space-y-2">
78
+ <Label htmlFor="conta">Conta</Label>
79
+ <Input id="conta" placeholder="00000-0" />
80
+ </div>
81
+ </div>
82
+ <div className="space-y-2">
83
+ <Label htmlFor="tipo">Tipo</Label>
84
+ <Select>
85
+ <SelectTrigger>
86
+ <SelectValue placeholder="Selecione..." />
87
+ </SelectTrigger>
88
+ <SelectContent>
89
+ <SelectItem value="corrente">Conta Corrente</SelectItem>
90
+ <SelectItem value="poupanca">Poupança</SelectItem>
91
+ <SelectItem value="investimento">Investimento</SelectItem>
92
+ <SelectItem value="caixa">Caixa Físico</SelectItem>
93
+ </SelectContent>
94
+ </Select>
95
+ </div>
96
+ <div className="space-y-2">
97
+ <Label htmlFor="descricao">Descrição</Label>
98
+ <Input id="descricao" placeholder="Ex: Conta Principal" />
99
+ </div>
100
+ <div className="space-y-2">
101
+ <Label htmlFor="saldoInicial">Saldo Inicial</Label>
102
+ <Input
103
+ id="saldoInicial"
104
+ type="number"
105
+ placeholder="0,00"
106
+ step="0.01"
107
+ />
108
+ </div>
109
+ </div>
110
+ <div className="flex justify-end gap-2 pt-4">
111
+ <Button type="button" variant="outline">
112
+ Cancelar
113
+ </Button>
114
+ <Button type="submit">Salvar</Button>
115
+ </div>
116
+ </form>
117
+ </SheetContent>
118
+ </Sheet>
119
+ );
120
+ }
121
+
122
+ export default function ContasBancariasPage() {
123
+ const { data } = useFinanceData();
124
+ const { contasBancarias } = data;
125
+
126
+ const saldoTotal = contasBancarias
127
+ .filter((c) => c.ativo)
128
+ .reduce((acc, c) => acc + c.saldoAtual, 0);
129
+
130
+ const saldoConciliadoTotal = contasBancarias
131
+ .filter((c) => c.ativo)
132
+ .reduce((acc, c) => acc + c.saldoConciliado, 0);
133
+
134
+ return (
135
+ <div className="space-y-6">
136
+ <PageHeader
137
+ title="Contas Bancárias"
138
+ description="Gerencie suas contas bancárias"
139
+ breadcrumbs={[
140
+ { label: 'Caixa e Bancos', href: '/finance/cash-and-banks/bank-accounts' },
141
+ { label: 'Contas Bancárias' },
142
+ ]}
143
+ actions={<NovaContaSheet />}
144
+ />
145
+
146
+ <div className="grid gap-4 md:grid-cols-2">
147
+ <Card>
148
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
149
+ <CardTitle className="text-sm font-medium">Saldo Total</CardTitle>
150
+ <Landmark className="h-4 w-4 text-muted-foreground" />
151
+ </CardHeader>
152
+ <CardContent>
153
+ <div className="text-2xl font-bold">
154
+ <Money value={saldoTotal} />
155
+ </div>
156
+ <p className="text-xs text-muted-foreground">
157
+ {contasBancarias.filter((c) => c.ativo).length} contas ativas
158
+ </p>
159
+ </CardContent>
160
+ </Card>
161
+ <Card>
162
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
163
+ <CardTitle className="text-sm font-medium">
164
+ Saldo Conciliado
165
+ </CardTitle>
166
+ <RefreshCw className="h-4 w-4 text-muted-foreground" />
167
+ </CardHeader>
168
+ <CardContent>
169
+ <div className="text-2xl font-bold">
170
+ <Money value={saldoConciliadoTotal} />
171
+ </div>
172
+ <p className="text-xs text-muted-foreground">
173
+ Diferença: <Money value={saldoTotal - saldoConciliadoTotal} />
174
+ </p>
175
+ </CardContent>
176
+ </Card>
177
+ </div>
178
+
179
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
180
+ {contasBancarias.map((conta) => {
181
+ const tipo =
182
+ tipoConfig[conta.tipo as keyof typeof tipoConfig] ||
183
+ tipoConfig.corrente;
184
+ const TipoIcon = tipo.icon;
185
+ const diferenca = conta.saldoAtual - conta.saldoConciliado;
186
+
187
+ return (
188
+ <Card key={conta.id} className={!conta.ativo ? 'opacity-60' : ''}>
189
+ <CardHeader>
190
+ <div className="flex items-center justify-between">
191
+ <div className="flex items-center gap-2">
192
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-muted">
193
+ <TipoIcon className="h-5 w-5" />
194
+ </div>
195
+ <div>
196
+ <CardTitle className="text-base">{conta.banco}</CardTitle>
197
+ <CardDescription>
198
+ {conta.agencia !== '-'
199
+ ? `Ag: ${conta.agencia} | Cc: ${conta.conta}`
200
+ : conta.descricao}
201
+ </CardDescription>
202
+ </div>
203
+ </div>
204
+ {!conta.ativo && (
205
+ <Badge variant="outline" className="text-muted-foreground">
206
+ Inativa
207
+ </Badge>
208
+ )}
209
+ </div>
210
+ </CardHeader>
211
+ <CardContent>
212
+ <div className="space-y-3">
213
+ <div>
214
+ <p className="text-sm text-muted-foreground">Saldo Atual</p>
215
+ <p className="text-2xl font-bold">
216
+ <Money value={conta.saldoAtual} />
217
+ </p>
218
+ </div>
219
+ <div className="flex items-center justify-between text-sm">
220
+ <span className="text-muted-foreground">
221
+ Saldo Conciliado
222
+ </span>
223
+ <Money value={conta.saldoConciliado} />
224
+ </div>
225
+ {diferenca !== 0 && (
226
+ <div className="flex items-center justify-between text-sm">
227
+ <span className="text-muted-foreground">Diferença</span>
228
+ <span
229
+ className={
230
+ diferenca > 0 ? 'text-green-600' : 'text-red-600'
231
+ }
232
+ >
233
+ <Money value={diferenca} showSign />
234
+ </span>
235
+ </div>
236
+ )}
237
+ <div className="flex gap-2 pt-2">
238
+ <Button
239
+ variant="outline"
240
+ size="sm"
241
+ className="flex-1 bg-transparent"
242
+ asChild
243
+ >
244
+ <Link href="/finance/cash-and-banks/statements">
245
+ <Eye className="mr-2 h-4 w-4" />
246
+ Extrato
247
+ </Link>
248
+ </Button>
249
+ <Button
250
+ variant="outline"
251
+ size="sm"
252
+ className="flex-1 bg-transparent"
253
+ asChild
254
+ >
255
+ <Link href="/finance/cash-and-banks/bank-reconciliation">
256
+ <RefreshCw className="mr-2 h-4 w-4" />
257
+ Conciliar
258
+ </Link>
259
+ </Button>
260
+ <Button variant="outline" size="sm">
261
+ <Upload className="h-4 w-4" />
262
+ </Button>
263
+ </div>
264
+ </div>
265
+ </CardContent>
266
+ </Card>
267
+ );
268
+ })}
269
+ </div>
270
+ </div>
271
+ );
272
+ }
273
+
274
+