@hed-hog/finance 0.0.223 → 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,20 @@
1
+ export function formatarData(value?: string | Date | null) {
2
+ if (!value) {
3
+ return '-';
4
+ }
5
+
6
+ const date = value instanceof Date ? value : new Date(value);
7
+
8
+ if (Number.isNaN(date.getTime())) {
9
+ return String(value);
10
+ }
11
+
12
+ return new Intl.DateTimeFormat('pt-BR').format(date);
13
+ }
14
+
15
+ export function formatarMoeda(value?: number | null) {
16
+ return new Intl.NumberFormat('pt-BR', {
17
+ style: 'currency',
18
+ currency: 'BRL',
19
+ }).format(value || 0);
20
+ }
@@ -0,0 +1,87 @@
1
+ 'use client';
2
+
3
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
4
+
5
+ type FinanceData = {
6
+ kpis: {
7
+ saldoCaixa: number;
8
+ aPagar30dias: number;
9
+ aPagar7dias: number;
10
+ aReceber30dias: number;
11
+ aReceber7dias: number;
12
+ inadimplencia: number;
13
+ };
14
+ fluxoCaixaPrevisto: any[];
15
+ titulosPagar: any[];
16
+ titulosReceber: any[];
17
+ extratos: any[];
18
+ contasBancarias: any[];
19
+ pessoas: any[];
20
+ categorias: any[];
21
+ centrosCusto: any[];
22
+ aprovacoesPendentes: any[];
23
+ agingInadimplencia: any[];
24
+ cenarios: any[];
25
+ transferencias: any[];
26
+ tags: any[];
27
+ logsAuditoria: any[];
28
+ recebiveis: any[];
29
+ adquirentes: any[];
30
+ historicoContatos: any[];
31
+ entradasPrevistas: any[];
32
+ saidasPrevistas: any[];
33
+ };
34
+
35
+ const EMPTY_FINANCE_DATA: FinanceData = {
36
+ kpis: {
37
+ saldoCaixa: 0,
38
+ aPagar30dias: 0,
39
+ aPagar7dias: 0,
40
+ aReceber30dias: 0,
41
+ aReceber7dias: 0,
42
+ inadimplencia: 0,
43
+ },
44
+ fluxoCaixaPrevisto: [],
45
+ titulosPagar: [],
46
+ titulosReceber: [],
47
+ extratos: [],
48
+ contasBancarias: [],
49
+ pessoas: [],
50
+ categorias: [],
51
+ centrosCusto: [],
52
+ aprovacoesPendentes: [],
53
+ agingInadimplencia: [],
54
+ cenarios: [],
55
+ transferencias: [],
56
+ tags: [],
57
+ logsAuditoria: [],
58
+ recebiveis: [],
59
+ adquirentes: [],
60
+ historicoContatos: [],
61
+ entradasPrevistas: [],
62
+ saidasPrevistas: [],
63
+ };
64
+
65
+ export function useFinanceData() {
66
+ const { request } = useApp();
67
+
68
+ return useQuery<FinanceData>({
69
+ queryKey: ['finance-data'],
70
+ queryFn: async () => {
71
+ try {
72
+ const response = await request({
73
+ url: '/finance/data',
74
+ method: 'GET',
75
+ });
76
+
77
+ return {
78
+ ...EMPTY_FINANCE_DATA,
79
+ ...(response?.data || {}),
80
+ } as FinanceData;
81
+ } catch {
82
+ return EMPTY_FINANCE_DATA;
83
+ }
84
+ },
85
+ initialData: EMPTY_FINANCE_DATA,
86
+ });
87
+ }
@@ -0,0 +1,265 @@
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 {
13
+ Dialog,
14
+ DialogContent,
15
+ DialogDescription,
16
+ DialogFooter,
17
+ DialogHeader,
18
+ DialogTitle,
19
+ DialogTrigger,
20
+ } from '@/components/ui/dialog';
21
+ import { Label } from '@/components/ui/label';
22
+ import { Money } from '@/components/ui/money';
23
+ import { PageHeader } from '@/components/ui/page-header';
24
+ import {
25
+ Table,
26
+ TableBody,
27
+ TableCell,
28
+ TableHead,
29
+ TableHeader,
30
+ TableRow,
31
+ } from '@/components/ui/table';
32
+ import { Textarea } from '@/components/ui/textarea';
33
+ import { AlertTriangle, CheckCircle, Clock, XCircle } from 'lucide-react';
34
+ import { useEffect, useState } from 'react';
35
+ import { formatarData } from '../../_lib/formatters';
36
+ import { useFinanceData } from '../../_lib/use-finance-data';
37
+
38
+ const urgenciaConfig = {
39
+ baixa: { label: 'Baixa', className: 'bg-slate-100 text-slate-700' },
40
+ media: { label: 'Média', className: 'bg-blue-100 text-blue-700' },
41
+ alta: { label: 'Alta', className: 'bg-orange-100 text-orange-700' },
42
+ critica: { label: 'Crítica', className: 'bg-red-100 text-red-700' },
43
+ };
44
+
45
+ function AprovacaoDialog({
46
+ tipo,
47
+ onConfirm,
48
+ }: {
49
+ tipo: 'aprovar' | 'reprovar';
50
+ onConfirm: () => void;
51
+ }) {
52
+ const isAprovar = tipo === 'aprovar';
53
+
54
+ return (
55
+ <Dialog>
56
+ <DialogTrigger asChild>
57
+ <Button variant={isAprovar ? 'default' : 'outline'} size="sm">
58
+ {isAprovar ? (
59
+ <>
60
+ <CheckCircle className="mr-2 h-4 w-4" />
61
+ Aprovar
62
+ </>
63
+ ) : (
64
+ <>
65
+ <XCircle className="mr-2 h-4 w-4" />
66
+ Reprovar
67
+ </>
68
+ )}
69
+ </Button>
70
+ </DialogTrigger>
71
+ <DialogContent>
72
+ <DialogHeader>
73
+ <DialogTitle>
74
+ {isAprovar ? 'Aprovar Título' : 'Reprovar Título'}
75
+ </DialogTitle>
76
+ <DialogDescription>
77
+ {isAprovar
78
+ ? 'Confirme a aprovação deste título para pagamento.'
79
+ : 'Informe o motivo da reprovação.'}
80
+ </DialogDescription>
81
+ </DialogHeader>
82
+ <div className="space-y-4">
83
+ <div className="space-y-2">
84
+ <Label htmlFor="comentario">Comentário</Label>
85
+ <Textarea
86
+ id="comentario"
87
+ placeholder={
88
+ isAprovar
89
+ ? 'Comentário opcional...'
90
+ : 'Informe o motivo da reprovação...'
91
+ }
92
+ />
93
+ </div>
94
+ </div>
95
+ <DialogFooter>
96
+ <Button variant="outline">Cancelar</Button>
97
+ <Button
98
+ variant={isAprovar ? 'default' : 'destructive'}
99
+ onClick={onConfirm}
100
+ >
101
+ {isAprovar ? 'Confirmar Aprovação' : 'Confirmar Reprovação'}
102
+ </Button>
103
+ </DialogFooter>
104
+ </DialogContent>
105
+ </Dialog>
106
+ );
107
+ }
108
+
109
+ export default function AprovacoesPage() {
110
+ const { data } = useFinanceData();
111
+ const { aprovacoesPendentes, titulosPagar, pessoas } = data;
112
+
113
+ const getPessoaById = (id?: string) => pessoas.find((p) => p.id === id);
114
+
115
+ const [aprovacoes, setAprovacoes] = useState(aprovacoesPendentes);
116
+
117
+ useEffect(() => {
118
+ setAprovacoes(aprovacoesPendentes);
119
+ }, [aprovacoesPendentes]);
120
+
121
+ const handleAprovacao = (id: string) => {
122
+ setAprovacoes(aprovacoes.filter((a) => a.id !== id));
123
+ };
124
+
125
+ const totalPendente = aprovacoes.reduce((acc, a) => acc + a.valor, 0);
126
+
127
+ return (
128
+ <div className="flex flex-col h-screen px-4">
129
+ <PageHeader
130
+ title="Aprovações"
131
+ description="Gerencie as aprovações de títulos"
132
+ breadcrumbs={[
133
+ {
134
+ label: 'Contas a Pagar',
135
+ href: '/finance/accounts-payable/installments',
136
+ },
137
+ { label: 'Aprovações' },
138
+ ]}
139
+ />
140
+
141
+ <div className="grid gap-4 md:grid-cols-3">
142
+ <Card>
143
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
144
+ <CardTitle className="text-sm font-medium">Pendentes</CardTitle>
145
+ <Clock className="h-4 w-4 text-muted-foreground" />
146
+ </CardHeader>
147
+ <CardContent>
148
+ <div className="text-2xl font-bold">{aprovacoes.length}</div>
149
+ </CardContent>
150
+ </Card>
151
+ <Card>
152
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
153
+ <CardTitle className="text-sm font-medium">Valor Total</CardTitle>
154
+ <AlertTriangle className="h-4 w-4 text-muted-foreground" />
155
+ </CardHeader>
156
+ <CardContent>
157
+ <div className="text-2xl font-bold">
158
+ <Money value={totalPendente} />
159
+ </div>
160
+ </CardContent>
161
+ </Card>
162
+ <Card>
163
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
164
+ <CardTitle className="text-sm font-medium">Urgentes</CardTitle>
165
+ <AlertTriangle className="h-4 w-4 text-orange-500" />
166
+ </CardHeader>
167
+ <CardContent>
168
+ <div className="text-2xl font-bold">
169
+ {
170
+ aprovacoes.filter(
171
+ (a) => a.urgencia === 'alta' || a.urgencia === 'critica'
172
+ ).length
173
+ }
174
+ </div>
175
+ </CardContent>
176
+ </Card>
177
+ </div>
178
+
179
+ <Card>
180
+ <CardHeader>
181
+ <CardTitle>Aprovações Pendentes</CardTitle>
182
+ <CardDescription>Títulos aguardando sua aprovação</CardDescription>
183
+ </CardHeader>
184
+ <CardContent>
185
+ {aprovacoes.length > 0 ? (
186
+ <Table>
187
+ <TableHeader>
188
+ <TableRow>
189
+ <TableHead>Documento</TableHead>
190
+ <TableHead>Fornecedor</TableHead>
191
+ <TableHead>Solicitante</TableHead>
192
+ <TableHead className="text-right">Valor</TableHead>
193
+ <TableHead>Política</TableHead>
194
+ <TableHead>Urgência</TableHead>
195
+ <TableHead>Data</TableHead>
196
+ <TableHead className="text-right">Ações</TableHead>
197
+ </TableRow>
198
+ </TableHeader>
199
+ <TableBody>
200
+ {aprovacoes.map((aprovacao) => {
201
+ const titulo = titulosPagar.find(
202
+ (t) => t.id === aprovacao.tituloId
203
+ );
204
+ const fornecedor = titulo
205
+ ? getPessoaById(titulo.fornecedorId)
206
+ : null;
207
+ const urgencia =
208
+ urgenciaConfig[
209
+ aprovacao.urgencia as keyof typeof urgenciaConfig
210
+ ] || urgenciaConfig.media;
211
+
212
+ return (
213
+ <TableRow key={aprovacao.id}>
214
+ <TableCell className="font-medium">
215
+ {titulo?.documento}
216
+ </TableCell>
217
+ <TableCell>{fornecedor?.nome}</TableCell>
218
+ <TableCell>{aprovacao.solicitante}</TableCell>
219
+ <TableCell className="text-right">
220
+ <Money value={aprovacao.valor} />
221
+ </TableCell>
222
+ <TableCell>
223
+ <span className="text-sm text-muted-foreground">
224
+ {aprovacao.politica}
225
+ </span>
226
+ </TableCell>
227
+ <TableCell>
228
+ <Badge className={urgencia.className} variant="outline">
229
+ {urgencia.label}
230
+ </Badge>
231
+ </TableCell>
232
+ <TableCell>
233
+ {formatarData(aprovacao.dataSolicitacao)}
234
+ </TableCell>
235
+ <TableCell>
236
+ <div className="flex justify-end gap-2">
237
+ <AprovacaoDialog
238
+ tipo="aprovar"
239
+ onConfirm={() => handleAprovacao(aprovacao.id)}
240
+ />
241
+ <AprovacaoDialog
242
+ tipo="reprovar"
243
+ onConfirm={() => handleAprovacao(aprovacao.id)}
244
+ />
245
+ </div>
246
+ </TableCell>
247
+ </TableRow>
248
+ );
249
+ })}
250
+ </TableBody>
251
+ </Table>
252
+ ) : (
253
+ <div className="flex flex-col items-center justify-center py-12 text-center">
254
+ <CheckCircle className="h-12 w-12 text-green-500" />
255
+ <h3 className="mt-4 text-lg font-semibold">Tudo em dia!</h3>
256
+ <p className="text-muted-foreground">
257
+ Não há aprovações pendentes.
258
+ </p>
259
+ </div>
260
+ )}
261
+ </CardContent>
262
+ </Card>
263
+ </div>
264
+ );
265
+ }