@hed-hog/finance 0.0.256 → 0.0.260

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 (64) hide show
  1. package/dist/dto/create-bank-statement-adjustment.dto.d.ts +8 -0
  2. package/dist/dto/create-bank-statement-adjustment.dto.d.ts.map +1 -0
  3. package/dist/dto/create-bank-statement-adjustment.dto.js +50 -0
  4. package/dist/dto/create-bank-statement-adjustment.dto.js.map +1 -0
  5. package/dist/dto/create-transfer.dto.d.ts +8 -0
  6. package/dist/dto/create-transfer.dto.d.ts.map +1 -0
  7. package/dist/dto/create-transfer.dto.js +52 -0
  8. package/dist/dto/create-transfer.dto.js.map +1 -0
  9. package/dist/dto/register-collection-agreement.dto.d.ts +7 -0
  10. package/dist/dto/register-collection-agreement.dto.d.ts.map +1 -0
  11. package/dist/dto/register-collection-agreement.dto.js +37 -0
  12. package/dist/dto/register-collection-agreement.dto.js.map +1 -0
  13. package/dist/dto/send-collection.dto.d.ts +5 -0
  14. package/dist/dto/send-collection.dto.d.ts.map +1 -0
  15. package/dist/dto/send-collection.dto.js +29 -0
  16. package/dist/dto/send-collection.dto.js.map +1 -0
  17. package/dist/dto/settle-installment.dto.d.ts +1 -0
  18. package/dist/dto/settle-installment.dto.d.ts.map +1 -1
  19. package/dist/dto/settle-installment.dto.js +6 -0
  20. package/dist/dto/settle-installment.dto.js.map +1 -1
  21. package/dist/finance-collections.controller.d.ts +35 -0
  22. package/dist/finance-collections.controller.d.ts.map +1 -0
  23. package/dist/finance-collections.controller.js +65 -0
  24. package/dist/finance-collections.controller.js.map +1 -0
  25. package/dist/finance-data.controller.d.ts +4 -0
  26. package/dist/finance-data.controller.d.ts.map +1 -1
  27. package/dist/finance-installments.controller.d.ts +44 -0
  28. package/dist/finance-installments.controller.d.ts.map +1 -1
  29. package/dist/finance-statements.controller.d.ts +16 -2
  30. package/dist/finance-statements.controller.d.ts.map +1 -1
  31. package/dist/finance-statements.controller.js +34 -6
  32. package/dist/finance-statements.controller.js.map +1 -1
  33. package/dist/finance-transfers.controller.d.ts +23 -0
  34. package/dist/finance-transfers.controller.d.ts.map +1 -0
  35. package/dist/finance-transfers.controller.js +56 -0
  36. package/dist/finance-transfers.controller.js.map +1 -0
  37. package/dist/finance.module.d.ts.map +1 -1
  38. package/dist/finance.module.js +4 -0
  39. package/dist/finance.module.js.map +1 -1
  40. package/dist/finance.service.d.ts +115 -2
  41. package/dist/finance.service.d.ts.map +1 -1
  42. package/dist/finance.service.js +632 -8
  43. package/dist/finance.service.js.map +1 -1
  44. package/dist/index.d.ts +2 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +2 -0
  47. package/dist/index.js.map +1 -1
  48. package/hedhog/data/route.yaml +63 -0
  49. package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +643 -440
  50. package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +825 -477
  51. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +367 -43
  52. package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +315 -75
  53. package/package.json +6 -6
  54. package/src/dto/create-bank-statement-adjustment.dto.ts +38 -0
  55. package/src/dto/create-transfer.dto.ts +46 -0
  56. package/src/dto/register-collection-agreement.dto.ts +27 -0
  57. package/src/dto/send-collection.dto.ts +14 -0
  58. package/src/dto/settle-installment.dto.ts +5 -0
  59. package/src/finance-collections.controller.ts +34 -0
  60. package/src/finance-statements.controller.ts +29 -1
  61. package/src/finance-transfers.controller.ts +26 -0
  62. package/src/finance.module.ts +4 -0
  63. package/src/finance.service.ts +775 -5
  64. package/src/index.ts +2 -0
