@hed-hog/finance 0.0.251 → 0.0.253

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.
@@ -10,7 +10,6 @@ import {
10
10
  AlertDialogFooter,
11
11
  AlertDialogHeader,
12
12
  AlertDialogTitle,
13
- AlertDialogTrigger,
14
13
  } from '@/components/ui/alert-dialog';
15
14
  import { AuditTimeline } from '@/components/ui/audit-timeline';
16
15
  import { Button } from '@/components/ui/button';
@@ -21,14 +20,6 @@ import {
21
20
  CardHeader,
22
21
  CardTitle,
23
22
  } from '@/components/ui/card';
24
- import {
25
- Dialog,
26
- DialogContent,
27
- DialogDescription,
28
- DialogFooter,
29
- DialogHeader,
30
- DialogTitle,
31
- } from '@/components/ui/dialog';
32
23
  import {
33
24
  DropdownMenu,
34
25
  DropdownMenuContent,
@@ -54,6 +45,13 @@ import {
54
45
  SelectTrigger,
55
46
  SelectValue,
56
47
  } from '@/components/ui/select';
48
+ import {
49
+ Sheet,
50
+ SheetContent,
51
+ SheetDescription,
52
+ SheetHeader,
53
+ SheetTitle,
54
+ } from '@/components/ui/sheet';
57
55
  import { StatusBadge } from '@/components/ui/status-badge';
58
56
  import {
59
57
  Table,
@@ -65,38 +63,83 @@ import {
65
63
  } from '@/components/ui/table';
66
64
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
67
65
  import { TagSelectorSheet } from '@/components/ui/tag-selector-sheet';
68
- import { useApp } from '@hed-hog/next-app-provider';
66
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
69
67
  import { zodResolver } from '@hookform/resolvers/zod';
70
68
  import {
71
69
  CheckCircle,
70
+ ChevronDown,
71
+ ChevronUp,
72
72
  Download,
73
73
  Edit,
74
74
  FileText,
75
+ Loader2,
75
76
  MoreHorizontal,
76
77
  Undo,
77
78
  XCircle,
78
79
  } from 'lucide-react';
79
80
  import { useTranslations } from 'next-intl';
80
81
  import Link from 'next/link';
81
- import { useParams, useRouter } from 'next/navigation';
82
- import { useEffect, useState } from 'react';
82
+ import { useParams, useRouter, useSearchParams } from 'next/navigation';
83
+ import { Fragment, useEffect, useMemo, useState } from 'react';
83
84
  import { useForm } from 'react-hook-form';
84
85
  import { z } from 'zod';
85
86
  import { formatarData } from '../../../_lib/formatters';
86
87
  import { useFinanceData } from '../../../_lib/use-finance-data';
87
88
 
88
- const settleSchema = z.object({
89
- installmentId: z.string().min(1, 'Parcela obrigatória'),
90
- amount: z.number().min(0.01, 'Valor deve ser maior que zero'),
91
- description: z.string().optional(),
92
- });
93
-
94
- type SettleFormValues = z.infer<typeof settleSchema>;
89
+ type SettleFormValues = {
90
+ installmentId: string;
91
+ amount: number;
92
+ description?: string;
93
+ };
94
+
95
+ type SettlementHistoryAllocation = {
96
+ installmentId: string;
97
+ installmentSeq: number;
98
+ amountCents: number;
99
+ };
100
+
101
+ type SettlementHistoryItem = {
102
+ normal: {
103
+ id: string;
104
+ paidAt: string | null;
105
+ amountCents: number;
106
+ type: 'NORMAL';
107
+ method?: string | null;
108
+ account?: string | null;
109
+ accountId?: string | null;
110
+ createdAt?: string | null;
111
+ createdBy?: string | null;
112
+ memo?: string | null;
113
+ reconciled: boolean;
114
+ reconciliationId?: string | null;
115
+ };
116
+ reversal?: {
117
+ id: string;
118
+ paidAt: string | null;
119
+ amountCents: number;
120
+ type: 'REVERSAL';
121
+ createdAt?: string | null;
122
+ createdBy?: string | null;
123
+ memo?: string | null;
124
+ } | null;
125
+ allocations: SettlementHistoryAllocation[];
126
+ statusLabel: 'ATIVO' | 'ESTORNADO';
127
+ };
95
128
 
96
129
  export default function TituloDetalhePage() {
97
130
  const t = useTranslations('finance.PayableInstallmentDetailPage');
131
+ const settleSchema = useMemo(
132
+ () =>
133
+ z.object({
134
+ installmentId: z.string().min(1, t('validation.installmentRequired')),
135
+ amount: z.number().min(0.01, t('validation.amountGreaterThanZero')),
136
+ description: z.string().optional(),
137
+ }),
138
+ [t]
139
+ );
98
140
  const { request, showToastHandler } = useApp();
99
141
  const router = useRouter();
142
+ const searchParams = useSearchParams();
100
143
  const params = useParams<{ id: string }>();
101
144
  const id = params?.id;
102
145
  const { data, refetch } = useFinanceData();
@@ -105,13 +148,34 @@ export default function TituloDetalhePage() {
105
148
  pessoas,
106
149
  categorias,
107
150
  centrosCusto,
108
- contasBancarias,
109
151
  logsAuditoria,
110
152
  tags,
111
153
  } = data;
112
154
 
113
155
  const titulo = titulosPagar.find((t) => t.id === id);
114
156
 
157
+ const {
158
+ data: settlementsHistory = [],
159
+ refetch: refetchSettlementsHistory,
160
+ isFetching: isFetchingSettlementsHistory,
161
+ } = useQuery<SettlementHistoryItem[]>({
162
+ queryKey: ['finance-title-settlements-history', id],
163
+ enabled: !!id,
164
+ queryFn: async () => {
165
+ const response = await request<SettlementHistoryItem[]>({
166
+ url: `/finance/titles/${id}/settlements-history`,
167
+ method: 'GET',
168
+ });
169
+
170
+ return (response.data || []) as SettlementHistoryItem[];
171
+ },
172
+ placeholderData: (old) => old,
173
+ });
174
+
175
+ const canSettle = ['aberto', 'parcial', 'vencido'].includes(
176
+ titulo?.status || ''
177
+ );
178
+
115
179
  const settleCandidates = (titulo?.parcelas || []).filter(
116
180
  (parcela: any) =>
117
181
  parcela.status === 'aberto' ||
@@ -136,9 +200,17 @@ export default function TituloDetalhePage() {
136
200
  const [isCanceling, setIsCanceling] = useState(false);
137
201
  const [isSettleDialogOpen, setIsSettleDialogOpen] = useState(false);
138
202
  const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false);
203
+ const [isReverseDialogOpen, setIsReverseDialogOpen] = useState(false);
204
+ const [selectedSettlementIdToReverse, setSelectedSettlementIdToReverse] =
205
+ useState<string | null>(null);
206
+ const [reverseReason, setReverseReason] = useState('');
139
207
  const [reversingSettlementId, setReversingSettlementId] = useState<
140
208
  string | null
141
209
  >(null);
210
+ const [unreconcilingId, setUnreconcilingId] = useState<string | null>(null);
211
+ const [expandedSettlementRows, setExpandedSettlementRows] = useState<
212
+ Record<string, boolean>
213
+ >({});
142
214
 
143
215
  useEffect(() => {
144
216
  setAvailableTags(tags || []);
@@ -173,6 +245,18 @@ export default function TituloDetalhePage() {
173
245
  });
174
246
  }, [settleForm, settleCandidates]);
175
247
 
248
+ useEffect(() => {
249
+ const action = searchParams.get('action');
250
+
251
+ if (action !== 'settle') {
252
+ return;
253
+ }
254
+
255
+ if (canSettle && settleCandidates.length > 0) {
256
+ setIsSettleDialogOpen(true);
257
+ }
258
+ }, [canSettle, searchParams, settleCandidates.length]);
259
+
176
260
  if (!titulo) {
177
261
  return (
178
262
  <div className="space-y-6">
@@ -200,8 +284,6 @@ export default function TituloDetalhePage() {
200
284
  categorias.find((c) => c.id === categoryId);
201
285
  const getCentroCustoById = (costCenterId?: string) =>
202
286
  centrosCusto.find((c) => c.id === costCenterId);
203
- const getContaBancariaById = (bankId?: string) =>
204
- contasBancarias.find((c) => c.id === bankId);
205
287
 
206
288
  const fornecedor = getPessoaById(titulo.fornecedorId);
207
289
  const categoria = getCategoriaById(titulo.categoriaId);
@@ -362,8 +444,14 @@ export default function TituloDetalhePage() {
362
444
 
363
445
  const canApprove = titulo.status === 'rascunho';
364
446
  const canEdit = titulo.status === 'rascunho';
365
- const canSettle = ['aberto', 'parcial', 'vencido'].includes(titulo.status);
366
447
  const canCancel = !['cancelado', 'liquidado'].includes(titulo.status);
448
+ const reversibleSettlements = settlementsHistory.filter(
449
+ (group) =>
450
+ group.normal?.id &&
451
+ group.statusLabel === 'ATIVO' &&
452
+ !group.normal?.reconciled
453
+ );
454
+ const canReverse = reversibleSettlements.length > 0;
367
455
 
368
456
  const getErrorMessage = (error: any, fallback: string) => {
369
457
  const message = error?.response?.data?.message;
@@ -392,11 +480,11 @@ export default function TituloDetalhePage() {
392
480
  });
393
481
 
394
482
  await refetch();
395
- showToastHandler?.('success', 'Título aprovado com sucesso');
483
+ showToastHandler?.('success', t('messages.approveSuccess'));
396
484
  } catch (error) {
397
485
  showToastHandler?.(
398
486
  'error',
399
- getErrorMessage(error, 'Não foi possível aprovar o título')
487
+ getErrorMessage(error, t('messages.approveError'))
400
488
  );
401
489
  } finally {
402
490
  setIsApproving(false);
@@ -422,11 +510,11 @@ export default function TituloDetalhePage() {
422
510
 
423
511
  await refetch();
424
512
  setIsSettleDialogOpen(false);
425
- showToastHandler?.('success', 'Baixa registrada com sucesso');
513
+ showToastHandler?.('success', t('messages.settleSuccess'));
426
514
  } catch (error) {
427
515
  showToastHandler?.(
428
516
  'error',
429
- getErrorMessage(error, 'Não foi possível registrar a baixa')
517
+ getErrorMessage(error, t('messages.settleError'))
430
518
  );
431
519
  } finally {
432
520
  setIsSettling(false);
@@ -441,23 +529,58 @@ export default function TituloDetalhePage() {
441
529
  setReversingSettlementId(settlementId);
442
530
  try {
443
531
  await request({
444
- url: `/finance/accounts-payable/installments/${titulo.id}/settlements/${settlementId}/reverse`,
445
- method: 'PATCH',
446
- data: {},
532
+ url: `/finance/settlements/${settlementId}/reverse`,
533
+ method: 'POST',
534
+ data: {
535
+ reason: reverseReason?.trim() || undefined,
536
+ memo: reverseReason?.trim() || undefined,
537
+ },
447
538
  });
448
539
 
449
- await refetch();
450
- showToastHandler?.('success', 'Estorno realizado com sucesso');
540
+ await Promise.all([refetch(), refetchSettlementsHistory()]);
541
+ setReverseReason('');
542
+ showToastHandler?.('success', t('messages.reverseSuccess'));
451
543
  } catch (error) {
452
544
  showToastHandler?.(
453
545
  'error',
454
- getErrorMessage(error, 'Não foi possível estornar a liquidação')
546
+ getErrorMessage(error, t('messages.reverseError'))
455
547
  );
456
548
  } finally {
457
549
  setReversingSettlementId(null);
458
550
  }
459
551
  };
460
552
 
553
+ const handleUnreconcileSettlement = async (reconciliationId: string) => {
554
+ if (!reconciliationId || unreconcilingId) {
555
+ return;
556
+ }
557
+
558
+ setUnreconcilingId(reconciliationId);
559
+ try {
560
+ await request({
561
+ url: `/finance/bank-reconciliations/${reconciliationId}`,
562
+ method: 'DELETE',
563
+ });
564
+
565
+ await Promise.all([refetch(), refetchSettlementsHistory()]);
566
+ showToastHandler?.('success', 'Conciliação removida com sucesso');
567
+ } catch (error) {
568
+ showToastHandler?.(
569
+ 'error',
570
+ getErrorMessage(error, 'Não foi possível desconciliar a liquidação')
571
+ );
572
+ } finally {
573
+ setUnreconcilingId(null);
574
+ }
575
+ };
576
+
577
+ const toggleSettlementExpanded = (settlementId: string) => {
578
+ setExpandedSettlementRows((current) => ({
579
+ ...current,
580
+ [settlementId]: !current[settlementId],
581
+ }));
582
+ };
583
+
461
584
  const handleCancel = async () => {
462
585
  if (!canCancel || isCanceling) {
463
586
  return;
@@ -473,11 +596,11 @@ export default function TituloDetalhePage() {
473
596
 
474
597
  await refetch();
475
598
  setIsCancelDialogOpen(false);
476
- showToastHandler?.('success', 'Título cancelado com sucesso');
599
+ showToastHandler?.('success', t('messages.cancelSuccess'));
477
600
  } catch (error) {
478
601
  showToastHandler?.(
479
602
  'error',
480
- getErrorMessage(error, 'Não foi possível cancelar o título')
603
+ getErrorMessage(error, t('messages.cancelError'))
481
604
  );
482
605
  } finally {
483
606
  setIsCanceling(false);
@@ -533,7 +656,21 @@ export default function TituloDetalhePage() {
533
656
  <Download className="mr-2 h-4 w-4" />
534
657
  {t('actions.settle')}
535
658
  </DropdownMenuItem>
536
- <DropdownMenuItem disabled>
659
+ <DropdownMenuItem
660
+ disabled={!canReverse || !!reversingSettlementId}
661
+ onClick={() => {
662
+ const latestSettlement = reversibleSettlements[0];
663
+ if (!latestSettlement?.normal?.id) {
664
+ return;
665
+ }
666
+
667
+ setSelectedSettlementIdToReverse(
668
+ String(latestSettlement.normal.id)
669
+ );
670
+ setReverseReason('');
671
+ setIsReverseDialogOpen(true);
672
+ }}
673
+ >
537
674
  <Undo className="mr-2 h-4 w-4" />
538
675
  {t('actions.reverse')}
539
676
  </DropdownMenuItem>
@@ -555,42 +692,100 @@ export default function TituloDetalhePage() {
555
692
  >
556
693
  <AlertDialogContent>
557
694
  <AlertDialogHeader>
558
- <AlertDialogTitle>Confirmar cancelamento</AlertDialogTitle>
695
+ <AlertDialogTitle>
696
+ {t('dialogs.cancel.title')}
697
+ </AlertDialogTitle>
559
698
  <AlertDialogDescription>
560
- Essa ação altera o título para cancelado e não remove os
561
- registros de auditoria.
699
+ {t('dialogs.cancel.description')}
562
700
  </AlertDialogDescription>
563
701
  </AlertDialogHeader>
564
702
  <AlertDialogFooter>
565
703
  <AlertDialogCancel disabled={isCanceling}>
566
- Cancelar
704
+ {t('dialogs.cancel.cancel')}
567
705
  </AlertDialogCancel>
568
706
  <AlertDialogAction
569
707
  disabled={isCanceling}
570
708
  onClick={() => void handleCancel()}
571
709
  >
572
- Confirmar cancelamento
710
+ {t('dialogs.cancel.confirm')}
711
+ </AlertDialogAction>
712
+ </AlertDialogFooter>
713
+ </AlertDialogContent>
714
+ </AlertDialog>
715
+
716
+ <AlertDialog
717
+ open={isReverseDialogOpen}
718
+ onOpenChange={(open) => {
719
+ setIsReverseDialogOpen(open);
720
+
721
+ if (!open) {
722
+ setSelectedSettlementIdToReverse(null);
723
+ setReverseReason('');
724
+ }
725
+ }}
726
+ >
727
+ <AlertDialogContent>
728
+ <AlertDialogHeader>
729
+ <AlertDialogTitle>
730
+ {t('dialogs.reverse.title')}
731
+ </AlertDialogTitle>
732
+ <AlertDialogDescription>
733
+ {t('dialogs.reverse.description')}
734
+ </AlertDialogDescription>
735
+ </AlertDialogHeader>
736
+ <div className="space-y-2">
737
+ <FormLabel>Motivo do estorno</FormLabel>
738
+ <Input
739
+ value={reverseReason}
740
+ onChange={(event) => setReverseReason(event.target.value)}
741
+ placeholder="Ex.: baixa duplicada"
742
+ maxLength={255}
743
+ disabled={!!reversingSettlementId}
744
+ />
745
+ </div>
746
+ <AlertDialogFooter>
747
+ <AlertDialogCancel disabled={!!reversingSettlementId}>
748
+ {t('dialogs.reverse.cancel')}
749
+ </AlertDialogCancel>
750
+ <AlertDialogAction
751
+ disabled={
752
+ !!reversingSettlementId || !selectedSettlementIdToReverse
753
+ }
754
+ onClick={() => {
755
+ if (!selectedSettlementIdToReverse) {
756
+ return;
757
+ }
758
+
759
+ void handleReverseSettlement(
760
+ selectedSettlementIdToReverse
761
+ ).finally(() => {
762
+ setIsReverseDialogOpen(false);
763
+ setSelectedSettlementIdToReverse(null);
764
+ setReverseReason('');
765
+ });
766
+ }}
767
+ >
768
+ {t('dialogs.reverse.confirm')}
573
769
  </AlertDialogAction>
574
770
  </AlertDialogFooter>
575
771
  </AlertDialogContent>
576
772
  </AlertDialog>
577
773
 
578
- <Dialog
774
+ <Sheet
579
775
  open={isSettleDialogOpen}
580
776
  onOpenChange={setIsSettleDialogOpen}
581
777
  >
582
- <DialogContent>
583
- <DialogHeader>
584
- <DialogTitle>Registrar baixa</DialogTitle>
585
- <DialogDescription>
586
- Informe a parcela e o valor da baixa. O backend valida os
587
- estados e limites de valor automaticamente.
588
- </DialogDescription>
589
- </DialogHeader>
778
+ <SheetContent className="w-full sm:max-w-lg overflow-y-auto">
779
+ <SheetHeader>
780
+ <SheetTitle>{t('settleSheet.title')}</SheetTitle>
781
+ <SheetDescription>
782
+ {t('settleSheet.description')}
783
+ </SheetDescription>
784
+ </SheetHeader>
590
785
 
591
786
  <Form {...settleForm}>
592
787
  <form
593
- className="space-y-4"
788
+ className="space-y-4 px-4"
594
789
  onSubmit={settleForm.handleSubmit(handleSettle)}
595
790
  >
596
791
  <FormField
@@ -598,7 +793,9 @@ export default function TituloDetalhePage() {
598
793
  name="installmentId"
599
794
  render={({ field }) => (
600
795
  <FormItem>
601
- <FormLabel>Parcela</FormLabel>
796
+ <FormLabel>
797
+ {t('settleSheet.installmentLabel')}
798
+ </FormLabel>
602
799
  <Select
603
800
  value={field.value}
604
801
  onValueChange={(value) => {
@@ -618,18 +815,24 @@ export default function TituloDetalhePage() {
618
815
  }}
619
816
  >
620
817
  <FormControl>
621
- <SelectTrigger>
622
- <SelectValue placeholder="Selecione" />
818
+ <SelectTrigger className="w-full">
819
+ <SelectValue
820
+ placeholder={t(
821
+ 'settleSheet.installmentPlaceholder'
822
+ )}
823
+ />
623
824
  </SelectTrigger>
624
825
  </FormControl>
625
826
  <SelectContent>
626
827
  {settleCandidates.map((parcela: any) => (
627
828
  <SelectItem key={parcela.id} value={parcela.id}>
628
- Parcela {parcela.numero} - em aberto:{' '}
629
- {new Intl.NumberFormat('pt-BR', {
630
- style: 'currency',
631
- currency: 'BRL',
632
- }).format(Number(parcela.valorAberto || 0))}
829
+ {t('settleSheet.installmentOption', {
830
+ number: parcela.numero,
831
+ amount: new Intl.NumberFormat('pt-BR', {
832
+ style: 'currency',
833
+ currency: 'BRL',
834
+ }).format(Number(parcela.valorAberto || 0)),
835
+ })}
633
836
  </SelectItem>
634
837
  ))}
635
838
  </SelectContent>
@@ -644,7 +847,7 @@ export default function TituloDetalhePage() {
644
847
  name="amount"
645
848
  render={({ field }) => (
646
849
  <FormItem>
647
- <FormLabel>Valor</FormLabel>
850
+ <FormLabel>{t('settleSheet.amountLabel')}</FormLabel>
648
851
  <FormControl>
649
852
  <InputMoney
650
853
  value={Number(field.value || 0)}
@@ -663,7 +866,9 @@ export default function TituloDetalhePage() {
663
866
  name="description"
664
867
  render={({ field }) => (
665
868
  <FormItem>
666
- <FormLabel>Descrição (opcional)</FormLabel>
869
+ <FormLabel>
870
+ {t('settleSheet.descriptionLabel')}
871
+ </FormLabel>
667
872
  <FormControl>
668
873
  <Input {...field} value={field.value || ''} />
669
874
  </FormControl>
@@ -672,23 +877,19 @@ export default function TituloDetalhePage() {
672
877
  )}
673
878
  />
674
879
 
675
- <DialogFooter>
880
+ <div className="flex flex-col gap-4">
676
881
  <Button
677
- type="button"
678
- variant="outline"
882
+ className="w-full"
883
+ type="submit"
679
884
  disabled={isSettling}
680
- onClick={() => setIsSettleDialogOpen(false)}
681
885
  >
682
- Cancelar
683
- </Button>
684
- <Button type="submit" disabled={isSettling}>
685
- Confirmar baixa
886
+ {t('settleSheet.confirm')}
686
887
  </Button>
687
- </DialogFooter>
888
+ </div>
688
889
  </form>
689
890
  </Form>
690
- </DialogContent>
691
- </Dialog>
891
+ </SheetContent>
892
+ </Sheet>
692
893
  </div>
693
894
  }
694
895
  />
@@ -886,98 +1087,212 @@ export default function TituloDetalhePage() {
886
1087
  <TabsContent value="liquidacoes" className="mt-4">
887
1088
  <Card>
888
1089
  <CardContent className="pt-6">
889
- {titulo.parcelas.some((p: any) => p.liquidacoes.length > 0) ? (
1090
+ {isFetchingSettlementsHistory ? (
1091
+ <div className="flex items-center justify-center py-8 text-muted-foreground">
1092
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
1093
+ Carregando histórico de liquidações...
1094
+ </div>
1095
+ ) : settlementsHistory.length > 0 ? (
890
1096
  <Table>
891
1097
  <TableHeader>
892
1098
  <TableRow>
893
- <TableHead>{t('settlementsTable.date')}</TableHead>
894
- <TableHead className="text-right">
895
- {t('settlementsTable.value')}
896
- </TableHead>
897
- <TableHead className="text-right">
898
- {t('settlementsTable.interest')}
899
- </TableHead>
900
- <TableHead className="text-right">
901
- {t('settlementsTable.discount')}
902
- </TableHead>
903
- <TableHead className="text-right">
904
- {t('settlementsTable.fine')}
905
- </TableHead>
906
- <TableHead>{t('settlementsTable.account')}</TableHead>
907
- <TableHead>{t('settlementsTable.method')}</TableHead>
908
- <TableHead className="text-right">Ações</TableHead>
1099
+ <TableHead>Date</TableHead>
1100
+ <TableHead>Type</TableHead>
1101
+ <TableHead>Status</TableHead>
1102
+ <TableHead className="text-right">Net Amount</TableHead>
1103
+ <TableHead>Method</TableHead>
1104
+ <TableHead>Account</TableHead>
1105
+ <TableHead>Reconciled</TableHead>
1106
+ <TableHead className="text-right">Actions</TableHead>
909
1107
  </TableRow>
910
1108
  </TableHeader>
911
1109
  <TableBody>
912
- {titulo.parcelas.flatMap((parcela: any) =>
913
- parcela.liquidacoes.map((liq: any) => {
914
- const conta = getContaBancariaById(liq.contaBancariaId);
915
- return (
916
- <TableRow key={liq.id}>
917
- <TableCell>{formatarData(liq.data)}</TableCell>
918
- <TableCell className="text-right">
919
- <Money value={liq.valor} />
1110
+ {settlementsHistory.map((group) => {
1111
+ const normalId = group.normal.id;
1112
+ const isExpanded = !!expandedSettlementRows[normalId];
1113
+ const canReverseNormal =
1114
+ group.statusLabel === 'ATIVO' &&
1115
+ !group.normal.reconciled &&
1116
+ !reversingSettlementId;
1117
+
1118
+ return (
1119
+ <Fragment key={normalId}>
1120
+ <TableRow>
1121
+ <TableCell>
1122
+ {formatarData(group.normal.paidAt)}
920
1123
  </TableCell>
921
- <TableCell className="text-right">
922
- <Money value={liq.juros} />
1124
+ <TableCell>
1125
+ <span className="rounded-md border px-2 py-1 text-xs font-medium">
1126
+ NORMAL
1127
+ </span>
923
1128
  </TableCell>
924
- <TableCell className="text-right">
925
- <Money value={liq.desconto} />
1129
+ <TableCell>
1130
+ <span
1131
+ className={`rounded-md px-2 py-1 text-xs font-medium ${
1132
+ group.statusLabel === 'ESTORNADO'
1133
+ ? 'bg-muted text-muted-foreground'
1134
+ : 'bg-primary/10 text-primary'
1135
+ }`}
1136
+ >
1137
+ {group.statusLabel}
1138
+ </span>
926
1139
  </TableCell>
927
- <TableCell className="text-right">
928
- <Money value={liq.multa} />
1140
+ <TableCell className="text-right font-medium">
1141
+ +
1142
+ <Money
1143
+ value={
1144
+ Math.abs(group.normal.amountCents || 0) / 100
1145
+ }
1146
+ />
929
1147
  </TableCell>
930
- <TableCell>{conta?.descricao}</TableCell>
931
1148
  <TableCell className="capitalize">
932
- {liq.metodo}
1149
+ {group.normal.method || '-'}
1150
+ </TableCell>
1151
+ <TableCell>{group.normal.account || '-'}</TableCell>
1152
+ <TableCell>
1153
+ <span
1154
+ className={`rounded-md px-2 py-1 text-xs font-medium ${
1155
+ group.normal.reconciled
1156
+ ? 'bg-primary/10 text-primary'
1157
+ : 'bg-muted text-muted-foreground'
1158
+ }`}
1159
+ >
1160
+ {group.normal.reconciled
1161
+ ? 'Conciliado'
1162
+ : 'Pendente'}
1163
+ </span>
933
1164
  </TableCell>
934
1165
  <TableCell className="text-right">
935
- <AlertDialog>
936
- <AlertDialogTrigger asChild>
1166
+ <div className="flex items-center justify-end gap-2">
1167
+ {group.normal.reconciled &&
1168
+ group.normal.reconciliationId ? (
937
1169
  <Button
938
1170
  variant="outline"
939
1171
  size="sm"
940
1172
  disabled={
941
- !liq.settlementId ||
942
- liq.status === 'reversed' ||
943
- !!reversingSettlementId
1173
+ unreconcilingId ===
1174
+ group.normal.reconciliationId
1175
+ }
1176
+ onClick={() =>
1177
+ void handleUnreconcileSettlement(
1178
+ String(group.normal.reconciliationId)
1179
+ )
944
1180
  }
945
1181
  >
946
- <Undo className="mr-2 h-4 w-4" />
947
- Estornar
1182
+ {unreconcilingId ===
1183
+ group.normal.reconciliationId
1184
+ ? 'Desconciliando...'
1185
+ : 'Desconciliar'}
948
1186
  </Button>
949
- </AlertDialogTrigger>
950
- <AlertDialogContent>
951
- <AlertDialogHeader>
952
- <AlertDialogTitle>
953
- Confirmar estorno
954
- </AlertDialogTitle>
955
- <AlertDialogDescription>
956
- Esta ação cria o estorno da liquidação e
957
- recalcula saldos e status.
958
- </AlertDialogDescription>
959
- </AlertDialogHeader>
960
- <AlertDialogFooter>
961
- <AlertDialogCancel>
962
- Cancelar
963
- </AlertDialogCancel>
964
- <AlertDialogAction
965
- onClick={() =>
966
- void handleReverseSettlement(
967
- String(liq.settlementId)
968
- )
969
- }
970
- >
971
- Confirmar estorno
972
- </AlertDialogAction>
973
- </AlertDialogFooter>
974
- </AlertDialogContent>
975
- </AlertDialog>
1187
+ ) : null}
1188
+
1189
+ <Button
1190
+ variant="outline"
1191
+ size="sm"
1192
+ title={
1193
+ group.normal.reconciled
1194
+ ? 'Desconciliar primeiro'
1195
+ : undefined
1196
+ }
1197
+ disabled={!canReverseNormal}
1198
+ onClick={() => {
1199
+ setSelectedSettlementIdToReverse(normalId);
1200
+ setReverseReason('');
1201
+ setIsReverseDialogOpen(true);
1202
+ }}
1203
+ >
1204
+ <Undo className="mr-2 h-4 w-4" />
1205
+ Estornar
1206
+ </Button>
1207
+
1208
+ <Button
1209
+ variant="ghost"
1210
+ size="sm"
1211
+ onClick={() =>
1212
+ toggleSettlementExpanded(normalId)
1213
+ }
1214
+ >
1215
+ {isExpanded ? (
1216
+ <ChevronUp className="h-4 w-4" />
1217
+ ) : (
1218
+ <ChevronDown className="h-4 w-4" />
1219
+ )}
1220
+ </Button>
1221
+ </div>
976
1222
  </TableCell>
977
1223
  </TableRow>
978
- );
979
- })
980
- )}
1224
+
1225
+ {group.reversal ? (
1226
+ <TableRow>
1227
+ <TableCell className="pl-8">
1228
+ {formatarData(group.reversal.paidAt)}
1229
+ </TableCell>
1230
+ <TableCell>
1231
+ <span className="rounded-md border px-2 py-1 text-xs font-medium">
1232
+ REVERSAL
1233
+ </span>
1234
+ </TableCell>
1235
+ <TableCell>
1236
+ <span className="rounded-md bg-muted px-2 py-1 text-xs font-medium text-muted-foreground">
1237
+ ESTORNO
1238
+ </span>
1239
+ </TableCell>
1240
+ <TableCell className="text-right font-medium text-destructive">
1241
+ -
1242
+ <Money
1243
+ value={
1244
+ Math.abs(group.reversal.amountCents || 0) /
1245
+ 100
1246
+ }
1247
+ />
1248
+ </TableCell>
1249
+ <TableCell>-</TableCell>
1250
+ <TableCell>-</TableCell>
1251
+ <TableCell>-</TableCell>
1252
+ <TableCell className="text-right text-xs text-muted-foreground">
1253
+ {group.reversal.memo || '-'}
1254
+ </TableCell>
1255
+ </TableRow>
1256
+ ) : null}
1257
+
1258
+ {isExpanded ? (
1259
+ <TableRow>
1260
+ <TableCell colSpan={8}>
1261
+ <div className="rounded-md border bg-muted/20 p-3">
1262
+ <p className="mb-2 text-xs font-medium text-muted-foreground">
1263
+ Allocations
1264
+ </p>
1265
+ <div className="space-y-1">
1266
+ {group.allocations.map((allocation) => (
1267
+ <div
1268
+ key={`${normalId}-${allocation.installmentId}`}
1269
+ className="flex items-center justify-between text-sm"
1270
+ >
1271
+ <span>
1272
+ Parcela #{allocation.installmentSeq}
1273
+ </span>
1274
+ <span className="font-medium">
1275
+ +
1276
+ <Money
1277
+ value={
1278
+ Math.abs(
1279
+ Number(
1280
+ allocation.amountCents || 0
1281
+ )
1282
+ ) / 100
1283
+ }
1284
+ />
1285
+ </span>
1286
+ </div>
1287
+ ))}
1288
+ </div>
1289
+ </div>
1290
+ </TableCell>
1291
+ </TableRow>
1292
+ ) : null}
1293
+ </Fragment>
1294
+ );
1295
+ })}
981
1296
  </TableBody>
982
1297
  </Table>
983
1298
  ) : (