@hed-hog/finance 0.0.253 → 0.0.256

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.
@@ -1,17 +1,6 @@
1
1
  'use client';
2
2
 
3
3
  import { Page, PageHeader } from '@/components/entity-list';
4
- import {
5
- AlertDialog,
6
- AlertDialogAction,
7
- AlertDialogCancel,
8
- AlertDialogContent,
9
- AlertDialogDescription,
10
- AlertDialogFooter,
11
- AlertDialogHeader,
12
- AlertDialogTitle,
13
- AlertDialogTrigger,
14
- } from '@/components/ui/alert-dialog';
15
4
  import { AuditTimeline } from '@/components/ui/audit-timeline';
16
5
  import { Badge } from '@/components/ui/badge';
17
6
  import { Button } from '@/components/ui/button';
@@ -38,6 +27,7 @@ import {
38
27
  } from '@/components/ui/form';
39
28
  import { Input } from '@/components/ui/input';
40
29
  import { InputMoney } from '@/components/ui/input-money';
30
+ import { Label } from '@/components/ui/label';
41
31
  import { Money } from '@/components/ui/money';
42
32
  import {
43
33
  Select,
@@ -78,19 +68,17 @@ import {
78
68
  import { useTranslations } from 'next-intl';
79
69
  import Link from 'next/link';
80
70
  import { useParams } from 'next/navigation';
81
- import { useEffect, useState } from 'react';
71
+ import { useEffect, useMemo, useState } from 'react';
82
72
  import { useForm } from 'react-hook-form';
83
73
  import { z } from 'zod';
84
74
  import { formatarData } from '../../../_lib/formatters';
85
75
  import { useFinanceData } from '../../../_lib/use-finance-data';
86
76
 
87
- const settleSchema = z.object({
88
- installmentId: z.string().min(1, 'Parcela obrigatória'),
89
- amount: z.number().min(0.01, 'Valor deve ser maior que zero'),
90
- description: z.string().optional(),
91
- });
92
-
93
- type SettleFormValues = z.infer<typeof settleSchema>;
77
+ type SettleFormValues = {
78
+ installmentId: string;
79
+ amount: number;
80
+ description?: string;
81
+ };
94
82
 
95
83
  export default function TituloReceberDetalhePage() {
96
84
  const t = useTranslations('finance.ReceivableInstallmentDetailPage');
@@ -117,6 +105,16 @@ export default function TituloReceberDetalhePage() {
117
105
  parcela.status === 'vencido'
118
106
  );
119
107
 
108
+ const settleSchema = useMemo(
109
+ () =>
110
+ z.object({
111
+ installmentId: z.string().min(1, t('validation.installmentRequired')),
112
+ amount: z.number().min(0.01, t('validation.amountGreaterThanZero')),
113
+ description: z.string().optional(),
114
+ }),
115
+ [t]
116
+ );
117
+
120
118
  const settleForm = useForm<SettleFormValues>({
121
119
  resolver: zodResolver(settleSchema),
122
120
  defaultValues: {
@@ -132,6 +130,10 @@ export default function TituloReceberDetalhePage() {
132
130
  const [isApproving, setIsApproving] = useState(false);
133
131
  const [isSettling, setIsSettling] = useState(false);
134
132
  const [isSettleDialogOpen, setIsSettleDialogOpen] = useState(false);
133
+ const [isReverseSheetOpen, setIsReverseSheetOpen] = useState(false);
134
+ const [selectedSettlementIdToReverse, setSelectedSettlementIdToReverse] =
135
+ useState<string | null>(null);
136
+ const [reverseReason, setReverseReason] = useState('');
135
137
  const [reversingSettlementId, setReversingSettlementId] = useState<
136
138
  string | null
137
139
  >(null);
@@ -273,11 +275,6 @@ export default function TituloReceberDetalhePage() {
273
275
  window.open(url, '_blank', 'noopener,noreferrer');
274
276
  };
275
277
 
276
- const tTagSelector = (key: string, fallback: string) => {
277
- const fullKey = `tagSelector.${key}`;
278
- return t.has(fullKey) ? t(fullKey) : fallback;
279
- };
280
-
281
278
  const toTagSlug = (value: string) => {
282
279
  return value
283
280
  .normalize('NFD')
@@ -325,10 +322,7 @@ export default function TituloReceberDetalhePage() {
325
322
  return [...current, newTag];
326
323
  });
327
324
 
328
- showToastHandler?.(
329
- 'success',
330
- tTagSelector('messages.createSuccess', 'Tag criada com sucesso')
331
- );
325
+ showToastHandler?.('success', t('tagSelector.messages.createSuccess'));
332
326
 
333
327
  return {
334
328
  id: newTag.id,
@@ -336,10 +330,7 @@ export default function TituloReceberDetalhePage() {
336
330
  color: newTag.cor,
337
331
  };
338
332
  } catch {
339
- showToastHandler?.(
340
- 'error',
341
- tTagSelector('messages.createError', 'Não foi possível criar a tag')
342
- );
333
+ showToastHandler?.('error', t('tagSelector.messages.createError'));
343
334
  return null;
344
335
  } finally {
345
336
  setIsCreatingTag(false);
@@ -366,18 +357,12 @@ export default function TituloReceberDetalhePage() {
366
357
  setSelectedTagIds(nextTagIds);
367
358
  }
368
359
  } catch {
369
- showToastHandler?.(
370
- 'error',
371
- tTagSelector(
372
- 'messages.updateError',
373
- 'Não foi possível atualizar as tags'
374
- )
375
- );
360
+ showToastHandler?.('error', t('tagSelector.messages.updateError'));
376
361
  }
377
362
  };
378
363
 
379
364
  const canApprove = titulo.status === 'rascunho';
380
- const canSettle = ['aberto', 'parcial'].includes(titulo.status);
365
+ const canSettle = ['aberto', 'parcial', 'vencido'].includes(titulo.status);
381
366
 
382
367
  const getErrorMessage = (error: any, fallback: string) => {
383
368
  const message = error?.response?.data?.message;
@@ -406,11 +391,11 @@ export default function TituloReceberDetalhePage() {
406
391
  });
407
392
 
408
393
  await refetch();
409
- showToastHandler?.('success', 'Título aprovado com sucesso');
394
+ showToastHandler?.('success', t('messages.approveSuccess'));
410
395
  } catch (error) {
411
396
  showToastHandler?.(
412
397
  'error',
413
- getErrorMessage(error, 'Não foi possível aprovar o título')
398
+ getErrorMessage(error, t('messages.approveError'))
414
399
  );
415
400
  } finally {
416
401
  setIsApproving(false);
@@ -436,18 +421,21 @@ export default function TituloReceberDetalhePage() {
436
421
 
437
422
  await refetch();
438
423
  setIsSettleDialogOpen(false);
439
- showToastHandler?.('success', 'Recebimento registrado com sucesso');
424
+ showToastHandler?.('success', t('messages.settleSuccess'));
440
425
  } catch (error) {
441
426
  showToastHandler?.(
442
427
  'error',
443
- getErrorMessage(error, 'Não foi possível registrar o recebimento')
428
+ getErrorMessage(error, t('messages.settleError'))
444
429
  );
445
430
  } finally {
446
431
  setIsSettling(false);
447
432
  }
448
433
  };
449
434
 
450
- const handleReverseSettlement = async (settlementId: string) => {
435
+ const handleReverseSettlement = async (
436
+ settlementId: string,
437
+ reasonOverride?: string
438
+ ) => {
451
439
  if (!settlementId || reversingSettlementId) {
452
440
  return;
453
441
  }
@@ -455,17 +443,27 @@ export default function TituloReceberDetalhePage() {
455
443
  setReversingSettlementId(settlementId);
456
444
  try {
457
445
  await request({
458
- url: `/finance/accounts-receivable/installments/${titulo.id}/settlements/${settlementId}/reverse`,
459
- method: 'PATCH',
460
- data: {},
446
+ url: `/finance/settlements/${settlementId}/reverse`,
447
+ method: 'POST',
448
+ data: {
449
+ reason:
450
+ reasonOverride?.trim() ||
451
+ reverseReason?.trim() ||
452
+ t('messages.reverseDefaultReason'),
453
+ memo:
454
+ reasonOverride?.trim() ||
455
+ reverseReason?.trim() ||
456
+ t('messages.reverseDefaultReason'),
457
+ },
461
458
  });
462
459
 
463
460
  await refetch();
464
- showToastHandler?.('success', 'Estorno realizado com sucesso');
461
+ setReverseReason('');
462
+ showToastHandler?.('success', t('messages.reverseSuccess'));
465
463
  } catch (error) {
466
464
  showToastHandler?.(
467
465
  'error',
468
- getErrorMessage(error, 'Não foi possível estornar o recebimento')
466
+ getErrorMessage(error, t('messages.reverseError'))
469
467
  );
470
468
  } finally {
471
469
  setReversingSettlementId(null);
@@ -505,7 +503,7 @@ export default function TituloReceberDetalhePage() {
505
503
  onClick={() => void handleApprove()}
506
504
  >
507
505
  <CheckCircle className="mr-2 h-4 w-4" />
508
- Aprovar
506
+ {t('actions.approve')}
509
507
  </DropdownMenuItem>
510
508
  <DropdownMenuItem
511
509
  disabled={!canSettle || settleCandidates.length === 0}
@@ -527,9 +525,9 @@ export default function TituloReceberDetalhePage() {
527
525
  >
528
526
  <SheetContent className="w-full sm:max-w-lg overflow-y-auto">
529
527
  <SheetHeader>
530
- <SheetTitle>Registrar recebimento</SheetTitle>
528
+ <SheetTitle>{t('settleSheet.title')}</SheetTitle>
531
529
  <SheetDescription>
532
- Informe a parcela e o valor para baixa parcial ou total.
530
+ {t('settleSheet.description')}
533
531
  </SheetDescription>
534
532
  </SheetHeader>
535
533
 
@@ -543,7 +541,9 @@ export default function TituloReceberDetalhePage() {
543
541
  name="installmentId"
544
542
  render={({ field }) => (
545
543
  <FormItem>
546
- <FormLabel>Parcela</FormLabel>
544
+ <FormLabel>
545
+ {t('settleSheet.installmentLabel')}
546
+ </FormLabel>
547
547
  <Select
548
548
  value={field.value}
549
549
  onValueChange={(value) => {
@@ -564,17 +564,23 @@ export default function TituloReceberDetalhePage() {
564
564
  >
565
565
  <FormControl>
566
566
  <SelectTrigger>
567
- <SelectValue placeholder="Selecione" />
567
+ <SelectValue
568
+ placeholder={t(
569
+ 'settleSheet.installmentPlaceholder'
570
+ )}
571
+ />
568
572
  </SelectTrigger>
569
573
  </FormControl>
570
574
  <SelectContent>
571
575
  {settleCandidates.map((parcela: any) => (
572
576
  <SelectItem key={parcela.id} value={parcela.id}>
573
- Parcela {parcela.numero} - em aberto:{' '}
574
- {new Intl.NumberFormat('pt-BR', {
575
- style: 'currency',
576
- currency: 'BRL',
577
- }).format(Number(parcela.valorAberto || 0))}
577
+ {t('settleSheet.installmentOption', {
578
+ number: parcela.numero,
579
+ amount: new Intl.NumberFormat('pt-BR', {
580
+ style: 'currency',
581
+ currency: 'BRL',
582
+ }).format(Number(parcela.valorAberto || 0)),
583
+ })}
578
584
  </SelectItem>
579
585
  ))}
580
586
  </SelectContent>
@@ -589,7 +595,7 @@ export default function TituloReceberDetalhePage() {
589
595
  name="amount"
590
596
  render={({ field }) => (
591
597
  <FormItem>
592
- <FormLabel>Valor</FormLabel>
598
+ <FormLabel>{t('settleSheet.amountLabel')}</FormLabel>
593
599
  <FormControl>
594
600
  <InputMoney
595
601
  value={Number(field.value || 0)}
@@ -608,7 +614,9 @@ export default function TituloReceberDetalhePage() {
608
614
  name="description"
609
615
  render={({ field }) => (
610
616
  <FormItem>
611
- <FormLabel>Descrição (opcional)</FormLabel>
617
+ <FormLabel>
618
+ {t('settleSheet.descriptionLabel')}
619
+ </FormLabel>
612
620
  <FormControl>
613
621
  <Input {...field} value={field.value || ''} />
614
622
  </FormControl>
@@ -624,16 +632,85 @@ export default function TituloReceberDetalhePage() {
624
632
  disabled={isSettling}
625
633
  onClick={() => setIsSettleDialogOpen(false)}
626
634
  >
627
- Cancelar
635
+ {t('common.cancel')}
628
636
  </Button>
629
637
  <Button type="submit" disabled={isSettling}>
630
- Confirmar recebimento
638
+ {t('settleSheet.confirm')}
631
639
  </Button>
632
640
  </div>
633
641
  </form>
634
642
  </Form>
635
643
  </SheetContent>
636
644
  </Sheet>
645
+
646
+ <Sheet
647
+ open={isReverseSheetOpen}
648
+ onOpenChange={(open) => {
649
+ setIsReverseSheetOpen(open);
650
+
651
+ if (!open) {
652
+ setSelectedSettlementIdToReverse(null);
653
+ setReverseReason('');
654
+ }
655
+ }}
656
+ >
657
+ <SheetContent className="w-full sm:max-w-lg overflow-y-auto">
658
+ <SheetHeader>
659
+ <SheetTitle>{t('reverseSheet.title')}</SheetTitle>
660
+ <SheetDescription>
661
+ {t('reverseSheet.description')}
662
+ </SheetDescription>
663
+ </SheetHeader>
664
+
665
+ <div className="space-y-4 px-4">
666
+ <div className="space-y-2">
667
+ <Label>{t('reverseSheet.reasonLabel')}</Label>
668
+ <Input
669
+ value={reverseReason}
670
+ onChange={(event) => setReverseReason(event.target.value)}
671
+ placeholder={t('reverseSheet.reasonPlaceholder')}
672
+ maxLength={255}
673
+ disabled={!!reversingSettlementId}
674
+ />
675
+ </div>
676
+
677
+ <div className="flex flex-col gap-2">
678
+ <Button
679
+ disabled={
680
+ !!reversingSettlementId ||
681
+ !selectedSettlementIdToReverse
682
+ }
683
+ onClick={() => {
684
+ if (!selectedSettlementIdToReverse) {
685
+ return;
686
+ }
687
+
688
+ void handleReverseSettlement(
689
+ selectedSettlementIdToReverse
690
+ ).finally(() => {
691
+ setIsReverseSheetOpen(false);
692
+ setSelectedSettlementIdToReverse(null);
693
+ setReverseReason('');
694
+ });
695
+ }}
696
+ >
697
+ {t('reverseSheet.confirm')}
698
+ </Button>
699
+ <Button
700
+ variant="outline"
701
+ disabled={!!reversingSettlementId}
702
+ onClick={() => {
703
+ setIsReverseSheetOpen(false);
704
+ setSelectedSettlementIdToReverse(null);
705
+ setReverseReason('');
706
+ }}
707
+ >
708
+ {t('common.cancel')}
709
+ </Button>
710
+ </div>
711
+ </div>
712
+ </SheetContent>
713
+ </Sheet>
637
714
  </div>
638
715
  }
639
716
  />
@@ -711,35 +788,21 @@ export default function TituloReceberDetalhePage() {
711
788
  onChange={handleChangeTags}
712
789
  onCreateTag={handleCreateTag}
713
790
  disabled={isCreatingTag}
714
- emptyText={tTagSelector('noTags', 'Sem tags')}
791
+ emptyText={t('tagSelector.noTags')}
715
792
  labels={{
716
- addTag: tTagSelector('addTag', 'Adicionar tag'),
717
- sheetTitle: tTagSelector('sheetTitle', 'Gerenciar tags'),
718
- sheetDescription: tTagSelector(
719
- 'sheetDescription',
720
- 'Selecione tags existentes ou crie uma nova.'
721
- ),
722
- createLabel: tTagSelector('createLabel', 'Nova tag'),
723
- createPlaceholder: tTagSelector(
724
- 'createPlaceholder',
725
- 'Digite o nome da tag'
726
- ),
727
- createAction: tTagSelector('createAction', 'Criar tag'),
728
- popularTitle: tTagSelector(
729
- 'popularTitle',
730
- 'Tags mais usadas'
731
- ),
732
- selectedTitle: tTagSelector(
733
- 'selectedTitle',
734
- 'Tags selecionadas'
735
- ),
736
- noTags: tTagSelector('noTags', 'Sem tags'),
737
- cancel: tTagSelector('cancel', 'Cancelar'),
738
- apply: tTagSelector('apply', 'Aplicar'),
793
+ addTag: t('tagSelector.addTag'),
794
+ sheetTitle: t('tagSelector.sheetTitle'),
795
+ sheetDescription: t('tagSelector.sheetDescription'),
796
+ createLabel: t('tagSelector.createLabel'),
797
+ createPlaceholder: t('tagSelector.createPlaceholder'),
798
+ createAction: t('tagSelector.createAction'),
799
+ popularTitle: t('tagSelector.popularTitle'),
800
+ selectedTitle: t('tagSelector.selectedTitle'),
801
+ noTags: t('tagSelector.noTags'),
802
+ cancel: t('tagSelector.cancel'),
803
+ apply: t('tagSelector.apply'),
739
804
  removeTagAria: (tagName: string) =>
740
- t.has('tagSelector.removeTagAria')
741
- ? t('tagSelector.removeTagAria', { tag: tagName })
742
- : `Remover tag ${tagName}`,
805
+ t('tagSelector.removeTagAria', { tag: tagName }),
743
806
  }}
744
807
  />
745
808
  </dd>
@@ -839,7 +902,9 @@ export default function TituloReceberDetalhePage() {
839
902
  </TableHead>
840
903
  <TableHead>{t('receiptsTable.account')}</TableHead>
841
904
  <TableHead>{t('receiptsTable.method')}</TableHead>
842
- <TableHead className="text-right">Ações</TableHead>
905
+ <TableHead className="text-right">
906
+ {t('receiptsTable.actions')}
907
+ </TableHead>
843
908
  </TableRow>
844
909
  </TableHeader>
845
910
  <TableBody>
@@ -863,47 +928,25 @@ export default function TituloReceberDetalhePage() {
863
928
  {liq.metodo}
864
929
  </TableCell>
865
930
  <TableCell className="text-right">
866
- <AlertDialog>
867
- <AlertDialogTrigger asChild>
868
- <Button
869
- variant="outline"
870
- size="sm"
871
- disabled={
872
- !liq.settlementId ||
873
- liq.status === 'reversed' ||
874
- !!reversingSettlementId
875
- }
876
- >
877
- <Undo className="mr-2 h-4 w-4" />
878
- Estornar
879
- </Button>
880
- </AlertDialogTrigger>
881
- <AlertDialogContent>
882
- <AlertDialogHeader>
883
- <AlertDialogTitle>
884
- Confirmar estorno
885
- </AlertDialogTitle>
886
- <AlertDialogDescription>
887
- Esta ação estorna o recebimento e
888
- recalcula saldos e status.
889
- </AlertDialogDescription>
890
- </AlertDialogHeader>
891
- <AlertDialogFooter>
892
- <AlertDialogCancel>
893
- Cancelar
894
- </AlertDialogCancel>
895
- <AlertDialogAction
896
- onClick={() =>
897
- void handleReverseSettlement(
898
- String(liq.settlementId)
899
- )
900
- }
901
- >
902
- Confirmar estorno
903
- </AlertDialogAction>
904
- </AlertDialogFooter>
905
- </AlertDialogContent>
906
- </AlertDialog>
931
+ <Button
932
+ variant="outline"
933
+ size="sm"
934
+ disabled={
935
+ !liq.settlementId ||
936
+ liq.status === 'reversed' ||
937
+ !!reversingSettlementId
938
+ }
939
+ onClick={() => {
940
+ setSelectedSettlementIdToReverse(
941
+ String(liq.settlementId)
942
+ );
943
+ setReverseReason('');
944
+ setIsReverseSheetOpen(true);
945
+ }}
946
+ >
947
+ <Undo className="mr-2 h-4 w-4" />
948
+ {t('receiptsTable.reverseButton')}
949
+ </Button>
907
950
  </TableCell>
908
951
  </TableRow>
909
952
  );