@hed-hog/finance 0.0.246 → 0.0.250

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.
@@ -9,6 +9,14 @@ import {
9
9
  CardHeader,
10
10
  CardTitle,
11
11
  } from '@/components/ui/card';
12
+ import {
13
+ Dialog,
14
+ DialogContent,
15
+ DialogDescription,
16
+ DialogFooter,
17
+ DialogHeader,
18
+ DialogTitle,
19
+ } from '@/components/ui/dialog';
12
20
  import { FilterBar } from '@/components/ui/filter-bar';
13
21
  import {
14
22
  Form,
@@ -166,7 +174,7 @@ function ImportarExtratoSheet({
166
174
  <FormLabel>{t('importDialog.bankAccount')}</FormLabel>
167
175
  <Select value={field.value} onValueChange={field.onChange}>
168
176
  <FormControl>
169
- <SelectTrigger>
177
+ <SelectTrigger className="w-full">
170
178
  <SelectValue
171
179
  placeholder={t('importDialog.selectAccount')}
172
180
  />
@@ -238,8 +246,10 @@ export default function ExtratosPage() {
238
246
 
239
247
  const [contaFilter, setContaFilter] = useState<string>('');
240
248
  const [search, setSearch] = useState('');
249
+ const [extratoSelecionado, setExtratoSelecionado] =
250
+ useState<Statement | null>(null);
241
251
 
242
- const { data: contasBancarias } = useQuery<BankAccount[]>({
252
+ const { data: contasBancarias = [] } = useQuery<BankAccount[]>({
243
253
  queryKey: ['finance-bank-accounts'],
244
254
  queryFn: async () => {
245
255
  const response = await request({
@@ -249,7 +259,6 @@ export default function ExtratosPage() {
249
259
 
250
260
  return (response?.data || []) as BankAccount[];
251
261
  },
252
- initialData: [],
253
262
  });
254
263
 
255
264
  useEffect(() => {
@@ -285,7 +294,9 @@ export default function ExtratosPage() {
285
294
  searchParams,
286
295
  ]);
287
296
 
288
- const { data: extratos, refetch: refetchExtratos } = useQuery<Statement[]>({
297
+ const { data: extratos = [], refetch: refetchExtratos } = useQuery<
298
+ Statement[]
299
+ >({
289
300
  queryKey: ['finance-statements', contaFilter],
290
301
  queryFn: async () => {
291
302
  if (!contaFilter) {
@@ -299,7 +310,6 @@ export default function ExtratosPage() {
299
310
 
300
311
  return (response?.data || []) as Statement[];
301
312
  },
302
- initialData: [],
303
313
  });
304
314
 
305
315
  const filteredExtratos = useMemo(
@@ -311,6 +321,11 @@ export default function ExtratosPage() {
311
321
  );
312
322
 
313
323
  const conta = contasBancarias.find((item) => item.id === contaFilter);
324
+ const contaExtratoSelecionado = extratoSelecionado
325
+ ? contasBancarias.find(
326
+ (item) => item.id === extratoSelecionado.contaBancariaId
327
+ )
328
+ : undefined;
314
329
  const totalEntradas = filteredExtratos
315
330
  .filter((e) => e.tipo === 'entrada')
316
331
  .reduce((acc, e) => acc + e.valor, 0);
@@ -466,60 +481,182 @@ export default function ExtratosPage() {
466
481
  </CardDescription>
467
482
  </CardHeader>
468
483
  <CardContent>
469
- <Table>
470
- <TableHeader>
471
- <TableRow>
472
- <TableHead>{t('table.headers.date')}</TableHead>
473
- <TableHead>{t('table.headers.description')}</TableHead>
474
- <TableHead className="text-right">
475
- {t('table.headers.value')}
476
- </TableHead>
477
- <TableHead>{t('table.headers.type')}</TableHead>
478
- <TableHead>{t('table.headers.reconciliation')}</TableHead>
479
- </TableRow>
480
- </TableHeader>
481
- <TableBody>
482
- {filteredExtratos.map((extrato) => (
483
- <TableRow key={extrato.id}>
484
- <TableCell>{formatarData(extrato.data)}</TableCell>
485
- <TableCell>{extrato.descricao}</TableCell>
486
- <TableCell className="text-right">
487
- <span
488
- className={
489
- extrato.tipo === 'entrada'
490
- ? 'text-green-600'
491
- : 'text-red-600'
484
+ <div className="overflow-x-auto">
485
+ <Table className="min-w-[760px] table-fixed">
486
+ <TableHeader>
487
+ <TableRow>
488
+ <TableHead className="w-[110px]">
489
+ {t('table.headers.date')}
490
+ </TableHead>
491
+ <TableHead>{t('table.headers.description')}</TableHead>
492
+ <TableHead className="w-[130px] text-right">
493
+ {t('table.headers.value')}
494
+ </TableHead>
495
+ <TableHead className="w-[110px]">
496
+ {t('table.headers.type')}
497
+ </TableHead>
498
+ <TableHead className="w-[140px]">
499
+ {t('table.headers.reconciliation')}
500
+ </TableHead>
501
+ </TableRow>
502
+ </TableHeader>
503
+ <TableBody>
504
+ {filteredExtratos.map((extrato) => (
505
+ <TableRow
506
+ key={extrato.id}
507
+ className="cursor-pointer"
508
+ onClick={() => setExtratoSelecionado(extrato)}
509
+ onKeyDown={(event) => {
510
+ if (event.key === 'Enter' || event.key === ' ') {
511
+ event.preventDefault();
512
+ setExtratoSelecionado(extrato);
492
513
  }
493
- >
494
- {extrato.tipo === 'saida' && '-'}
495
- <Money value={extrato.valor} />
496
- </span>
497
- </TableCell>
498
- <TableCell>
499
- {extrato.tipo === 'entrada' ? (
500
- <span className="flex items-center gap-1 text-green-600">
501
- <ArrowUpRight className="h-4 w-4" />
502
- {t('types.inflow')}
514
+ }}
515
+ role="button"
516
+ tabIndex={0}
517
+ >
518
+ <TableCell>{formatarData(extrato.data)}</TableCell>
519
+ <TableCell className="truncate" title={extrato.descricao}>
520
+ {extrato.descricao}
521
+ </TableCell>
522
+ <TableCell className="text-right">
523
+ <span
524
+ className={
525
+ extrato.tipo === 'entrada'
526
+ ? 'text-green-600'
527
+ : 'text-red-600'
528
+ }
529
+ >
530
+ <Money value={extrato.valor} />
503
531
  </span>
504
- ) : (
505
- <span className="flex items-center gap-1 text-red-600">
506
- <ArrowDownRight className="h-4 w-4" />
507
- {t('types.outflow')}
508
- </span>
509
- )}
510
- </TableCell>
511
- <TableCell>
512
- <StatusBadge
513
- status={extrato.statusConciliacao}
514
- type="conciliacao"
515
- />
516
- </TableCell>
517
- </TableRow>
518
- ))}
519
- </TableBody>
520
- </Table>
532
+ </TableCell>
533
+ <TableCell>
534
+ {extrato.tipo === 'entrada' ? (
535
+ <span className="flex items-center gap-1 text-green-600">
536
+ <ArrowUpRight className="h-4 w-4" />
537
+ {t('types.inflow')}
538
+ </span>
539
+ ) : (
540
+ <span className="flex items-center gap-1 text-red-600">
541
+ <ArrowDownRight className="h-4 w-4" />
542
+ {t('types.outflow')}
543
+ </span>
544
+ )}
545
+ </TableCell>
546
+ <TableCell>
547
+ <StatusBadge
548
+ status={extrato.statusConciliacao}
549
+ type="conciliacao"
550
+ />
551
+ </TableCell>
552
+ </TableRow>
553
+ ))}
554
+ </TableBody>
555
+ </Table>
556
+ </div>
521
557
  </CardContent>
522
558
  </Card>
559
+
560
+ <Dialog
561
+ open={!!extratoSelecionado}
562
+ onOpenChange={(open) => {
563
+ if (!open) {
564
+ setExtratoSelecionado(null);
565
+ }
566
+ }}
567
+ >
568
+ <DialogContent className="sm:max-w-lg">
569
+ <DialogHeader>
570
+ <DialogTitle>{t('table.title')}</DialogTitle>
571
+ <DialogDescription>{t('header.description')}</DialogDescription>
572
+ </DialogHeader>
573
+
574
+ {extratoSelecionado ? (
575
+ <div className="space-y-4 rounded-md border p-4">
576
+ <div className="text-center">
577
+ <p className="text-sm text-muted-foreground">
578
+ {t('table.headers.reconciliation')}
579
+ </p>
580
+ <div className="mt-1 flex justify-center">
581
+ <StatusBadge
582
+ status={extratoSelecionado.statusConciliacao}
583
+ type="conciliacao"
584
+ />
585
+ </div>
586
+ </div>
587
+
588
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
589
+ <div>
590
+ <p className="text-xs text-muted-foreground">
591
+ {t('table.headers.date')}
592
+ </p>
593
+ <p className="font-medium">
594
+ {formatarData(extratoSelecionado.data)}
595
+ </p>
596
+ </div>
597
+ <div>
598
+ <p className="text-xs text-muted-foreground">
599
+ {t('table.headers.type')}
600
+ </p>
601
+ <p className="font-medium">
602
+ {extratoSelecionado.tipo === 'entrada'
603
+ ? t('types.inflow')
604
+ : t('types.outflow')}
605
+ </p>
606
+ </div>
607
+ <div className="sm:col-span-2">
608
+ <p className="text-xs text-muted-foreground">
609
+ {t('table.headers.description')}
610
+ </p>
611
+ <p className="font-medium wrap-break-word">
612
+ {extratoSelecionado.descricao}
613
+ </p>
614
+ </div>
615
+ <div>
616
+ <p className="text-xs text-muted-foreground">
617
+ {t('table.headers.value')}
618
+ </p>
619
+ <p
620
+ className={`font-medium ${
621
+ extratoSelecionado.tipo === 'entrada'
622
+ ? 'text-green-600'
623
+ : 'text-red-600'
624
+ }`}
625
+ >
626
+ <Money value={extratoSelecionado.valor} />
627
+ </p>
628
+ </div>
629
+ <div>
630
+ <p className="text-xs text-muted-foreground">
631
+ {t('importDialog.bankAccount')}
632
+ </p>
633
+ <p className="font-medium">
634
+ {contaExtratoSelecionado
635
+ ? `${contaExtratoSelecionado.banco} - ${contaExtratoSelecionado.descricao}`
636
+ : '-'}
637
+ </p>
638
+ </div>
639
+ <div className="sm:col-span-2">
640
+ <p className="text-xs text-muted-foreground">ID</p>
641
+ <p className="font-mono text-xs break-all">
642
+ {extratoSelecionado.id}
643
+ </p>
644
+ </div>
645
+ </div>
646
+ </div>
647
+ ) : null}
648
+
649
+ <DialogFooter>
650
+ <Button
651
+ type="button"
652
+ variant="outline"
653
+ onClick={() => setExtratoSelecionado(null)}
654
+ >
655
+ {t('common.cancel')}
656
+ </Button>
657
+ </DialogFooter>
658
+ </DialogContent>
659
+ </Dialog>
523
660
  </Page>
524
661
  );
525
662
  }
@@ -20,7 +20,7 @@ import {
20
20
  TrendingUp,
21
21
  Wallet,
22
22
  } from 'lucide-react';
23
- import { useTranslations } from 'next-intl';
23
+ import { useLocale, useTranslations } from 'next-intl';
24
24
  import {
25
25
  CartesianGrid,
26
26
  Legend,
@@ -204,10 +204,14 @@ function ProximosVencimentos({
204
204
  function Alertas({
205
205
  titulosPagar,
206
206
  extratos,
207
+ periodoAberto,
208
+ locale,
207
209
  t,
208
210
  }: {
209
211
  titulosPagar: any[];
210
212
  extratos: any[];
213
+ periodoAberto?: { inicio?: string | null } | null;
214
+ locale: string;
211
215
  t: ReturnType<typeof useTranslations>;
212
216
  }) {
213
217
  const vencidos = titulosPagar.filter((t) =>
@@ -217,6 +221,10 @@ function Alertas({
217
221
  (e) => e.statusConciliacao === 'pendente'
218
222
  ).length;
219
223
 
224
+ const periodoBase = periodoAberto?.inicio ? new Date(periodoAberto.inicio) : new Date();
225
+ const mes = new Intl.DateTimeFormat(locale, { month: 'long' }).format(periodoBase);
226
+ const periodoAtual = `${mes.charAt(0).toUpperCase()}${mes.slice(1)}/${periodoBase.getFullYear()}`;
227
+
220
228
  return (
221
229
  <Card>
222
230
  <CardHeader>
@@ -249,7 +257,7 @@ function Alertas({
249
257
  <div className="flex items-center justify-between rounded-lg bg-blue-50 p-3">
250
258
  <span className="text-sm">{t('alerts.openPeriod')}</span>
251
259
  <Badge variant="outline" className="border-blue-500 text-blue-700">
252
- {t('alerts.currentPeriod')}
260
+ {periodoAtual}
253
261
  </Badge>
254
262
  </div>
255
263
  </div>
@@ -260,6 +268,7 @@ function Alertas({
260
268
 
261
269
  export default function DashboardPage() {
262
270
  const t = useTranslations('finance.DashboardPage');
271
+ const locale = useLocale();
263
272
  const { data } = useFinanceData();
264
273
  const {
265
274
  kpis,
@@ -268,6 +277,7 @@ export default function DashboardPage() {
268
277
  titulosReceber,
269
278
  extratos,
270
279
  pessoas,
280
+ periodoAberto,
271
281
  } = data;
272
282
 
273
283
  const getPessoaById = (id?: string) => pessoas.find((p) => p.id === id);
@@ -324,7 +334,13 @@ export default function DashboardPage() {
324
334
  <DashboardChart fluxoCaixaPrevisto={fluxoCaixaPrevisto} t={t} />
325
335
  </CardContent>
326
336
  </Card>
327
- <Alertas titulosPagar={titulosPagar} extratos={extratos} t={t} />
337
+ <Alertas
338
+ titulosPagar={titulosPagar}
339
+ extratos={extratos}
340
+ periodoAberto={periodoAberto}
341
+ locale={locale}
342
+ t={t}
343
+ />
328
344
  </div>
329
345
 
330
346
  <ProximosVencimentos
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/finance",
3
- "version": "0.0.246",
3
+ "version": "0.0.250",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -9,14 +9,14 @@
9
9
  "@nestjs/core": "^11",
10
10
  "@nestjs/jwt": "^11",
11
11
  "@nestjs/mapped-types": "*",
12
+ "@hed-hog/tag": "0.0.250",
12
13
  "@hed-hog/api-pagination": "0.0.5",
13
- "@hed-hog/tag": "0.0.240",
14
+ "@hed-hog/contact": "0.0.250",
14
15
  "@hed-hog/api-prisma": "0.0.4",
15
- "@hed-hog/api": "0.0.3",
16
- "@hed-hog/contact": "0.0.240",
17
16
  "@hed-hog/api-types": "0.0.1",
18
17
  "@hed-hog/api-locale": "0.0.11",
19
- "@hed-hog/core": "0.0.240"
18
+ "@hed-hog/api": "0.0.3",
19
+ "@hed-hog/core": "0.0.250"
20
20
  },
21
21
  "exports": {
22
22
  ".": {
@@ -728,6 +728,7 @@ export class FinanceService {
728
728
  bankAccountsResult,
729
729
  tagsResult,
730
730
  auditLogsResult,
731
+ openPeriodResult,
731
732
  ] = await Promise.allSettled([
732
733
  this.loadTitles('payable'),
733
734
  this.loadTitles('receivable'),
@@ -737,6 +738,7 @@ export class FinanceService {
737
738
  this.loadBankAccounts(),
738
739
  this.loadTags(),
739
740
  this.loadAuditLogs(),
741
+ this.loadOpenPeriod(),
740
742
  ]);
741
743
 
742
744
  const payables = payablesResult.status === 'fulfilled' ? payablesResult.value : [];
@@ -752,6 +754,8 @@ export class FinanceService {
752
754
  const tags = tagsResult.status === 'fulfilled' ? tagsResult.value : [];
753
755
  const auditLogs =
754
756
  auditLogsResult.status === 'fulfilled' ? auditLogsResult.value : [];
757
+ const openPeriod =
758
+ openPeriodResult.status === 'fulfilled' ? openPeriodResult.value : null;
755
759
 
756
760
  if (payablesResult.status === 'rejected') {
757
761
  this.logger.error('Failed to load finance payables', payablesResult.reason);
@@ -777,6 +781,9 @@ export class FinanceService {
777
781
  if (auditLogsResult.status === 'rejected') {
778
782
  this.logger.error('Failed to load finance audit logs', auditLogsResult.reason);
779
783
  }
784
+ if (openPeriodResult.status === 'rejected') {
785
+ this.logger.error('Failed to load finance open period', openPeriodResult.reason);
786
+ }
780
787
 
781
788
  const aprovacoesPendentes = payables
782
789
  .filter((title: any) => title.status === 'rascunho')
@@ -790,15 +797,10 @@ export class FinanceService {
790
797
  dataSolicitacao: title.criadoEm,
791
798
  }));
792
799
 
800
+ const kpis = this.calculateDashboardKpis(payables, receivables, bankAccounts);
801
+
793
802
  return {
794
- kpis: {
795
- saldoCaixa: 0,
796
- aPagar30dias: 0,
797
- aPagar7dias: 0,
798
- aReceber30dias: 0,
799
- aReceber7dias: 0,
800
- inadimplencia: 0,
801
- },
803
+ kpis,
802
804
  fluxoCaixaPrevisto: [],
803
805
  titulosPagar: payables,
804
806
  titulosReceber: receivables,
@@ -818,9 +820,96 @@ export class FinanceService {
818
820
  historicoContatos: [],
819
821
  entradasPrevistas: [],
820
822
  saidasPrevistas: [],
823
+ periodoAberto: openPeriod,
821
824
  };
822
825
  }
823
826
 
827
+ private calculateDashboardKpis(
828
+ payables: any[],
829
+ receivables: any[],
830
+ bankAccounts: any[],
831
+ ) {
832
+ const today = this.startOfDay(new Date());
833
+ const day7 = this.addDays(today, 7);
834
+ const day30 = this.addDays(today, 30);
835
+
836
+ const payableInstallments = this.extractOpenInstallments(payables);
837
+ const receivableInstallments = this.extractOpenInstallments(receivables);
838
+
839
+ const saldoCaixa = (bankAccounts || [])
840
+ .filter((account) => account?.ativo !== false)
841
+ .reduce((acc, account) => acc + Number(account?.saldoAtual || 0), 0);
842
+
843
+ const aPagar7dias = this.sumInstallmentsDueBetween(
844
+ payableInstallments,
845
+ today,
846
+ day7,
847
+ );
848
+ const aPagar30dias = this.sumInstallmentsDueBetween(
849
+ payableInstallments,
850
+ today,
851
+ day30,
852
+ );
853
+
854
+ const aReceber7dias = this.sumInstallmentsDueBetween(
855
+ receivableInstallments,
856
+ today,
857
+ day7,
858
+ );
859
+ const aReceber30dias = this.sumInstallmentsDueBetween(
860
+ receivableInstallments,
861
+ today,
862
+ day30,
863
+ );
864
+
865
+ const inadimplencia = receivableInstallments
866
+ .filter((installment) => this.startOfDay(new Date(installment.vencimento)) < today)
867
+ .reduce((acc, installment) => acc + Number(installment.valor || 0), 0);
868
+
869
+ return {
870
+ saldoCaixa: Number(saldoCaixa.toFixed(2)),
871
+ aPagar30dias: Number(aPagar30dias.toFixed(2)),
872
+ aPagar7dias: Number(aPagar7dias.toFixed(2)),
873
+ aReceber30dias: Number(aReceber30dias.toFixed(2)),
874
+ aReceber7dias: Number(aReceber7dias.toFixed(2)),
875
+ inadimplencia: Number(inadimplencia.toFixed(2)),
876
+ };
877
+ }
878
+
879
+ private extractOpenInstallments(titles: any[]) {
880
+ return (titles || []).flatMap((title) =>
881
+ (title?.parcelas || []).filter(
882
+ (installment) =>
883
+ installment?.status === 'aberto' ||
884
+ installment?.status === 'vencido' ||
885
+ installment?.status === 'parcial',
886
+ ),
887
+ );
888
+ }
889
+
890
+ private sumInstallmentsDueBetween(
891
+ installments: any[],
892
+ startDate: Date,
893
+ endDate: Date,
894
+ ) {
895
+ return (installments || [])
896
+ .filter((installment) => {
897
+ const dueDate = this.startOfDay(new Date(installment.vencimento));
898
+ return dueDate >= startDate && dueDate <= endDate;
899
+ })
900
+ .reduce((acc, installment) => acc + Number(installment.valor || 0), 0);
901
+ }
902
+
903
+ private startOfDay(date: Date) {
904
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
905
+ }
906
+
907
+ private addDays(date: Date, days: number) {
908
+ const next = new Date(date);
909
+ next.setDate(next.getDate() + days);
910
+ return next;
911
+ }
912
+
824
913
  async listAccountsPayableInstallments(
825
914
  paginationParams: PaginationDTO,
826
915
  status?: string,
@@ -3416,6 +3505,30 @@ export class FinanceService {
3416
3505
  }));
3417
3506
  }
3418
3507
 
3508
+ private async loadOpenPeriod() {
3509
+ const openPeriod = await this.prisma.period_close.findFirst({
3510
+ where: {
3511
+ status: 'open',
3512
+ },
3513
+ orderBy: {
3514
+ period_start: 'desc',
3515
+ },
3516
+ select: {
3517
+ period_start: true,
3518
+ period_end: true,
3519
+ },
3520
+ });
3521
+
3522
+ if (!openPeriod) {
3523
+ return null;
3524
+ }
3525
+
3526
+ return {
3527
+ inicio: openPeriod.period_start?.toISOString?.() || null,
3528
+ fim: openPeriod.period_end?.toISOString?.() || null,
3529
+ };
3530
+ }
3531
+
3419
3532
  private async resolvePaymentMethodId(tx: any, paymentChannel?: string) {
3420
3533
  const paymentType = this.mapPaymentMethodFromPt(paymentChannel);
3421
3534