@hed-hog/finance 0.0.278 → 0.0.285

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 (39) hide show
  1. package/README.md +65 -29
  2. package/dist/dto/finance-report-query.dto.d.ts +16 -0
  3. package/dist/dto/finance-report-query.dto.d.ts.map +1 -0
  4. package/dist/dto/finance-report-query.dto.js +59 -0
  5. package/dist/dto/finance-report-query.dto.js.map +1 -0
  6. package/dist/finance-reports.controller.d.ts +71 -0
  7. package/dist/finance-reports.controller.d.ts.map +1 -0
  8. package/dist/finance-reports.controller.js +61 -0
  9. package/dist/finance-reports.controller.js.map +1 -0
  10. package/dist/finance.module.d.ts.map +1 -1
  11. package/dist/finance.module.js +2 -0
  12. package/dist/finance.module.js.map +1 -1
  13. package/dist/finance.service.d.ts +93 -0
  14. package/dist/finance.service.d.ts.map +1 -1
  15. package/dist/finance.service.js +456 -0
  16. package/dist/finance.service.js.map +1 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/hedhog/data/menu.yaml +46 -0
  22. package/hedhog/data/route.yaml +27 -0
  23. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +158 -125
  24. package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +102 -88
  25. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +113 -89
  26. package/hedhog/frontend/app/reports/_lib/report-aggregations.ts.ejs +275 -0
  27. package/hedhog/frontend/app/reports/_lib/report-mocks.ts.ejs +186 -0
  28. package/hedhog/frontend/app/reports/_lib/use-finance-reports.ts.ejs +233 -0
  29. package/hedhog/frontend/app/reports/overview-results/page.tsx.ejs +355 -0
  30. package/hedhog/frontend/app/reports/top-customers/page.tsx.ejs +427 -0
  31. package/hedhog/frontend/app/reports/top-operational-expenses/page.tsx.ejs +433 -0
  32. package/hedhog/frontend/messages/en.json +179 -0
  33. package/hedhog/frontend/messages/pt.json +179 -0
  34. package/package.json +7 -7
  35. package/src/dto/finance-report-query.dto.ts +49 -0
  36. package/src/finance-reports.controller.ts +28 -0
  37. package/src/finance.module.ts +2 -0
  38. package/src/finance.service.ts +645 -10
  39. package/src/index.ts +1 -0
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { Page, PageHeader } from '@/components/entity-list';
3
+ import { EmptyState, Page, PageHeader } from '@/components/entity-list';
4
4
  import { RichTextEditor } from '@/components/rich-text-editor';
5
5
  import { Button } from '@/components/ui/button';
