@hed-hog/finance 0.0.298 → 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 +142 -0
- package/hedhog/data/dashboard_component_role.yaml +78 -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/public/dashboard-previews/cash-balance-kpi.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/cash-flow-chart.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/default-kpi.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/financial-alerts.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/payable-30d-kpi.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/receivable-30d-kpi.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/upcoming-payable.png +0 -0
- package/hedhog/frontend/public/dashboard-previews/upcoming-receivable.png +0 -0
- package/hedhog/frontend/widgets/alerts.tsx.ejs +108 -0
- package/hedhog/frontend/widgets/bank-reconciliation-status.tsx.ejs +142 -0
- package/hedhog/frontend/widgets/cash-balance-kpi.tsx.ejs +66 -0
- package/hedhog/frontend/widgets/cash-flow-chart.tsx.ejs +122 -0
- package/hedhog/frontend/widgets/default-kpi.tsx.ejs +63 -0
- package/hedhog/frontend/widgets/payable-30d-kpi.tsx.ejs +73 -0
- 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 +73 -0
- package/hedhog/frontend/widgets/receivable-aging-analysis.tsx.ejs +163 -0
- package/hedhog/frontend/widgets/upcoming-payable.tsx.ejs +123 -0
- package/hedhog/frontend/widgets/upcoming-receivable.tsx.ejs +118 -0
- 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,108 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Badge } from '@/components/ui/badge';
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
5
|
+
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
6
|
+
import { AlertTriangle } from 'lucide-react';
|
|
7
|
+
import { useLocale, useTranslations } from 'next-intl';
|
|
8
|
+
import { WidgetWrapper } from '@/app/(app)/(libraries)/core/dashboard/components';
|
|
9
|
+
|
|
10
|
+
interface FinanceData {
|
|
11
|
+
titulosPagar?: Array<{
|
|
12
|
+
status: string;
|
|
13
|
+
parcelas: Array<{ status: string }>;
|
|
14
|
+
}>;
|
|
15
|
+
extratos?: Array<{
|
|
16
|
+
statusConciliacao: string;
|
|
17
|
+
}>;
|
|
18
|
+
periodoAberto?: {
|
|
19
|
+
inicio?: string | null;
|
|
20
|
+
} | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface AlertsProps {
|
|
24
|
+
widget?: { name?: string };
|
|
25
|
+
onRemove?: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function Alerts({ widget, onRemove }: AlertsProps) {
|
|
29
|
+
const t = useTranslations('finance.DashboardPage');
|
|
30
|
+
const locale = useLocale();
|
|
31
|
+
|
|
32
|
+
const { data, isLoading, isAccessDenied, isError } =
|
|
33
|
+
useWidgetData<FinanceData>({
|
|
34
|
+
endpoint: '/finance/data',
|
|
35
|
+
queryKey: 'finance-alerts',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const approvedPayables = (data?.titulosPagar || []).filter(
|
|
39
|
+
(titulo) => titulo.status !== 'rascunho' && titulo.status !== 'cancelado'
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const overdue = approvedPayables.filter((titulo) =>
|
|
43
|
+
titulo.parcelas.some((p) => p.status === 'vencido')
|
|
44
|
+
).length;
|
|
45
|
+
|
|
46
|
+
const pendingReconciliation = (data?.extratos || []).filter(
|
|
47
|
+
(e) => e.statusConciliacao === 'pendente'
|
|
48
|
+
).length;
|
|
49
|
+
|
|
50
|
+
const periodBase = data?.periodoAberto?.inicio
|
|
51
|
+
? new Date(data.periodoAberto.inicio)
|
|
52
|
+
: new Date();
|
|
53
|
+
const month = new Intl.DateTimeFormat(locale, { month: 'long' }).format(
|
|
54
|
+
periodBase
|
|
55
|
+
);
|
|
56
|
+
const currentPeriod = `${month.charAt(0).toUpperCase()}${month.slice(1)}/${periodBase.getFullYear()}`;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<WidgetWrapper
|
|
60
|
+
isLoading={isLoading}
|
|
61
|
+
isAccessDenied={isAccessDenied}
|
|
62
|
+
isError={isError}
|
|
63
|
+
widgetName={widget?.name || t('alerts.title')}
|
|
64
|
+
onRemove={onRemove}
|
|
65
|
+
>
|
|
66
|
+
<Card className="h-full">
|
|
67
|
+
<CardHeader>
|
|
68
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
69
|
+
<AlertTriangle className="h-4 w-4 text-yellow-500" />
|
|
70
|
+
{t('alerts.title')}
|
|
71
|
+
</CardTitle>
|
|
72
|
+
</CardHeader>
|
|
73
|
+
<CardContent>
|
|
74
|
+
<div className="space-y-3">
|
|
75
|
+
{overdue > 0 && (
|
|
76
|
+
<div className="flex items-center justify-between rounded-lg bg-red-50 p-3">
|
|
77
|
+
<span className="text-sm">{t('alerts.overdueTitles')}</span>
|
|
78
|
+
<Badge variant="destructive">{overdue}</Badge>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
{pendingReconciliation > 0 && (
|
|
82
|
+
<div className="flex items-center justify-between rounded-lg bg-yellow-50 p-3">
|
|
83
|
+
<span className="text-sm">
|
|
84
|
+
{t('alerts.pendingReconciliation')}
|
|
85
|
+
</span>
|
|
86
|
+
<Badge
|
|
87
|
+
variant="outline"
|
|
88
|
+
className="border-yellow-500 text-yellow-700"
|
|
89
|
+
>
|
|
90
|
+
{pendingReconciliation}
|
|
91
|
+
</Badge>
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
<div className="flex items-center justify-between rounded-lg bg-blue-50 p-3">
|
|
95
|
+
<span className="text-sm">{t('alerts.openPeriod')}</span>
|
|
96
|
+
<Badge
|
|
97
|
+
variant="outline"
|
|
98
|
+
className="border-blue-500 text-blue-700"
|
|
99
|
+
>
|
|
100
|
+
{currentPeriod}
|
|
101
|
+
</Badge>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</CardContent>
|
|
105
|
+
</Card>
|
|
106
|
+
</WidgetWrapper>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
4
|
+
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
5
|
+
import { Wallet } from 'lucide-react';
|
|
6
|
+
import { useTranslations } from 'next-intl';
|
|
7
|
+
import { WidgetWrapper } from '@/app/(app)/(libraries)/core/dashboard/components';
|
|
8
|
+
|
|
9
|
+
interface CashBalanceKpiProps {
|
|
10
|
+
widget?: { name?: string };
|
|
11
|
+
onRemove?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface FinanceData {
|
|
15
|
+
kpis?: {
|
|
16
|
+
saldoCaixa: number;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function CashBalanceKpi({
|
|
21
|
+
widget,
|
|
22
|
+
onRemove,
|
|
23
|
+
}: CashBalanceKpiProps) {
|
|
24
|
+
const t = useTranslations('finance.DashboardPage');
|
|
25
|
+
|
|
26
|
+
const { data, isLoading, isAccessDenied, isError } =
|
|
27
|
+
useWidgetData<FinanceData>({
|
|
28
|
+
endpoint: '/finance/data',
|
|
29
|
+
queryKey: 'finance-kpi-cash-balance',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const value = data?.kpis?.saldoCaixa ?? 0;
|
|
33
|
+
const formatted = new Intl.NumberFormat('pt-BR', {
|
|
34
|
+
style: 'currency',
|
|
35
|
+
currency: 'BRL',
|
|
36
|
+
}).format(value);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<WidgetWrapper
|
|
40
|
+
isLoading={isLoading}
|
|
41
|
+
isAccessDenied={isAccessDenied}
|
|
42
|
+
isError={isError}
|
|
43
|
+
widgetName={widget?.name || t('kpis.cashBalance.title')}
|
|
44
|
+
onRemove={onRemove}
|
|
45
|
+
>
|
|
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
|
+
</div>
|
|
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
|
+
{t('kpis.cashBalance.title')}
|
|
54
|
+
</span>
|
|
55
|
+
<span className="truncate text-base font-bold leading-none tracking-tight text-foreground sm:text-lg">
|
|
56
|
+
{formatted}
|
|
57
|
+
</span>
|
|
58
|
+
<span className="truncate text-[9px] text-muted-foreground">
|
|
59
|
+
{t('kpis.cashBalance.description')}
|
|
60
|
+
</span>
|
|
61
|
+
</div>
|
|
62
|
+
</CardContent>
|
|
63
|
+
</Card>
|
|
64
|
+
</WidgetWrapper>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardDescription,
|
|
7
|
+
CardHeader,
|
|
8
|
+
CardTitle,
|
|
9
|
+
} from '@/components/ui/card';
|
|
10
|
+
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
11
|
+
import { useTranslations } from 'next-intl';
|
|
12
|
+
import {
|
|
13
|
+
CartesianGrid,
|
|
14
|
+
Legend,
|
|
15
|
+
Line,
|
|
16
|
+
LineChart,
|
|
17
|
+
ResponsiveContainer,
|
|
18
|
+
Tooltip,
|
|
19
|
+
XAxis,
|
|
20
|
+
YAxis,
|
|
21
|
+
} from 'recharts';
|
|
22
|
+
import { WidgetWrapper } from '@/app/(app)/(libraries)/core/dashboard/components';
|
|
23
|
+
|
|
24
|
+
interface CashFlowPoint {
|
|
25
|
+
data: string;
|
|
26
|
+
saldoPrevisto: number;
|
|
27
|
+
saldoRealizado: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface FinanceData {
|
|
31
|
+
fluxoCaixaPrevisto?: CashFlowPoint[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface CashFlowChartProps {
|
|
35
|
+
widget?: { name?: string };
|
|
36
|
+
onRemove?: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default function CashFlowChart({
|
|
40
|
+
widget,
|
|
41
|
+
onRemove,
|
|
42
|
+
}: CashFlowChartProps) {
|
|
43
|
+
const t = useTranslations('finance.DashboardPage');
|
|
44
|
+
|
|
45
|
+
const { data, isLoading, isAccessDenied, isError } =
|
|
46
|
+
useWidgetData<FinanceData>({
|
|
47
|
+
endpoint: '/finance/data',
|
|
48
|
+
queryKey: 'finance-cash-flow-chart',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const chartData = (data?.fluxoCaixaPrevisto || []).map((item) => ({
|
|
52
|
+
...item,
|
|
53
|
+
data: new Date(item.data).toLocaleDateString('pt-BR', {
|
|
54
|
+
day: '2-digit',
|
|
55
|
+
month: '2-digit',
|
|
56
|
+
}),
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<WidgetWrapper
|
|
61
|
+
isLoading={isLoading}
|
|
62
|
+
isAccessDenied={isAccessDenied}
|
|
63
|
+
isError={isError}
|
|
64
|
+
widgetName={widget?.name || t('cashFlow.title')}
|
|
65
|
+
onRemove={onRemove}
|
|
66
|
+
>
|
|
67
|
+
<Card className="h-full flex flex-col">
|
|
68
|
+
<CardHeader className="pb-2">
|
|
69
|
+
<CardTitle className="text-base">{t('cashFlow.title')}</CardTitle>
|
|
70
|
+
<CardDescription>{t('cashFlow.description')}</CardDescription>
|
|
71
|
+
</CardHeader>
|
|
72
|
+
<CardContent className="flex-1 pt-0">
|
|
73
|
+
<div className="h-[280px] w-full">
|
|
74
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
75
|
+
<LineChart data={chartData}>
|
|
76
|
+
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
|
77
|
+
<XAxis
|
|
78
|
+
dataKey="data"
|
|
79
|
+
tick={{ fontSize: 12 }}
|
|
80
|
+
tickLine={false}
|
|
81
|
+
axisLine={false}
|
|
82
|
+
/>
|
|
83
|
+
<YAxis
|
|
84
|
+
tick={{ fontSize: 12 }}
|
|
85
|
+
tickLine={false}
|
|
86
|
+
axisLine={false}
|
|
87
|
+
tickFormatter={(value) => `${(value / 1000).toFixed(0)}k`}
|
|
88
|
+
/>
|
|
89
|
+
<Tooltip
|
|
90
|
+
formatter={(value: number) =>
|
|
91
|
+
new Intl.NumberFormat('pt-BR', {
|
|
92
|
+
style: 'currency',
|
|
93
|
+
currency: 'BRL',
|
|
94
|
+
}).format(value)
|
|
95
|
+
}
|
|
96
|
+
/>
|
|
97
|
+
<Legend />
|
|
98
|
+
<Line
|
|
99
|
+
type="monotone"
|
|
100
|
+
dataKey="saldoPrevisto"
|
|
101
|
+
name={t('chart.predicted')}
|
|
102
|
+
stroke="hsl(var(--primary))"
|
|
103
|
+
strokeWidth={2}
|
|
104
|
+
dot={false}
|
|
105
|
+
/>
|
|
106
|
+
<Line
|
|
107
|
+
type="monotone"
|
|
108
|
+
dataKey="saldoRealizado"
|
|
109
|
+
name={t('chart.actual')}
|
|
110
|
+
stroke="hsl(var(--chart-2))"
|
|
111
|
+
strokeWidth={2}
|
|
112
|
+
dot={false}
|
|
113
|
+
connectNulls
|
|
114
|
+
/>
|
|
115
|
+
</LineChart>
|
|
116
|
+
</ResponsiveContainer>
|
|
117
|
+
</div>
|
|
118
|
+
</CardContent>
|
|
119
|
+
</Card>
|
|
120
|
+
</WidgetWrapper>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
4
|
+
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
5
|
+
import { AlertTriangle } from 'lucide-react';
|
|
6
|
+
import { useTranslations } from 'next-intl';
|
|
7
|
+
import { WidgetWrapper } from '@/app/(app)/(libraries)/core/dashboard/components';
|
|
8
|
+
|
|
9
|
+
interface DefaultKpiProps {
|
|
10
|
+
widget?: { name?: string };
|
|
11
|
+
onRemove?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface FinanceData {
|
|
15
|
+
kpis?: {
|
|
16
|
+
inadimplencia: number;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function DefaultKpi({ widget, onRemove }: DefaultKpiProps) {
|
|
21
|
+
const t = useTranslations('finance.DashboardPage');
|
|
22
|
+
|
|
23
|
+
const { data, isLoading, isAccessDenied, isError } =
|
|
24
|
+
useWidgetData<FinanceData>({
|
|
25
|
+
endpoint: '/finance/data',
|
|
26
|
+
queryKey: 'finance-kpi-default',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const value = data?.kpis?.inadimplencia ?? 0;
|
|
30
|
+
const formatted = new Intl.NumberFormat('pt-BR', {
|
|
31
|
+
style: 'currency',
|
|
32
|
+
currency: 'BRL',
|
|
33
|
+
}).format(value);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<WidgetWrapper
|
|
37
|
+
isLoading={isLoading}
|
|
38
|
+
isAccessDenied={isAccessDenied}
|
|
39
|
+
isError={isError}
|
|
40
|
+
widgetName={widget?.name || t('kpis.default.title')}
|
|
41
|
+
onRemove={onRemove}
|
|
42
|
+
>
|
|
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
|
+
</div>
|
|
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
|
+
{t('kpis.default.title')}
|
|
51
|
+
</span>
|
|
52
|
+
<span className="truncate text-base font-bold leading-none tracking-tight text-foreground sm:text-lg">
|
|
53
|
+
{formatted}
|
|
54
|
+
</span>
|
|
55
|
+
<span className="truncate text-[9px] text-muted-foreground">
|
|
56
|
+
{t('kpis.default.description')}
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
</CardContent>
|
|
60
|
+
</Card>
|
|
61
|
+
</WidgetWrapper>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
4
|
+
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
5
|
+
import { TrendingDown } from 'lucide-react';
|
|
6
|
+
import { useTranslations } from 'next-intl';
|
|
7
|
+
import { WidgetWrapper } from '@/app/(app)/(libraries)/core/dashboard/components';
|
|
8
|
+
|
|
9
|
+
interface Payable30dKpiProps {
|
|
10
|
+
widget?: { name?: string };
|
|
11
|
+
onRemove?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface FinanceData {
|
|
15
|
+
kpis?: {
|
|
16
|
+
aPagar30dias: number;
|
|
17
|
+
aPagar7dias: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function Payable30dKpi({
|
|
22
|
+
widget,
|
|
23
|
+
onRemove,
|
|
24
|
+
}: Payable30dKpiProps) {
|
|
25
|
+
const t = useTranslations('finance.DashboardPage');
|
|
26
|
+
|
|
27
|
+
const { data, isLoading, isAccessDenied, isError } =
|
|
28
|
+
useWidgetData<FinanceData>({
|
|
29
|
+
endpoint: '/finance/data',
|
|
30
|
+
queryKey: 'finance-kpi-payable-30d',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const value = data?.kpis?.aPagar30dias ?? 0;
|
|
34
|
+
const sevenDays = data?.kpis?.aPagar7dias ?? 0;
|
|
35
|
+
const formatted = new Intl.NumberFormat('pt-BR', {
|
|
36
|
+
style: 'currency',
|
|
37
|
+
currency: 'BRL',
|
|
38
|
+
}).format(value);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<WidgetWrapper
|
|
42
|
+
isLoading={isLoading}
|
|
43
|
+
isAccessDenied={isAccessDenied}
|
|
44
|
+
isError={isError}
|
|
45
|
+
widgetName={widget?.name || t('kpis.payable30.title')}
|
|
46
|
+
onRemove={onRemove}
|
|
47
|
+
>
|
|
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
|
+
</div>
|
|
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
|
+
{t('kpis.payable30.title')}
|
|
56
|
+
</span>
|
|
57
|
+
<span className="truncate text-base font-bold leading-none tracking-tight text-foreground sm:text-lg">
|
|
58
|
+
{formatted}
|
|
59
|
+
</span>
|
|
60
|
+
<span className="truncate text-[9px] text-muted-foreground">
|
|
61
|
+
{t('kpis.payable30.sevenDays', {
|
|
62
|
+
value: new Intl.NumberFormat('pt-BR', {
|
|
63
|
+
style: 'currency',
|
|
64
|
+
currency: 'BRL',
|
|
65
|
+
}).format(sevenDays),
|
|
66
|
+
})}
|
|
67
|
+
</span>
|
|
68
|
+
</div>
|
|
69
|
+
</CardContent>
|
|
70
|
+
</Card>
|
|
71
|
+
</WidgetWrapper>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -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
|
+
}
|