@hed-hog/finance 0.0.297 → 0.0.299
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/hedhog/data/dashboard_component.yaml +87 -0
- package/hedhog/data/dashboard_component_role.yaml +48 -0
- 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/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/receivable-30d-kpi.tsx.ejs +73 -0
- package/hedhog/frontend/widgets/upcoming-payable.tsx.ejs +123 -0
- package/hedhog/frontend/widgets/upcoming-receivable.tsx.ejs +118 -0
- package/package.json +7 -7
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
- slug: cash-balance-kpi
|
|
2
|
+
library_slug: finance
|
|
3
|
+
width: 2
|
|
4
|
+
height: 2
|
|
5
|
+
name:
|
|
6
|
+
en: Cash Balance
|
|
7
|
+
pt: Saldo em Caixa
|
|
8
|
+
description:
|
|
9
|
+
en: Shows the current company cash balance.
|
|
10
|
+
pt: Mostra o saldo atual de caixa da empresa.
|
|
11
|
+
|
|
12
|
+
- slug: payable-30d-kpi
|
|
13
|
+
library_slug: finance
|
|
14
|
+
width: 2
|
|
15
|
+
height: 2
|
|
16
|
+
name:
|
|
17
|
+
en: Payables 30 Days
|
|
18
|
+
pt: A Pagar 30 Dias
|
|
19
|
+
description:
|
|
20
|
+
en: Shows payables due in 30 days and the 7-day subtotal.
|
|
21
|
+
pt: Mostra contas a pagar em 30 dias e o subtotal de 7 dias.
|
|
22
|
+
|
|
23
|
+
- slug: receivable-30d-kpi
|
|
24
|
+
library_slug: finance
|
|
25
|
+
width: 2
|
|
26
|
+
height: 2
|
|
27
|
+
name:
|
|
28
|
+
en: Receivables 30 Days
|
|
29
|
+
pt: A Receber 30 Dias
|
|
30
|
+
description:
|
|
31
|
+
en: Shows receivables due in 30 days and the 7-day subtotal.
|
|
32
|
+
pt: Mostra contas a receber em 30 dias e o subtotal de 7 dias.
|
|
33
|
+
|
|
34
|
+
- slug: default-kpi
|
|
35
|
+
library_slug: finance
|
|
36
|
+
width: 2
|
|
37
|
+
height: 2
|
|
38
|
+
name:
|
|
39
|
+
en: Default Amount
|
|
40
|
+
pt: Inadimplencia
|
|
41
|
+
description:
|
|
42
|
+
en: Shows the current default amount.
|
|
43
|
+
pt: Mostra o valor atual de inadimplencia.
|
|
44
|
+
|
|
45
|
+
- slug: cash-flow-chart
|
|
46
|
+
library_slug: finance
|
|
47
|
+
width: 6
|
|
48
|
+
height: 5
|
|
49
|
+
name:
|
|
50
|
+
en: Cash Flow Chart
|
|
51
|
+
pt: Grafico de Fluxo de Caixa
|
|
52
|
+
description:
|
|
53
|
+
en: Compares predicted and realized cash flow.
|
|
54
|
+
pt: Compara o fluxo de caixa previsto e realizado.
|
|
55
|
+
|
|
56
|
+
- slug: financial-alerts
|
|
57
|
+
library_slug: finance
|
|
58
|
+
width: 3
|
|
59
|
+
height: 4
|
|
60
|
+
name:
|
|
61
|
+
en: Financial Alerts
|
|
62
|
+
pt: Alertas Financeiros
|
|
63
|
+
description:
|
|
64
|
+
en: Highlights overdue items, pending reconciliation and open period.
|
|
65
|
+
pt: Destaca titulos vencidos, conciliacao pendente e periodo aberto.
|
|
66
|
+
|
|
67
|
+
- slug: upcoming-payable
|
|
68
|
+
library_slug: finance
|
|
69
|
+
width: 3
|
|
70
|
+
height: 4
|
|
71
|
+
name:
|
|
72
|
+
en: Upcoming Payables
|
|
73
|
+
pt: Proximos A Pagar
|
|
74
|
+
description:
|
|
75
|
+
en: Lists upcoming payable due dates.
|
|
76
|
+
pt: Lista os proximos vencimentos a pagar.
|
|
77
|
+
|
|
78
|
+
- slug: upcoming-receivable
|
|
79
|
+
library_slug: finance
|
|
80
|
+
width: 3
|
|
81
|
+
height: 4
|
|
82
|
+
name:
|
|
83
|
+
en: Upcoming Receivables
|
|
84
|
+
pt: Proximos A Receber
|
|
85
|
+
description:
|
|
86
|
+
en: Lists upcoming receivable due dates.
|
|
87
|
+
pt: Lista os proximos vencimentos a receber.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
- component_id:
|
|
2
|
+
where:
|
|
3
|
+
slug: cash-balance-kpi
|
|
4
|
+
role_id:
|
|
5
|
+
where:
|
|
6
|
+
slug: admin-finance
|
|
7
|
+
- component_id:
|
|
8
|
+
where:
|
|
9
|
+
slug: payable-30d-kpi
|
|
10
|
+
role_id:
|
|
11
|
+
where:
|
|
12
|
+
slug: admin-finance
|
|
13
|
+
- component_id:
|
|
14
|
+
where:
|
|
15
|
+
slug: receivable-30d-kpi
|
|
16
|
+
role_id:
|
|
17
|
+
where:
|
|
18
|
+
slug: admin-finance
|
|
19
|
+
- component_id:
|
|
20
|
+
where:
|
|
21
|
+
slug: default-kpi
|
|
22
|
+
role_id:
|
|
23
|
+
where:
|
|
24
|
+
slug: admin-finance
|
|
25
|
+
- component_id:
|
|
26
|
+
where:
|
|
27
|
+
slug: cash-flow-chart
|
|
28
|
+
role_id:
|
|
29
|
+
where:
|
|
30
|
+
slug: admin-finance
|
|
31
|
+
- component_id:
|
|
32
|
+
where:
|
|
33
|
+
slug: financial-alerts
|
|
34
|
+
role_id:
|
|
35
|
+
where:
|
|
36
|
+
slug: admin-finance
|
|
37
|
+
- component_id:
|
|
38
|
+
where:
|
|
39
|
+
slug: upcoming-payable
|
|
40
|
+
role_id:
|
|
41
|
+
where:
|
|
42
|
+
slug: admin-finance
|
|
43
|
+
- component_id:
|
|
44
|
+
where:
|
|
45
|
+
slug: upcoming-receivable
|
|
46
|
+
role_id:
|
|
47
|
+
where:
|
|
48
|
+
slug: admin-finance
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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 '../widget-wrapper';
|
|
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,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 '../widget-wrapper';
|
|
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 transition-all duration-300 hover:shadow-md">
|
|
47
|
+
<CardContent className="flex h-full items-center gap-3 p-4">
|
|
48
|
+
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-emerald-50">
|
|
49
|
+
<Wallet className="h-5 w-5 text-emerald-600" />
|
|
50
|
+
</div>
|
|
51
|
+
<div className="flex min-w-0 flex-col">
|
|
52
|
+
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
53
|
+
{t('kpis.cashBalance.title')}
|
|
54
|
+
</span>
|
|
55
|
+
<span className="truncate text-lg font-bold tracking-tight text-foreground">
|
|
56
|
+
{formatted}
|
|
57
|
+
</span>
|
|
58
|
+
<span className="text-[10px] 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 '../widget-wrapper';
|
|
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 '../widget-wrapper';
|
|
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 transition-all duration-300 hover:shadow-md">
|
|
44
|
+
<CardContent className="flex h-full items-center gap-3 p-4">
|
|
45
|
+
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-amber-50">
|
|
46
|
+
<AlertTriangle className="h-5 w-5 text-amber-600" />
|
|
47
|
+
</div>
|
|
48
|
+
<div className="flex min-w-0 flex-col">
|
|
49
|
+
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
50
|
+
{t('kpis.default.title')}
|
|
51
|
+
</span>
|
|
52
|
+
<span className="truncate text-lg font-bold tracking-tight text-foreground">
|
|
53
|
+
{formatted}
|
|
54
|
+
</span>
|
|
55
|
+
<span className="text-[10px] 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 '../widget-wrapper';
|
|
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 transition-all duration-300 hover:shadow-md">
|
|
49
|
+
<CardContent className="flex h-full items-center gap-3 p-4">
|
|
50
|
+
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-red-50">
|
|
51
|
+
<TrendingDown className="h-5 w-5 text-red-600" />
|
|
52
|
+
</div>
|
|
53
|
+
<div className="flex min-w-0 flex-col">
|
|
54
|
+
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
55
|
+
{t('kpis.payable30.title')}
|
|
56
|
+
</span>
|
|
57
|
+
<span className="truncate text-lg font-bold tracking-tight text-foreground">
|
|
58
|
+
{formatted}
|
|
59
|
+
</span>
|
|
60
|
+
<span className="text-[10px] 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,73 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
4
|
+
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
5
|
+
import { TrendingUp } from 'lucide-react';
|
|
6
|
+
import { useTranslations } from 'next-intl';
|
|
7
|
+
import { WidgetWrapper } from '../widget-wrapper';
|
|
8
|
+
|
|
9
|
+
interface Receivable30dKpiProps {
|
|
10
|
+
widget?: { name?: string };
|
|
11
|
+
onRemove?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface FinanceData {
|
|
15
|
+
kpis?: {
|
|
16
|
+
aReceber30dias: number;
|
|
17
|
+
aReceber7dias: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function Receivable30dKpi({
|
|
22
|
+
widget,
|
|
23
|
+
onRemove,
|
|
24
|
+
}: Receivable30dKpiProps) {
|
|
25
|
+
const t = useTranslations('finance.DashboardPage');
|
|
26
|
+
|
|
27
|
+
const { data, isLoading, isAccessDenied, isError } =
|
|
28
|
+
useWidgetData<FinanceData>({
|
|
29
|
+
endpoint: '/finance/data',
|
|
30
|
+
queryKey: 'finance-kpi-receivable-30d',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const value = data?.kpis?.aReceber30dias ?? 0;
|
|
34
|
+
const sevenDays = data?.kpis?.aReceber7dias ?? 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.receivable30.title')}
|
|
46
|
+
onRemove={onRemove}
|
|
47
|
+
>
|
|
48
|
+
<Card className="h-full overflow-hidden transition-all duration-300 hover:shadow-md">
|
|
49
|
+
<CardContent className="flex h-full items-center gap-3 p-4">
|
|
50
|
+
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-green-50">
|
|
51
|
+
<TrendingUp className="h-5 w-5 text-green-600" />
|
|
52
|
+
</div>
|
|
53
|
+
<div className="flex min-w-0 flex-col">
|
|
54
|
+
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
55
|
+
{t('kpis.receivable30.title')}
|
|
56
|
+
</span>
|
|
57
|
+
<span className="truncate text-lg font-bold tracking-tight text-foreground">
|
|
58
|
+
{formatted}
|
|
59
|
+
</span>
|
|
60
|
+
<span className="text-[10px] text-muted-foreground">
|
|
61
|
+
{t('kpis.receivable30.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,123 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardDescription,
|
|
7
|
+
CardHeader,
|
|
8
|
+
CardTitle,
|
|
9
|
+
} from '@/components/ui/card';
|
|
10
|
+
import { Money } from '@/components/ui/money';
|
|
11
|
+
import { StatusBadge } from '@/components/ui/status-badge';
|
|
12
|
+
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
13
|
+
import { ArrowDownRight } from 'lucide-react';
|
|
14
|
+
import { useTranslations } from 'next-intl';
|
|
15
|
+
import Link from 'next/link';
|
|
16
|
+
import { WidgetWrapper } from '../widget-wrapper';
|
|
17
|
+
|
|
18
|
+
interface FinanceData {
|
|
19
|
+
titulosPagar?: Array<{
|
|
20
|
+
id: string;
|
|
21
|
+
status: string;
|
|
22
|
+
documento: string;
|
|
23
|
+
fornecedorId?: string;
|
|
24
|
+
parcelas: Array<{
|
|
25
|
+
status: string;
|
|
26
|
+
vencimento: string;
|
|
27
|
+
valor: number;
|
|
28
|
+
}>;
|
|
29
|
+
}>;
|
|
30
|
+
pessoas?: Array<{
|
|
31
|
+
id: string;
|
|
32
|
+
nome: string;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface UpcomingPayableProps {
|
|
37
|
+
widget?: { name?: string };
|
|
38
|
+
onRemove?: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatDate(value: string) {
|
|
42
|
+
return new Date(value).toLocaleDateString('pt-BR');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default function UpcomingPayable({
|
|
46
|
+
widget,
|
|
47
|
+
onRemove,
|
|
48
|
+
}: UpcomingPayableProps) {
|
|
49
|
+
const t = useTranslations('finance.DashboardPage');
|
|
50
|
+
|
|
51
|
+
const { data, isLoading, isAccessDenied, isError } =
|
|
52
|
+
useWidgetData<FinanceData>({
|
|
53
|
+
endpoint: '/finance/data',
|
|
54
|
+
queryKey: 'finance-upcoming-payable',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const getPersonById = (id?: string) =>
|
|
58
|
+
(data?.pessoas || []).find((p) => p.id === id);
|
|
59
|
+
|
|
60
|
+
const approvedPayables = (data?.titulosPagar || []).filter(
|
|
61
|
+
(titulo) => titulo.status !== 'rascunho' && titulo.status !== 'cancelado'
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const rows = approvedPayables
|
|
65
|
+
.flatMap((titulo) =>
|
|
66
|
+
titulo.parcelas
|
|
67
|
+
.filter((p) => p.status === 'aberto' || p.status === 'vencido')
|
|
68
|
+
.map((parcela) => ({
|
|
69
|
+
tituloId: titulo.id,
|
|
70
|
+
documento: titulo.documento,
|
|
71
|
+
person: getPersonById(titulo.fornecedorId)?.nome || '',
|
|
72
|
+
dueDate: parcela.vencimento,
|
|
73
|
+
value: parcela.valor,
|
|
74
|
+
status: parcela.status,
|
|
75
|
+
}))
|
|
76
|
+
)
|
|
77
|
+
.slice(0, 3);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<WidgetWrapper
|
|
81
|
+
isLoading={isLoading}
|
|
82
|
+
isAccessDenied={isAccessDenied}
|
|
83
|
+
isError={isError}
|
|
84
|
+
widgetName={widget?.name || t('upcoming.payable')}
|
|
85
|
+
onRemove={onRemove}
|
|
86
|
+
>
|
|
87
|
+
<Card className="h-full">
|
|
88
|
+
<CardHeader>
|
|
89
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
90
|
+
<ArrowDownRight className="h-4 w-4 text-red-500" />
|
|
91
|
+
{t('upcoming.payable')}
|
|
92
|
+
</CardTitle>
|
|
93
|
+
<CardDescription>{t('upcoming.nextDueDates')}</CardDescription>
|
|
94
|
+
</CardHeader>
|
|
95
|
+
<CardContent>
|
|
96
|
+
<div className="space-y-4">
|
|
97
|
+
{rows.map((item, index) => (
|
|
98
|
+
<Link
|
|
99
|
+
key={index}
|
|
100
|
+
href={`/finance/accounts-payable/installments/${item.tituloId}`}
|
|
101
|
+
className="-m-2 flex cursor-pointer items-center justify-between rounded-md p-2 transition-colors hover:bg-muted/50 active:bg-muted"
|
|
102
|
+
>
|
|
103
|
+
<div className="space-y-1">
|
|
104
|
+
<p className="text-sm font-medium">{item.documento}</p>
|
|
105
|
+
<p className="text-xs text-muted-foreground">{item.person}</p>
|
|
106
|
+
</div>
|
|
107
|
+
<div className="text-right">
|
|
108
|
+
<p className="text-sm font-medium">
|
|
109
|
+
<Money value={item.value} />
|
|
110
|
+
</p>
|
|
111
|
+
<p className="text-xs text-muted-foreground">
|
|
112
|
+
{formatDate(item.dueDate)}
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
<StatusBadge status={item.status as any} />
|
|
116
|
+
</Link>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
</CardContent>
|
|
120
|
+
</Card>
|
|
121
|
+
</WidgetWrapper>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardDescription,
|
|
7
|
+
CardHeader,
|
|
8
|
+
CardTitle,
|
|
9
|
+
} from '@/components/ui/card';
|
|
10
|
+
import { Money } from '@/components/ui/money';
|
|
11
|
+
import { StatusBadge } from '@/components/ui/status-badge';
|
|
12
|
+
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
13
|
+
import { ArrowUpRight } from 'lucide-react';
|
|
14
|
+
import { useTranslations } from 'next-intl';
|
|
15
|
+
import Link from 'next/link';
|
|
16
|
+
import { WidgetWrapper } from '../widget-wrapper';
|
|
17
|
+
|
|
18
|
+
interface FinanceData {
|
|
19
|
+
titulosReceber?: Array<{
|
|
20
|
+
id: string;
|
|
21
|
+
documento: string;
|
|
22
|
+
clienteId?: string;
|
|
23
|
+
parcelas: Array<{
|
|
24
|
+
status: string;
|
|
25
|
+
vencimento: string;
|
|
26
|
+
valor: number;
|
|
27
|
+
}>;
|
|
28
|
+
}>;
|
|
29
|
+
pessoas?: Array<{
|
|
30
|
+
id: string;
|
|
31
|
+
nome: string;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface UpcomingReceivableProps {
|
|
36
|
+
widget?: { name?: string };
|
|
37
|
+
onRemove?: () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatDate(value: string) {
|
|
41
|
+
return new Date(value).toLocaleDateString('pt-BR');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function UpcomingReceivable({
|
|
45
|
+
widget,
|
|
46
|
+
onRemove,
|
|
47
|
+
}: UpcomingReceivableProps) {
|
|
48
|
+
const t = useTranslations('finance.DashboardPage');
|
|
49
|
+
|
|
50
|
+
const { data, isLoading, isAccessDenied, isError } =
|
|
51
|
+
useWidgetData<FinanceData>({
|
|
52
|
+
endpoint: '/finance/data',
|
|
53
|
+
queryKey: 'finance-upcoming-receivable',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const getPersonById = (id?: string) =>
|
|
57
|
+
(data?.pessoas || []).find((p) => p.id === id);
|
|
58
|
+
|
|
59
|
+
const rows = (data?.titulosReceber || [])
|
|
60
|
+
.flatMap((titulo) =>
|
|
61
|
+
titulo.parcelas
|
|
62
|
+
.filter((p) => p.status === 'aberto' || p.status === 'vencido')
|
|
63
|
+
.map((parcela) => ({
|
|
64
|
+
tituloId: titulo.id,
|
|
65
|
+
documento: titulo.documento,
|
|
66
|
+
person: getPersonById(titulo.clienteId)?.nome || '',
|
|
67
|
+
dueDate: parcela.vencimento,
|
|
68
|
+
value: parcela.valor,
|
|
69
|
+
status: parcela.status,
|
|
70
|
+
}))
|
|
71
|
+
)
|
|
72
|
+
.slice(0, 3);
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<WidgetWrapper
|
|
76
|
+
isLoading={isLoading}
|
|
77
|
+
isAccessDenied={isAccessDenied}
|
|
78
|
+
isError={isError}
|
|
79
|
+
widgetName={widget?.name || t('upcoming.receivable')}
|
|
80
|
+
onRemove={onRemove}
|
|
81
|
+
>
|
|
82
|
+
<Card className="h-full">
|
|
83
|
+
<CardHeader>
|
|
84
|
+
<CardTitle className="flex items-center gap-2 text-base">
|
|
85
|
+
<ArrowUpRight className="h-4 w-4 text-green-500" />
|
|
86
|
+
{t('upcoming.receivable')}
|
|
87
|
+
</CardTitle>
|
|
88
|
+
<CardDescription>{t('upcoming.nextDueDates')}</CardDescription>
|
|
89
|
+
</CardHeader>
|
|
90
|
+
<CardContent>
|
|
91
|
+
<div className="space-y-4">
|
|
92
|
+
{rows.map((item, index) => (
|
|
93
|
+
<Link
|
|
94
|
+
key={index}
|
|
95
|
+
href={`/finance/accounts-receivable/installments/${item.tituloId}`}
|
|
96
|
+
className="-m-2 flex cursor-pointer items-center justify-between rounded-md p-2 transition-colors hover:bg-muted/50 active:bg-muted"
|
|
97
|
+
>
|
|
98
|
+
<div className="space-y-1">
|
|
99
|
+
<p className="text-sm font-medium">{item.documento}</p>
|
|
100
|
+
<p className="text-xs text-muted-foreground">{item.person}</p>
|
|
101
|
+
</div>
|
|
102
|
+
<div className="text-right">
|
|
103
|
+
<p className="text-sm font-medium">
|
|
104
|
+
<Money value={item.value} />
|
|
105
|
+
</p>
|
|
106
|
+
<p className="text-xs text-muted-foreground">
|
|
107
|
+
{formatDate(item.dueDate)}
|
|
108
|
+
</p>
|
|
109
|
+
</div>
|
|
110
|
+
<StatusBadge status={item.status as any} />
|
|
111
|
+
</Link>
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
114
|
+
</CardContent>
|
|
115
|
+
</Card>
|
|
116
|
+
</WidgetWrapper>
|
|
117
|
+
);
|
|
118
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/finance",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.299",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"dependencies": {
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
"@nestjs/jwt": "^11",
|
|
11
11
|
"@nestjs/mapped-types": "*",
|
|
12
12
|
"@hed-hog/api": "0.0.6",
|
|
13
|
-
"@hed-hog/contact": "0.0.297",
|
|
14
|
-
"@hed-hog/tag": "0.0.297",
|
|
15
|
-
"@hed-hog/api-prisma": "0.0.6",
|
|
16
13
|
"@hed-hog/api-locale": "0.0.14",
|
|
17
|
-
"@hed-hog/
|
|
18
|
-
"@hed-hog/
|
|
19
|
-
"@hed-hog/
|
|
14
|
+
"@hed-hog/api-pagination": "0.0.7",
|
|
15
|
+
"@hed-hog/tag": "0.0.299",
|
|
16
|
+
"@hed-hog/core": "0.0.299",
|
|
17
|
+
"@hed-hog/contact": "0.0.299",
|
|
18
|
+
"@hed-hog/api-prisma": "0.0.6",
|
|
19
|
+
"@hed-hog/api-types": "0.0.1"
|
|
20
20
|
},
|
|
21
21
|
"exports": {
|
|
22
22
|
".": {
|