6
6
  import {
@@ -548,94 +548,108 @@ export default function CobrancaPage() {
548
548
  <CardDescription>{t('table.description')}</CardDescription>
549
549
  </CardHeader>
550
550
  <CardContent>
551
- <Table>
552
- <TableHeader>
553
- <TableRow>
554
- <TableHead>{t('table.headers.client')}</TableHead>
555
- <TableHead className="text-right">
556
- {t('table.headers.range0to30')}
557
- </TableHead>
558
- <TableHead className="text-right">
559
- {t('table.headers.range31to60')}
560
- </TableHead>
561
- <TableHead className="text-right">
562
- {t('table.headers.range61to90')}
563
- </TableHead>
564
- <TableHead className="text-right">
565
- {t('table.headers.range90plus')}
566
- </TableHead>
567
- <TableHead className="text-right">
568
- {t('table.headers.total')}
569
- </TableHead>
570
- <TableHead className="text-right">
571
- {t('table.headers.actions')}
572
- </TableHead>
573
- </TableRow>
574
- </TableHeader>
575
- <TableBody>
576
- {agingInadimplencia.map((item) => (
577
- <TableRow key={item.clienteId}>
578
- <TableCell className="font-medium">{item.cliente}</TableCell>
579
- <TableCell className="text-right">
580
- {item.bucket0_30 > 0 ? (
581
- <Money value={item.bucket0_30} />
582
- ) : (
583
- '-'
584
- )}
585
- </TableCell>
586
- <TableCell className="text-right">
587
- {item.bucket31_60 > 0 ? (
588
- <Money value={item.bucket31_60} />
589
- ) : (
590
- '-'
591
- )}
592
- </TableCell>
593
- <TableCell className="text-right">
594
- {item.bucket61_90 > 0 ? (
595
- <Money value={item.bucket61_90} />
596
- ) : (
597
- '-'
598
- )}
599
- </TableCell>
600
- <TableCell className="text-right">
601
- {item.bucket90plus > 0 ? (
602
- <span className="font-medium text-destructive">
603
- <Money value={item.bucket90plus} />
604
- </span>
605
- ) : (
606
- '-'
607
- )}
608
- </TableCell>
609
- <TableCell className="text-right font-semibold">
610
- <Money value={item.total} />
611
- </TableCell>
612
- <TableCell>
613
- <div className="flex justify-end gap-2">
614
- <HistoricoContatosSheet
615
- cliente={item.cliente}
616
- historicoContatos={historicoContatos.filter(
617
- (contato) => contato.clienteId === item.clienteId
618
- )}
619
- t={t}
620
- />
621
- <EnviarCobrancaDialog
622
- cliente={item.cliente}
623
- clienteId={item.clienteId}
624
- onSuccess={refetch}
625
- t={t}
626
- />
627
- <RegistrarAcordoDialog
628
- cliente={item.cliente}
629
- clienteId={item.clienteId}
630
- onSuccess={refetch}
631
- t={t}
632
- />
633
- </div>
634
- </TableCell>
551
+ {agingInadimplencia.length > 0 ? (
552
+ <Table>
553
+ <TableHeader>
554
+ <TableRow>
555
+ <TableHead>{t('table.headers.client')}</TableHead>
556
+ <TableHead className="text-right">
557
+ {t('table.headers.range0to30')}
558
+ </TableHead>
559
+ <TableHead className="text-right">
560
+ {t('table.headers.range31to60')}
561
+ </TableHead>
562
+ <TableHead className="text-right">
563
+ {t('table.headers.range61to90')}
564
+ </TableHead>
565
+ <TableHead className="text-right">
566
+ {t('table.headers.range90plus')}
567
+ </TableHead>
568
+ <TableHead className="text-right">
569
+ {t('table.headers.total')}
570
+ </TableHead>
571
+ <TableHead className="text-right">
572
+ {t('table.headers.actions')}
573
+ </TableHead>
635
574
  </TableRow>
636
- ))}
637
- </TableBody>
638
- </Table>
575
+ </TableHeader>
576
+ <TableBody>
577
+ {agingInadimplencia.map((item) => (
578
+ <TableRow key={item.clienteId}>
579
+ <TableCell className="font-medium">
580
+ {item.cliente}
581
+ </TableCell>
582
+ <TableCell className="text-right">
583
+ {item.bucket0_30 > 0 ? (
584
+ <Money value={item.bucket0_30} />
585
+ ) : (
586
+ '-'
587
+ )}
588
+ </TableCell>
589
+ <TableCell className="text-right">
590
+ {item.bucket31_60 > 0 ? (
591
+ <Money value={item.bucket31_60} />
592
+ ) : (
593
+ '-'
594
+ )}
595
+ </TableCell>
596
+ <TableCell className="text-right">
597
+ {item.bucket61_90 > 0 ? (
598
+ <Money value={item.bucket61_90} />
599
+ ) : (
600
+ '-'
601
+ )}
602
+ </TableCell>
603
+ <TableCell className="text-right">
604
+ {item.bucket90plus > 0 ? (
605
+ <span className="font-medium text-destructive">
606
+ <Money value={item.bucket90plus} />
607
+ </span>
608
+ ) : (
609
+ '-'
610
+ )}
611
+ </TableCell>
612
+ <TableCell className="text-right font-semibold">
613
+ <Money value={item.total} />
614
+ </TableCell>
615
+ <TableCell>
616
+ <div className="flex justify-end gap-2">
617
+ <HistoricoContatosSheet
618
+ cliente={item.cliente}
619
+ historicoContatos={historicoContatos.filter(
620
+ (contato) => contato.clienteId === item.clienteId
621
+ )}
622
+ t={t}
623
+ />
624
+ <EnviarCobrancaDialog
625
+ cliente={item.cliente}
626
+ clienteId={item.clienteId}
627
+ onSuccess={refetch}
628
+ t={t}
629
+ />
630
+ <RegistrarAcordoDialog
631
+ cliente={item.cliente}
632
+ clienteId={item.clienteId}
633
+ onSuccess={refetch}
634
+ t={t}
635
+ />
636
+ </div>
637
+ </TableCell>
638
+ </TableRow>
639
+ ))}
640
+ </TableBody>
641
+ </Table>
642
+ ) : (
643
+ <EmptyState
644
+ icon={<FileText className="h-12 w-12" />}
645
+ title={t('empty.title')}
646
+ description={t('empty.description')}
647
+ actionLabel={t('empty.action')}
648
+ onAction={() => {
649
+ void refetch();
650
+ }}
651
+ />
652
+ )}
639
653
  </CardContent>
640
654
  </Card>
641
655
  </Page>
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { Page, PageHeader } from '@/components/entity-list';
3
+ import { EmptyState, Page, PageHeader } from '@/components/entity-list';
4
4
  import { Button } from '@/components/ui/button';
5
5
  import {
6
6
  Card,
@@ -357,16 +357,27 @@ function ImportarExtratoSheet({
357
357
  defaultBankAccountId,
358
358
  onImported,
359
359
  onBankAccountCreated,
360
+ open,
361
+ onOpenChange,
360
362
  }: {
361
363
  contasBancarias: BankAccount[];
362
364
  t: ReturnType<typeof useTranslations>;
363
365
  defaultBankAccountId?: string;
364
366
  onImported: () => Promise<any> | void;
365
367
  onBankAccountCreated: (createdBankAccountId?: string) => Promise<void> | void;
368
+ open?: boolean;
369
+ onOpenChange?: (open: boolean) => void;
366
370
  }) {
367
371
  const { request, showToastHandler } = useApp();
368
- const [open, setOpen] = useState(false);
372
+ const [internalOpen, setInternalOpen] = useState(false);
369
373
  const [openNovaContaSheet, setOpenNovaContaSheet] = useState(false);
374
+ const isOpen = open ?? internalOpen;
375
+ const handleOpenChange = (nextOpen: boolean) => {
376
+ onOpenChange?.(nextOpen);
377
+ if (open === undefined) {
378
+ setInternalOpen(nextOpen);
379
+ }
380
+ };
370
381
 
371
382
  const form = useForm<ImportStatementFormValues>({
372
383
  resolver: zodResolver(importStatementSchema),
@@ -376,7 +387,7 @@ function ImportarExtratoSheet({
376
387
  });
377
388
 
378
389
  useEffect(() => {
379
- if (!open) {
390
+ if (!isOpen) {
380
391
  return;
381
392
  }
382
393
 
@@ -384,7 +395,7 @@ function ImportarExtratoSheet({
384
395
  bankAccountId: defaultBankAccountId || '',
385
396
  file: undefined as unknown as File,
386
397
  });
387
- }, [defaultBankAccountId, form, open]);
398
+ }, [defaultBankAccountId, form, isOpen]);
388
399
 
389
400
  const handleSubmit = async (values: ImportStatementFormValues) => {
390
401
  const formData = new FormData();
@@ -400,15 +411,15 @@ function ImportarExtratoSheet({
400
411
 
401
412
  await onImported();
402
413
  showToastHandler?.('success', 'Extrato importado com sucesso');
403
- setOpen(false);
414
+ handleOpenChange(false);
404
415
  } catch {
405
416
  showToastHandler?.('error', 'Não foi possível importar o extrato');
406
417
  }
407
418
  };
408
419
 
409
420
  return (
410
- <Sheet open={open} onOpenChange={setOpen}>
411
- <Button onClick={() => setOpen(true)}>
421
+ <Sheet open={isOpen} onOpenChange={handleOpenChange}>
422
+ <Button onClick={() => handleOpenChange(true)}>
412
423
  <Upload className="mr-2 h-4 w-4" />
413
424
  {t('importDialog.action')}
414
425
  </Button>
@@ -510,7 +521,7 @@ function ImportarExtratoSheet({
510
521
  <Button
511
522
  type="button"
512
523
  variant="outline"
513
- onClick={() => setOpen(false)}
524
+ onClick={() => handleOpenChange(false)}
514
525
  >
515
526
  {t('common.cancel')}
516
527
  </Button>
@@ -536,6 +547,7 @@ export default function ExtratosPage() {
536
547
  const [contaFilter, setContaFilter] = useState<string>('');
537
548
  const [search, setSearch] = useState('');
538
549
  const [debouncedSearch, setDebouncedSearch] = useState('');
550
+ const [isImportSheetOpen, setIsImportSheetOpen] = useState(false);
539
551
  const [extratoSelecionado, setExtratoSelecionado] =
540
552
  useState<Statement | null>(null);
541
553
 
@@ -717,6 +729,8 @@ export default function ExtratosPage() {
717
729
  contasBancarias={contasBancarias}
718
730
  t={t}
719
731
  defaultBankAccountId={contaFilter}
732
+ open={isImportSheetOpen}
733
+ onOpenChange={setIsImportSheetOpen}
720
734
  onImported={refetchExtratos}
721
735
  onBankAccountCreated={handleBankAccountCreated}
722
736
  />
@@ -797,89 +811,99 @@ export default function ExtratosPage() {
797
811
  </Card>
798
812
  </div>
799
813
 
800
- <Card>
801
- <CardHeader>
802
- <CardTitle>{t('table.title')}</CardTitle>
803
- <CardDescription>
804
- {t('table.foundTransactions', { count: extratos.length })}
805
- </CardDescription>
806
- </CardHeader>
807
- <CardContent>
808
- <div className="overflow-x-auto">
809
- <Table className="min-w-[760px] table-fixed">
810
- <TableHeader>
811
- <TableRow>
812
- <TableHead className="w-[110px]">
813
- {t('table.headers.date')}
814
- </TableHead>
815
- <TableHead>{t('table.headers.description')}</TableHead>
816
- <TableHead className="w-[130px] text-right">
817
- {t('table.headers.value')}
818
- </TableHead>
819
- <TableHead className="w-[110px]">
820
- {t('table.headers.type')}
821
- </TableHead>
822
- <TableHead className="w-[140px]">
823
- {t('table.headers.reconciliation')}
824
- </TableHead>
825
- </TableRow>
826
- </TableHeader>
827
- <TableBody>
828
- {extratos.map((extrato) => (
829
- <TableRow
830
- key={extrato.id}
831
- className="cursor-pointer"
832
- onClick={() => setExtratoSelecionado(extrato)}
833
- onKeyDown={(event) => {
834
- if (event.key === 'Enter' || event.key === ' ') {
835
- event.preventDefault();
836
- setExtratoSelecionado(extrato);
837
- }
838
- }}
839
- role="button"
840
- tabIndex={0}
841
- >
842
- <TableCell>{formatarData(extrato.data)}</TableCell>
843
- <TableCell className="truncate" title={extrato.descricao}>
844
- {extrato.descricao}
845
- </TableCell>
846
- <TableCell className="text-right">
847
- <span
848
- className={
849
- extrato.tipo === 'entrada'
850
- ? 'text-green-600'
851
- : 'text-red-600'
814
+ {extratos.length > 0 ? (
815
+ <Card>
816
+ <CardHeader>
817
+ <CardTitle>{t('table.title')}</CardTitle>
818
+ <CardDescription>
819
+ {t('table.foundTransactions', { count: extratos.length })}
820
+ </CardDescription>
821
+ </CardHeader>
822
+ <CardContent>
823
+ <div className="overflow-x-auto">
824
+ <Table className="min-w-[760px] table-fixed">
825
+ <TableHeader>
826
+ <TableRow>
827
+ <TableHead className="w-[110px]">
828
+ {t('table.headers.date')}
829
+ </TableHead>
830
+ <TableHead>{t('table.headers.description')}</TableHead>
831
+ <TableHead className="w-[130px] text-right">
832
+ {t('table.headers.value')}
833
+ </TableHead>
834
+ <TableHead className="w-[110px]">
835
+ {t('table.headers.type')}
836
+ </TableHead>
837
+ <TableHead className="w-[140px]">
838
+ {t('table.headers.reconciliation')}
839
+ </TableHead>
840
+ </TableRow>
841
+ </TableHeader>
842
+ <TableBody>
843
+ {extratos.map((extrato) => (
844
+ <TableRow
845
+ key={extrato.id}
846
+ className="cursor-pointer"
847
+ onClick={() => setExtratoSelecionado(extrato)}
848
+ onKeyDown={(event) => {
849
+ if (event.key === 'Enter' || event.key === ' ') {
850
+ event.preventDefault();
851
+ setExtratoSelecionado(extrato);
852
852
  }
853
- >
854
- <Money value={extrato.valor} />
855
- </span>
856
- </TableCell>
857
- <TableCell>
858
- {extrato.tipo === 'entrada' ? (
859
- <span className="flex items-center gap-1 text-green-600">
860
- <ArrowUpRight className="h-4 w-4" />
861
- {t('types.inflow')}
862
- </span>
863
- ) : (
864
- <span className="flex items-center gap-1 text-red-600">
865
- <ArrowDownRight className="h-4 w-4" />
866
- {t('types.outflow')}
853
+ }}
854
+ role="button"
855
+ tabIndex={0}
856
+ >
857
+ <TableCell>{formatarData(extrato.data)}</TableCell>
858
+ <TableCell className="truncate" title={extrato.descricao}>
859
+ {extrato.descricao}
860
+ </TableCell>
861
+ <TableCell className="text-right">
862
+ <span
863
+ className={
864
+ extrato.tipo === 'entrada'
865
+ ? 'text-green-600'
866
+ : 'text-red-600'
867
+ }
868
+ >
869
+ <Money value={extrato.valor} />
867
870
  </span>
868
- )}
869
- </TableCell>
870
- <TableCell>
871
- <StatusBadge
872
- status={extrato.statusConciliacao}
873
- type="conciliacao"
874
- />
875
- </TableCell>
876
- </TableRow>
877
- ))}
878
- </TableBody>
879
- </Table>
880
- </div>
881
- </CardContent>
882
- </Card>
871
+ </TableCell>
872
+ <TableCell>
873
+ {extrato.tipo === 'entrada' ? (
874
+ <span className="flex items-center gap-1 text-green-600">
875
+ <ArrowUpRight className="h-4 w-4" />
876
+ {t('types.inflow')}
877
+ </span>
878
+ ) : (
879
+ <span className="flex items-center gap-1 text-red-600">
880
+ <ArrowDownRight className="h-4 w-4" />
881
+ {t('types.outflow')}
882
+ </span>
883
+ )}
884
+ </TableCell>
885
+ <TableCell>
886
+ <StatusBadge
887
+ status={extrato.statusConciliacao}
888
+ type="conciliacao"
889
+ />
890
+ </TableCell>
891
+ </TableRow>
892
+ ))}
893
+ </TableBody>
894
+ </Table>
895
+ </div>
896
+ </CardContent>
897
+ </Card>
898
+ ) : (
899
+ <EmptyState
900
+ icon={<Upload className="h-12 w-12" />}
901
+ title={t('empty.title')}
902
+ description={t('empty.description')}
903
+ actionLabel={t('importDialog.action')}
904
+ onAction={() => setIsImportSheetOpen(true)}
905
+ />
906
+ )}
883
907
 
884
908
  <Dialog
885
909
  open={!!extratoSelecionado}