@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,244 @@
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 { Input } from '@/components/ui/input';
12
+ import { Label } from '@/components/ui/label';
13
+ import { Money } from '@/components/ui/money';
14
+ import { PageHeader } from '@/components/ui/page-header';
15
+ import {
16
+ Select,
17
+ SelectContent,
18
+ SelectItem,
19
+ SelectTrigger,
20
+ SelectValue,
21
+ } from '@/components/ui/select';
22
+ import {
23
+ Sheet,
24
+ SheetContent,
25
+ SheetDescription,
26
+ SheetHeader,
27
+ SheetTitle,
28
+ SheetTrigger,
29
+ } from '@/components/ui/sheet';
30
+ import {
31
+ Table,
32
+ TableBody,
33
+ TableCell,
34
+ TableHead,
35
+ TableHeader,
36
+ TableRow,
37
+ } from '@/components/ui/table';
38
+ import { Textarea } from '@/components/ui/textarea';
39
+ import { ArrowRight, Plus } from 'lucide-react';
40
+ import { formatarData } from '../../_lib/formatters';
41
+ import { useFinanceData } from '../../_lib/use-finance-data';
42
+
43
+ function NovaTransferenciaSheet({
44
+ contasBancarias,
45
+ }: {
46
+ contasBancarias: any[];
47
+ }) {
48
+ return (
49
+ <Sheet>
50
+ <SheetTrigger asChild>
51
+ <Button>
52
+ <Plus className="mr-2 h-4 w-4" />
53
+ Nova Transferência
54
+ </Button>
55
+ </SheetTrigger>
56
+ <SheetContent className="w-full sm:max-w-lg">
57
+ <SheetHeader>
58
+ <SheetTitle>Nova Transferência</SheetTitle>
59
+ <SheetDescription>
60
+ Registre uma transferência entre contas.
61
+ </SheetDescription>
62
+ </SheetHeader>
63
+ <form className="mt-6 space-y-4">
64
+ <div className="grid gap-4">
65
+ <div className="space-y-2">
66
+ <Label htmlFor="contaOrigem">Conta de Origem</Label>
67
+ <Select>
68
+ <SelectTrigger>
69
+ <SelectValue placeholder="Selecione..." />
70
+ </SelectTrigger>
71
+ <SelectContent>
72
+ {contasBancarias.map((conta) => (
73
+ <SelectItem key={conta.id} value={conta.id}>
74
+ {conta.banco} - {conta.descricao}
75
+ </SelectItem>
76
+ ))}
77
+ </SelectContent>
78
+ </Select>
79
+ </div>
80
+ <div className="space-y-2">
81
+ <Label htmlFor="contaDestino">Conta de Destino</Label>
82
+ <Select>
83
+ <SelectTrigger>
84
+ <SelectValue placeholder="Selecione..." />
85
+ </SelectTrigger>
86
+ <SelectContent>
87
+ {contasBancarias.map((conta) => (
88
+ <SelectItem key={conta.id} value={conta.id}>
89
+ {conta.banco} - {conta.descricao}
90
+ </SelectItem>
91
+ ))}
92
+ </SelectContent>
93
+ </Select>
94
+ </div>
95
+ <div className="grid grid-cols-2 gap-4">
96
+ <div className="space-y-2">
97
+ <Label htmlFor="data">Data</Label>
98
+ <Input id="data" type="date" />
99
+ </div>
100
+ <div className="space-y-2">
101
+ <Label htmlFor="valor">Valor</Label>
102
+ <Input
103
+ id="valor"
104
+ type="number"
105
+ placeholder="0,00"
106
+ step="0.01"
107
+ />
108
+ </div>
109
+ </div>
110
+ <div className="space-y-2">
111
+ <Label htmlFor="descricao">Descrição</Label>
112
+ <Textarea
113
+ id="descricao"
114
+ placeholder="Motivo da transferência..."
115
+ />
116
+ </div>
117
+ </div>
118
+ <div className="flex justify-end gap-2 pt-4">
119
+ <Button type="button" variant="outline">
120
+ Cancelar
121
+ </Button>
122
+ <Button type="submit">Registrar</Button>
123
+ </div>
124
+ </form>
125
+ </SheetContent>
126
+ </Sheet>
127
+ );
128
+ }
129
+
130
+ export default function TransferenciasPage() {
131
+ const { data } = useFinanceData();
132
+ const { transferencias, contasBancarias } = data;
133
+
134
+ const totalTransferido = transferencias.reduce((acc, t) => acc + t.valor, 0);
135
+
136
+ return (
137
+ <div className="space-y-6">
138
+ <PageHeader
139
+ title="Transferências"
140
+ description="Gerencie transferências entre contas"
141
+ breadcrumbs={[
142
+ { label: 'Caixa e Bancos', href: '/finance/cash-and-banks/bank-accounts' },
143
+ { label: 'Transferências' },
144
+ ]}
145
+ actions={<NovaTransferenciaSheet contasBancarias={contasBancarias} />}
146
+ />
147
+
148
+ <div className="grid gap-4 md:grid-cols-2">
149
+ <Card>
150
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
151
+ <CardTitle className="text-sm font-medium">
152
+ Total Transferido
153
+ </CardTitle>
154
+ </CardHeader>
155
+ <CardContent>
156
+ <div className="text-2xl font-bold">
157
+ <Money value={totalTransferido} />
158
+ </div>
159
+ <p className="text-xs text-muted-foreground">
160
+ {transferencias.length} transferências
161
+ </p>
162
+ </CardContent>
163
+ </Card>
164
+ <Card>
165
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
166
+ <CardTitle className="text-sm font-medium">Contas Ativas</CardTitle>
167
+ </CardHeader>
168
+ <CardContent>
169
+ <div className="text-2xl font-bold">
170
+ {contasBancarias.filter((c) => c.ativo).length}
171
+ </div>
172
+ <p className="text-xs text-muted-foreground">disponíveis</p>
173
+ </CardContent>
174
+ </Card>
175
+ </div>
176
+
177
+ <Card>
178
+ <CardHeader>
179
+ <CardTitle>Transferências Recentes</CardTitle>
180
+ <CardDescription>
181
+ Histórico de transferências entre contas
182
+ </CardDescription>
183
+ </CardHeader>
184
+ <CardContent>
185
+ <Table>
186
+ <TableHeader>
187
+ <TableRow>
188
+ <TableHead>Data</TableHead>
189
+ <TableHead>Origem</TableHead>
190
+ <TableHead className="text-center">→</TableHead>
191
+ <TableHead>Destino</TableHead>
192
+ <TableHead className="text-right">Valor</TableHead>
193
+ <TableHead>Descrição</TableHead>
194
+ </TableRow>
195
+ </TableHeader>
196
+ <TableBody>
197
+ {transferencias.map((transferencia) => {
198
+ const contaOrigem = contasBancarias.find(
199
+ (conta) => conta.id === transferencia.contaOrigemId
200
+ );
201
+ const contaDestino = contasBancarias.find(
202
+ (conta) => conta.id === transferencia.contaDestinoId
203
+ );
204
+
205
+ return (
206
+ <TableRow key={transferencia.id}>
207
+ <TableCell>{formatarData(transferencia.data)}</TableCell>
208
+ <TableCell>
209
+ <div>
210
+ <p className="font-medium">{contaOrigem?.banco}</p>
211
+ <p className="text-xs text-muted-foreground">
212
+ {contaOrigem?.descricao}
213
+ </p>
214
+ </div>
215
+ </TableCell>
216
+ <TableCell className="text-center">
217
+ <ArrowRight className="h-4 w-4 text-muted-foreground mx-auto" />
218
+ </TableCell>
219
+ <TableCell>
220
+ <div>
221
+ <p className="font-medium">{contaDestino?.banco}</p>
222
+ <p className="text-xs text-muted-foreground">
223
+ {contaDestino?.descricao}
224
+ </p>
225
+ </div>
226
+ </TableCell>
227
+ <TableCell className="text-right font-semibold">
228
+ <Money value={transferencia.valor} />
229
+ </TableCell>
230
+ <TableCell className="text-muted-foreground">
231
+ {transferencia.descricao}
232
+ </TableCell>
233
+ </TableRow>
234
+ );
235
+ })}
236
+ </TableBody>
237
+ </Table>
238
+ </CardContent>
239
+ </Card>
240
+ </div>
241
+ );
242
+ }
243
+
244
+
@@ -1,17 +1,315 @@
1
- import { PageHeader } from '@/components/entity-list';
2
- import { useTranslations } from 'next-intl';
1
+ 'use client';
2
+
3
+ import { Badge } from '@/components/ui/badge';
4
+ import {
5
+ Card,
6
+ CardContent,
7
+ CardDescription,
8
+ CardHeader,
9
+ CardTitle,
10
+ } from '@/components/ui/card';
11
+ import { KpiCard } from '@/components/ui/kpi-card';
12
+ import { Money } from '@/components/ui/money';
13
+ import { PageHeader } from '@/components/ui/page-header';
14
+ import { StatusBadge } from '@/components/ui/status-badge';
15
+ import {
16
+ AlertTriangle,
17
+ ArrowDownRight,
18
+ ArrowUpRight,
19
+ TrendingDown,
20
+ TrendingUp,
21
+ Wallet,
22
+ } from 'lucide-react';
23
+ import {
24
+ CartesianGrid,
25
+ Legend,
26
+ Line,
27
+ LineChart,
28
+ ResponsiveContainer,
29
+ Tooltip,
30
+ XAxis,
31
+ YAxis,
32
+ } from 'recharts';
33
+ import { formatarData, formatarMoeda } from './_lib/formatters';
34
+ import { useFinanceData } from './_lib/use-finance-data';
35
+
36
+ function DashboardChart({ fluxoCaixaPrevisto }: { fluxoCaixaPrevisto: any[] }) {
37
+ const chartData = fluxoCaixaPrevisto.map((item) => ({
38
+ ...item,
39
+ data: formatarData(item.data),
40
+ }));
41
+
42
+ return (
43
+ <ResponsiveContainer width="100%" height={300}>
44
+ <LineChart data={chartData}>
45
+ <CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
46
+ <XAxis
47
+ dataKey="data"
48
+ tick={{ fontSize: 12 }}
49
+ tickLine={false}
50
+ axisLine={false}
51
+ />
52
+ <YAxis
53
+ tick={{ fontSize: 12 }}
54
+ tickLine={false}
55
+ axisLine={false}
56
+ tickFormatter={(value) => `${(value / 1000).toFixed(0)}k`}
57
+ />
58
+ <Tooltip
59
+ formatter={(value: number) => formatarMoeda(value)}
60
+ contentStyle={{
61
+ backgroundColor: 'hsl(var(--background))',
62
+ border: '1px solid hsl(var(--border))',
63
+ borderRadius: '8px',
64
+ }}
65
+ />
66
+ <Legend />
67
+ <Line
68
+ type="monotone"
69
+ dataKey="saldoPrevisto"
70
+ name="Previsto"
71
+ stroke="hsl(var(--primary))"
72
+ strokeWidth={2}
73
+ dot={false}
74
+ />
75
+ <Line
76
+ type="monotone"
77
+ dataKey="saldoRealizado"
78
+ name="Realizado"
79
+ stroke="hsl(var(--chart-2))"
80
+ strokeWidth={2}
81
+ dot={false}
82
+ connectNulls
83
+ />
84
+ </LineChart>
85
+ </ResponsiveContainer>
86
+ );
87
+ }
88
+
89
+ function ProximosVencimentos({
90
+ titulosPagar,
91
+ titulosReceber,
92
+ getPessoaById,
93
+ }: {
94
+ titulosPagar: any[];
95
+ titulosReceber: any[];
96
+ getPessoaById: (id?: string) => any | undefined;
97
+ }) {
98
+ const vencimentosPagar = titulosPagar
99
+ .flatMap((t: any) =>
100
+ t.parcelas
101
+ .filter((p: any) => p.status === 'aberto' || p.status === 'vencido')
102
+ .map((p: any) => ({
103
+ tipo: 'pagar' as const,
104
+ documento: t.documento,
105
+ pessoa: getPessoaById(t.fornecedorId)?.nome || '',
106
+ vencimento: p.vencimento,
107
+ valor: p.valor,
108
+ status: p.status,
109
+ }))
110
+ )
111
+ .slice(0, 3);
112
+
113
+ const vencimentosReceber = titulosReceber
114
+ .flatMap((t: any) =>
115
+ t.parcelas
116
+ .filter((p: any) => p.status === 'aberto' || p.status === 'vencido')
117
+ .map((p: any) => ({
118
+ tipo: 'receber' as const,
119
+ documento: t.documento,
120
+ pessoa: getPessoaById(t.clienteId)?.nome || '',
121
+ vencimento: p.vencimento,
122
+ valor: p.valor,
123
+ status: p.status,
124
+ }))
125
+ )
126
+ .slice(0, 3);
127
+
128
+ return (
129
+ <div className="grid gap-6 lg:grid-cols-2">
130
+ <Card>
131
+ <CardHeader>
132
+ <CardTitle className="flex items-center gap-2 text-base">
133
+ <ArrowDownRight className="h-4 w-4 text-red-500" />A Pagar
134
+ </CardTitle>
135
+ <CardDescription>Próximos vencimentos</CardDescription>
136
+ </CardHeader>
137
+ <CardContent>
138
+ <div className="space-y-4">
139
+ {vencimentosPagar.map((item, i) => (
140
+ <div key={i} className="flex items-center justify-between">
141
+ <div className="space-y-1">
142
+ <p className="text-sm font-medium">{item.documento}</p>
143
+ <p className="text-xs text-muted-foreground">{item.pessoa}</p>
144
+ </div>
145
+ <div className="text-right">
146
+ <p className="text-sm font-medium">
147
+ <Money value={item.valor} />
148
+ </p>
149
+ <p className="text-xs text-muted-foreground">
150
+ {formatarData(item.vencimento)}
151
+ </p>
152
+ </div>
153
+ <StatusBadge status={item.status} />
154
+ </div>
155
+ ))}
156
+ </div>
157
+ </CardContent>
158
+ </Card>
159
+
160
+ <Card>
161
+ <CardHeader>
162
+ <CardTitle className="flex items-center gap-2 text-base">
163
+ <ArrowUpRight className="h-4 w-4 text-green-500" />A Receber
164
+ </CardTitle>
165
+ <CardDescription>Próximos vencimentos</CardDescription>
166
+ </CardHeader>
167
+ <CardContent>
168
+ <div className="space-y-4">
169
+ {vencimentosReceber.map((item, i) => (
170
+ <div key={i} className="flex items-center justify-between">
171
+ <div className="space-y-1">
172
+ <p className="text-sm font-medium">{item.documento}</p>
173
+ <p className="text-xs text-muted-foreground">{item.pessoa}</p>
174
+ </div>
175
+ <div className="text-right">
176
+ <p className="text-sm font-medium">
177
+ <Money value={item.valor} />
178
+ </p>
179
+ <p className="text-xs text-muted-foreground">
180
+ {formatarData(item.vencimento)}
181
+ </p>
182
+ </div>
183
+ <StatusBadge status={item.status} />
184
+ </div>
185
+ ))}
186
+ </div>
187
+ </CardContent>
188
+ </Card>
189
+ </div>
190
+ );
191
+ }
192
+
193
+ function Alertas({
194
+ titulosPagar,
195
+ extratos,
196
+ }: {
197
+ titulosPagar: any[];
198
+ extratos: any[];
199
+ }) {
200
+ const vencidos = titulosPagar.filter((t) =>
201
+ t.parcelas.some((p: any) => p.status === 'vencido')
202
+ ).length;
203
+ const pendenteConciliacao = extratos.filter(
204
+ (e) => e.statusConciliacao === 'pendente'
205
+ ).length;
206
+
207
+ return (
208
+ <Card>
209
+ <CardHeader>
210
+ <CardTitle className="flex items-center gap-2 text-base">
211
+ <AlertTriangle className="h-4 w-4 text-yellow-500" />
212
+ Alertas
213
+ </CardTitle>
214
+ </CardHeader>
215
+ <CardContent>
216
+ <div className="space-y-3">
217
+ {vencidos > 0 && (
218
+ <div className="flex items-center justify-between rounded-lg bg-red-50 p-3">
219
+ <span className="text-sm">Títulos vencidos</span>
220
+ <Badge variant="destructive">{vencidos}</Badge>
221
+ </div>
222
+ )}
223
+ {pendenteConciliacao > 0 && (
224
+ <div className="flex items-center justify-between rounded-lg bg-yellow-50 p-3">
225
+ <span className="text-sm">Conciliação pendente</span>
226
+ <Badge
227
+ variant="outline"
228
+ className="border-yellow-500 text-yellow-700"
229
+ >
230
+ {pendenteConciliacao}
231
+ </Badge>
232
+ </div>
233
+ )}
234
+ <div className="flex items-center justify-between rounded-lg bg-blue-50 p-3">
235
+ <span className="text-sm">Período aberto</span>
236
+ <Badge variant="outline" className="border-blue-500 text-blue-700">
237
+ Janeiro/2024
238
+ </Badge>
239
+ </div>
240
+ </div>
241
+ </CardContent>
242
+ </Card>
243
+ );
244
+ }
245
+
246
+ export default function DashboardPage() {
247
+ const { data } = useFinanceData();
248
+ const {
249
+ kpis,
250
+ fluxoCaixaPrevisto,
251
+ titulosPagar,
252
+ titulosReceber,
253
+ extratos,
254
+ pessoas,
255
+ } = data;
256
+
257
+ const getPessoaById = (id?: string) => pessoas.find((p) => p.id === id);
258
+
259
+ return (
260
+ <div className="space-y-6">
261
+ <PageHeader
262
+ title="Visão Geral"
263
+ description="Acompanhe os principais indicadores financeiros"
264
+ />
265
+
266
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
267
+ <KpiCard
268
+ title="Saldo em Caixa"
269
+ value={formatarMoeda(kpis.saldoCaixa)}
270
+ icon={Wallet}
271
+ description="Todas as contas"
272
+ />
273
+ <KpiCard
274
+ title="A Pagar (30 dias)"
275
+ value={formatarMoeda(kpis.aPagar30dias)}
276
+ icon={TrendingDown}
277
+ description={`7 dias: ${formatarMoeda(kpis.aPagar7dias)}`}
278
+ />
279
+ <KpiCard
280
+ title="A Receber (30 dias)"
281
+ value={formatarMoeda(kpis.aReceber30dias)}
282
+ icon={TrendingUp}
283
+ description={`7 dias: ${formatarMoeda(kpis.aReceber7dias)}`}
284
+ />
285
+ <KpiCard
286
+ title="Inadimplência"
287
+ value={formatarMoeda(kpis.inadimplencia)}
288
+ icon={AlertTriangle}
289
+ description="Títulos vencidos"
290
+ />
291
+ </div>
292
+
293
+ <div className="grid gap-6 lg:grid-cols-3">
294
+ <Card className="lg:col-span-2">
295
+ <CardHeader>
296
+ <CardTitle>Fluxo de Caixa</CardTitle>
297
+ <CardDescription>Previsto vs Realizado</CardDescription>
298
+ </CardHeader>
299
+ <CardContent>
300
+ <DashboardChart fluxoCaixaPrevisto={fluxoCaixaPrevisto} />
301
+ </CardContent>
302
+ </Card>
303
+ <Alertas titulosPagar={titulosPagar} extratos={extratos} />
304
+ </div>
305
+
306
+ <ProximosVencimentos
307
+ titulosPagar={titulosPagar}
308
+ titulosReceber={titulosReceber}
309
+ getPessoaById={getPessoaById}
310
+ />
311
+ </div>
312
+ );
313
+ }
3
314
 
4
- export const Page = () => {
5
- const t = useTranslations('finance.Home');
6
315
 
7
- return (
8
- <div className="flex flex-col h-screen px-4">
9
- <PageHeader
10
- breadcrumbs={[{ label: 'Home', href: '/' }, { label: t('title') }]}
11
- title={t('title')}
12
- description={t('description')}
13
- />
14
- Finance
15
- </div>
16
- );
17
- };