@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.
Files changed (40) hide show
  1. package/README.md +228 -126
  2. package/dist/dto/create-bank-reconciliation.dto.d.ts +8 -0
  3. package/dist/dto/create-bank-reconciliation.dto.d.ts.map +1 -0
  4. package/dist/dto/create-bank-reconciliation.dto.js +43 -0
  5. package/dist/dto/create-bank-reconciliation.dto.js.map +1 -0
  6. package/dist/finance-data.controller.d.ts +2 -0
  7. package/dist/finance-data.controller.d.ts.map +1 -1
  8. package/dist/finance-statements.controller.d.ts +42 -0
  9. package/dist/finance-statements.controller.d.ts.map +1 -1
  10. package/dist/finance-statements.controller.js +13 -0
  11. package/dist/finance-statements.controller.js.map +1 -1
  12. package/dist/finance.service.d.ts +44 -0
  13. package/dist/finance.service.d.ts.map +1 -1
  14. package/dist/finance.service.js +98 -9
  15. package/dist/finance.service.js.map +1 -1
  16. package/hedhog/data/route.yaml +9 -0
  17. package/hedhog/frontend/app/_components/person-field-with-create.tsx.ejs +126 -126
  18. package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +373 -373
  19. package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +1270 -1270
  20. package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +982 -982
  21. package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +686 -686
  22. package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +152 -32
  23. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +986 -986
  24. package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +492 -492
  25. package/hedhog/frontend/app/page.tsx.ejs +372 -372
  26. package/hedhog/frontend/app/planning/cash-flow-forecast/page.tsx.ejs +329 -329
  27. package/hedhog/frontend/app/planning/receivables-calendar/page.tsx.ejs +227 -227
  28. package/hedhog/frontend/app/planning/scenarios/page.tsx.ejs +408 -408
  29. package/hedhog/frontend/messages/en.json +15 -5
  30. package/hedhog/frontend/messages/pt.json +15 -5
  31. package/package.json +7 -7
  32. package/src/dto/create-bank-reconciliation.dto.ts +24 -0
  33. package/src/finance-statements.controller.ts +14 -0
  34. package/src/finance.module.ts +43 -43
  35. package/src/finance.service.ts +118 -0
  36. package/src/index.ts +14 -14
  37. package/dist/finance.controller.d.ts +0 -276
  38. package/dist/finance.controller.d.ts.map +0 -1
  39. package/dist/finance.controller.js +0 -110
  40. 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
+ }