@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
@@ -627,6 +627,355 @@ let FinanceService = FinanceService_1 = class FinanceService {
627
627
  periodoAberto: openPeriod,
628
628
  };
629
629
  }
630
+ async getOverviewResultsReport(filters) {
631
+ const { fromDate, toDate } = this.resolveReportDateRange(filters === null || filters === void 0 ? void 0 : filters.from, filters === null || filters === void 0 ? void 0 : filters.to);
632
+ const groupBy = this.resolveReportGroupBy(filters === null || filters === void 0 ? void 0 : filters.groupBy);
633
+ const installments = await this.prisma.financial_installment.findMany({
634
+ where: {
635
+ competence_date: {
636
+ gte: fromDate,
637
+ lte: toDate,
638
+ },
639
+ status: {
640
+ not: 'canceled',
641
+ },
642
+ financial_title: {
643
+ status: {
644
+ notIn: ['draft', 'canceled'],
645
+ },
646
+ },
647
+ },
648
+ select: {
649
+ competence_date: true,
650
+ amount_cents: true,
651
+ financial_title: {
652
+ select: {
653
+ title_type: true,
654
+ finance_category: {
655
+ select: {
656
+ code: true,
657
+ name: true,
658
+ kind: true,
659
+ },
660
+ },
661
+ },
662
+ },
663
+ },
664
+ orderBy: {
665
+ competence_date: 'asc',
666
+ },
667
+ });
668
+ const grouped = new Map();
669
+ for (const installment of installments) {
670
+ const category = installment.financial_title.finance_category;
671
+ if (this.isTransferReportCategory(category)) {
672
+ continue;
673
+ }
674
+ const period = this.getReportBucketKey(installment.competence_date, groupBy);
675
+ const current = grouped.get(period) || {
676
+ period,
677
+ faturamento: 0,
678
+ despesasEmprestimos: 0,
679
+ diferenca: 0,
680
+ aporteInvestidor: 0,
681
+ emprestimoBanco: 0,
682
+ despesas: 0,
683
+ };
684
+ const amount = this.fromCents(installment.amount_cents);
685
+ if (installment.financial_title.title_type === 'receivable') {
686
+ if (this.isInvestorContributionReportCategory(category)) {
687
+ current.aporteInvestidor += amount;
688
+ }
689
+ else if (this.isLoanReportCategory(category)) {
690
+ current.emprestimoBanco += amount;
691
+ }
692
+ else {
693
+ current.faturamento += amount;
694
+ }
695
+ }
696
+ else if (this.isLoanReportCategory(category)) {
697
+ current.emprestimoBanco += amount;
698
+ }
699
+ else {
700
+ current.despesas += amount;
701
+ }
702
+ current.despesasEmprestimos =
703
+ current.despesas + current.emprestimoBanco;
704
+ current.diferenca = current.faturamento - current.despesasEmprestimos;
705
+ grouped.set(period, current);
706
+ }
707
+ const rows = Array.from(grouped.values())
708
+ .sort((a, b) => this.sortReportBuckets(a.period, b.period, groupBy))
709
+ .map((row) => ({
710
+ period: row.period,
711
+ faturamento: this.roundCurrency(row.faturamento),
712
+ despesasEmprestimos: this.roundCurrency(row.despesasEmprestimos),
713
+ diferenca: this.roundCurrency(row.diferenca),
714
+ aporteInvestidor: this.roundCurrency(row.aporteInvestidor),
715
+ emprestimoBanco: this.roundCurrency(row.emprestimoBanco),
716
+ despesas: this.roundCurrency(row.despesas),
717
+ }));
718
+ const totals = rows.reduce((acc, row) => {
719
+ acc.faturamento += row.faturamento;
720
+ acc.despesasEmprestimos += row.despesasEmprestimos;
721
+ acc.diferenca += row.diferenca;
722
+ acc.aporteInvestidor += row.aporteInvestidor;
723
+ acc.emprestimoBanco += row.emprestimoBanco;
724
+ acc.despesas += row.despesas;
725
+ return acc;
726
+ }, {
727
+ faturamento: 0,
728
+ despesasEmprestimos: 0,
729
+ diferenca: 0,
730
+ aporteInvestidor: 0,
731
+ emprestimoBanco: 0,
732
+ despesas: 0,
733
+ });
734
+ return {
735
+ rows,
736
+ totals: {
737
+ faturamento: this.roundCurrency(totals.faturamento),
738
+ despesasEmprestimos: this.roundCurrency(totals.despesasEmprestimos),
739
+ diferenca: this.roundCurrency(totals.diferenca),
740
+ aporteInvestidor: this.roundCurrency(totals.aporteInvestidor),
741
+ emprestimoBanco: this.roundCurrency(totals.emprestimoBanco),
742
+ despesas: this.roundCurrency(totals.despesas),
743
+ margem: totals.faturamento > 0
744
+ ? this.roundCurrency((totals.diferenca / Math.abs(totals.faturamento)) * 100)
745
+ : 0,
746
+ },
747
+ };
748
+ }
749
+ async getTopCustomersReport(filters) {
750
+ var _a;
751
+ const { fromDate, toDate } = this.resolveReportDateRange(filters === null || filters === void 0 ? void 0 : filters.from, filters === null || filters === void 0 ? void 0 : filters.to);
752
+ const groupBy = this.resolveReportGroupBy(filters === null || filters === void 0 ? void 0 : filters.groupBy);
753
+ const topN = this.resolveTopN(filters === null || filters === void 0 ? void 0 : filters.topN);
754
+ const normalizedSearch = this.normalizeReportText((filters === null || filters === void 0 ? void 0 : filters.search) || '');
755
+ const installments = await this.prisma.financial_installment.findMany({
756
+ where: {
757
+ competence_date: {
758
+ gte: fromDate,
759
+ lte: toDate,
760
+ },
761
+ status: {
762
+ not: 'canceled',
763
+ },
764
+ financial_title: {
765
+ title_type: 'receivable',
766
+ status: {
767
+ notIn: ['draft', 'canceled'],
768
+ },
769
+ },
770
+ },
771
+ select: {
772
+ competence_date: true,
773
+ amount_cents: true,
774
+ financial_title: {
775
+ select: {
776
+ person_id: true,
777
+ person: {
778
+ select: {
779
+ name: true,
780
+ },
781
+ },
782
+ finance_category: {
783
+ select: {
784
+ code: true,
785
+ name: true,
786
+ kind: true,
787
+ },
788
+ },
789
+ },
790
+ },
791
+ },
792
+ orderBy: {
793
+ competence_date: 'asc',
794
+ },
795
+ });
796
+ const byCustomer = new Map();
797
+ const byBucket = new Map();
798
+ for (const installment of installments) {
799
+ const category = installment.financial_title.finance_category;
800
+ if (this.isTransferReportCategory(category) ||
801
+ this.isLoanReportCategory(category) ||
802
+ this.isInvestorContributionReportCategory(category)) {
803
+ continue;
804
+ }
805
+ const customer = ((_a = installment.financial_title.person) === null || _a === void 0 ? void 0 : _a.name) ||
806
+ `Customer ${installment.financial_title.person_id}`;
807
+ const amount = this.fromCents(installment.amount_cents);
808
+ const current = byCustomer.get(customer) || { customer, value: 0 };
809
+ current.value += amount;
810
+ byCustomer.set(customer, current);
811
+ const period = this.getReportBucketKey(installment.competence_date, groupBy);
812
+ byBucket.set(period, (byBucket.get(period) || 0) + amount);
813
+ }
814
+ const filteredCustomers = Array.from(byCustomer.values())
815
+ .filter((item) => normalizedSearch.length === 0
816
+ ? true
817
+ : this.normalizeReportText(item.customer).includes(normalizedSearch))
818
+ .sort((a, b) => b.value - a.value);
819
+ const topCustomers = filteredCustomers.slice(0, topN).map((item) => ({
820
+ customer: item.customer,
821
+ value: this.roundCurrency(item.value),
822
+ }));
823
+ const total = this.roundCurrency(topCustomers.reduce((acc, item) => acc + item.value, 0));
824
+ const top5 = this.roundCurrency(topCustomers.slice(0, 5).reduce((acc, item) => acc + item.value, 0));
825
+ const pieData = topCustomers.slice(0, 9).map((item) => ({
826
+ customer: item.customer,
827
+ value: item.value,
828
+ }));
829
+ const othersValue = this.roundCurrency(topCustomers.slice(9).reduce((acc, item) => acc + item.value, 0));
830
+ if (othersValue > 0) {
831
+ pieData.push({
832
+ customer: 'Outros',
833
+ value: othersValue,
834
+ });
835
+ }
836
+ const groupedPeriods = Array.from(byBucket.entries())
837
+ .map(([period, value]) => ({
838
+ period,
839
+ value: this.roundCurrency(value),
840
+ }))
841
+ .sort((a, b) => this.sortReportBuckets(a.period, b.period, groupBy));
842
+ return {
843
+ total,
844
+ top5Percent: total > 0 ? this.roundCurrency((top5 / total) * 100) : 0,
845
+ topCustomers,
846
+ pieData,
847
+ groupedPeriods,
848
+ leader: topCustomers[0] || null,
849
+ };
850
+ }
851
+ async getTopOperationalExpensesReport(filters) {
852
+ const { fromDate, toDate } = this.resolveReportDateRange(filters === null || filters === void 0 ? void 0 : filters.from, filters === null || filters === void 0 ? void 0 : filters.to);
853
+ const groupBy = this.resolveReportGroupBy(filters === null || filters === void 0 ? void 0 : filters.groupBy);
854
+ const topN = this.resolveTopN(filters === null || filters === void 0 ? void 0 : filters.topN);
855
+ const normalizedSearch = this.normalizeReportText((filters === null || filters === void 0 ? void 0 : filters.search) || '');
856
+ const installments = await this.prisma.financial_installment.findMany({
857
+ where: {
858
+ competence_date: {
859
+ gte: fromDate,
860
+ lte: toDate,
861
+ },
862
+ status: {
863
+ not: 'canceled',
864
+ },
865
+ financial_title: {
866
+ title_type: 'payable',
867
+ status: {
868
+ notIn: ['draft', 'canceled'],
869
+ },
870
+ },
871
+ },
872
+ select: {
873
+ competence_date: true,
874
+ amount_cents: true,
875
+ installment_allocation: {
876
+ select: {
877
+ allocated_amount_cents: true,
878
+ cost_center: {
879
+ select: {
880
+ name: true,
881
+ },
882
+ },
883
+ },
884
+ },
885
+ financial_title: {
886
+ select: {
887
+ finance_category: {
888
+ select: {
889
+ code: true,
890
+ name: true,
891
+ kind: true,
892
+ },
893
+ },
894
+ },
895
+ },
896
+ },
897
+ orderBy: {
898
+ competence_date: 'asc',
899
+ },
900
+ });
901
+ const byExpense = new Map();
902
+ const byCostCenter = new Map();
903
+ const byBucket = new Map();
904
+ for (const installment of installments) {
905
+ const category = installment.financial_title.finance_category;
906
+ if (this.isTransferReportCategory(category) ||
907
+ this.isLoanReportCategory(category)) {
908
+ continue;
909
+ }
910
+ const allocations = installment.installment_allocation.length > 0
911
+ ? installment.installment_allocation.map((allocation) => {
912
+ var _a;
913
+ return ({
914
+ costCenter: ((_a = allocation.cost_center) === null || _a === void 0 ? void 0 : _a.name) || 'N/A',
915
+ amount: this.fromCents(allocation.allocated_amount_cents),
916
+ });
917
+ })
918
+ : [
919
+ {
920
+ costCenter: 'N/A',
921
+ amount: this.fromCents(installment.amount_cents),
922
+ },
923
+ ];
924
+ const period = this.getReportBucketKey(installment.competence_date, groupBy);
925
+ for (const allocation of allocations) {
926
+ const categoryName = (category === null || category === void 0 ? void 0 : category.name) || 'Sem categoria';
927
+ const key = `${categoryName}::${allocation.costCenter}`;
928
+ const current = byExpense.get(key) || {
929
+ category: categoryName,
930
+ costCenter: allocation.costCenter,
931
+ label: `${categoryName} - ${allocation.costCenter}`,
932
+ value: 0,
933
+ };
934
+ current.value += allocation.amount;
935
+ byExpense.set(key, current);
936
+ byCostCenter.set(allocation.costCenter, (byCostCenter.get(allocation.costCenter) || 0) + allocation.amount);
937
+ byBucket.set(period, (byBucket.get(period) || 0) + allocation.amount);
938
+ }
939
+ }
940
+ const filteredExpenses = Array.from(byExpense.values())
941
+ .filter((item) => normalizedSearch.length === 0
942
+ ? true
943
+ : this.normalizeReportText(`${item.category} ${item.costCenter}`).includes(normalizedSearch))
944
+ .sort((a, b) => b.value - a.value);
945
+ const topExpenses = filteredExpenses.slice(0, topN).map((item) => ({
946
+ category: item.category,
947
+ costCenter: item.costCenter,
948
+ label: item.label,
949
+ value: this.roundCurrency(item.value),
950
+ }));
951
+ const total = this.roundCurrency(topExpenses.reduce((acc, item) => acc + item.value, 0));
952
+ const pieByCostCenter = new Map();
953
+ for (const item of topExpenses) {
954
+ pieByCostCenter.set(item.costCenter, (pieByCostCenter.get(item.costCenter) || 0) + item.value);
955
+ }
956
+ const pieData = Array.from(pieByCostCenter.entries())
957
+ .map(([name, value]) => ({
958
+ name,
959
+ value: this.roundCurrency(value),
960
+ }))
961
+ .sort((a, b) => b.value - a.value);
962
+ const groupedPeriods = Array.from(byBucket.entries())
963
+ .map(([period, value]) => ({
964
+ period,
965
+ value: this.roundCurrency(value),
966
+ }))
967
+ .sort((a, b) => this.sortReportBuckets(a.period, b.period, groupBy));
968
+ return {
969
+ total,
970
+ average: topExpenses.length > 0
971
+ ? this.roundCurrency(total / topExpenses.length)
972
+ : 0,
973
+ topExpenses,
974
+ pieData,
975
+ groupedPeriods,
976
+ highest: topExpenses[0] || null,
977
+ };
978
+ }
630
979
  async updateScenarioSettings(scenario, data) {
631
980
  const scenarioSlug = this.resolveForecastScenarioStrict(scenario);
632
981
  if (data.atrasoMedio < 0 || data.atrasoMedio > 365) {
@@ -4361,6 +4710,113 @@ let FinanceService = FinanceService_1 = class FinanceService {
4361
4710
  currentParentId = (parent === null || parent === void 0 ? void 0 : parent.parent_id) || null;
4362
4711
  }
4363
4712
  }
4713
+ resolveReportDateRange(from, to) {
4714
+ const fromDate = from
4715
+ ? new Date(`${from}T00:00:00.000`)
4716
+ : new Date('2021-01-01T00:00:00.000');
4717
+ const toDate = to
4718
+ ? new Date(`${to}T23:59:59.999`)
4719
+ : new Date('2026-12-31T23:59:59.999');
4720
+ if (Number.isNaN(fromDate.getTime()) ||
4721
+ Number.isNaN(toDate.getTime()) ||
4722
+ fromDate > toDate) {
4723
+ throw new common_1.BadRequestException('Invalid report date range');
4724
+ }
4725
+ return { fromDate, toDate };
4726
+ }
4727
+ resolveReportGroupBy(groupBy) {
4728
+ if (groupBy === 'day' || groupBy === 'week' || groupBy === 'month') {
4729
+ return groupBy;
4730
+ }
4731
+ return 'year';
4732
+ }
4733
+ resolveTopN(topN) {
4734
+ if (!topN || !Number.isFinite(topN)) {
4735
+ return 20;
4736
+ }
4737
+ return Math.max(1, Math.min(100, Math.trunc(topN)));
4738
+ }
4739
+ getReportBucketKey(date, groupBy) {
4740
+ const baseDate = this.startOfDay(date);
4741
+ if (groupBy === 'day') {
4742
+ return baseDate.toISOString().slice(0, 10);
4743
+ }
4744
+ if (groupBy === 'week') {
4745
+ const weekStart = this.startOfWeek(baseDate);
4746
+ const isoWeek = this.getIsoWeek(weekStart);
4747
+ return `${weekStart.getFullYear()}-W${String(isoWeek).padStart(2, '0')}`;
4748
+ }
4749
+ if (groupBy === 'month') {
4750
+ return `${baseDate.getFullYear()}-${String(baseDate.getMonth() + 1).padStart(2, '0')}`;
4751
+ }
4752
+ return String(baseDate.getFullYear());
4753
+ }
4754
+ sortReportBuckets(a, b, groupBy) {
4755
+ if (groupBy === 'year' || groupBy === 'month' || groupBy === 'day') {
4756
+ return a.localeCompare(b);
4757
+ }
4758
+ const [aYear, aWeek] = a.split('-W');
4759
+ const [bYear, bWeek] = b.split('-W');
4760
+ const yearDiff = Number(aYear) - Number(bYear);
4761
+ if (yearDiff !== 0) {
4762
+ return yearDiff;
4763
+ }
4764
+ return Number(aWeek) - Number(bWeek);
4765
+ }
4766
+ startOfWeek(date) {
4767
+ const current = new Date(date);
4768
+ const day = current.getDay();
4769
+ const diff = day === 0 ? -6 : 1 - day;
4770
+ current.setDate(current.getDate() + diff);
4771
+ current.setHours(0, 0, 0, 0);
4772
+ return current;
4773
+ }
4774
+ getIsoWeek(date) {
4775
+ const current = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
4776
+ const dayNum = current.getUTCDay() || 7;
4777
+ current.setUTCDate(current.getUTCDate() + 4 - dayNum);
4778
+ const yearStart = new Date(Date.UTC(current.getUTCFullYear(), 0, 1));
4779
+ return Math.ceil(((current.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
4780
+ }
4781
+ normalizeReportText(value) {
4782
+ return value
4783
+ .normalize('NFD')
4784
+ .replace(/[\u0300-\u036f]/g, '')
4785
+ .toLowerCase()
4786
+ .trim();
4787
+ }
4788
+ isTransferReportCategory(category) {
4789
+ if (!category) {
4790
+ return false;
4791
+ }
4792
+ if (category.kind === 'transfer') {
4793
+ return true;
4794
+ }
4795
+ const haystack = this.normalizeReportText(`${category.code || ''} ${category.name || ''}`);
4796
+ return haystack.includes('transfer');
4797
+ }
4798
+ isLoanReportCategory(category) {
4799
+ if (!category) {
4800
+ return false;
4801
+ }
4802
+ const haystack = this.normalizeReportText(`${category.code || ''} ${category.name || ''}`);
4803
+ return (haystack.includes('emprestimo') ||
4804
+ haystack.includes('financiamento') ||
4805
+ haystack.includes('loan'));
4806
+ }
4807
+ isInvestorContributionReportCategory(category) {
4808
+ if (!category) {
4809
+ return false;
4810
+ }
4811
+ const haystack = this.normalizeReportText(`${category.code || ''} ${category.name || ''}`);
4812
+ return (haystack.includes('aporte') ||
4813
+ haystack.includes('investidor') ||
4814
+ haystack.includes('capital') ||
4815
+ haystack.includes('socio'));
4816
+ }
4817
+ roundCurrency(value) {
4818
+ return Number(value.toFixed(2));
4819
+ }
4364
4820
  toCents(value) {
4365
4821
  return Math.round(value * 100);
4366
4822
  }