@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.
- 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 +2 -2
|
@@ -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
|
+
}
|