@hed-hog/finance 0.0.299 → 0.0.300
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.
- package/dist/dto/create-bank-account.dto.d.ts +1 -0
- package/dist/dto/create-bank-account.dto.d.ts.map +1 -1
- package/dist/dto/create-bank-account.dto.js +7 -0
- package/dist/dto/create-bank-account.dto.js.map +1 -1
- package/dist/dto/update-bank-account.dto.d.ts +1 -0
- package/dist/dto/update-bank-account.dto.d.ts.map +1 -1
- package/dist/dto/update-bank-account.dto.js +7 -0
- package/dist/dto/update-bank-account.dto.js.map +1 -1
- package/dist/finance-bank-accounts.controller.d.ts +3 -0
- package/dist/finance-bank-accounts.controller.d.ts.map +1 -1
- package/dist/finance-data.controller.d.ts +1 -0
- package/dist/finance-data.controller.d.ts.map +1 -1
- package/dist/finance.service.d.ts +4 -0
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +5 -0
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/dashboard.yaml +6 -0
- package/hedhog/data/dashboard_component.yaml +72 -17
- package/hedhog/data/dashboard_component_role.yaml +30 -0
- package/hedhog/data/dashboard_item.yaml +155 -0
- package/hedhog/data/dashboard_role.yaml +6 -0
- package/hedhog/data/role_menu.yaml +6 -0
- package/hedhog/data/role_route.yaml +127 -0
- package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +328 -12
- package/hedhog/frontend/messages/en.json +48 -2
- package/hedhog/frontend/messages/pt.json +48 -2
- package/hedhog/frontend/widgets/alerts.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/bank-reconciliation-status.tsx.ejs +142 -0
- package/hedhog/frontend/widgets/cash-balance-kpi.tsx.ejs +9 -9
- package/hedhog/frontend/widgets/cash-flow-chart.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/default-kpi.tsx.ejs +9 -9
- package/hedhog/frontend/widgets/payable-30d-kpi.tsx.ejs +9 -9
- package/hedhog/frontend/widgets/pending-approvals-kpi.tsx.ejs +78 -0
- package/hedhog/frontend/widgets/pending-approvals-list.tsx.ejs +147 -0
- package/hedhog/frontend/widgets/pending-reconciliation-kpi.tsx.ejs +84 -0
- package/hedhog/frontend/widgets/receivable-30d-kpi.tsx.ejs +9 -9
- package/hedhog/frontend/widgets/receivable-aging-analysis.tsx.ejs +163 -0
- package/hedhog/frontend/widgets/upcoming-payable.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/upcoming-receivable.tsx.ejs +1 -1
- package/hedhog/table/bank_account.yaml +8 -0
- package/package.json +5 -5
- package/src/dto/create-bank-account.dto.ts +7 -1
- package/src/dto/update-bank-account.dto.ts +7 -1
- package/src/finance.service.ts +4 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { WidgetWrapper } from '@/app/(app)/(libraries)/core/dashboard/components';
|
|
4
|
+
import { Badge } from '@/components/ui/badge';
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardDescription,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
} from '@/components/ui/card';
|
|
12
|
+
import { Money } from '@/components/ui/money';
|
|
13
|
+
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
14
|
+
import { Landmark } from 'lucide-react';
|
|
15
|
+
import { useTranslations } from 'next-intl';
|
|
16
|
+
|
|
17
|
+
interface ReconciliationWidgetProps {
|
|
18
|
+
widget?: { name?: string };
|
|
19
|
+
onRemove?: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface FinanceData {
|
|
23
|
+
extratos?: Array<{
|
|
24
|
+
statusConciliacao?: string | null;
|
|
25
|
+
}>;
|
|
26
|
+
contasBancarias?: Array<{
|
|
27
|
+
id: string;
|
|
28
|
+
banco?: string | null;
|
|
29
|
+
nome?: string | null;
|
|
30
|
+
saldoAtual?: number | null;
|
|
31
|
+
saldoConciliado?: number | null;
|
|
32
|
+
ativo?: boolean | null;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const normalize = (value?: string | null) =>
|
|
37
|
+
String(value || '')
|
|
38
|
+
.normalize('NFD')
|
|
39
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
40
|
+
.toLowerCase();
|
|
41
|
+
|
|
42
|
+
export default function BankReconciliationStatus({
|
|
43
|
+
widget,
|
|
44
|
+
onRemove,
|
|
45
|
+
}: ReconciliationWidgetProps) {
|
|
46
|
+
const t = useTranslations('finance.DashboardPage');
|
|
47
|
+
|
|
48
|
+
const { data, isLoading, isAccessDenied, isError } =
|
|
49
|
+
useWidgetData<FinanceData>({
|
|
50
|
+
endpoint: '/finance/data',
|
|
51
|
+
queryKey: 'finance-bank-reconciliation-status',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const statements = data?.extratos || [];
|
|
55
|
+
const bankAccounts = (data?.contasBancarias || []).filter(
|
|
56
|
+
(account) => account?.ativo !== false
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const pendingCount = statements.filter((statement) =>
|
|
60
|
+
normalize(statement?.statusConciliacao).includes('pend')
|
|
61
|
+
).length;
|
|
62
|
+
const reconciledCount = Math.max(statements.length - pendingCount, 0);
|
|
63
|
+
|
|
64
|
+
const accountDiffs = bankAccounts
|
|
65
|
+
.map((account) => ({
|
|
66
|
+
id: account.id,
|
|
67
|
+
name: account.nome || account.banco || t('bankReconciliation.unnamedAccount'),
|
|
68
|
+
difference: Math.abs(
|
|
69
|
+
Number(account.saldoAtual || 0) - Number(account.saldoConciliado || 0)
|
|
70
|
+
),
|
|
71
|
+
}))
|
|
72
|
+
.sort((a, b) => b.difference - a.difference);
|
|
73
|
+
|
|
74
|
+
const totalDifference = accountDiffs.reduce(
|
|
75
|
+
(acc, account) => acc + account.difference,
|
|
76
|
+
0
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<WidgetWrapper
|
|
81
|
+
isLoading={isLoading}
|
|
82
|
+
isAccessDenied={isAccessDenied}
|
|
83
|
+
isError={isError}
|
|
84
|
+
widgetName={widget?.name || t('bankReconciliation.title')}
|
|
85
|
+
onRemove={onRemove}
|
|
86
|
+
>
|
|
87
|
+
<Card className="flex h-full flex-col">
|
|
88
|
+
<CardHeader className="pb-2">
|
|
89
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
90
|
+
<Landmark className="h-4 w-4 text-sky-600" />
|
|
91
|
+
{t('bankReconciliation.title')}
|
|
92
|
+
</CardTitle>
|
|
93
|
+
<CardDescription>{t('bankReconciliation.description')}</CardDescription>
|
|
94
|
+
</CardHeader>
|
|
95
|
+
<CardContent className="flex flex-1 flex-col gap-3 pt-0">
|
|
96
|
+
<div className="flex flex-wrap gap-2">
|
|
97
|
+
<Badge variant="outline" className="border-amber-200 bg-amber-50 text-amber-700">
|
|
98
|
+
{t('bankReconciliation.pending')}: {pendingCount}
|
|
99
|
+
</Badge>
|
|
100
|
+
<Badge variant="outline" className="border-emerald-200 bg-emerald-50 text-emerald-700">
|
|
101
|
+
{t('bankReconciliation.reconciled')}: {reconciledCount}
|
|
102
|
+
</Badge>
|
|
103
|
+
<Badge variant="secondary" className="bg-slate-100 text-slate-700">
|
|
104
|
+
{t('bankReconciliation.accounts')}: {bankAccounts.length}
|
|
105
|
+
</Badge>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div className="rounded-lg bg-muted/40 p-3">
|
|
109
|
+
<p className="text-[11px] font-medium uppercase tracking-[0.18em] text-muted-foreground">
|
|
110
|
+
{t('bankReconciliation.difference')}
|
|
111
|
+
</p>
|
|
112
|
+
<p className="text-lg font-semibold text-foreground">
|
|
113
|
+
<Money value={totalDifference} />
|
|
114
|
+
</p>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="space-y-2">
|
|
118
|
+
{accountDiffs.slice(0, 3).map((account) => (
|
|
119
|
+
<div
|
|
120
|
+
key={account.id}
|
|
121
|
+
className="flex items-center justify-between rounded-md border border-border/60 px-3 py-2"
|
|
122
|
+
>
|
|
123
|
+
<span className="truncate pr-3 text-sm text-foreground">
|
|
124
|
+
{account.name}
|
|
125
|
+
</span>
|
|
126
|
+
<span className="text-sm font-medium text-muted-foreground">
|
|
127
|
+
<Money value={account.difference} />
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
))}
|
|
131
|
+
|
|
132
|
+
{accountDiffs.length === 0 && (
|
|
133
|
+
<div className="rounded-md border border-dashed border-emerald-200 bg-emerald-50 px-3 py-4 text-sm text-emerald-700">
|
|
134
|
+
{t('bankReconciliation.allClear')}
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
</CardContent>
|
|
139
|
+
</Card>
|
|
140
|
+
</WidgetWrapper>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -4,7 +4,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
|
|
4
4
|
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
5
5
|
import { Wallet } from 'lucide-react';
|
|
6
6
|
import { useTranslations } from 'next-intl';
|
|
7
|
-
import { WidgetWrapper } from '
|
|
7
|
+
import { WidgetWrapper } from '@/app/(app)/(libraries)/core/dashboard/components';
|
|
8
8
|
|
|
9
9
|
interface CashBalanceKpiProps {
|
|
10
10
|
widget?: { name?: string };
|
|
@@ -43,19 +43,19 @@ export default function CashBalanceKpi({
|
|
|
43
43
|
widgetName={widget?.name || t('kpis.cashBalance.title')}
|
|
44
44
|
onRemove={onRemove}
|
|
45
45
|
>
|
|
46
|
-
<Card className="h-full overflow-hidden transition-all duration-300 hover:shadow-md">
|
|
47
|
-
<CardContent className="flex h-full items-center gap-
|
|
48
|
-
<div className="flex h-
|
|
49
|
-
<Wallet className="h-5 w-5 text-emerald-600" />
|
|
46
|
+
<Card className="h-full overflow-hidden border-border/60 bg-linear-to-br from-background to-emerald-50/40 shadow-sm transition-all duration-300 hover:-translate-y-0.5 hover:shadow-md">
|
|
47
|
+
<CardContent className="flex h-full items-center gap-2.5 p-3">
|
|
48
|
+
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-emerald-100/80">
|
|
49
|
+
<Wallet className="h-4.5 w-4.5 text-emerald-600" />
|
|
50
50
|
</div>
|
|
51
|
-
<div className="flex min-w-0 flex-col">
|
|
52
|
-
<span className="text-[
|
|
51
|
+
<div className="flex min-w-0 flex-1 flex-col justify-center">
|
|
52
|
+
<span className="text-[9px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
|
53
53
|
{t('kpis.cashBalance.title')}
|
|
54
54
|
</span>
|
|
55
|
-
<span className="truncate text-
|
|
55
|
+
<span className="truncate text-base font-bold leading-none tracking-tight text-foreground sm:text-lg">
|
|
56
56
|
{formatted}
|
|
57
57
|
</span>
|
|
58
|
-
<span className="text-[
|
|
58
|
+
<span className="truncate text-[9px] text-muted-foreground">
|
|
59
59
|
{t('kpis.cashBalance.description')}
|
|
60
60
|
</span>
|
|
61
61
|
</div>
|
|
@@ -4,7 +4,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
|
|
4
4
|
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
5
5
|
import { AlertTriangle } from 'lucide-react';
|
|
6
6
|
import { useTranslations } from 'next-intl';
|
|
7
|
-
import { WidgetWrapper } from '
|
|
7
|
+
import { WidgetWrapper } from '@/app/(app)/(libraries)/core/dashboard/components';
|
|
8
8
|
|
|
9
9
|
interface DefaultKpiProps {
|
|
10
10
|
widget?: { name?: string };
|
|
@@ -40,19 +40,19 @@ export default function DefaultKpi({ widget, onRemove }: DefaultKpiProps) {
|
|
|
40
40
|
widgetName={widget?.name || t('kpis.default.title')}
|
|
41
41
|
onRemove={onRemove}
|
|
42
42
|
>
|
|
43
|
-
<Card className="h-full overflow-hidden transition-all duration-300 hover:shadow-md">
|
|
44
|
-
<CardContent className="flex h-full items-center gap-
|
|
45
|
-
<div className="flex h-
|
|
46
|
-
<AlertTriangle className="h-5 w-5 text-amber-600" />
|
|
43
|
+
<Card className="h-full overflow-hidden border-border/60 bg-linear-to-br from-background to-amber-50/50 shadow-sm transition-all duration-300 hover:-translate-y-0.5 hover:shadow-md">
|
|
44
|
+
<CardContent className="flex h-full items-center gap-2.5 p-3">
|
|
45
|
+
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-amber-100/80">
|
|
46
|
+
<AlertTriangle className="h-4.5 w-4.5 text-amber-600" />
|
|
47
47
|
</div>
|
|
48
|
-
<div className="flex min-w-0 flex-col">
|
|
49
|
-
<span className="text-[
|
|
48
|
+
<div className="flex min-w-0 flex-1 flex-col justify-center">
|
|
49
|
+
<span className="text-[9px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
|
50
50
|
{t('kpis.default.title')}
|
|
51
51
|
</span>
|
|
52
|
-
<span className="truncate text-
|
|
52
|
+
<span className="truncate text-base font-bold leading-none tracking-tight text-foreground sm:text-lg">
|
|
53
53
|
{formatted}
|
|
54
54
|
</span>
|
|
55
|
-
<span className="text-[
|
|
55
|
+
<span className="truncate text-[9px] text-muted-foreground">
|
|
56
56
|
{t('kpis.default.description')}
|
|
57
57
|
</span>
|
|
58
58
|
</div>
|
|
@@ -4,7 +4,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
|
|
4
4
|
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
5
5
|
import { TrendingDown } from 'lucide-react';
|
|
6
6
|
import { useTranslations } from 'next-intl';
|
|
7
|
-
import { WidgetWrapper } from '
|
|
7
|
+
import { WidgetWrapper } from '@/app/(app)/(libraries)/core/dashboard/components';
|
|
8
8
|
|
|
9
9
|
interface Payable30dKpiProps {
|
|
10
10
|
widget?: { name?: string };
|
|
@@ -45,19 +45,19 @@ export default function Payable30dKpi({
|
|
|
45
45
|
widgetName={widget?.name || t('kpis.payable30.title')}
|
|
46
46
|
onRemove={onRemove}
|
|
47
47
|
>
|
|
48
|
-
<Card className="h-full overflow-hidden transition-all duration-300 hover:shadow-md">
|
|
49
|
-
<CardContent className="flex h-full items-center gap-
|
|
50
|
-
<div className="flex h-
|
|
51
|
-
<TrendingDown className="h-5 w-5 text-red-600" />
|
|
48
|
+
<Card className="h-full overflow-hidden border-border/60 bg-linear-to-br from-background to-red-50/50 shadow-sm transition-all duration-300 hover:-translate-y-0.5 hover:shadow-md">
|
|
49
|
+
<CardContent className="flex h-full items-center gap-2.5 p-3">
|
|
50
|
+
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-red-100/80">
|
|
51
|
+
<TrendingDown className="h-4.5 w-4.5 text-red-600" />
|
|
52
52
|
</div>
|
|
53
|
-
<div className="flex min-w-0 flex-col">
|
|
54
|
-
<span className="text-[
|
|
53
|
+
<div className="flex min-w-0 flex-1 flex-col justify-center">
|
|
54
|
+
<span className="text-[9px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
|
55
55
|
{t('kpis.payable30.title')}
|
|
56
56
|
</span>
|
|
57
|
-
<span className="truncate text-
|
|
57
|
+
<span className="truncate text-base font-bold leading-none tracking-tight text-foreground sm:text-lg">
|
|
58
58
|
{formatted}
|
|
59
59
|
</span>
|
|
60
|
-
<span className="text-[
|
|
60
|
+
<span className="truncate text-[9px] text-muted-foreground">
|
|
61
61
|
{t('kpis.payable30.sevenDays', {
|
|
62
62
|
value: new Intl.NumberFormat('pt-BR', {
|
|
63
63
|
style: 'currency',
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { WidgetWrapper } from '@/app/(app)/(libraries)/core/dashboard/components';
|
|
4
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
5
|
+
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
6
|
+
import { ClipboardList } from 'lucide-react';
|
|
7
|
+
import { useTranslations } from 'next-intl';
|
|
8
|
+
|
|
9
|
+
interface PendingApprovalsKpiProps {
|
|
10
|
+
widget?: { name?: string };
|
|
11
|
+
onRemove?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface FinanceData {
|
|
15
|
+
aprovacoesPendentes?: Array<{
|
|
16
|
+
valor?: number | null;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function PendingApprovalsKpi({
|
|
21
|
+
widget,
|
|
22
|
+
onRemove,
|
|
23
|
+
}: PendingApprovalsKpiProps) {
|
|
24
|
+
const t = useTranslations('finance.DashboardPage');
|
|
25
|
+
|
|
26
|
+
const { data, isLoading, isAccessDenied, isError } =
|
|
27
|
+
useWidgetData<FinanceData>({
|
|
28
|
+
endpoint: '/finance/data',
|
|
29
|
+
queryKey: 'finance-kpi-pending-approvals',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const pendingApprovals = data?.aprovacoesPendentes || [];
|
|
33
|
+
const count = pendingApprovals.length;
|
|
34
|
+
const totalValue = pendingApprovals.reduce(
|
|
35
|
+
(acc, item) => acc + Number(item?.valor || 0),
|
|
36
|
+
0
|
|
37
|
+
);
|
|
38
|
+
const formattedValue = new Intl.NumberFormat('pt-BR', {
|
|
39
|
+
style: 'currency',
|
|
40
|
+
currency: 'BRL',
|
|
41
|
+
}).format(totalValue);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<WidgetWrapper
|
|
45
|
+
isLoading={isLoading}
|
|
46
|
+
isAccessDenied={isAccessDenied}
|
|
47
|
+
isError={isError}
|
|
48
|
+
widgetName={widget?.name || t('kpis.pendingApprovals.title')}
|
|
49
|
+
onRemove={onRemove}
|
|
50
|
+
>
|
|
51
|
+
<Card className="h-full overflow-hidden border-border/60 bg-linear-to-br from-background to-sky-50/50 shadow-sm transition-all duration-300 hover:-translate-y-0.5 hover:shadow-md">
|
|
52
|
+
<CardContent className="flex h-full items-center gap-2.5 p-3">
|
|
53
|
+
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-sky-100/80">
|
|
54
|
+
<ClipboardList className="h-4.5 w-4.5 text-sky-600" />
|
|
55
|
+
</div>
|
|
56
|
+
<div className="flex min-w-0 flex-1 flex-col justify-center">
|
|
57
|
+
<span className="text-[9px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
|
58
|
+
{t('kpis.pendingApprovals.title')}
|
|
59
|
+
</span>
|
|
60
|
+
<div className="flex items-end gap-2">
|
|
61
|
+
<span className="truncate text-base font-bold leading-none tracking-tight text-foreground sm:text-lg">
|
|
62
|
+
{count}
|
|
63
|
+
</span>
|
|
64
|
+
<span className="mb-0.5 text-[9px] text-muted-foreground">
|
|
65
|
+
{t('kpis.pendingApprovals.countLabel')}
|
|
66
|
+
</span>
|
|
67
|
+
</div>
|
|
68
|
+
<span className="truncate text-[9px] text-muted-foreground">
|
|
69
|
+
{t('kpis.pendingApprovals.description', {
|
|
70
|
+
value: formattedValue,
|
|
71
|
+
})}
|
|
72
|
+
</span>
|
|
73
|
+
</div>
|
|
74
|
+
</CardContent>
|
|
75
|
+
</Card>
|
|
76
|
+
</WidgetWrapper>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { WidgetWrapper } from '@/app/(app)/(libraries)/core/dashboard/components';
|
|
4
|
+
import { Badge } from '@/components/ui/badge';
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardDescription,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
} from '@/components/ui/card';
|
|
12
|
+
import { Money } from '@/components/ui/money';
|
|
13
|
+
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
14
|
+
import { ClipboardCheck } from 'lucide-react';
|
|
15
|
+
import { useTranslations } from 'next-intl';
|
|
16
|
+
import Link from 'next/link';
|
|
17
|
+
|
|
18
|
+
interface PendingApprovalsListProps {
|
|
19
|
+
widget?: { name?: string };
|
|
20
|
+
onRemove?: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ApprovalItem {
|
|
24
|
+
id: string;
|
|
25
|
+
tituloId?: string;
|
|
26
|
+
valor?: number | null;
|
|
27
|
+
politica?: string | null;
|
|
28
|
+
urgencia?: string | null;
|
|
29
|
+
dataSolicitacao?: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface FinanceData {
|
|
33
|
+
aprovacoesPendentes?: ApprovalItem[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatDate(value?: string | null) {
|
|
37
|
+
if (!value) return '—';
|
|
38
|
+
|
|
39
|
+
const parsed = new Date(value);
|
|
40
|
+
return Number.isNaN(parsed.getTime())
|
|
41
|
+
? '—'
|
|
42
|
+
: parsed.toLocaleDateString('pt-BR');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default function PendingApprovalsList({
|
|
46
|
+
widget,
|
|
47
|
+
onRemove,
|
|
48
|
+
}: PendingApprovalsListProps) {
|
|
49
|
+
const t = useTranslations('finance.DashboardPage');
|
|
50
|
+
const approvalT = useTranslations('finance.PayableApprovalsPage');
|
|
51
|
+
|
|
52
|
+
const { data, isLoading, isAccessDenied, isError } =
|
|
53
|
+
useWidgetData<FinanceData>({
|
|
54
|
+
endpoint: '/finance/data',
|
|
55
|
+
queryKey: 'finance-pending-approvals-list',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const rows = [...(data?.aprovacoesPendentes || [])]
|
|
59
|
+
.sort(
|
|
60
|
+
(a, b) =>
|
|
61
|
+
new Date(b?.dataSolicitacao || 0).getTime() -
|
|
62
|
+
new Date(a?.dataSolicitacao || 0).getTime()
|
|
63
|
+
)
|
|
64
|
+
.slice(0, 4);
|
|
65
|
+
|
|
66
|
+
const defaultUrgency = {
|
|
67
|
+
label: approvalT('urgency.medium'),
|
|
68
|
+
className: 'bg-blue-100 text-blue-700',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const urgencyConfig: Record<
|
|
72
|
+
string,
|
|
73
|
+
{ label: string; className: string }
|
|
74
|
+
> = {
|
|
75
|
+
baixa: {
|
|
76
|
+
label: approvalT('urgency.low'),
|
|
77
|
+
className: 'bg-slate-100 text-slate-700',
|
|
78
|
+
},
|
|
79
|
+
media: defaultUrgency,
|
|
80
|
+
alta: {
|
|
81
|
+
label: approvalT('urgency.high'),
|
|
82
|
+
className: 'bg-orange-100 text-orange-700',
|
|
83
|
+
},
|
|
84
|
+
critica: {
|
|
85
|
+
label: approvalT('urgency.critical'),
|
|
86
|
+
className: 'bg-red-100 text-red-700',
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<WidgetWrapper
|
|
92
|
+
isLoading={isLoading}
|
|
93
|
+
isAccessDenied={isAccessDenied}
|
|
94
|
+
isError={isError}
|
|
95
|
+
widgetName={widget?.name || t('approvalQueue.title')}
|
|
96
|
+
onRemove={onRemove}
|
|
97
|
+
>
|
|
98
|
+
<Card className="h-full">
|
|
99
|
+
<CardHeader className="pb-2">
|
|
100
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
101
|
+
<ClipboardCheck className="h-4 w-4 text-sky-600" />
|
|
102
|
+
{t('approvalQueue.title')}
|
|
103
|
+
</CardTitle>
|
|
104
|
+
<CardDescription>{t('approvalQueue.description')}</CardDescription>
|
|
105
|
+
</CardHeader>
|
|
106
|
+
<CardContent>
|
|
107
|
+
<div className="space-y-2.5">
|
|
108
|
+
{rows.map((item) => {
|
|
109
|
+
const urgency =
|
|
110
|
+
urgencyConfig[String(item.urgencia || 'media').toLowerCase()] ??
|
|
111
|
+
defaultUrgency;
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<Link
|
|
115
|
+
key={item.id}
|
|
116
|
+
href="/finance/accounts-payable/approvals"
|
|
117
|
+
className="flex cursor-pointer items-center justify-between rounded-md border border-border/60 px-3 py-2 transition-colors hover:bg-muted/50 active:bg-muted"
|
|
118
|
+
>
|
|
119
|
+
<div className="min-w-0 pr-3">
|
|
120
|
+
<p className="truncate text-sm font-medium text-foreground">
|
|
121
|
+
{item.politica || t('approvalQueue.defaultPolicy')}
|
|
122
|
+
</p>
|
|
123
|
+
<p className="text-[11px] text-muted-foreground">
|
|
124
|
+
{formatDate(item.dataSolicitacao)}
|
|
125
|
+
</p>
|
|
126
|
+
</div>
|
|
127
|
+
<div className="flex items-center gap-2">
|
|
128
|
+
<div className="text-right text-sm font-medium text-foreground">
|
|
129
|
+
<Money value={Number(item.valor || 0)} />
|
|
130
|
+
</div>
|
|
131
|
+
<Badge className={urgency.className}>{urgency.label}</Badge>
|
|
132
|
+
</div>
|
|
133
|
+
</Link>
|
|
134
|
+
);
|
|
135
|
+
})}
|
|
136
|
+
|
|
137
|
+
{rows.length === 0 && (
|
|
138
|
+
<div className="rounded-md border border-dashed border-border/70 px-3 py-5 text-center text-sm text-muted-foreground">
|
|
139
|
+
{t('approvalQueue.empty')}
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
</CardContent>
|
|
144
|
+
</Card>
|
|
145
|
+
</WidgetWrapper>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { WidgetWrapper } from '@/app/(app)/(libraries)/core/dashboard/components';
|
|
4
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
5
|
+
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
6
|
+
import { ArrowLeftRight } from 'lucide-react';
|
|
7
|
+
import { useTranslations } from 'next-intl';
|
|
8
|
+
|
|
9
|
+
interface PendingReconciliationKpiProps {
|
|
10
|
+
widget?: { name?: string };
|
|
11
|
+
onRemove?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface FinanceData {
|
|
15
|
+
extratos?: Array<{
|
|
16
|
+
statusConciliacao?: string | null;
|
|
17
|
+
}>;
|
|
18
|
+
contasBancarias?: Array<{
|
|
19
|
+
ativo?: boolean | null;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const normalize = (value?: string | null) =>
|
|
24
|
+
String(value || '')
|
|
25
|
+
.normalize('NFD')
|
|
26
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
27
|
+
.toLowerCase();
|
|
28
|
+
|
|
29
|
+
export default function PendingReconciliationKpi({
|
|
30
|
+
widget,
|
|
31
|
+
onRemove,
|
|
32
|
+
}: PendingReconciliationKpiProps) {
|
|
33
|
+
const t = useTranslations('finance.DashboardPage');
|
|
34
|
+
|
|
35
|
+
const { data, isLoading, isAccessDenied, isError } =
|
|
36
|
+
useWidgetData<FinanceData>({
|
|
37
|
+
endpoint: '/finance/data',
|
|
38
|
+
queryKey: 'finance-kpi-pending-reconciliation',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const pendingCount = (data?.extratos || []).filter((statement) =>
|
|
42
|
+
normalize(statement?.statusConciliacao).includes('pend')
|
|
43
|
+
).length;
|
|
44
|
+
|
|
45
|
+
const activeAccounts = (data?.contasBancarias || []).filter(
|
|
46
|
+
(account) => account?.ativo !== false
|
|
47
|
+
).length;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<WidgetWrapper
|
|
51
|
+
isLoading={isLoading}
|
|
52
|
+
isAccessDenied={isAccessDenied}
|
|
53
|
+
isError={isError}
|
|
54
|
+
widgetName={widget?.name || t('kpis.pendingReconciliation.title')}
|
|
55
|
+
onRemove={onRemove}
|
|
56
|
+
>
|
|
57
|
+
<Card className="h-full overflow-hidden border-border/60 bg-linear-to-br from-background to-violet-50/50 shadow-sm transition-all duration-300 hover:-translate-y-0.5 hover:shadow-md">
|
|
58
|
+
<CardContent className="flex h-full items-center gap-2.5 p-3">
|
|
59
|
+
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-violet-100/80">
|
|
60
|
+
<ArrowLeftRight className="h-4.5 w-4.5 text-violet-600" />
|
|
61
|
+
</div>
|
|
62
|
+
<div className="flex min-w-0 flex-1 flex-col justify-center">
|
|
63
|
+
<span className="truncate text-[9px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
|
64
|
+
{t('kpis.pendingReconciliation.title')}
|
|
65
|
+
</span>
|
|
66
|
+
<div className="flex items-end gap-2">
|
|
67
|
+
<span className="truncate text-base font-bold leading-none tracking-tight text-foreground sm:text-lg">
|
|
68
|
+
{pendingCount}
|
|
69
|
+
</span>
|
|
70
|
+
<span className="mb-0.5 truncate text-[9px] text-muted-foreground">
|
|
71
|
+
{t('kpis.pendingReconciliation.countLabel')}
|
|
72
|
+
</span>
|
|
73
|
+
</div>
|
|
74
|
+
<span className="truncate text-[9px] text-muted-foreground">
|
|
75
|
+
{t('kpis.pendingReconciliation.description', {
|
|
76
|
+
value: activeAccounts,
|
|
77
|
+
})}
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
</CardContent>
|
|
81
|
+
</Card>
|
|
82
|
+
</WidgetWrapper>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -4,7 +4,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
|
|
4
4
|
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
5
5
|
import { TrendingUp } from 'lucide-react';
|
|
6
6
|
import { useTranslations } from 'next-intl';
|
|
7
|
-
import { WidgetWrapper } from '
|
|
7
|
+
import { WidgetWrapper } from '@/app/(app)/(libraries)/core/dashboard/components';
|
|
8
8
|
|
|
9
9
|
interface Receivable30dKpiProps {
|
|
10
10
|
widget?: { name?: string };
|
|
@@ -45,19 +45,19 @@ export default function Receivable30dKpi({
|
|
|
45
45
|
widgetName={widget?.name || t('kpis.receivable30.title')}
|
|
46
46
|
onRemove={onRemove}
|
|
47
47
|
>
|
|
48
|
-
<Card className="h-full overflow-hidden transition-all duration-300 hover:shadow-md">
|
|
49
|
-
<CardContent className="flex h-full items-center gap-
|
|
50
|
-
<div className="flex h-
|
|
51
|
-
<TrendingUp className="h-5 w-5 text-green-600" />
|
|
48
|
+
<Card className="h-full overflow-hidden border-border/60 bg-linear-to-br from-background to-green-50/50 shadow-sm transition-all duration-300 hover:-translate-y-0.5 hover:shadow-md">
|
|
49
|
+
<CardContent className="flex h-full items-center gap-2.5 p-3">
|
|
50
|
+
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-green-100/80">
|
|
51
|
+
<TrendingUp className="h-4.5 w-4.5 text-green-600" />
|
|
52
52
|
</div>
|
|
53
|
-
<div className="flex min-w-0 flex-col">
|
|
54
|
-
<span className="text-[
|
|
53
|
+
<div className="flex min-w-0 flex-1 flex-col justify-center">
|
|
54
|
+
<span className="text-[9px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
|
55
55
|
{t('kpis.receivable30.title')}
|
|
56
56
|
</span>
|
|
57
|
-
<span className="truncate text-
|
|
57
|
+
<span className="truncate text-base font-bold leading-none tracking-tight text-foreground sm:text-lg">
|
|
58
58
|
{formatted}
|
|
59
59
|
</span>
|
|
60
|
-
<span className="text-[
|
|
60
|
+
<span className="truncate text-[9px] text-muted-foreground">
|
|
61
61
|
{t('kpis.receivable30.sevenDays', {
|
|
62
62
|
value: new Intl.NumberFormat('pt-BR', {
|
|
63
63
|
style: 'currency',
|