@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.
@@ -4,6 +4,7 @@
4
4
  en: Finance
5
5
  pt: Finanças
6
6
  slug: /finance
7
+ order: 100
7
8
  relations:
8
9
  role:
9
10
  - where:
@@ -16,6 +17,7 @@
16
17
  en: Payable Bills
17
18
  pt: Contas a Pagar
18
19
  slug: /finance/accounts-payable
20
+ order: 101
19
21
  relations:
20
22
  role:
21
23
  - where:
@@ -58,6 +60,7 @@
58
60
  en: Receivable Bills
59
61
  pt: Contas a Receber
60
62
  slug: /finance/accounts-receivable
63
+ order: 102
61
64
  relations:
62
65
  role:
63
66
  - where:
@@ -100,6 +103,7 @@
100
103
  en: Cash and Banks
101
104
  pt: Caixa e Bancos
102
105
  slug: /finance/cash-and-banks
106
+ order: 103
103
107
  relations:
104
108
  role:
105
109
  - where:
@@ -172,6 +176,7 @@
172
176
  en: Planning
173
177
  pt: Planejamento
174
178
  slug: /finance/planning
179
+ order: 104
175
180
  relations:
176
181
  role:
177
182
  - where:
@@ -229,6 +234,7 @@
229
234
  en: Reports
230
235
  pt: Relatórios
231
236
  slug: /finance/reports
237
+ order: 105
232
238
  relations:
233
239
  role:
234
240
  - where:
@@ -281,26 +287,15 @@
281
287
  - where:
282
288
  slug: admin-finance
283
289
 
284
- - icon: settings
285
- name:
286
- en: Administration
287
- pt: Administração
288
- slug: /finance/administration
289
- relations:
290
- role:
291
- - where:
292
- slug: admin
293
- - where:
294
- slug: admin-finance
295
290
  - menu_id:
296
291
  where:
297
- slug: /finance/administration
292
+ slug: /core/management
298
293
  icon: clipboard-list
299
- url: /finance/administration/audit-logs
294
+ url: /core/management/audit-logs
300
295
  name:
301
296
  en: Audit and Logs
302
297
  pt: Auditoria e Logs
303
- slug: /finance/administration/audit-logs
298
+ slug: /core/management/audit-logs
304
299
  relations:
305
300
  role:
306
301
  - where:
@@ -309,13 +304,13 @@
309
304
  slug: admin-finance
310
305
  - menu_id:
311
306
  where:
312
- slug: /finance/administration
307
+ slug: /core/management
313
308
  icon: calendar-check-2
314
- url: /finance/administration/period-close
309
+ url: /core/management/period-close
315
310
  name:
316
311
  en: Period Close
317
312
  pt: Fechamento de Período
318
- slug: /finance/administration/period-close
313
+ slug: /core/management/period-close
319
314
  relations:
320
315
  role:
321
316
  - where:
@@ -324,13 +319,13 @@
324
319
  slug: admin-finance
325
320
  - menu_id:
326
321
  where:
327
- slug: /finance/administration
322
+ slug: /core/management
328
323
  icon: folder-tree
329
- url: /finance/administration/categories
324
+ url: /core/management/categories
330
325
  name:
331
326
  en: Categories
332
327
  pt: Categorias
333
- slug: /finance/administration/categories
328
+ slug: /core/management/categories
334
329
  relations:
335
330
  role:
336
331
  - where:
@@ -339,13 +334,13 @@
339
334
  slug: admin-finance
340
335
  - menu_id:
341
336
  where:
342
- slug: /finance/administration
337
+ slug: /core/management
343
338
  icon: pie-chart
344
- url: /finance/administration/cost-centers
339
+ url: /core/management/cost-centers
345
340
  name:
346
341
  en: Cost Centers
347
342
  pt: Centros de Custo
348
- slug: /finance/administration/cost-centers
343
+ slug: /core/management/cost-centers
349
344
  relations:
350
345
  role:
351
346
  - where:
@@ -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
+ }