@hed-hog/finance 0.0.274 → 0.0.276
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/README.md +228 -126
- package/dist/dto/create-bank-reconciliation.dto.d.ts +8 -0
- package/dist/dto/create-bank-reconciliation.dto.d.ts.map +1 -0
- package/dist/dto/create-bank-reconciliation.dto.js +43 -0
- package/dist/dto/create-bank-reconciliation.dto.js.map +1 -0
- package/dist/finance-data.controller.d.ts +2 -0
- package/dist/finance-data.controller.d.ts.map +1 -1
- package/dist/finance-statements.controller.d.ts +42 -0
- package/dist/finance-statements.controller.d.ts.map +1 -1
- package/dist/finance-statements.controller.js +13 -0
- package/dist/finance-statements.controller.js.map +1 -1
- package/dist/finance.service.d.ts +44 -0
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +98 -9
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/route.yaml +9 -0
- package/hedhog/frontend/app/_components/person-field-with-create.tsx.ejs +126 -126
- package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +373 -373
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +1270 -1270
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +982 -982
- package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +686 -686
- package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +152 -32
- package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +986 -986
- package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +492 -492
- package/hedhog/frontend/app/page.tsx.ejs +372 -372
- package/hedhog/frontend/app/planning/cash-flow-forecast/page.tsx.ejs +329 -329
- package/hedhog/frontend/app/planning/receivables-calendar/page.tsx.ejs +227 -227
- package/hedhog/frontend/app/planning/scenarios/page.tsx.ejs +408 -408
- package/hedhog/frontend/messages/en.json +15 -5
- package/hedhog/frontend/messages/pt.json +15 -5
- package/package.json +7 -7
- package/src/dto/create-bank-reconciliation.dto.ts +24 -0
- package/src/finance-statements.controller.ts +14 -0
- package/src/finance.module.ts +43 -43
- package/src/finance.service.ts +118 -0
- package/src/index.ts +14 -14
- package/dist/finance.controller.d.ts +0 -276
- package/dist/finance.controller.d.ts.map +0 -1
- package/dist/finance.controller.js +0 -110
- package/dist/finance.controller.js.map +0 -1
|
@@ -1,373 +1,373 @@
|
|
|
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
|
-
|
|
24
|
-
import { Page, PageHeader } from '@/components/entity-list';
|
|
25
|
-
import {
|
|
26
|
-
Table,
|
|
27
|
-
TableBody,
|
|
28
|
-
TableCell,
|
|
29
|
-
TableHead,
|
|
30
|
-
TableHeader,
|
|
31
|
-
TableRow,
|
|
32
|
-
} from '@/components/ui/table';
|
|
33
|
-
import { Textarea } from '@/components/ui/textarea';
|
|
34
|
-
import { useApp } from '@hed-hog/next-app-provider';
|
|
35
|
-
import { AlertTriangle, CheckCircle, Clock, XCircle } from 'lucide-react';
|
|
36
|
-
import { useTranslations } from 'next-intl';
|
|
37
|
-
import { useMemo, useState } from 'react';
|
|
38
|
-
import { formatarData } from '../../_lib/formatters';
|
|
39
|
-
import { useFinanceData } from '../../_lib/use-finance-data';
|
|
40
|
-
|
|
41
|
-
function AprovacaoDialog({
|
|
42
|
-
tipo,
|
|
43
|
-
onConfirm,
|
|
44
|
-
t,
|
|
45
|
-
}: {
|
|
46
|
-
tipo: 'aprovar' | 'reprovar';
|
|
47
|
-
onConfirm: (comment?: string) => void;
|
|
48
|
-
t: ReturnType<typeof useTranslations>;
|
|
49
|
-
}) {
|
|
50
|
-
const isAprovar = tipo === 'aprovar';
|
|
51
|
-
const [comment, setComment] = useState('');
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<Dialog>
|
|
55
|
-
<DialogTrigger asChild>
|
|
56
|
-
<Button variant={isAprovar ? 'default' : 'outline'} size="sm">
|
|
57
|
-
{isAprovar ? (
|
|
58
|
-
<>
|
|
59
|
-
<CheckCircle className="mr-2 h-4 w-4" />
|
|
60
|
-
{t('dialog.approveAction')}
|
|
61
|
-
</>
|
|
62
|
-
) : (
|
|
63
|
-
<>
|
|
64
|
-
<XCircle className="mr-2 h-4 w-4" />
|
|
65
|
-
{t('dialog.rejectAction')}
|
|
66
|
-
</>
|
|
67
|
-
)}
|
|
68
|
-
</Button>
|
|
69
|
-
</DialogTrigger>
|
|
70
|
-
<DialogContent>
|
|
71
|
-
<DialogHeader>
|
|
72
|
-
<DialogTitle>
|
|
73
|
-
{isAprovar ? t('dialog.approveTitle') : t('dialog.rejectTitle')}
|
|
74
|
-
</DialogTitle>
|
|
75
|
-
<DialogDescription>
|
|
76
|
-
{isAprovar
|
|
77
|
-
? t('dialog.approveDescription')
|
|
78
|
-
: t('dialog.rejectDescription')}
|
|
79
|
-
</DialogDescription>
|
|
80
|
-
</DialogHeader>
|
|
81
|
-
<div className="space-y-4">
|
|
82
|
-
<div className="space-y-2">
|
|
83
|
-
<Label htmlFor="comentario">{t('dialog.comment')}</Label>
|
|
84
|
-
<Textarea
|
|
85
|
-
id="comentario"
|
|
86
|
-
value={comment}
|
|
87
|
-
onChange={(event) => setComment(event.target.value)}
|
|
88
|
-
placeholder={
|
|
89
|
-
isAprovar
|
|
90
|
-
? t('dialog.optionalCommentPlaceholder')
|
|
91
|
-
: t('dialog.rejectReasonPlaceholder')
|
|
92
|
-
}
|
|
93
|
-
/>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
<DialogFooter>
|
|
97
|
-
<Button type="button" variant="outline">
|
|
98
|
-
{t('dialog.cancel')}
|
|
99
|
-
</Button>
|
|
100
|
-
<Button
|
|
101
|
-
type="button"
|
|
102
|
-
variant={isAprovar ? 'default' : 'destructive'}
|
|
103
|
-
onClick={() => onConfirm(comment || undefined)}
|
|
104
|
-
>
|
|
105
|
-
{isAprovar ? t('dialog.confirmApprove') : t('dialog.confirmReject')}
|
|
106
|
-
</Button>
|
|
107
|
-
</DialogFooter>
|
|
108
|
-
</DialogContent>
|
|
109
|
-
</Dialog>
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export default function AprovacoesPage() {
|
|
114
|
-
const t = useTranslations('finance.PayableApprovalsPage');
|
|
115
|
-
const { request, showToastHandler } = useApp();
|
|
116
|
-
const { data, refetch } = useFinanceData();
|
|
117
|
-
const { aprovacoesPendentes, titulosPagar, pessoas } = data;
|
|
118
|
-
|
|
119
|
-
const getPessoaById = (id?: string) => pessoas.find((p) => p.id === id);
|
|
120
|
-
|
|
121
|
-
const aprovacoes = useMemo(() => {
|
|
122
|
-
const pendenciasDoBackend = Array.isArray(aprovacoesPendentes)
|
|
123
|
-
? aprovacoesPendentes
|
|
124
|
-
: [];
|
|
125
|
-
|
|
126
|
-
const pendenciasDerivadasDosTitulos = (
|
|
127
|
-
Array.isArray(titulosPagar) ? titulosPagar : []
|
|
128
|
-
)
|
|
129
|
-
.filter(
|
|
130
|
-
(titulo) => titulo.status === 'rascunho' || titulo.status === 'draft'
|
|
131
|
-
)
|
|
132
|
-
.map((titulo) => ({
|
|
133
|
-
id: String(titulo.id),
|
|
134
|
-
tituloId: String(titulo.id),
|
|
135
|
-
solicitante: '-',
|
|
136
|
-
valor: Number(titulo.valorTotal || 0),
|
|
137
|
-
politica: 'Aprovação financeira',
|
|
138
|
-
urgencia: 'media',
|
|
139
|
-
dataSolicitacao: titulo.criadoEm,
|
|
140
|
-
}));
|
|
141
|
-
|
|
142
|
-
const basePendencias =
|
|
143
|
-
pendenciasDoBackend.length > 0
|
|
144
|
-
? pendenciasDoBackend
|
|
145
|
-
: pendenciasDerivadasDosTitulos;
|
|
146
|
-
|
|
147
|
-
return basePendencias;
|
|
148
|
-
}, [aprovacoesPendentes, titulosPagar]);
|
|
149
|
-
|
|
150
|
-
const handleAprovacao = async (_id: string, tituloId?: string) => {
|
|
151
|
-
if (!tituloId) {
|
|
152
|
-
showToastHandler?.('error', 'Título inválido para aprovação');
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
await request({
|
|
158
|
-
url: `/finance/accounts-payable/installments/${tituloId}/approve`,
|
|
159
|
-
method: 'PATCH',
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
await refetch();
|
|
163
|
-
showToastHandler?.('success', 'Título aprovado com sucesso');
|
|
164
|
-
} catch {
|
|
165
|
-
showToastHandler?.('error', 'Não foi possível aprovar o título');
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
const handleReprovacao = async (
|
|
170
|
-
_id: string,
|
|
171
|
-
tituloId?: string,
|
|
172
|
-
reason?: string
|
|
173
|
-
) => {
|
|
174
|
-
if (!tituloId) {
|
|
175
|
-
showToastHandler?.('error', 'Título inválido para reprovação');
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
await request({
|
|
181
|
-
url: `/finance/accounts-payable/installments/${tituloId}/reject`,
|
|
182
|
-
method: 'PATCH',
|
|
183
|
-
data: {
|
|
184
|
-
reason: reason || undefined,
|
|
185
|
-
},
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
await refetch();
|
|
189
|
-
showToastHandler?.('success', 'Título reprovado com sucesso');
|
|
190
|
-
} catch {
|
|
191
|
-
showToastHandler?.('error', 'Não foi possível reprovar o título');
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const urgenciaConfig = {
|
|
196
|
-
baixa: {
|
|
197
|
-
label: t('urgency.low'),
|
|
198
|
-
className: 'bg-slate-100 text-slate-700',
|
|
199
|
-
},
|
|
200
|
-
media: {
|
|
201
|
-
label: t('urgency.medium'),
|
|
202
|
-
className: 'bg-blue-100 text-blue-700',
|
|
203
|
-
},
|
|
204
|
-
alta: {
|
|
205
|
-
label: t('urgency.high'),
|
|
206
|
-
className: 'bg-orange-100 text-orange-700',
|
|
207
|
-
},
|
|
208
|
-
critica: {
|
|
209
|
-
label: t('urgency.critical'),
|
|
210
|
-
className: 'bg-red-100 text-red-700',
|
|
211
|
-
},
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
const totalPendente = aprovacoes.reduce((acc, a) => acc + a.valor, 0);
|
|
215
|
-
|
|
216
|
-
return (
|
|
217
|
-
<Page>
|
|
218
|
-
<PageHeader
|
|
219
|
-
title={t('header.title')}
|
|
220
|
-
description={t('header.description')}
|
|
221
|
-
breadcrumbs={[
|
|
222
|
-
{ label: t('breadcrumbs.home'), href: '/' },
|
|
223
|
-
{ label: t('breadcrumbs.finance'), href: '/finance' },
|
|
224
|
-
{ label: t('breadcrumbs.current') },
|
|
225
|
-
]}
|
|
226
|
-
/>
|
|
227
|
-
|
|
228
|
-
<div className="grid gap-4 md:grid-cols-3">
|
|
229
|
-
<Card>
|
|
230
|
-
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
231
|
-
<CardTitle className="text-sm font-medium">
|
|
232
|
-
{t('cards.pending')}
|
|
233
|
-
</CardTitle>
|
|
234
|
-
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
235
|
-
</CardHeader>
|
|
236
|
-
<CardContent>
|
|
237
|
-
<div className="text-2xl font-bold">{aprovacoes.length}</div>
|
|
238
|
-
</CardContent>
|
|
239
|
-
</Card>
|
|
240
|
-
<Card>
|
|
241
|
-
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
242
|
-
<CardTitle className="text-sm font-medium">
|
|
243
|
-
{t('cards.totalValue')}
|
|
244
|
-
</CardTitle>
|
|
245
|
-
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
|
|
246
|
-
</CardHeader>
|
|
247
|
-
<CardContent>
|
|
248
|
-
<div className="text-2xl font-bold">
|
|
249
|
-
<Money value={totalPendente} />
|
|
250
|
-
</div>
|
|
251
|
-
</CardContent>
|
|
252
|
-
</Card>
|
|
253
|
-
<Card>
|
|
254
|
-
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
255
|
-
<CardTitle className="text-sm font-medium">
|
|
256
|
-
{t('cards.urgent')}
|
|
257
|
-
</CardTitle>
|
|
258
|
-
<AlertTriangle className="h-4 w-4 text-orange-500" />
|
|
259
|
-
</CardHeader>
|
|
260
|
-
<CardContent>
|
|
261
|
-
<div className="text-2xl font-bold">
|
|
262
|
-
{
|
|
263
|
-
aprovacoes.filter(
|
|
264
|
-
(a) => a.urgencia === 'alta' || a.urgencia === 'critica'
|
|
265
|
-
).length
|
|
266
|
-
}
|
|
267
|
-
</div>
|
|
268
|
-
</CardContent>
|
|
269
|
-
</Card>
|
|
270
|
-
</div>
|
|
271
|
-
|
|
272
|
-
<Card>
|
|
273
|
-
<CardHeader>
|
|
274
|
-
<CardTitle>{t('table.title')}</CardTitle>
|
|
275
|
-
<CardDescription>{t('table.description')}</CardDescription>
|
|
276
|
-
</CardHeader>
|
|
277
|
-
<CardContent>
|
|
278
|
-
{aprovacoes.length > 0 ? (
|
|
279
|
-
<Table>
|
|
280
|
-
<TableHeader>
|
|
281
|
-
<TableRow>
|
|
282
|
-
<TableHead>{t('table.headers.document')}</TableHead>
|
|
283
|
-
<TableHead>{t('table.headers.supplier')}</TableHead>
|
|
284
|
-
<TableHead>{t('table.headers.requester')}</TableHead>
|
|
285
|
-
<TableHead className="text-right">
|
|
286
|
-
{t('table.headers.value')}
|
|
287
|
-
</TableHead>
|
|
288
|
-
<TableHead>{t('table.headers.policy')}</TableHead>
|
|
289
|
-
<TableHead>{t('table.headers.urgency')}</TableHead>
|
|
290
|
-
<TableHead>{t('table.headers.date')}</TableHead>
|
|
291
|
-
<TableHead className="text-right">
|
|
292
|
-
{t('table.headers.actions')}
|
|
293
|
-
</TableHead>
|
|
294
|
-
</TableRow>
|
|
295
|
-
</TableHeader>
|
|
296
|
-
<TableBody>
|
|
297
|
-
{aprovacoes.map((aprovacao) => {
|
|
298
|
-
const titulo = titulosPagar.find(
|
|
299
|
-
(t) => t.id === aprovacao.tituloId
|
|
300
|
-
);
|
|
301
|
-
const fornecedor = titulo
|
|
302
|
-
? getPessoaById(titulo.fornecedorId)
|
|
303
|
-
: null;
|
|
304
|
-
const urgencia =
|
|
305
|
-
urgenciaConfig[
|
|
306
|
-
aprovacao.urgencia as keyof typeof urgenciaConfig
|
|
307
|
-
] || urgenciaConfig.media;
|
|
308
|
-
|
|
309
|
-
return (
|
|
310
|
-
<TableRow key={aprovacao.id}>
|
|
311
|
-
<TableCell className="font-medium">
|
|
312
|
-
{titulo?.documento}
|
|
313
|
-
</TableCell>
|
|
314
|
-
<TableCell>{fornecedor?.nome}</TableCell>
|
|
315
|
-
<TableCell>{aprovacao.solicitante}</TableCell>
|
|
316
|
-
<TableCell className="text-right">
|
|
317
|
-
<Money value={aprovacao.valor} />
|
|
318
|
-
</TableCell>
|
|
319
|
-
<TableCell>
|
|
320
|
-
<span className="text-sm text-muted-foreground">
|
|
321
|
-
{aprovacao.politica}
|
|
322
|
-
</span>
|
|
323
|
-
</TableCell>
|
|
324
|
-
<TableCell>
|
|
325
|
-
<Badge className={urgencia.className} variant="outline">
|
|
326
|
-
{urgencia.label}
|
|
327
|
-
</Badge>
|
|
328
|
-
</TableCell>
|
|
329
|
-
<TableCell>
|
|
330
|
-
{formatarData(aprovacao.dataSolicitacao)}
|
|
331
|
-
</TableCell>
|
|
332
|
-
<TableCell>
|
|
333
|
-
<div className="flex justify-end gap-2">
|
|
334
|
-
<AprovacaoDialog
|
|
335
|
-
tipo="aprovar"
|
|
336
|
-
t={t}
|
|
337
|
-
onConfirm={() =>
|
|
338
|
-
void handleAprovacao(
|
|
339
|
-
aprovacao.id,
|
|
340
|
-
aprovacao.tituloId
|
|
341
|
-
)
|
|
342
|
-
}
|
|
343
|
-
/>
|
|
344
|
-
<AprovacaoDialog
|
|
345
|
-
tipo="reprovar"
|
|
346
|
-
t={t}
|
|
347
|
-
onConfirm={(comment) =>
|
|
348
|
-
void handleReprovacao(
|
|
349
|
-
aprovacao.id,
|
|
350
|
-
aprovacao.tituloId,
|
|
351
|
-
comment
|
|
352
|
-
)
|
|
353
|
-
}
|
|
354
|
-
/>
|
|
355
|
-
</div>
|
|
356
|
-
</TableCell>
|
|
357
|
-
</TableRow>
|
|
358
|
-
);
|
|
359
|
-
})}
|
|
360
|
-
</TableBody>
|
|
361
|
-
</Table>
|
|
362
|
-
) : (
|
|
363
|
-
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
364
|
-
<CheckCircle className="h-12 w-12 text-green-500" />
|
|
365
|
-
<h3 className="mt-4 text-lg font-semibold">{t('empty.title')}</h3>
|
|
366
|
-
<p className="text-muted-foreground">{t('empty.description')}</p>
|
|
367
|
-
</div>
|
|
368
|
-
)}
|
|
369
|
-
</CardContent>
|
|
370
|
-
</Card>
|
|
371
|
-
</Page>
|
|
372
|
-
);
|
|
373
|
-
}
|
|
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
|
+
|
|
24
|
+
import { Page, PageHeader } from '@/components/entity-list';
|
|
25
|
+
import {
|
|
26
|
+
Table,
|
|
27
|
+
TableBody,
|
|
28
|
+
TableCell,
|
|
29
|
+
TableHead,
|
|
30
|
+
TableHeader,
|
|
31
|
+
TableRow,
|
|
32
|
+
} from '@/components/ui/table';
|
|
33
|
+
import { Textarea } from '@/components/ui/textarea';
|
|
34
|
+
import { useApp } from '@hed-hog/next-app-provider';
|
|
35
|
+
import { AlertTriangle, CheckCircle, Clock, XCircle } from 'lucide-react';
|
|
36
|
+
import { useTranslations } from 'next-intl';
|
|
37
|
+
import { useMemo, useState } from 'react';
|
|
38
|
+
import { formatarData } from '../../_lib/formatters';
|
|
39
|
+
import { useFinanceData } from '../../_lib/use-finance-data';
|
|
40
|
+
|
|
41
|
+
function AprovacaoDialog({
|
|
42
|
+
tipo,
|
|
43
|
+
onConfirm,
|
|
44
|
+
t,
|
|
45
|
+
}: {
|
|
46
|
+
tipo: 'aprovar' | 'reprovar';
|
|
47
|
+
onConfirm: (comment?: string) => void;
|
|
48
|
+
t: ReturnType<typeof useTranslations>;
|
|
49
|
+
}) {
|
|
50
|
+
const isAprovar = tipo === 'aprovar';
|
|
51
|
+
const [comment, setComment] = useState('');
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Dialog>
|
|
55
|
+
<DialogTrigger asChild>
|
|
56
|
+
<Button variant={isAprovar ? 'default' : 'outline'} size="sm">
|
|
57
|
+
{isAprovar ? (
|
|
58
|
+
<>
|
|
59
|
+
<CheckCircle className="mr-2 h-4 w-4" />
|
|
60
|
+
{t('dialog.approveAction')}
|
|
61
|
+
</>
|
|
62
|
+
) : (
|
|
63
|
+
<>
|
|
64
|
+
<XCircle className="mr-2 h-4 w-4" />
|
|
65
|
+
{t('dialog.rejectAction')}
|
|
66
|
+
</>
|
|
67
|
+
)}
|
|
68
|
+
</Button>
|
|
69
|
+
</DialogTrigger>
|
|
70
|
+
<DialogContent>
|
|
71
|
+
<DialogHeader>
|
|
72
|
+
<DialogTitle>
|
|
73
|
+
{isAprovar ? t('dialog.approveTitle') : t('dialog.rejectTitle')}
|
|
74
|
+
</DialogTitle>
|
|
75
|
+
<DialogDescription>
|
|
76
|
+
{isAprovar
|
|
77
|
+
? t('dialog.approveDescription')
|
|
78
|
+
: t('dialog.rejectDescription')}
|
|
79
|
+
</DialogDescription>
|
|
80
|
+
</DialogHeader>
|
|
81
|
+
<div className="space-y-4">
|
|
82
|
+
<div className="space-y-2">
|
|
83
|
+
<Label htmlFor="comentario">{t('dialog.comment')}</Label>
|
|
84
|
+
<Textarea
|
|
85
|
+
id="comentario"
|
|
86
|
+
value={comment}
|
|
87
|
+
onChange={(event) => setComment(event.target.value)}
|
|
88
|
+
placeholder={
|
|
89
|
+
isAprovar
|
|
90
|
+
? t('dialog.optionalCommentPlaceholder')
|
|
91
|
+
: t('dialog.rejectReasonPlaceholder')
|
|
92
|
+
}
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
<DialogFooter>
|
|
97
|
+
<Button type="button" variant="outline">
|
|
98
|
+
{t('dialog.cancel')}
|
|
99
|
+
</Button>
|
|
100
|
+
<Button
|
|
101
|
+
type="button"
|
|
102
|
+
variant={isAprovar ? 'default' : 'destructive'}
|
|
103
|
+
onClick={() => onConfirm(comment || undefined)}
|
|
104
|
+
>
|
|
105
|
+
{isAprovar ? t('dialog.confirmApprove') : t('dialog.confirmReject')}
|
|
106
|
+
</Button>
|
|
107
|
+
</DialogFooter>
|
|
108
|
+
</DialogContent>
|
|
109
|
+
</Dialog>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default function AprovacoesPage() {
|
|
114
|
+
const t = useTranslations('finance.PayableApprovalsPage');
|
|
115
|
+
const { request, showToastHandler } = useApp();
|
|
116
|
+
const { data, refetch } = useFinanceData();
|
|
117
|
+
const { aprovacoesPendentes, titulosPagar, pessoas } = data;
|
|
118
|
+
|
|
119
|
+
const getPessoaById = (id?: string) => pessoas.find((p) => p.id === id);
|
|
120
|
+
|
|
121
|
+
const aprovacoes = useMemo(() => {
|
|
122
|
+
const pendenciasDoBackend = Array.isArray(aprovacoesPendentes)
|
|
123
|
+
? aprovacoesPendentes
|
|
124
|
+
: [];
|
|
125
|
+
|
|
126
|
+
const pendenciasDerivadasDosTitulos = (
|
|
127
|
+
Array.isArray(titulosPagar) ? titulosPagar : []
|
|
128
|
+
)
|
|
129
|
+
.filter(
|
|
130
|
+
(titulo) => titulo.status === 'rascunho' || titulo.status === 'draft'
|
|
131
|
+
)
|
|
132
|
+
.map((titulo) => ({
|
|
133
|
+
id: String(titulo.id),
|
|
134
|
+
tituloId: String(titulo.id),
|
|
135
|
+
solicitante: '-',
|
|
136
|
+
valor: Number(titulo.valorTotal || 0),
|
|
137
|
+
politica: 'Aprovação financeira',
|
|
138
|
+
urgencia: 'media',
|
|
139
|
+
dataSolicitacao: titulo.criadoEm,
|
|
140
|
+
}));
|
|
141
|
+
|
|
142
|
+
const basePendencias =
|
|
143
|
+
pendenciasDoBackend.length > 0
|
|
144
|
+
? pendenciasDoBackend
|
|
145
|
+
: pendenciasDerivadasDosTitulos;
|
|
146
|
+
|
|
147
|
+
return basePendencias;
|
|
148
|
+
}, [aprovacoesPendentes, titulosPagar]);
|
|
149
|
+
|
|
150
|
+
const handleAprovacao = async (_id: string, tituloId?: string) => {
|
|
151
|
+
if (!tituloId) {
|
|
152
|
+
showToastHandler?.('error', 'Título inválido para aprovação');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
await request({
|
|
158
|
+
url: `/finance/accounts-payable/installments/${tituloId}/approve`,
|
|
159
|
+
method: 'PATCH',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await refetch();
|
|
163
|
+
showToastHandler?.('success', 'Título aprovado com sucesso');
|
|
164
|
+
} catch {
|
|
165
|
+
showToastHandler?.('error', 'Não foi possível aprovar o título');
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handleReprovacao = async (
|
|
170
|
+
_id: string,
|
|
171
|
+
tituloId?: string,
|
|
172
|
+
reason?: string
|
|
173
|
+
) => {
|
|
174
|
+
if (!tituloId) {
|
|
175
|
+
showToastHandler?.('error', 'Título inválido para reprovação');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await request({
|
|
181
|
+
url: `/finance/accounts-payable/installments/${tituloId}/reject`,
|
|
182
|
+
method: 'PATCH',
|
|
183
|
+
data: {
|
|
184
|
+
reason: reason || undefined,
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await refetch();
|
|
189
|
+
showToastHandler?.('success', 'Título reprovado com sucesso');
|
|
190
|
+
} catch {
|
|
191
|
+
showToastHandler?.('error', 'Não foi possível reprovar o título');
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const urgenciaConfig = {
|
|
196
|
+
baixa: {
|
|
197
|
+
label: t('urgency.low'),
|
|
198
|
+
className: 'bg-slate-100 text-slate-700',
|
|
199
|
+
},
|
|
200
|
+
media: {
|
|
201
|
+
label: t('urgency.medium'),
|
|
202
|
+
className: 'bg-blue-100 text-blue-700',
|
|
203
|
+
},
|
|
204
|
+
alta: {
|
|
205
|
+
label: t('urgency.high'),
|
|
206
|
+
className: 'bg-orange-100 text-orange-700',
|
|
207
|
+
},
|
|
208
|
+
critica: {
|
|
209
|
+
label: t('urgency.critical'),
|
|
210
|
+
className: 'bg-red-100 text-red-700',
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const totalPendente = aprovacoes.reduce((acc, a) => acc + a.valor, 0);
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<Page>
|
|
218
|
+
<PageHeader
|
|
219
|
+
title={t('header.title')}
|
|
220
|
+
description={t('header.description')}
|
|
221
|
+
breadcrumbs={[
|
|
222
|
+
{ label: t('breadcrumbs.home'), href: '/' },
|
|
223
|
+
{ label: t('breadcrumbs.finance'), href: '/finance' },
|
|
224
|
+
{ label: t('breadcrumbs.current') },
|
|
225
|
+
]}
|
|
226
|
+
/>
|
|
227
|
+
|
|
228
|
+
<div className="grid gap-4 md:grid-cols-3">
|
|
229
|
+
<Card>
|
|
230
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
231
|
+
<CardTitle className="text-sm font-medium">
|
|
232
|
+
{t('cards.pending')}
|
|
233
|
+
</CardTitle>
|
|
234
|
+
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
235
|
+
</CardHeader>
|
|
236
|
+
<CardContent>
|
|
237
|
+
<div className="text-2xl font-bold">{aprovacoes.length}</div>
|
|
238
|
+
</CardContent>
|
|
239
|
+
</Card>
|
|
240
|
+
<Card>
|
|
241
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
242
|
+
<CardTitle className="text-sm font-medium">
|
|
243
|
+
{t('cards.totalValue')}
|
|
244
|
+
</CardTitle>
|
|
245
|
+
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
|
|
246
|
+
</CardHeader>
|
|
247
|
+
<CardContent>
|
|
248
|
+
<div className="text-2xl font-bold">
|
|
249
|
+
<Money value={totalPendente} />
|
|
250
|
+
</div>
|
|
251
|
+
</CardContent>
|
|
252
|
+
</Card>
|
|
253
|
+
<Card>
|
|
254
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
255
|
+
<CardTitle className="text-sm font-medium">
|
|
256
|
+
{t('cards.urgent')}
|
|
257
|
+
</CardTitle>
|
|
258
|
+
<AlertTriangle className="h-4 w-4 text-orange-500" />
|
|
259
|
+
</CardHeader>
|
|
260
|
+
<CardContent>
|
|
261
|
+
<div className="text-2xl font-bold">
|
|
262
|
+
{
|
|
263
|
+
aprovacoes.filter(
|
|
264
|
+
(a) => a.urgencia === 'alta' || a.urgencia === 'critica'
|
|
265
|
+
).length
|
|
266
|
+
}
|
|
267
|
+
</div>
|
|
268
|
+
</CardContent>
|
|
269
|
+
</Card>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<Card>
|
|
273
|
+
<CardHeader>
|
|
274
|
+
<CardTitle>{t('table.title')}</CardTitle>
|
|
275
|
+
<CardDescription>{t('table.description')}</CardDescription>
|
|
276
|
+
</CardHeader>
|
|
277
|
+
<CardContent>
|
|
278
|
+
{aprovacoes.length > 0 ? (
|
|
279
|
+
<Table>
|
|
280
|
+
<TableHeader>
|
|
281
|
+
<TableRow>
|
|
282
|
+
<TableHead>{t('table.headers.document')}</TableHead>
|
|
283
|
+
<TableHead>{t('table.headers.supplier')}</TableHead>
|
|
284
|
+
<TableHead>{t('table.headers.requester')}</TableHead>
|
|
285
|
+
<TableHead className="text-right">
|
|
286
|
+
{t('table.headers.value')}
|
|
287
|
+
</TableHead>
|
|
288
|
+
<TableHead>{t('table.headers.policy')}</TableHead>
|
|
289
|
+
<TableHead>{t('table.headers.urgency')}</TableHead>
|
|
290
|
+
<TableHead>{t('table.headers.date')}</TableHead>
|
|
291
|
+
<TableHead className="text-right">
|
|
292
|
+
{t('table.headers.actions')}
|
|
293
|
+
</TableHead>
|
|
294
|
+
</TableRow>
|
|
295
|
+
</TableHeader>
|
|
296
|
+
<TableBody>
|
|
297
|
+
{aprovacoes.map((aprovacao) => {
|
|
298
|
+
const titulo = titulosPagar.find(
|
|
299
|
+
(t) => t.id === aprovacao.tituloId
|
|
300
|
+
);
|
|
301
|
+
const fornecedor = titulo
|
|
302
|
+
? getPessoaById(titulo.fornecedorId)
|
|
303
|
+
: null;
|
|
304
|
+
const urgencia =
|
|
305
|
+
urgenciaConfig[
|
|
306
|
+
aprovacao.urgencia as keyof typeof urgenciaConfig
|
|
307
|
+
] || urgenciaConfig.media;
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<TableRow key={aprovacao.id}>
|
|
311
|
+
<TableCell className="font-medium">
|
|
312
|
+
{titulo?.documento}
|
|
313
|
+
</TableCell>
|
|
314
|
+
<TableCell>{fornecedor?.nome}</TableCell>
|
|
315
|
+
<TableCell>{aprovacao.solicitante}</TableCell>
|
|
316
|
+
<TableCell className="text-right">
|
|
317
|
+
<Money value={aprovacao.valor} />
|
|
318
|
+
</TableCell>
|
|
319
|
+
<TableCell>
|
|
320
|
+
<span className="text-sm text-muted-foreground">
|
|
321
|
+
{aprovacao.politica}
|
|
322
|
+
</span>
|
|
323
|
+
</TableCell>
|
|
324
|
+
<TableCell>
|
|
325
|
+
<Badge className={urgencia.className} variant="outline">
|
|
326
|
+
{urgencia.label}
|
|
327
|
+
</Badge>
|
|
328
|
+
</TableCell>
|
|
329
|
+
<TableCell>
|
|
330
|
+
{formatarData(aprovacao.dataSolicitacao)}
|
|
331
|
+
</TableCell>
|
|
332
|
+
<TableCell>
|
|
333
|
+
<div className="flex justify-end gap-2">
|
|
334
|
+
<AprovacaoDialog
|
|
335
|
+
tipo="aprovar"
|
|
336
|
+
t={t}
|
|
337
|
+
onConfirm={() =>
|
|
338
|
+
void handleAprovacao(
|
|
339
|
+
aprovacao.id,
|
|
340
|
+
aprovacao.tituloId
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
/>
|
|
344
|
+
<AprovacaoDialog
|
|
345
|
+
tipo="reprovar"
|
|
346
|
+
t={t}
|
|
347
|
+
onConfirm={(comment) =>
|
|
348
|
+
void handleReprovacao(
|
|
349
|
+
aprovacao.id,
|
|
350
|
+
aprovacao.tituloId,
|
|
351
|
+
comment
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
/>
|
|
355
|
+
</div>
|
|
356
|
+
</TableCell>
|
|
357
|
+
</TableRow>
|
|
358
|
+
);
|
|
359
|
+
})}
|
|
360
|
+
</TableBody>
|
|
361
|
+
</Table>
|
|
362
|
+
) : (
|
|
363
|
+
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
364
|
+
<CheckCircle className="h-12 w-12 text-green-500" />
|
|
365
|
+
<h3 className="mt-4 text-lg font-semibold">{t('empty.title')}</h3>
|
|
366
|
+
<p className="text-muted-foreground">{t('empty.description')}</p>
|
|
367
|
+
</div>
|
|
368
|
+
)}
|
|
369
|
+
</CardContent>
|
|
370
|
+
</Card>
|
|
371
|
+
</Page>
|
|
372
|
+
);
|
|
373
|
+
}
|