@@ -1,477 +1,825 @@
1
- 'use client';
2
-
3
- import { Page, PageHeader } from '@/components/entity-list';
4
- import { Badge } from '@/components/ui/badge';
5
- import { Button } from '@/components/ui/button';
6
- import {
7
- Card,
8
- CardContent,
9
- CardDescription,
10
- CardHeader,
11
- CardTitle,
12
- } from '@/components/ui/card';
13
- import { Checkbox } from '@/components/ui/checkbox';
14
- import {
15
- Dialog,
16
- DialogContent,
17
- DialogDescription,
18
- DialogFooter,
19
- DialogHeader,
20
- DialogTitle,
21
- DialogTrigger,
22
- } from '@/components/ui/dialog';
23
- import { Input } from '@/components/ui/input';
24
- import { InputMoney } from '@/components/ui/input-money';
25
- import { Label } from '@/components/ui/label';
26
- import { Money } from '@/components/ui/money';
27
- import { Progress } from '@/components/ui/progress';
28
- import {
29
- Select,
30
- SelectContent,
31
- SelectItem,
32
- SelectTrigger,
33
- SelectValue,
34
- } from '@/components/ui/select';
35
- import { StatusBadge } from '@/components/ui/status-badge';
36
- import { Check, DollarSign, Link2, RefreshCw } from 'lucide-react';
37
- import { useTranslations } from 'next-intl';
38
- import { usePathname, useRouter, useSearchParams } from 'next/navigation';
39
- import { useEffect, useState } from 'react';
40
- import { formatarData, formatarMoeda } from '../../_lib/formatters';
41
- import { useFinanceData } from '../../_lib/use-finance-data';
42
-
43
- function CriarAjusteDialog({ t }: { t: ReturnType<typeof useTranslations> }) {
44
- return (
45
- <Dialog>
46
- <DialogTrigger asChild>
47
- <Button variant="outline" size="sm">
48
- <DollarSign className="mr-2 h-4 w-4" />
49
- {t('adjustment.action')}
50
- </Button>
51
- </DialogTrigger>
52
- <DialogContent>
53
- <DialogHeader>
54
- <DialogTitle>{t('adjustment.title')}</DialogTitle>
55
- <DialogDescription>{t('adjustment.description')}</DialogDescription>
56
- </DialogHeader>
57
- <div className="space-y-4">
58
- <div className="space-y-2">
59
- <Label htmlFor="tipo">{t('adjustment.type')}</Label>
60
- <Select>
61
- <SelectTrigger>
62
- <SelectValue placeholder={t('common.select')} />
63
- </SelectTrigger>
64
- <SelectContent>
65
- <SelectItem value="tarifa">
66
- {t('adjustment.types.bankFee')}
67
- </SelectItem>
68
- <SelectItem value="juros">
69
- {t('adjustment.types.interest')}
70
- </SelectItem>
71
- <SelectItem value="diferenca">
72
- {t('adjustment.types.valueDifference')}
73
- </SelectItem>
74
- <SelectItem value="outro">
75
- {t('adjustment.types.other')}
76
- </SelectItem>
77
- </SelectContent>
78
- </Select>
79
- </div>
80
- <div className="space-y-2">
81
- <Label htmlFor="valor">{t('adjustment.value')}</Label>
82
- <InputMoney id="valor" placeholder="0,00" />
83
- </div>
84
- <div className="space-y-2">
85
- <Label htmlFor="descricao">{t('adjustment.details')}</Label>
86
- <Input
87
- id="descricao"
88
- placeholder={t('adjustment.detailsPlaceholder')}
89
- />
90
- </div>
91
- </div>
92
- <DialogFooter>
93
- <Button type="button" variant="outline">
94
- {t('common.cancel')}
95
- </Button>
96
- <Button type="button">{t('adjustment.submit')}</Button>
97
- </DialogFooter>
98
- </DialogContent>
99
- </Dialog>
100
- );
101
- }
102
-
103
- export default function ConciliacaoPage() {
104
- const t = useTranslations('finance.BankReconciliationPage');
105
- const pathname = usePathname();
106
- const router = useRouter();
107
- const searchParams = useSearchParams();
108
- const bankAccountIdFromUrl = searchParams.get('bank_account_id');
109
- const { data } = useFinanceData();
110
- const { extratos, titulosPagar, titulosReceber, contasBancarias, pessoas } =
111
- data;
112
-
113
- const getPessoaById = (id?: string) => pessoas.find((p) => p.id === id);
114
-
115
- const extratosPendentes = extratos.filter(
116
- (e) => e.statusConciliacao === 'pendente'
117
- );
118
-
119
- const titulosAbertos = [
120
- ...titulosPagar.flatMap((t: any) =>
121
- t.parcelas
122
- .filter((p: any) => p.status === 'aberto' || p.status === 'vencido')
123
- .map((p: any) => ({
124
- id: `pagar-${t.id}-${p.id}`,
125
- tipo: 'pagar' as const,
126
- documento: t.documento,
127
- pessoa: getPessoaById(t.fornecedorId)?.nome || '',
128
- vencimento: p.vencimento,
129
- valor: p.valor,
130
- }))
131
- ),
132
- ...titulosReceber.flatMap((t: any) =>
133
- t.parcelas
134
- .filter((p: any) => p.status === 'aberto' || p.status === 'vencido')
135
- .map((p: any) => ({
136
- id: `receber-${t.id}-${p.id}`,
137
- tipo: 'receber' as const,
138
- documento: t.documento,
139
- pessoa: getPessoaById(t.clienteId)?.nome || '',
140
- vencimento: p.vencimento,
141
- valor: p.valor,
142
- }))
143
- ),
144
- ];
145
-
146
- const [contaFilter, setContaFilter] = useState<string>('');
147
- const [selectedExtrato, setSelectedExtrato] = useState<string | null>(null);
148
- const [selectedTitulo, setSelectedTitulo] = useState<string | null>(null);
149
-
150
- useEffect(() => {
151
- const firstAccount = contasBancarias[0];
152
-
153
- if (!firstAccount) {
154
- return;
155
- }
156
-
157
- const hasAccountFromUrl =
158
- !!bankAccountIdFromUrl &&
159
- contasBancarias.some((account) => account.id === bankAccountIdFromUrl);
160
-
161
- const nextAccountId = hasAccountFromUrl
162
- ? (bankAccountIdFromUrl as string)
163
- : firstAccount.id;
164
-
165
- if (contaFilter !== nextAccountId) {
166
- setContaFilter(nextAccountId);
167
- }
168
-
169
- if (bankAccountIdFromUrl !== nextAccountId) {
170
- const params = new URLSearchParams(searchParams.toString());
171
- params.set('bank_account_id', nextAccountId);
172
- router.replace(`${pathname}?${params.toString()}`);
173
- }
174
- }, [
175
- bankAccountIdFromUrl,
176
- contaFilter,
177
- contasBancarias,
178
- pathname,
179
- router,
180
- searchParams,
181
- ]);
182
-
183
- const extratosFiltered = extratosPendentes.filter(
184
- (e) => e.contaBancariaId === contaFilter
185
- );
186
-
187
- const totalConciliado = extratos.filter(
188
- (e) =>
189
- e.contaBancariaId === contaFilter && e.statusConciliacao === 'conciliado'
190
- ).length;
191
- const totalExtratoConta = extratos.filter(
192
- (e) => e.contaBancariaId === contaFilter
193
- ).length;
194
- const percentualConciliado =
195
- totalExtratoConta > 0
196
- ? Math.round((totalConciliado / totalExtratoConta) * 100)
197
- : 0;
198
-
199
- return (
200
- <Page>
201
- <PageHeader
202
- title={t('header.title')}
203
- description={t('header.description')}
204
- breadcrumbs={[
205
- { label: t('breadcrumbs.home'), href: '/' },
206
- { label: t('breadcrumbs.finance'), href: '/finance' },
207
- { label: t('breadcrumbs.current') },
208
- ]}
209
- />
210
-
211
- <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
212
- <Select
213
- value={contaFilter}
214
- onValueChange={(value) => {
215
- setContaFilter(value);
216
-
217
- const params = new URLSearchParams(searchParams.toString());
218
- params.set('bank_account_id', value);
219
- router.replace(`${pathname}?${params.toString()}`);
220
- }}
221
- >
222
- <SelectTrigger className="w-full sm:w-[280px]">
223
- <SelectValue placeholder={t('filters.selectAccount')} />
224
- </SelectTrigger>
225
- <SelectContent>
226
- {contasBancarias.map((conta) => (
227
- <SelectItem key={conta.id} value={conta.id}>
228
- {conta.banco} - {conta.descricao}
229
- </SelectItem>
230
- ))}
231
- </SelectContent>
232
- </Select>
233
- <div className="flex gap-2">
234
- <CriarAjusteDialog t={t} />
235
- <Button disabled={!selectedExtrato || !selectedTitulo}>
236
- <Link2 className="mr-2 h-4 w-4" />
237
- {t('actions.reconcileSelected')}
238
- </Button>
239
- </div>
240
- </div>
241
-
242
- <div className="grid gap-4 md:grid-cols-4">
243
- <Card>
244
- <CardHeader className="pb-2">
245
- <CardTitle className="text-sm font-medium">
246
- {t('cards.reconciled')}
247
- </CardTitle>
248
- </CardHeader>
249
- <CardContent>
250
- <div className="text-2xl font-bold">{percentualConciliado}%</div>
251
- <Progress value={percentualConciliado} className="mt-2" />
252
- </CardContent>
253
- </Card>
254
- <Card>
255
- <CardHeader className="pb-2">
256
- <CardTitle className="text-sm font-medium">
257
- {t('cards.pending')}
258
- </CardTitle>
259
- </CardHeader>
260
- <CardContent>
261
- <div className="text-2xl font-bold">{extratosFiltered.length}</div>
262
- <p className="text-xs text-muted-foreground">
263
- {t('cards.transactions')}
264
- </p>
265
- </CardContent>
266
- </Card>
267
- <Card>
268
- <CardHeader className="pb-2">
269
- <CardTitle className="text-sm font-medium">
270
- {t('cards.discrepancies')}
271
- </CardTitle>
272
- </CardHeader>
273
- <CardContent>
274
- <div className="text-2xl font-bold text-orange-600">0</div>
275
- <p className="text-xs text-muted-foreground">
276
- {t('cards.toResolve')}
277
- </p>
278
- </CardContent>
279
- </Card>
280
- <Card>
281
- <CardHeader className="pb-2">
282
- <CardTitle className="text-sm font-medium">
283
- {t('cards.difference')}
284
- </CardTitle>
285
- </CardHeader>
286
- <CardContent>
287
- <div className="text-2xl font-bold">
288
- <Money value={2230.5} />
289
- </div>
290
- <p className="text-xs text-muted-foreground">
291
- {t('cards.notReconciled')}
292
- </p>
293
- </CardContent>
294
- </Card>
295
- </div>
296
-
297
- <div className="grid gap-6 lg:grid-cols-2">
298
- <Card>
299
- <CardHeader>
300
- <CardTitle className="flex items-center gap-2">
301
- <RefreshCw className="h-4 w-4" />
302
- {t('statement.title')}
303
- </CardTitle>
304
- <CardDescription>{t('statement.description')}</CardDescription>
305
- </CardHeader>
306
- <CardContent>
307
- <div className="space-y-2">
308
- {extratosFiltered.length > 0 ? (
309
- extratosFiltered.map((extrato) => (
310
- <div
311
- key={extrato.id}
312
- className={`flex items-center gap-3 rounded-lg border p-3 cursor-pointer transition-colors ${
313
- selectedExtrato === extrato.id
314
- ? 'border-primary bg-primary/5'
315
- : 'hover:bg-muted/50'
316
- }`}
317
- onClick={() =>
318
- setSelectedExtrato(
319
- selectedExtrato === extrato.id ? null : extrato.id
320
- )
321
- }
322
- >
323
- <Checkbox
324
- checked={selectedExtrato === extrato.id}
325
- onCheckedChange={() =>
326
- setSelectedExtrato(
327
- selectedExtrato === extrato.id ? null : extrato.id
328
- )
329
- }
330
- />
331
- <div className="flex-1">
332
- <div className="flex items-center justify-between">
333
- <span className="text-sm font-medium">
334
- {extrato.descricao}
335
- </span>
336
- <span
337
- className={`font-semibold ${
338
- extrato.tipo === 'entrada'
339
- ? 'text-green-600'
340
- : 'text-red-600'
341
- }`}
342
- >
343
- {extrato.tipo === 'saida' && '-'}
344
- {formatarMoeda(extrato.valor)}
345
- </span>
346
- </div>
347
- <div className="flex items-center gap-2 text-xs text-muted-foreground">
348
- <span>{formatarData(extrato.data)}</span>
349
- <StatusBadge
350
- status={extrato.statusConciliacao}
351
- type="conciliacao"
352
- />
353
- </div>
354
- </div>
355
- </div>
356
- ))
357
- ) : (
358
- <div className="flex flex-col items-center justify-center py-8 text-center">
359
- <Check className="h-12 w-12 text-green-500" />
360
- <p className="mt-2 font-medium">
361
- {t('statement.emptyTitle')}
362
- </p>
363
- <p className="text-sm text-muted-foreground">
364
- {t('statement.emptyDescription')}
365
- </p>
366
- </div>
367
- )}
368
- </div>
369
- </CardContent>
370
- </Card>
371
-
372
- <Card>
373
- <CardHeader>
374
- <CardTitle className="flex items-center gap-2">
375
- <DollarSign className="h-4 w-4" />
376
- {t('openTitles.title')}
377
- </CardTitle>
378
- <CardDescription>{t('openTitles.description')}</CardDescription>
379
- </CardHeader>
380
- <CardContent>
381
- <div className="space-y-2 max-h-[400px] overflow-y-auto">
382
- {titulosAbertos.map((titulo) => (
383
- <div
384
- key={titulo.id}
385
- className={`flex items-center gap-3 rounded-lg border p-3 cursor-pointer transition-colors ${
386
- selectedTitulo === titulo.id
387
- ? 'border-primary bg-primary/5'
388
- : 'hover:bg-muted/50'
389
- }`}
390
- onClick={() =>
391
- setSelectedTitulo(
392
- selectedTitulo === titulo.id ? null : titulo.id
393
- )
394
- }
395
- >
396
- <Checkbox
397
- checked={selectedTitulo === titulo.id}
398
- onCheckedChange={() =>
399
- setSelectedTitulo(
400
- selectedTitulo === titulo.id ? null : titulo.id
401
- )
402
- }
403
- />
404
- <div className="flex-1">
405
- <div className="flex items-center justify-between">
406
- <div className="flex items-center gap-2">
407
- <span className="text-sm font-medium">
408
- {titulo.documento}
409
- </span>
410
- <Badge
411
- variant="outline"
412
- className={
413
- titulo.tipo === 'pagar'
414
- ? 'border-red-200 text-red-700'
415
- : 'border-green-200 text-green-700'
416
- }
417
- >
418
- {titulo.tipo === 'pagar'
419
- ? t('openTitles.pay')
420
- : t('openTitles.receive')}
421
- </Badge>
422
- </div>
423
- <span className="font-semibold">
424
- {formatarMoeda(titulo.valor)}
425
- </span>
426
- </div>
427
- <div className="text-xs text-muted-foreground">
428
- {t('openTitles.personDue', {
429
- person: titulo.pessoa,
430
- dueDate: formatarData(titulo.vencimento),
431
- })}
432
- </div>
433
- </div>
434
- </div>
435
- ))}
436
- </div>
437
- </CardContent>
438
- </Card>
439
- </div>
440
-
441
- {selectedExtrato && selectedTitulo && (
442
- <Card className="border-primary">
443
- <CardHeader>
444
- <CardTitle className="flex items-center gap-2 text-primary">
445
- <Link2 className="h-4 w-4" />
446
- {t('ready.title')}
447
- </CardTitle>
448
- </CardHeader>
449
- <CardContent>
450
- <div className="flex items-center justify-between">
451
- <div>
452
- <p className="text-sm text-muted-foreground">
453
- {t('ready.description')}
454
- </p>
455
- </div>
456
- <div className="flex gap-2">
457
- <Button
458
- variant="outline"
459
- onClick={() => {
460
- setSelectedExtrato(null);
461
- setSelectedTitulo(null);
462
- }}
463
- >
464
- {t('common.cancel')}
465
- </Button>
466
- <Button>
467
- <Check className="mr-2 h-4 w-4" />
468
- {t('actions.reconcile')}
469
- </Button>
470
- </div>
471
- </div>
472
- </CardContent>
473
- </Card>
474
- )}
475
- </Page>
476
- );
477
- }
1
+ 'use client';
2
+
3
+ import { Page, PageHeader } from '@/components/entity-list';
4
+ import { Badge } from '@/components/ui/badge';
5
+ import { Button } from '@/components/ui/button';
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from '@/components/ui/card';
13
+ import { Checkbox } from '@/components/ui/checkbox';
14
+ import {
15
+ Form,
16
+ FormControl,
17
+ FormField,
18
+ FormItem,
19
+ FormLabel,
20
+ FormMessage,
21
+ } from '@/components/ui/form';
22
+ import { Input } from '@/components/ui/input';
23
+ import { InputMoney } from '@/components/ui/input-money';
24
+ import { Money } from '@/components/ui/money';
25
+ import { Progress } from '@/components/ui/progress';
26
+ import {
27
+ Select,
28
+ SelectContent,
29
+ SelectItem,
30
+ SelectTrigger,
31
+ SelectValue,
32
+ } from '@/components/ui/select';
33
+ import {
34
+ Sheet,
35
+ SheetContent,
36
+ SheetDescription,
37
+ SheetHeader,
38
+ SheetTitle,
39
+ } from '@/components/ui/sheet';
40
+ import { StatusBadge } from '@/components/ui/status-badge';
41
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
42
+ import { zodResolver } from '@hookform/resolvers/zod';
43
+ import { Check, DollarSign, Link2, RefreshCw } from 'lucide-react';
44
+ import { useTranslations } from 'next-intl';
45
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
46
+ import { useEffect, useMemo, useState } from 'react';
47
+ import { useForm } from 'react-hook-form';
48
+ import { z } from 'zod';
49
+ import { formatarData, formatarMoeda } from '../../_lib/formatters';
50
+
51
+ type BankAccount = {
52
+ id: string;
53
+ banco: string;
54
+ descricao: string;
55
+ };
56
+
57
+ type Statement = {
58
+ id: string;
59
+ contaBancariaId: string;
60
+ data: string;
61
+ descricao: string;
62
+ valor: number;
63
+ tipo: 'entrada' | 'saida';
64
+ statusConciliacao:
65
+ | 'importado'
66
+ | 'pendente'
67
+ | 'conciliado'
68
+ | 'estornado'
69
+ | 'ajustado';
70
+ };
71
+
72
+ type Installment = {
73
+ id: string;
74
+ vencimento: string;
75
+ valor: number;
76
+ status: string;
77
+ };
78
+
79
+ type FinancialTitle = {
80
+ id: string;
81
+ documento: string;
82
+ fornecedor?: string;
83
+ cliente?: string;
84
+ parcelas: Installment[];
85
+ };
86
+
87
+ type PaginatedResponse<T> = {
88
+ data: T[];
89
+ };
90
+
91
+ type OpenTitleItem = {
92
+ id: string;
93
+ tipo: 'pagar' | 'receber';
94
+ titleId: string;
95
+ installmentId: string;
96
+ documento: string;
97
+ pessoa: string;
98
+ vencimento: string;
99
+ valor: number;
100
+ };
101
+
102
+ type BankReconciliationSummary = {
103
+ discrepancies: number;
104
+ difference: number;
105
+ };
106
+
107
+ const ajusteSchema = z.object({
108
+ type: z.string().trim().min(1),
109
+ value: z.number().min(0.01),
110
+ description: z.string().optional(),
111
+ });
112
+
113
+ type AjusteFormValues = z.infer<typeof ajusteSchema>;
114
+
115
+ function CriarAjusteSheet({
116
+ t,
117
+ contaFilter,
118
+ onCreated,
119
+ }: {
120
+ t: ReturnType<typeof useTranslations>;
121
+ contaFilter: string;
122
+ onCreated: () => Promise<void> | void;
123
+ }) {
124
+ const { request, showToastHandler } = useApp();
125
+ const [open, setOpen] = useState(false);
126
+
127
+ const form = useForm<AjusteFormValues>({
128
+ resolver: zodResolver(ajusteSchema),
129
+ defaultValues: {
130
+ type: '',
131
+ value: 0,
132
+ description: '',
133
+ },
134
+ });
135
+
136
+ useEffect(() => {
137
+ if (!open) {
138
+ return;
139
+ }
140
+
141
+ form.reset({
142
+ type: '',
143
+ value: 0,
144
+ description: '',
145
+ });
146
+ }, [form, open]);
147
+
148
+ const handleSubmit = async (values: AjusteFormValues) => {
149
+ if (!contaFilter) {
150
+ showToastHandler?.('error', 'Selecione uma conta bancária');
151
+ return;
152
+ }
153
+
154
+ try {
155
+ await request({
156
+ url: '/finance/statements/adjustments',
157
+ method: 'POST',
158
+ data: {
159
+ bank_account_id: Number(contaFilter),
160
+ amount: values.value,
161
+ type: values.type,
162
+ description: values.description?.trim() || undefined,
163
+ },
164
+ });
165
+
166
+ await onCreated();
167
+ showToastHandler?.('success', 'Ajuste criado com sucesso');
168
+ setOpen(false);
169
+ } catch {
170
+ showToastHandler?.('error', 'Não foi possível criar o ajuste');
171
+ }
172
+ };
173
+
174
+ return (
175
+ <Sheet open={open} onOpenChange={setOpen}>
176
+ <Button variant="outline" size="sm" onClick={() => setOpen(true)}>
177
+ <DollarSign className="mr-2 h-4 w-4" />
178
+ {t('adjustment.action')}
179
+ </Button>
180
+
181
+ <SheetContent className="w-full sm:max-w-lg overflow-y-auto">
182
+ <SheetHeader>
183
+ <SheetTitle>{t('adjustment.title')}</SheetTitle>
184
+ <SheetDescription>{t('adjustment.description')}</SheetDescription>
185
+ </SheetHeader>
186
+
187
+ <Form {...form}>
188
+ <form
189
+ className="space-y-4 px-4"
190
+ onSubmit={form.handleSubmit(handleSubmit)}
191
+ >
192
+ <FormField
193
+ control={form.control}
194
+ name="type"
195
+ render={({ field }) => (
196
+ <FormItem>
197
+ <FormLabel>{t('adjustment.type')}</FormLabel>
198
+ <Select value={field.value} onValueChange={field.onChange}>
199
+ <FormControl>
200
+ <SelectTrigger>
201
+ <SelectValue placeholder={t('common.select')} />
202
+ </SelectTrigger>
203
+ </FormControl>
204
+ <SelectContent>
205
+ <SelectItem value="tarifa">
206
+ {t('adjustment.types.bankFee')}
207
+ </SelectItem>
208
+ <SelectItem value="juros">
209
+ {t('adjustment.types.interest')}
210
+ </SelectItem>
211
+ <SelectItem value="diferenca">
212
+ {t('adjustment.types.valueDifference')}
213
+ </SelectItem>
214
+ <SelectItem value="outro">
215
+ {t('adjustment.types.other')}
216
+ </SelectItem>
217
+ </SelectContent>
218
+ </Select>
219
+ <FormMessage />
220
+ </FormItem>
221
+ )}
222
+ />
223
+
224
+ <FormField
225
+ control={form.control}
226
+ name="value"
227
+ render={({ field }) => (
228
+ <FormItem>
229
+ <FormLabel>{t('adjustment.value')}</FormLabel>
230
+ <FormControl>
231
+ <InputMoney
232
+ ref={field.ref}
233
+ name={field.name}
234
+ value={field.value}
235
+ onBlur={field.onBlur}
236
+ onValueChange={(value) => field.onChange(value ?? 0)}
237
+ placeholder="0,00"
238
+ />
239
+ </FormControl>
240
+ <FormMessage />
241
+ </FormItem>
242
+ )}
243
+ />
244
+
245
+ <FormField
246
+ control={form.control}
247
+ name="description"
248
+ render={({ field }) => (
249
+ <FormItem>
250
+ <FormLabel>{t('adjustment.details')}</FormLabel>
251
+ <FormControl>
252
+ <Input
253
+ placeholder={t('adjustment.detailsPlaceholder')}
254
+ {...field}
255
+ value={field.value || ''}
256
+ />
257
+ </FormControl>
258
+ <FormMessage />
259
+ </FormItem>
260
+ )}
261
+ />
262
+
263
+ <div className="flex justify-end gap-2 pt-4">
264
+ <Button
265
+ type="button"
266
+ variant="outline"
267
+ onClick={() => setOpen(false)}
268
+ >
269
+ {t('common.cancel')}
270
+ </Button>
271
+ <Button type="submit" disabled={form.formState.isSubmitting}>
272
+ {t('adjustment.submit')}
273
+ </Button>
274
+ </div>
275
+ </form>
276
+ </Form>
277
+ </SheetContent>
278
+ </Sheet>
279
+ );
280
+ }
281
+
282
+ export default function ConciliacaoPage() {
283
+ const t = useTranslations('finance.BankReconciliationPage');
284
+ const { request, showToastHandler } = useApp();
285
+ const pathname = usePathname();
286
+ const router = useRouter();
287
+ const searchParams = useSearchParams();
288
+ const bankAccountIdFromUrl = searchParams.get('bank_account_id');
289
+
290
+ const [contaFilter, setContaFilter] = useState<string>('');
291
+ const [selectedExtrato, setSelectedExtrato] = useState<string | null>(null);
292
+ const [selectedTitulo, setSelectedTitulo] = useState<string | null>(null);
293
+
294
+ const { data: contasBancarias = [] } = useQuery<BankAccount[]>({
295
+ queryKey: ['finance-bank-accounts'],
296
+ queryFn: async () => {
297
+ const response = await request({
298
+ url: '/finance/bank-accounts',
299
+ method: 'GET',
300
+ });
301
+
302
+ return (response?.data || []) as BankAccount[];
303
+ },
304
+ });
305
+
306
+ useEffect(() => {
307
+ const firstAccount = contasBancarias[0];
308
+
309
+ if (!firstAccount) {
310
+ return;
311
+ }
312
+
313
+ const hasAccountFromUrl =
314
+ !!bankAccountIdFromUrl &&
315
+ contasBancarias.some((account) => account.id === bankAccountIdFromUrl);
316
+
317
+ const nextAccountId = hasAccountFromUrl
318
+ ? (bankAccountIdFromUrl as string)
319
+ : firstAccount.id;
320
+
321
+ if (contaFilter !== nextAccountId) {
322
+ setContaFilter(nextAccountId);
323
+ }
324
+
325
+ if (bankAccountIdFromUrl !== nextAccountId) {
326
+ const params = new URLSearchParams(searchParams.toString());
327
+ params.set('bank_account_id', nextAccountId);
328
+ router.replace(`${pathname}?${params.toString()}`);
329
+ }
330
+ }, [
331
+ bankAccountIdFromUrl,
332
+ contaFilter,
333
+ contasBancarias,
334
+ pathname,
335
+ router,
336
+ searchParams,
337
+ ]);
338
+
339
+ const { data: extratos = [], refetch: refetchExtratos } = useQuery<
340
+ Statement[]
341
+ >({
342
+ queryKey: ['finance-statements', contaFilter],
343
+ queryFn: async () => {
344
+ if (!contaFilter) {
345
+ return [];
346
+ }
347
+
348
+ const response = await request({
349
+ url: `/finance/statements?bank_account_id=${contaFilter}`,
350
+ method: 'GET',
351
+ });
352
+
353
+ return (response?.data || []) as Statement[];
354
+ },
355
+ });
356
+
357
+ const { data: reconciliationSummary } = useQuery<BankReconciliationSummary>({
358
+ queryKey: ['finance-bank-reconciliation-summary', contaFilter],
359
+ queryFn: async () => {
360
+ if (!contaFilter) {
361
+ return {
362
+ discrepancies: 0,
363
+ difference: 0,
364
+ };
365
+ }
366
+
367
+ const response = await request<BankReconciliationSummary>({
368
+ url: `/finance/bank-reconciliation/summary?bank_account_id=${contaFilter}`,
369
+ method: 'GET',
370
+ });
371
+
372
+ return (
373
+ (response?.data as BankReconciliationSummary) || {
374
+ discrepancies: 0,
375
+ difference: 0,
376
+ }
377
+ );
378
+ },
379
+ });
380
+
381
+ const { data: titulosPagar = [], refetch: refetchTitulosPagar } = useQuery<
382
+ FinancialTitle[]
383
+ >({
384
+ queryKey: ['finance-payable-titles-open'],
385
+ queryFn: async () => {
386
+ const response = await request<PaginatedResponse<FinancialTitle>>({
387
+ url: '/finance/accounts-payable/installments?page=1&pageSize=200',
388
+ method: 'GET',
389
+ });
390
+
391
+ const payload = response?.data as PaginatedResponse<FinancialTitle>;
392
+ return Array.isArray(payload?.data) ? payload.data : [];
393
+ },
394
+ });
395
+
396
+ const { data: titulosReceber = [], refetch: refetchTitulosReceber } =
397
+ useQuery<FinancialTitle[]>({
398
+ queryKey: ['finance-receivable-titles-open'],
399
+ queryFn: async () => {
400
+ const response = await request<PaginatedResponse<FinancialTitle>>({
401
+ url: '/finance/accounts-receivable/installments?page=1&pageSize=200',
402
+ method: 'GET',
403
+ });
404
+
405
+ const payload = response?.data as PaginatedResponse<FinancialTitle>;
406
+ return Array.isArray(payload?.data) ? payload.data : [];
407
+ },
408
+ });
409
+
410
+ const extratosPendentes = extratos.filter(
411
+ (e) =>
412
+ e.statusConciliacao === 'pendente' || e.statusConciliacao === 'importado'
413
+ );
414
+
415
+ const extratosFiltered = extratosPendentes.filter(
416
+ (e) => e.contaBancariaId === contaFilter
417
+ );
418
+
419
+ const titulosAbertos = useMemo<OpenTitleItem[]>(() => {
420
+ const payables: OpenTitleItem[] = titulosPagar.flatMap((titulo) =>
421
+ (titulo.parcelas || [])
422
+ .filter(
423
+ (parcela) =>
424
+ parcela.status === 'aberto' || parcela.status === 'vencido'
425
+ )
426
+ .map((parcela) => ({
427
+ id: `pagar-${titulo.id}-${parcela.id}`,
428
+ tipo: 'pagar' as const,
429
+ titleId: titulo.id,
430
+ installmentId: parcela.id,
431
+ documento: titulo.documento,
432
+ pessoa: titulo.fornecedor || '',
433
+ vencimento: parcela.vencimento,
434
+ valor: parcela.valor,
435
+ }))
436
+ );
437
+
438
+ const receivables: OpenTitleItem[] = titulosReceber.flatMap((titulo) =>
439
+ (titulo.parcelas || [])
440
+ .filter(
441
+ (parcela) =>
442
+ parcela.status === 'aberto' || parcela.status === 'vencido'
443
+ )
444
+ .map((parcela) => ({
445
+ id: `receber-${titulo.id}-${parcela.id}`,
446
+ tipo: 'receber' as const,
447
+ titleId: titulo.id,
448
+ installmentId: parcela.id,
449
+ documento: titulo.documento,
450
+ pessoa: titulo.cliente || '',
451
+ vencimento: parcela.vencimento,
452
+ valor: parcela.valor,
453
+ }))
454
+ );
455
+
456
+ return [...payables, ...receivables];
457
+ }, [titulosPagar, titulosReceber]);
458
+
459
+ const selectedExtratoItem = extratosFiltered.find(
460
+ (extrato) => extrato.id === selectedExtrato
461
+ );
462
+ const selectedTituloItem = titulosAbertos.find(
463
+ (titulo) => titulo.id === selectedTitulo
464
+ );
465
+
466
+ const titulosCompativeis = useMemo(() => {
467
+ if (!selectedExtratoItem) {
468
+ return titulosAbertos;
469
+ }
470
+
471
+ return titulosAbertos.filter((titulo) =>
472
+ selectedExtratoItem.tipo === 'saida'
473
+ ? titulo.tipo === 'pagar'
474
+ : titulo.tipo === 'receber'
475
+ );
476
+ }, [selectedExtratoItem, titulosAbertos]);
477
+
478
+ const totalConciliado = extratos.filter(
479
+ (e) =>
480
+ e.contaBancariaId === contaFilter && e.statusConciliacao === 'conciliado'
481
+ ).length;
482
+ const totalExtratoConta = extratos.filter(
483
+ (e) => e.contaBancariaId === contaFilter
484
+ ).length;
485
+ const percentualConciliado =
486
+ totalExtratoConta > 0
487
+ ? Math.round((totalConciliado / totalExtratoConta) * 100)
488
+ : 0;
489
+
490
+ const handleRefreshData = async () => {
491
+ await Promise.all([
492
+ refetchExtratos(),
493
+ refetchTitulosPagar(),
494
+ refetchTitulosReceber(),
495
+ ]);
496
+ };
497
+
498
+ const handleReconcile = async () => {
499
+ if (!selectedExtratoItem || !selectedTituloItem || !contaFilter) {
500
+ return;
501
+ }
502
+
503
+ const endpointBase =
504
+ selectedTituloItem.tipo === 'pagar'
505
+ ? '/finance/accounts-payable/installments'
506
+ : '/finance/accounts-receivable/installments';
507
+
508
+ try {
509
+ await request({
510
+ url: `${endpointBase}/${selectedTituloItem.titleId}/settlements`,
511
+ method: 'POST',
512
+ data: {
513
+ installment_id: Number(selectedTituloItem.installmentId),
514
+ amount: Math.abs(selectedExtratoItem.valor),
515
+ settled_at: selectedExtratoItem.data,
516
+ bank_account_id: Number(contaFilter),
517
+ payment_channel: 'transferencia',
518
+ description: selectedExtratoItem.descricao,
519
+ bank_statement_line_id: Number(selectedExtratoItem.id),
520
+ },
521
+ });
522
+
523
+ setSelectedExtrato(null);
524
+ setSelectedTitulo(null);
525
+ await handleRefreshData();
526
+ showToastHandler?.('success', 'Conciliação realizada com sucesso');
527
+ } catch {
528
+ showToastHandler?.('error', 'Não foi possível conciliar os registros');
529
+ }
530
+ };
531
+
532
+ return (
533
+ <Page>
534
+ <PageHeader
535
+ title={t('header.title')}
536
+ description={t('header.description')}
537
+ breadcrumbs={[
538
+ { label: t('breadcrumbs.home'), href: '/' },
539
+ { label: t('breadcrumbs.finance'), href: '/finance' },
540
+ { label: t('breadcrumbs.current') },
541
+ ]}
542
+ />
543
+
544
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
545
+ <Select
546
+ value={contaFilter}
547
+ onValueChange={(value) => {
548
+ setContaFilter(value);
549
+ setSelectedExtrato(null);
550
+ setSelectedTitulo(null);
551
+
552
+ const params = new URLSearchParams(searchParams.toString());
553
+ params.set('bank_account_id', value);
554
+ router.replace(`${pathname}?${params.toString()}`);
555
+ }}
556
+ >
557
+ <SelectTrigger className="w-full sm:w-[280px]">
558
+ <SelectValue placeholder={t('filters.selectAccount')} />
559
+ </SelectTrigger>
560
+ <SelectContent>
561
+ {contasBancarias.map((conta) => (
562
+ <SelectItem key={conta.id} value={conta.id}>
563
+ {conta.banco} - {conta.descricao}
564
+ </SelectItem>
565
+ ))}
566
+ </SelectContent>
567
+ </Select>
568
+
569
+ <div className="flex gap-2">
570
+ <CriarAjusteSheet
571
+ t={t}
572
+ contaFilter={contaFilter}
573
+ onCreated={handleRefreshData}
574
+ />
575
+ <Button
576
+ disabled={!selectedExtratoItem || !selectedTituloItem}
577
+ onClick={() => void handleReconcile()}
578
+ >
579
+ <Link2 className="mr-2 h-4 w-4" />
580
+ {t('actions.reconcileSelected')}
581
+ </Button>
582
+ </div>
583
+ </div>
584
+
585
+ <div className="grid gap-4 md:grid-cols-4">
586
+ <Card>
587
+ <CardHeader className="pb-2">
588
+ <CardTitle className="text-sm font-medium">
589
+ {t('cards.reconciled')}
590
+ </CardTitle>
591
+ </CardHeader>
592
+ <CardContent>
593
+ <div className="text-2xl font-bold">{percentualConciliado}%</div>
594
+ <Progress value={percentualConciliado} className="mt-2" />
595
+ </CardContent>
596
+ </Card>
597
+
598
+ <Card>
599
+ <CardHeader className="pb-2">
600
+ <CardTitle className="text-sm font-medium">
601
+ {t('cards.pending')}
602
+ </CardTitle>
603
+ </CardHeader>
604
+ <CardContent>
605
+ <div className="text-2xl font-bold">{extratosFiltered.length}</div>
606
+ <p className="text-xs text-muted-foreground">
607
+ {t('cards.transactions')}
608
+ </p>
609
+ </CardContent>
610
+ </Card>
611
+
612
+ <Card>
613
+ <CardHeader className="pb-2">
614
+ <CardTitle className="text-sm font-medium">
615
+ {t('cards.discrepancies')}
616
+ </CardTitle>
617
+ </CardHeader>
618
+ <CardContent>
619
+ <div className="text-2xl font-bold text-orange-600">
620
+ {reconciliationSummary?.discrepancies || 0}
621
+ </div>
622
+ <p className="text-xs text-muted-foreground">
623
+ {t('cards.toResolve')}
624
+ </p>
625
+ </CardContent>
626
+ </Card>
627
+
628
+ <Card>
629
+ <CardHeader className="pb-2">
630
+ <CardTitle className="text-sm font-medium">
631
+ {t('cards.difference')}
632
+ </CardTitle>
633
+ </CardHeader>
634
+ <CardContent>
635
+ <div className="text-2xl font-bold">
636
+ <Money value={reconciliationSummary?.difference || 0} />
637
+ </div>
638
+ <p className="text-xs text-muted-foreground">
639
+ {t('cards.notReconciled')}
640
+ </p>
641
+ </CardContent>
642
+ </Card>
643
+ </div>
644
+
645
+ <div className="grid gap-6 lg:grid-cols-2">
646
+ <Card>
647
+ <CardHeader>
648
+ <CardTitle className="flex items-center gap-2">
649
+ <RefreshCw className="h-4 w-4" />
650
+ {t('statement.title')}
651
+ </CardTitle>
652
+ <CardDescription>{t('statement.description')}</CardDescription>
653
+ </CardHeader>
654
+ <CardContent>
655
+ <div className="space-y-2">
656
+ {extratosFiltered.length > 0 ? (
657
+ extratosFiltered.map((extrato) => (
658
+ <div
659
+ key={extrato.id}
660
+ className={`flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors ${
661
+ selectedExtrato === extrato.id
662
+ ? 'border-primary bg-primary/5'
663
+ : 'hover:bg-muted/50'
664
+ }`}
665
+ onClick={() =>
666
+ setSelectedExtrato(
667
+ selectedExtrato === extrato.id ? null : extrato.id
668
+ )
669
+ }
670
+ >
671
+ <Checkbox
672
+ checked={selectedExtrato === extrato.id}
673
+ onCheckedChange={() =>
674
+ setSelectedExtrato(
675
+ selectedExtrato === extrato.id ? null : extrato.id
676
+ )
677
+ }
678
+ />
679
+ <div className="flex-1">
680
+ <div className="flex items-center justify-between">
681
+ <span className="text-sm font-medium">
682
+ {extrato.descricao}
683
+ </span>
684
+ <span
685
+ className={`font-semibold ${
686
+ extrato.tipo === 'entrada'
687
+ ? 'text-green-600'
688
+ : 'text-red-600'
689
+ }`}
690
+ >
691
+ {extrato.tipo === 'saida' && '-'}
692
+ {formatarMoeda(Math.abs(extrato.valor))}
693
+ </span>
694
+ </div>
695
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
696
+ <span>{formatarData(extrato.data)}</span>
697
+ <StatusBadge
698
+ status={extrato.statusConciliacao}
699
+ type="conciliacao"
700
+ />
701
+ </div>
702
+ </div>
703
+ </div>
704
+ ))
705
+ ) : (
706
+ <div className="flex flex-col items-center justify-center py-8 text-center">
707
+ <Check className="h-12 w-12 text-green-500" />
708
+ <p className="mt-2 font-medium">
709
+ {t('statement.emptyTitle')}
710
+ </p>
711
+ <p className="text-sm text-muted-foreground">
712
+ {t('statement.emptyDescription')}
713
+ </p>
714
+ </div>
715
+ )}
716
+ </div>
717
+ </CardContent>
718
+ </Card>
719
+
720
+ <Card>
721
+ <CardHeader>
722
+ <CardTitle className="flex items-center gap-2">
723
+ <DollarSign className="h-4 w-4" />
724
+ {t('openTitles.title')}
725
+ </CardTitle>
726
+ <CardDescription>{t('openTitles.description')}</CardDescription>
727
+ </CardHeader>
728
+ <CardContent>
729
+ <div className="max-h-[400px] space-y-2 overflow-y-auto">
730
+ {titulosCompativeis.map((titulo) => (
731
+ <div
732
+ key={titulo.id}
733
+ className={`flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors ${
734
+ selectedTitulo === titulo.id
735
+ ? 'border-primary bg-primary/5'
736
+ : 'hover:bg-muted/50'
737
+ }`}
738
+ onClick={() =>
739
+ setSelectedTitulo(
740
+ selectedTitulo === titulo.id ? null : titulo.id
741
+ )
742
+ }
743
+ >
744
+ <Checkbox
745
+ checked={selectedTitulo === titulo.id}
746
+ onCheckedChange={() =>
747
+ setSelectedTitulo(
748
+ selectedTitulo === titulo.id ? null : titulo.id
749
+ )
750
+ }
751
+ />
752
+ <div className="flex-1">
753
+ <div className="flex items-center justify-between">
754
+ <div className="flex items-center gap-2">
755
+ <span className="text-sm font-medium">
756
+ {titulo.documento}
757
+ </span>
758
+ <Badge
759
+ variant="outline"
760
+ className={
761
+ titulo.tipo === 'pagar'
762
+ ? 'border-red-200 text-red-700'
763
+ : 'border-green-200 text-green-700'
764
+ }
765
+ >
766
+ {titulo.tipo === 'pagar'
767
+ ? t('openTitles.pay')
768
+ : t('openTitles.receive')}
769
+ </Badge>
770
+ </div>
771
+ <span className="font-semibold">
772
+ {formatarMoeda(titulo.valor)}
773
+ </span>
774
+ </div>
775
+ <div className="text-xs text-muted-foreground">
776
+ {t('openTitles.personDue', {
777
+ person: titulo.pessoa,
778
+ dueDate: formatarData(titulo.vencimento),
779
+ })}
780
+ </div>
781
+ </div>
782
+ </div>
783
+ ))}
784
+ </div>
785
+ </CardContent>
786
+ </Card>
787
+ </div>
788
+
789
+ {selectedExtratoItem && selectedTituloItem && (
790
+ <Card className="border-primary">
791
+ <CardHeader>
792
+ <CardTitle className="flex items-center gap-2 text-primary">
793
+ <Link2 className="h-4 w-4" />
794
+ {t('ready.title')}
795
+ </CardTitle>
796
+ </CardHeader>
797
+ <CardContent>
798
+ <div className="flex items-center justify-between">
799
+ <div>
800
+ <p className="text-sm text-muted-foreground">
801
+ {t('ready.description')}
802
+ </p>
803
+ </div>
804
+ <div className="flex gap-2">
805
+ <Button
806
+ variant="outline"
807
+ onClick={() => {
808
+ setSelectedExtrato(null);
809
+ setSelectedTitulo(null);
810
+ }}
811
+ >
812
+ {t('common.cancel')}
813
+ </Button>
814
+ <Button onClick={() => void handleReconcile()}>
815
+ <Check className="mr-2 h-4 w-4" />
816
+ {t('actions.reconcile')}
817
+ </Button>
818
+ </div>
819
+ </div>
820
+ </CardContent>
821
+ </Card>
822
+ )}
823
+ </Page>
824
+ );
825
+ }