@hed-hog/finance 0.0.299 → 0.0.301

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 (79) hide show
  1. package/dist/dto/create-bank-account.dto.d.ts +1 -0
  2. package/dist/dto/create-bank-account.dto.d.ts.map +1 -1
  3. package/dist/dto/create-bank-account.dto.js +7 -0
  4. package/dist/dto/create-bank-account.dto.js.map +1 -1
  5. package/dist/dto/update-bank-account.dto.d.ts +1 -0
  6. package/dist/dto/update-bank-account.dto.d.ts.map +1 -1
  7. package/dist/dto/update-bank-account.dto.js +7 -0
  8. package/dist/dto/update-bank-account.dto.js.map +1 -1
  9. package/dist/finance-bank-accounts.controller.d.ts +3 -0
  10. package/dist/finance-bank-accounts.controller.d.ts.map +1 -1
  11. package/dist/finance-data.controller.d.ts +1 -0
  12. package/dist/finance-data.controller.d.ts.map +1 -1
  13. package/dist/finance.contract-activated.subscriber.d.ts +24 -0
  14. package/dist/finance.contract-activated.subscriber.d.ts.map +1 -0
  15. package/dist/finance.contract-activated.subscriber.js +519 -0
  16. package/dist/finance.contract-activated.subscriber.js.map +1 -0
  17. package/dist/finance.contract-activated.subscriber.spec.d.ts +2 -0
  18. package/dist/finance.contract-activated.subscriber.spec.d.ts.map +1 -0
  19. package/dist/finance.contract-activated.subscriber.spec.js +302 -0
  20. package/dist/finance.contract-activated.subscriber.spec.js.map +1 -0
  21. package/dist/finance.module.d.ts.map +1 -1
  22. package/dist/finance.module.js +6 -1
  23. package/dist/finance.module.js.map +1 -1
  24. package/dist/finance.service.d.ts +4 -0
  25. package/dist/finance.service.d.ts.map +1 -1
  26. package/dist/finance.service.js +5 -0
  27. package/dist/finance.service.js.map +1 -1
  28. package/hedhog/data/dashboard.yaml +6 -0
  29. package/hedhog/data/dashboard_component.yaml +72 -17
  30. package/hedhog/data/dashboard_component_role.yaml +30 -0
  31. package/hedhog/data/dashboard_item.yaml +155 -0
  32. package/hedhog/data/dashboard_role.yaml +6 -0
  33. package/hedhog/data/role_menu.yaml +6 -0
  34. package/hedhog/data/role_route.yaml +127 -0
  35. package/hedhog/frontend/app/_components/finance-layout.tsx.ejs +108 -0
  36. package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +91 -106
  37. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +1306 -1145
  38. package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +288 -268
  39. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +491 -351
  40. package/hedhog/frontend/app/administration/audit-logs/page.tsx.ejs +157 -173
  41. package/hedhog/frontend/app/administration/categories/page.tsx.ejs +44 -62
  42. package/hedhog/frontend/app/administration/cost-centers/page.tsx.ejs +62 -80
  43. package/hedhog/frontend/app/administration/period-close/page.tsx.ejs +151 -170
  44. package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +586 -224
  45. package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +204 -226
  46. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +122 -140
  47. package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +32 -49
  48. package/hedhog/frontend/app/planning/cash-flow-forecast/page.tsx.ejs +84 -108
  49. package/hedhog/frontend/app/planning/receivables-calendar/page.tsx.ejs +53 -70
  50. package/hedhog/frontend/app/planning/scenarios/page.tsx.ejs +98 -95
  51. package/hedhog/frontend/app/reports/actual-vs-forecast/page.tsx.ejs +100 -125
  52. package/hedhog/frontend/app/reports/aging-default/page.tsx.ejs +77 -105
  53. package/hedhog/frontend/app/reports/cash-position/page.tsx.ejs +99 -134
  54. package/hedhog/frontend/app/reports/overview-results/page.tsx.ejs +147 -182
  55. package/hedhog/frontend/app/reports/top-customers/page.tsx.ejs +49 -61
  56. package/hedhog/frontend/app/reports/top-operational-expenses/page.tsx.ejs +49 -67
  57. package/hedhog/frontend/messages/en.json +224 -70
  58. package/hedhog/frontend/messages/pt.json +224 -70
  59. package/hedhog/frontend/widgets/alerts.tsx.ejs +1 -1
  60. package/hedhog/frontend/widgets/bank-reconciliation-status.tsx.ejs +142 -0
  61. package/hedhog/frontend/widgets/cash-balance-kpi.tsx.ejs +9 -9
  62. package/hedhog/frontend/widgets/cash-flow-chart.tsx.ejs +1 -1
  63. package/hedhog/frontend/widgets/default-kpi.tsx.ejs +9 -9
  64. package/hedhog/frontend/widgets/payable-30d-kpi.tsx.ejs +9 -9
  65. package/hedhog/frontend/widgets/pending-approvals-kpi.tsx.ejs +78 -0
  66. package/hedhog/frontend/widgets/pending-approvals-list.tsx.ejs +147 -0
  67. package/hedhog/frontend/widgets/pending-reconciliation-kpi.tsx.ejs +84 -0
  68. package/hedhog/frontend/widgets/receivable-30d-kpi.tsx.ejs +9 -9
  69. package/hedhog/frontend/widgets/receivable-aging-analysis.tsx.ejs +163 -0
  70. package/hedhog/frontend/widgets/upcoming-payable.tsx.ejs +1 -1
  71. package/hedhog/frontend/widgets/upcoming-receivable.tsx.ejs +1 -1
  72. package/hedhog/table/bank_account.yaml +8 -0
  73. package/package.json +7 -6
  74. package/src/dto/create-bank-account.dto.ts +7 -1
  75. package/src/dto/update-bank-account.dto.ts +7 -1
  76. package/src/finance.contract-activated.subscriber.spec.ts +392 -0
  77. package/src/finance.contract-activated.subscriber.ts +780 -0
  78. package/src/finance.module.ts +6 -1
  79. package/src/finance.service.ts +4 -0
@@ -1,35 +1,46 @@
1
1
  'use client';
2
2
 
3
- import {
4
- CategoryFieldWithCreate,
5
- CostCenterFieldWithCreate,
6
- } from '@/app/(app)/(libraries)/finance/_components/finance-entity-field-with-create';
7
- import { PersonFieldWithCreate } from '@/app/(app)/(libraries)/contact/person/_components/person-field-with-create';
8
- import { Page, PageHeader } from '@/components/entity-list';
9
- import { Badge } from '@/components/ui/badge';
10
- import { Button } from '@/components/ui/button';
11
- import { Checkbox } from '@/components/ui/checkbox';
3
+ import {
4
+ CategoryFieldWithCreate,
5
+ CostCenterFieldWithCreate,
6
+ } from '@/app/(app)/(libraries)/finance/_components/finance-entity-field-with-create';
7
+ import {
8
+ FinancePageSection,
9
+ FinanceSheetBody,
10
+ FinanceSheetSection,
11
+ } from '@/app/(app)/(libraries)/finance/_components/finance-layout';
12
+ import { PersonFieldWithCreate } from '@/app/(app)/(libraries)/contact/person/_components/person-field-with-create';
13
+ import {
14
+ EmptyState,
15
+ Page,
16
+ PageHeader,
17
+ PaginationFooter,
18
+ SearchBar,
19
+ } from '@/components/entity-list';
20
+ import { Badge } from '@/components/ui/badge';
21
+ import { Button } from '@/components/ui/button';
22
+ import { Checkbox } from '@/components/ui/checkbox';
12
23
  import {
13
24
  DropdownMenu,
14
- DropdownMenuContent,
15
- DropdownMenuItem,
16
- DropdownMenuSeparator,
17
- DropdownMenuTrigger,
18
- } from '@/components/ui/dropdown-menu';
19
- import { FilterBar } from '@/components/ui/filter-bar';
20
- import {
21
- Form,
22
- FormControl,
25
+ DropdownMenuContent,
26
+ DropdownMenuItem,
27
+ DropdownMenuSeparator,
28
+ DropdownMenuTrigger,
29
+ } from '@/components/ui/dropdown-menu';
30
+ import {
31
+ Form,
32
+ FormControl,
23
33
  FormField,
24
34
  FormItem,
25
35
  FormLabel,
26
36
  FormMessage,
27
- } from '@/components/ui/form';
28
- import { Input } from '@/components/ui/input';
29
- import { InputMoney } from '@/components/ui/input-money';
30
- import { Label } from '@/components/ui/label';
31
- import { Money } from '@/components/ui/money';
32
- import { Progress } from '@/components/ui/progress';
37
+ } from '@/components/ui/form';
38
+ import { Input } from '@/components/ui/input';
39
+ import { InputMoney } from '@/components/ui/input-money';
40
+ import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
41
+ import { Label } from '@/components/ui/label';
42
+ import { Money } from '@/components/ui/money';
43
+ import { Progress } from '@/components/ui/progress';
33
44
  import {
34
45
  Select,
35
46
  SelectContent,
@@ -61,19 +72,22 @@ import {
61
72
  TooltipTrigger,
62
73
  } from '@/components/ui/tooltip';
63
74
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
64
- import { zodResolver } from '@hookform/resolvers/zod';
65
- import {
66
- Download,
67
- Edit,
68
- Eye,
69
- Loader2,
70
- MoreHorizontal,
71
- Paperclip,
75
+ import { zodResolver } from '@hookform/resolvers/zod';
76
+ import {
77
+ AlertTriangle,
78
+ Download,
79
+ Edit,
80
+ Eye,
81
+ FileText,
82
+ Loader2,
83
+ MoreHorizontal,
84
+ Paperclip,
72
85
  Plus,
73
- Send,
74
- Trash2,
75
- Upload,
76
- } from 'lucide-react';
86
+ Send,
87
+ Trash2,
88
+ Upload,
89
+ Wallet,
90
+ } from 'lucide-react';
77
91
  import { useTranslations } from 'next-intl';
78
92
  import Link from 'next/link';
79
93
  import { usePathname, useRouter, useSearchParams } from 'next/navigation';
@@ -628,14 +642,21 @@ function NovoTituloSheet({
628
642
  {t('newTitle.action')}
629
643
  </Button>
630
644
  </SheetTrigger>
631
- <SheetContent className="w-full sm:max-w-lg overflow-y-auto">
632
- <SheetHeader>
633
- <SheetTitle>{t('newTitle.title')}</SheetTitle>
634
- <SheetDescription>{t('newTitle.description')}</SheetDescription>
635
- </SheetHeader>
636
- <Form {...form}>
637
- <form className="px-4" onSubmit={form.handleSubmit(handleSubmit)}>
638
- <div className="grid gap-3">
645
+ <SheetContent className="flex h-full w-full flex-col overflow-hidden p-0 sm:max-w-xl lg:max-w-4xl">
646
+ <SheetHeader className="border-b border-border/50 px-4 py-4 sm:px-6">
647
+ <SheetTitle>{t('newTitle.title')}</SheetTitle>
648
+ <SheetDescription>{t('newTitle.description')}</SheetDescription>
649
+ </SheetHeader>
650
+ <Form {...form}>
651
+ <form
652
+ className="flex h-full flex-col overflow-hidden"
653
+ onSubmit={form.handleSubmit(handleSubmit)}
654
+ >
655
+ <FinanceSheetBody>
656
+ <FinanceSheetSection
657
+ title={t('sections.main.title')}
658
+ description={t('sections.main.description')}
659
+ >
639
660
  <div className="grid grid-cols-1 items-start gap-4 sm:grid-cols-2">
640
661
  <div className="grid gap-2">
641
662
  <FormLabel>{t('common.upload.label')}</FormLabel>
@@ -916,12 +937,11 @@ function NovoTituloSheet({
916
937
  {t('installmentsEditor.autoRedistributeLabel')}
917
938
  </Label>
918
939
  </div>
919
- {autoRedistributeInstallments && (
920
- <p className="text-xs text-muted-foreground">
921
- A redistribuição ocorre ao parar de digitar e ao sair do
922
- campo.
923
- </p>
924
- )}
940
+ {autoRedistributeInstallments && (
941
+ <p className="text-xs text-muted-foreground">
942
+ {t('installmentsEditor.autoRedistributeHint')}
943
+ </p>
944
+ )}
925
945
 
926
946
  <div className="space-y-2">
927
947
  {installmentFields.map((installment, index) => (
@@ -1021,17 +1041,23 @@ function NovoTituloSheet({
1021
1041
  )}
1022
1042
  </div>
1023
1043
 
1024
- <CategoryFieldWithCreate
1025
- form={form}
1026
- name="categoriaId"
1027
- label={t('fields.category')}
1028
- selectPlaceholder={t('common.select')}
1029
- categories={categorias}
1030
- categoryKind="receita"
1031
- onCreated={onOptionsUpdated}
1032
- />
1033
-
1034
- <CostCenterFieldWithCreate
1044
+ </FinanceSheetSection>
1045
+
1046
+ <FinanceSheetSection
1047
+ title={t('sections.classification.title')}
1048
+ description={t('sections.classification.description')}
1049
+ >
1050
+ <CategoryFieldWithCreate
1051
+ form={form}
1052
+ name="categoriaId"
1053
+ label={t('fields.category')}
1054
+ selectPlaceholder={t('common.select')}
1055
+ categories={categorias}
1056
+ categoryKind="receita"
1057
+ onCreated={onOptionsUpdated}
1058
+ />
1059
+
1060
+ <CostCenterFieldWithCreate
1035
1061
  form={form}
1036
1062
  name="centroCustoId"
1037
1063
  label={t('fields.costCenter')}
@@ -1065,15 +1091,20 @@ function NovoTituloSheet({
1065
1091
  </SelectItem>
1066
1092
  </SelectContent>
1067
1093
  </Select>
1068
- <FormMessage />
1069
- </FormItem>
1070
- )}
1071
- />
1072
-
1073
- <FormField
1074
- control={form.control}
1075
- name="descricao"
1076
- render={({ field }) => (
1094
+ <FormMessage />
1095
+ </FormItem>
1096
+ )}
1097
+ />
1098
+ </FinanceSheetSection>
1099
+
1100
+ <FinanceSheetSection
1101
+ title={t('sections.notes.title')}
1102
+ description={t('sections.notes.description')}
1103
+ >
1104
+ <FormField
1105
+ control={form.control}
1106
+ name="descricao"
1107
+ render={({ field }) => (
1077
1108
  <FormItem>
1078
1109
  <FormLabel>{t('fields.description')}</FormLabel>
1079
1110
  <FormControl>
@@ -1083,33 +1114,39 @@ function NovoTituloSheet({
1083
1114
  value={field.value || ''}
1084
1115
  />
1085
1116
  </FormControl>
1086
- <FormMessage />
1087
- </FormItem>
1088
- )}
1089
- />
1090
- </div>
1091
-
1092
- <div className="flex flex-col gap-2 py-4">
1093
- <Button
1094
- type="submit"
1095
- disabled={
1096
- form.formState.isSubmitting ||
1097
- isUploadingFile ||
1098
- isExtractingFileData
1099
- }
1100
- >
1101
- {(isUploadingFile || isExtractingFileData) && (
1102
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
1103
- )}
1104
- {isExtractingFileData
1105
- ? t('common.upload.fillingWithAi')
1106
- : isUploadingFile
1107
- ? t('common.upload.uploadingFile')
1108
- : t('common.save')}
1109
- </Button>
1110
- </div>
1111
- </form>
1112
- </Form>
1117
+ <FormMessage />
1118
+ </FormItem>
1119
+ )}
1120
+ />
1121
+ </FinanceSheetSection>
1122
+ </FinanceSheetBody>
1123
+
1124
+ <div className="border-t border-border/50 px-4 py-4 sm:px-6">
1125
+ <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
1126
+ <Button type="button" variant="outline" onClick={() => setOpen(false)}>
1127
+ {t('common.cancel')}
1128
+ </Button>
1129
+ <Button
1130
+ type="submit"
1131
+ disabled={
1132
+ form.formState.isSubmitting ||
1133
+ isUploadingFile ||
1134
+ isExtractingFileData
1135
+ }
1136
+ >
1137
+ {(isUploadingFile || isExtractingFileData) && (
1138
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
1139
+ )}
1140
+ {isExtractingFileData
1141
+ ? t('common.upload.fillingWithAi')
1142
+ : isUploadingFile
1143
+ ? t('common.upload.uploadingFile')
1144
+ : t('common.save')}
1145
+ </Button>
1146
+ </div>
1147
+ </div>
1148
+ </form>
1149
+ </Form>
1113
1150
  </SheetContent>
1114
1151
  </Sheet>
1115
1152
  );
@@ -1536,9 +1573,9 @@ function EditarTituloSheet({
1536
1573
  }
1537
1574
  };
1538
1575
 
1539
- const handleSubmit = async (values: NewTitleFormValues) => {
1540
- if (!titulo?.id) {
1541
- showToastHandler?.('error', t('messages.invalidTitleForEdit'));
1576
+ const handleSubmit = async (values: NewTitleFormValues) => {
1577
+ if (!titulo?.id) {
1578
+ showToastHandler?.('error', t('messages.invalidTitleForEdit'));
1542
1579
  return;
1543
1580
  }
1544
1581
 
@@ -1575,20 +1612,31 @@ function EditarTituloSheet({
1575
1612
  showToastHandler?.('success', t('messages.updateSuccess'));
1576
1613
  onOpenChange(false);
1577
1614
  } catch {
1578
- showToastHandler?.('error', t('messages.updateError'));
1579
- }
1580
- };
1581
-
1582
- return (
1583
- <Sheet open={open} onOpenChange={onOpenChange}>
1584
- <SheetContent className="w-full sm:max-w-lg overflow-y-auto">
1585
- <SheetHeader>
1586
- <SheetTitle>{t('table.actions.edit')}</SheetTitle>
1587
- <SheetDescription>{t('editTitle.description')}</SheetDescription>
1588
- </SheetHeader>
1589
- <Form {...form}>
1590
- <form className="px-4" onSubmit={form.handleSubmit(handleSubmit)}>
1591
- <div className="grid gap-3">
1615
+ showToastHandler?.('error', t('messages.updateError'));
1616
+ }
1617
+ };
1618
+
1619
+ const handleCancel = () => {
1620
+ onOpenChange(false);
1621
+ };
1622
+
1623
+ return (
1624
+ <Sheet open={open} onOpenChange={onOpenChange}>
1625
+ <SheetContent className="flex h-full w-full flex-col overflow-hidden p-0 sm:max-w-xl lg:max-w-4xl">
1626
+ <SheetHeader className="border-b border-border/50 px-4 py-4 sm:px-6">
1627
+ <SheetTitle>{t('table.actions.edit')}</SheetTitle>
1628
+ <SheetDescription>{t('editTitle.description')}</SheetDescription>
1629
+ </SheetHeader>
1630
+ <Form {...form}>
1631
+ <form
1632
+ className="flex h-full flex-col overflow-hidden"
1633
+ onSubmit={form.handleSubmit(handleSubmit)}
1634
+ >
1635
+ <FinanceSheetBody>
1636
+ <FinanceSheetSection
1637
+ title={t('sections.main.title')}
1638
+ description={t('sections.main.description')}
1639
+ >
1592
1640
  <div className="grid grid-cols-1 items-start gap-4 sm:grid-cols-2">
1593
1641
  <div className="grid gap-2">
1594
1642
  <FormLabel>{t('common.upload.label')}</FormLabel>
@@ -1871,8 +1919,14 @@ function EditarTituloSheet({
1871
1919
  </Label>
1872
1920
  </div>
1873
1921
 
1874
- <div className="space-y-2">
1875
- {installmentFields.map((installment, index) => (
1922
+ {autoRedistributeInstallments && (
1923
+ <p className="text-xs text-muted-foreground">
1924
+ {t('installmentsEditor.autoRedistributeHint')}
1925
+ </p>
1926
+ )}
1927
+
1928
+ <div className="space-y-2">
1929
+ {installmentFields.map((installment, index) => (
1876
1930
  <div
1877
1931
  key={installment.id}
1878
1932
  className="grid grid-cols-1 items-start gap-2 rounded-md border p-2 sm:grid-cols-[96px_1fr_160px]"
@@ -1969,10 +2023,16 @@ function EditarTituloSheet({
1969
2023
  )}
1970
2024
  </div>
1971
2025
 
1972
- <CategoryFieldWithCreate
1973
- form={form}
1974
- name="categoriaId"
1975
- label={t('fields.category')}
2026
+ </FinanceSheetSection>
2027
+
2028
+ <FinanceSheetSection
2029
+ title={t('sections.classification.title')}
2030
+ description={t('sections.classification.description')}
2031
+ >
2032
+ <CategoryFieldWithCreate
2033
+ form={form}
2034
+ name="categoriaId"
2035
+ label={t('fields.category')}
1976
2036
  selectPlaceholder={t('common.select')}
1977
2037
  categories={categorias}
1978
2038
  categoryKind="receita"
@@ -2013,15 +2073,20 @@ function EditarTituloSheet({
2013
2073
  </SelectItem>
2014
2074
  </SelectContent>
2015
2075
  </Select>
2016
- <FormMessage />
2017
- </FormItem>
2018
- )}
2019
- />
2020
-
2021
- <FormField
2022
- control={form.control}
2023
- name="descricao"
2024
- render={({ field }) => (
2076
+ <FormMessage />
2077
+ </FormItem>
2078
+ )}
2079
+ />
2080
+ </FinanceSheetSection>
2081
+
2082
+ <FinanceSheetSection
2083
+ title={t('sections.notes.title')}
2084
+ description={t('sections.notes.description')}
2085
+ >
2086
+ <FormField
2087
+ control={form.control}
2088
+ name="descricao"
2089
+ render={({ field }) => (
2025
2090
  <FormItem>
2026
2091
  <FormLabel>{t('fields.description')}</FormLabel>
2027
2092
  <FormControl>
@@ -2031,33 +2096,39 @@ function EditarTituloSheet({
2031
2096
  value={field.value || ''}
2032
2097
  />
2033
2098
  </FormControl>
2034
- <FormMessage />
2035
- </FormItem>
2036
- )}
2037
- />
2038
- </div>
2039
-
2040
- <div className="flex flex-col gap-2 py-4">
2041
- <Button
2042
- type="submit"
2043
- disabled={
2044
- form.formState.isSubmitting ||
2045
- isUploadingFile ||
2046
- isExtractingFileData
2047
- }
2048
- >
2049
- {(isUploadingFile || isExtractingFileData) && (
2050
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
2051
- )}
2052
- {isExtractingFileData
2053
- ? t('common.upload.fillingWithAi')
2054
- : isUploadingFile
2055
- ? t('common.upload.uploadingFile')
2056
- : t('common.save')}
2057
- </Button>
2058
- </div>
2059
- </form>
2060
- </Form>
2099
+ <FormMessage />
2100
+ </FormItem>
2101
+ )}
2102
+ />
2103
+ </FinanceSheetSection>
2104
+ </FinanceSheetBody>
2105
+
2106
+ <div className="border-t border-border/50 px-4 py-4 sm:px-6">
2107
+ <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
2108
+ <Button type="button" variant="outline" onClick={handleCancel}>
2109
+ {t('common.cancel')}
2110
+ </Button>
2111
+ <Button
2112
+ type="submit"
2113
+ disabled={
2114
+ form.formState.isSubmitting ||
2115
+ isUploadingFile ||
2116
+ isExtractingFileData
2117
+ }
2118
+ >
2119
+ {(isUploadingFile || isExtractingFileData) && (
2120
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
2121
+ )}
2122
+ {isExtractingFileData
2123
+ ? t('common.upload.fillingWithAi')
2124
+ : isUploadingFile
2125
+ ? t('common.upload.uploadingFile')
2126
+ : t('common.save')}
2127
+ </Button>
2128
+ </div>
2129
+ </div>
2130
+ </form>
2131
+ </Form>
2061
2132
  </SheetContent>
2062
2133
  </Sheet>
2063
2134
  );
@@ -2077,21 +2148,20 @@ export default function TitulosReceberPage() {
2077
2148
  centrosCusto,
2078
2149
  } = data;
2079
2150
 
2080
- const getPessoaById = (id?: string) => pessoas.find((p) => p.id === id);
2081
-
2082
- const [search, setSearch] = useState('');
2083
- const [statusFilter, setStatusFilter] = useState<string>('');
2084
- const [page, setPage] = useState(1);
2085
- const pageSize = 10;
2151
+ const getPessoaById = (id?: string) => pessoas.find((p) => p.id === id);
2152
+
2153
+ const [search, setSearch] = useState('');
2154
+ const [statusFilter, setStatusFilter] = useState<string>('all');
2155
+ const [page, setPage] = useState(1);
2156
+ const pageSize = 10;
2086
2157
 
2087
2158
  const normalizedStatusFilter =
2088
2159
  statusFilter && statusFilter !== 'all' ? statusFilter : undefined;
2089
2160
 
2090
- const {
2091
- data: paginatedTitlesResponse,
2092
- refetch: refetchTitles,
2093
- isFetching: isFetchingTitles,
2094
- } = useQuery<{
2161
+ const {
2162
+ data: paginatedTitlesResponse,
2163
+ refetch: refetchTitles,
2164
+ } = useQuery<{
2095
2165
  data: any[];
2096
2166
  total: number;
2097
2167
  page: number;
@@ -2131,9 +2201,74 @@ export default function TitulosReceberPage() {
2131
2201
  },
2132
2202
  placeholderData: (old) => old,
2133
2203
  });
2134
-
2135
- const titulosReceber = paginatedTitlesResponse?.data || [];
2136
- const [editingTitleId, setEditingTitleId] = useState<string | null>(null);
2204
+
2205
+ const titulosReceber = paginatedTitlesResponse?.data || [];
2206
+ const visibleTitlesTotal = useMemo(
2207
+ () =>
2208
+ titulosReceber.reduce(
2209
+ (acc, title) => acc + Number(title?.valorTotal || 0),
2210
+ 0
2211
+ ),
2212
+ [titulosReceber]
2213
+ );
2214
+ const visiblePendingTitles = useMemo(
2215
+ () =>
2216
+ titulosReceber.filter((title) =>
2217
+ ['aberto', 'parcial', 'vencido'].includes(String(title?.status || ''))
2218
+ ).length,
2219
+ [titulosReceber]
2220
+ );
2221
+ const visibleOverdueTitles = useMemo(
2222
+ () =>
2223
+ titulosReceber.filter(
2224
+ (title) =>
2225
+ title?.status === 'vencido' ||
2226
+ (Array.isArray(title?.parcelas) &&
2227
+ title.parcelas.some((installment: any) => installment.status === 'vencido'))
2228
+ ).length,
2229
+ [titulosReceber]
2230
+ );
2231
+ const summaryCards = useMemo(
2232
+ () => [
2233
+ {
2234
+ key: 'visible',
2235
+ title: t('summary.cards.visible.title'),
2236
+ value: titulosReceber.length,
2237
+ description: t('summary.cards.visible.description', {
2238
+ total: paginatedTitlesResponse?.total || 0,
2239
+ }),
2240
+ icon: FileText,
2241
+ layout: 'compact' as const,
2242
+ },
2243
+ {
2244
+ key: 'value',
2245
+ title: t('summary.cards.value.title'),
2246
+ value: <Money value={visibleTitlesTotal} />,
2247
+ description: t('summary.cards.value.description'),
2248
+ icon: Wallet,
2249
+ layout: 'compact' as const,
2250
+ },
2251
+ {
2252
+ key: 'attention',
2253
+ title: t('summary.cards.attention.title'),
2254
+ value: visiblePendingTitles,
2255
+ description: t('summary.cards.attention.description', {
2256
+ overdue: visibleOverdueTitles,
2257
+ }),
2258
+ icon: AlertTriangle,
2259
+ layout: 'compact' as const,
2260
+ },
2261
+ ],
2262
+ [
2263
+ paginatedTitlesResponse?.total,
2264
+ t,
2265
+ titulosReceber.length,
2266
+ visibleOverdueTitles,
2267
+ visiblePendingTitles,
2268
+ visibleTitlesTotal,
2269
+ ]
2270
+ );
2271
+ const [editingTitleId, setEditingTitleId] = useState<string | null>(null);
2137
2272
 
2138
2273
  const editingTitle = useMemo(
2139
2274
  () =>
@@ -2229,9 +2364,9 @@ export default function TitulosReceberPage() {
2229
2364
 
2230
2365
  return (
2231
2366
  <Page>
2232
- <PageHeader
2233
- title={t('header.title')}
2234
- description={t('header.description')}
2367
+ <PageHeader
2368
+ title={t('header.title')}
2369
+ description={t('header.description')}
2235
2370
  breadcrumbs={[
2236
2371
  { label: t('breadcrumbs.home'), href: '/' },
2237
2372
  { label: t('breadcrumbs.finance'), href: '/finance' },
@@ -2260,181 +2395,186 @@ export default function TitulosReceberPage() {
2260
2395
  }}
2261
2396
  onOptionsUpdated={refetchFinanceData}
2262
2397
  />
2263
- </>
2264
- }
2265
- />
2266
-
2267
- <FilterBar
2268
- searchPlaceholder={t('filters.searchPlaceholder')}
2269
- searchValue={search}
2270
- onSearchChange={setSearch}
2271
- filters={[
2272
- {
2273
- id: 'status',
2274
- label: t('filters.status'),
2275
- value: statusFilter,
2276
- onChange: setStatusFilter,
2277
- options: [
2278
- { value: 'all', label: t('statuses.all') },
2279
- { value: 'aberto', label: t('statuses.aberto') },
2280
- { value: 'parcial', label: t('statuses.parcial') },
2281
- { value: 'liquidado', label: t('statuses.liquidado') },
2282
- { value: 'vencido', label: t('statuses.vencido') },
2283
- { value: 'cancelado', label: t('statuses.cancelado') },
2284
- ],
2285
- },
2286
- ]}
2287
- activeFilters={normalizedStatusFilter ? 1 : 0}
2288
- onClearFilters={() => setStatusFilter('all')}
2289
- />
2290
-
2291
- <div className="rounded-md border">
2292
- <Table>
2293
- <TableHeader>
2294
- <TableRow>
2295
- <TableHead>{t('table.headers.document')}</TableHead>
2296
- <TableHead>{t('table.headers.client')}</TableHead>
2297
- <TableHead>{t('table.headers.competency')}</TableHead>
2298
- <TableHead>{t('table.headers.dueDate')}</TableHead>
2299
- <TableHead className="text-right">
2300
- {t('table.headers.value')}
2301
- </TableHead>
2302
- <TableHead>{t('table.headers.channel')}</TableHead>
2303
- <TableHead>{t('table.headers.status')}</TableHead>
2304
- <TableHead className="w-[50px]" />
2305
- </TableRow>
2306
- </TableHeader>
2307
- <TableBody>
2308
- {titulosReceber.map((titulo) => {
2309
- const cliente = getPessoaById(titulo.clienteId);
2310
- const canal =
2311
- canalBadge[titulo.canal as keyof typeof canalBadge] ||
2312
- canalBadge.transferencia;
2313
- const proximaParcela = titulo.parcelas.find(
2314
- (p: any) => p.status === 'aberto' || p.status === 'vencido'
2315
- );
2316
-
2317
- return (
2318
- <TableRow key={titulo.id}>
2319
- <TableCell className="font-medium">
2320
- <Link
2321
- href={`/finance/accounts-receivable/installments/${titulo.id}`}
2322
- className="hover:underline"
2323
- >
2324
- {titulo.documento}
2325
- </Link>
2326
- {titulo.anexos.length > 0 && (
2327
- <Button
2328
- type="button"
2329
- variant="ghost"
2330
- size="icon"
2331
- className="ml-1 inline-flex h-5 w-5 align-middle text-muted-foreground"
2332
- onClick={(event) => {
2333
- event.preventDefault();
2334
- event.stopPropagation();
2335
- const firstAttachmentId =
2336
- titulo.anexosDetalhes?.[0]?.id;
2337
- void handleOpenAttachment(firstAttachmentId);
2338
- }}
2339
- aria-label={t('table.actions.openAttachment')}
2340
- >
2341
- <Paperclip className="h-3 w-3" />
2342
- </Button>
2343
- )}
2344
- </TableCell>
2345
- <TableCell>{cliente?.nome}</TableCell>
2346
- <TableCell>{titulo.competencia}</TableCell>
2347
- <TableCell>
2348
- {proximaParcela
2349
- ? formatarData(proximaParcela.vencimento)
2350
- : '-'}
2351
- </TableCell>
2352
- <TableCell className="text-right">
2353
- <Money value={titulo.valorTotal} />
2354
- </TableCell>
2355
- <TableCell>
2356
- <Badge className={canal.className} variant="outline">
2357
- {canal.label}
2358
- </Badge>
2359
- </TableCell>
2360
- <TableCell>
2361
- <StatusBadge status={titulo.status} />
2362
- </TableCell>
2363
- <TableCell>
2364
- <DropdownMenu>
2365
- <DropdownMenuTrigger asChild>
2366
- <Button variant="ghost" size="icon">
2367
- <MoreHorizontal className="h-4 w-4" />
2368
- <span className="sr-only">
2369
- {t('table.actions.srActions')}
2370
- </span>
2371
- </Button>
2372
- </DropdownMenuTrigger>
2373
- <DropdownMenuContent align="end">
2374
- <DropdownMenuItem asChild>
2375
- <Link
2376
- href={`/finance/accounts-receivable/installments/${titulo.id}`}
2377
- >
2378
- <Eye className="mr-2 h-4 w-4" />
2379
- {t('table.actions.viewDetails')}
2380
- </Link>
2381
- </DropdownMenuItem>
2382
- <DropdownMenuItem
2383
- disabled={titulo.status !== 'rascunho'}
2384
- onClick={() => setEditingTitleId(titulo.id)}
2385
- >
2386
- <Edit className="mr-2 h-4 w-4" />
2387
- {t('table.actions.edit')}
2388
- </DropdownMenuItem>
2389
- <DropdownMenuSeparator />
2390
- <DropdownMenuItem
2391
- disabled={
2392
- !['aberto', 'parcial'].includes(titulo.status)
2393
- }
2394
- >
2395
- <Download className="mr-2 h-4 w-4" />
2396
- {t('table.actions.registerReceipt')}
2397
- </DropdownMenuItem>
2398
- <DropdownMenuItem>
2399
- <Send className="mr-2 h-4 w-4" />
2400
- {t('table.actions.sendCollection')}
2401
- </DropdownMenuItem>
2402
- </DropdownMenuContent>
2403
- </DropdownMenu>
2404
- </TableCell>
2405
- </TableRow>
2406
- );
2407
- })}
2408
- </TableBody>
2409
- </Table>
2410
- </div>
2411
-
2412
- <div className="flex items-center justify-between">
2413
- <p className="text-sm text-muted-foreground">
2414
- {t('footer.showing', {
2415
- filtered: titulosReceber.length,
2416
- total: paginatedTitlesResponse?.total || 0,
2417
- })}
2418
- </p>
2419
- <div className="flex items-center gap-2">
2420
- <Button
2421
- variant="outline"
2422
- size="sm"
2423
- disabled={!paginatedTitlesResponse?.prev || isFetchingTitles}
2424
- onClick={() => setPage((current) => Math.max(1, current - 1))}
2425
- >
2426
- {t('footer.previous')}
2427
- </Button>
2428
- <Button
2429
- variant="outline"
2430
- size="sm"
2431
- disabled={!paginatedTitlesResponse?.next || isFetchingTitles}
2432
- onClick={() => setPage((current) => current + 1)}
2433
- >
2434
- {t('footer.next')}
2435
- </Button>
2436
- </div>
2437
- </div>
2438
- </Page>
2439
- );
2440
- }
2398
+ </>
2399
+ }
2400
+ />
2401
+
2402
+ <KpiCardsGrid items={summaryCards} columns={3} />
2403
+
2404
+ <div className="min-w-0">
2405
+ <SearchBar
2406
+ searchQuery={search}
2407
+ onSearchChange={setSearch}
2408
+ onSearch={() => undefined}
2409
+ placeholder={t('filters.searchPlaceholder')}
2410
+ controls={[
2411
+ {
2412
+ id: 'status',
2413
+ type: 'select',
2414
+ value: statusFilter,
2415
+ onChange: setStatusFilter,
2416
+ placeholder: t('filters.status'),
2417
+ options: [
2418
+ { value: 'all', label: t('statuses.all') },
2419
+ { value: 'aberto', label: t('statuses.aberto') },
2420
+ { value: 'parcial', label: t('statuses.parcial') },
2421
+ { value: 'liquidado', label: t('statuses.liquidado') },
2422
+ { value: 'vencido', label: t('statuses.vencido') },
2423
+ { value: 'cancelado', label: t('statuses.cancelado') },
2424
+ ],
2425
+ },
2426
+ ]}
2427
+ />
2428
+ </div>
2429
+
2430
+ <FinancePageSection
2431
+ title={t('list.title')}
2432
+ description={t('list.description')}
2433
+ >
2434
+ {titulosReceber.length > 0 ? (
2435
+ <div className="overflow-x-auto">
2436
+ <Table className="min-w-[760px]">
2437
+ <TableHeader>
2438
+ <TableRow>
2439
+ <TableHead>{t('table.headers.document')}</TableHead>
2440
+ <TableHead>{t('table.headers.client')}</TableHead>
2441
+ <TableHead>{t('table.headers.competency')}</TableHead>
2442
+ <TableHead>{t('table.headers.dueDate')}</TableHead>
2443
+ <TableHead className="text-right">
2444
+ {t('table.headers.value')}
2445
+ </TableHead>
2446
+ <TableHead>{t('table.headers.channel')}</TableHead>
2447
+ <TableHead>{t('table.headers.status')}</TableHead>
2448
+ <TableHead className="w-[50px]" />
2449
+ </TableRow>
2450
+ </TableHeader>
2451
+ <TableBody>
2452
+ {titulosReceber.map((titulo) => {
2453
+ const cliente = getPessoaById(titulo.clienteId);
2454
+ const canal =
2455
+ canalBadge[titulo.canal as keyof typeof canalBadge] ||
2456
+ canalBadge.transferencia;
2457
+ const proximaParcela = titulo.parcelas.find(
2458
+ (p: any) => p.status === 'aberto' || p.status === 'vencido'
2459
+ );
2460
+
2461
+ return (
2462
+ <TableRow key={titulo.id} className="hover:bg-muted/30">
2463
+ <TableCell className="font-medium">
2464
+ <Link
2465
+ href={`/finance/accounts-receivable/installments/${titulo.id}`}
2466
+ className="cursor-pointer hover:underline"
2467
+ >
2468
+ {titulo.documento}
2469
+ </Link>
2470
+ {titulo.anexos.length > 0 && (
2471
+ <Button
2472
+ type="button"
2473
+ variant="ghost"
2474
+ size="icon"
2475
+ className="ml-1 inline-flex h-5 w-5 align-middle text-muted-foreground"
2476
+ onClick={(event) => {
2477
+ event.preventDefault();
2478
+ event.stopPropagation();
2479
+ const firstAttachmentId =
2480
+ titulo.anexosDetalhes?.[0]?.id;
2481
+ void handleOpenAttachment(firstAttachmentId);
2482
+ }}
2483
+ aria-label={t('table.actions.openAttachment')}
2484
+ >
2485
+ <Paperclip className="h-3 w-3" />
2486
+ </Button>
2487
+ )}
2488
+ </TableCell>
2489
+ <TableCell>{cliente?.nome}</TableCell>
2490
+ <TableCell>{titulo.competencia}</TableCell>
2491
+ <TableCell>
2492
+ {proximaParcela
2493
+ ? formatarData(proximaParcela.vencimento)
2494
+ : '-'}
2495
+ </TableCell>
2496
+ <TableCell className="text-right">
2497
+ <Money value={titulo.valorTotal} />
2498
+ </TableCell>
2499
+ <TableCell>
2500
+ <Badge className={canal.className} variant="outline">
2501
+ {canal.label}
2502
+ </Badge>
2503
+ </TableCell>
2504
+ <TableCell>
2505
+ <StatusBadge status={titulo.status} />
2506
+ </TableCell>
2507
+ <TableCell>
2508
+ <DropdownMenu>
2509
+ <DropdownMenuTrigger asChild>
2510
+ <Button variant="ghost" size="icon">
2511
+ <MoreHorizontal className="h-4 w-4" />
2512
+ <span className="sr-only">
2513
+ {t('table.actions.srActions')}
2514
+ </span>
2515
+ </Button>
2516
+ </DropdownMenuTrigger>
2517
+ <DropdownMenuContent align="end">
2518
+ <DropdownMenuItem asChild>
2519
+ <Link
2520
+ href={`/finance/accounts-receivable/installments/${titulo.id}`}
2521
+ >
2522
+ <Eye className="mr-2 h-4 w-4" />
2523
+ {t('table.actions.viewDetails')}
2524
+ </Link>
2525
+ </DropdownMenuItem>
2526
+ <DropdownMenuItem
2527
+ disabled={titulo.status !== 'rascunho'}
2528
+ onClick={() => setEditingTitleId(titulo.id)}
2529
+ >
2530
+ <Edit className="mr-2 h-4 w-4" />
2531
+ {t('table.actions.edit')}
2532
+ </DropdownMenuItem>
2533
+ <DropdownMenuSeparator />
2534
+ <DropdownMenuItem
2535
+ disabled={
2536
+ !['aberto', 'parcial'].includes(titulo.status)
2537
+ }
2538
+ >
2539
+ <Download className="mr-2 h-4 w-4" />
2540
+ {t('table.actions.registerReceipt')}
2541
+ </DropdownMenuItem>
2542
+ <DropdownMenuItem>
2543
+ <Send className="mr-2 h-4 w-4" />
2544
+ {t('table.actions.sendCollection')}
2545
+ </DropdownMenuItem>
2546
+ </DropdownMenuContent>
2547
+ </DropdownMenu>
2548
+ </TableCell>
2549
+ </TableRow>
2550
+ );
2551
+ })}
2552
+ </TableBody>
2553
+ </Table>
2554
+ </div>
2555
+ ) : (
2556
+ <div className="px-4 py-8 sm:px-6">
2557
+ <EmptyState
2558
+ icon={<FileText className="h-12 w-12" />}
2559
+ title={t('empty.title')}
2560
+ description={t('empty.description')}
2561
+ actionLabel={t('newTitle.action')}
2562
+ onAction={() => void refetchTitles()}
2563
+ />
2564
+ </div>
2565
+ )}
2566
+
2567
+ <div className="border-t border-border/50 px-4 py-4 sm:px-6">
2568
+ <PaginationFooter
2569
+ currentPage={page}
2570
+ pageSize={pageSize}
2571
+ totalItems={paginatedTitlesResponse?.total || 0}
2572
+ onPageChange={setPage}
2573
+ onPageSizeChange={() => undefined}
2574
+ pageSizeOptions={[10]}
2575
+ />
2576
+ </div>
2577
+ </FinancePageSection>
2578
+ </Page>
2579
+ );
2580
+ }