@hed-hog/finance 0.0.299 → 0.0.301
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.contract-activated.subscriber.d.ts +24 -0
- package/dist/finance.contract-activated.subscriber.d.ts.map +1 -0
- package/dist/finance.contract-activated.subscriber.js +519 -0
- package/dist/finance.contract-activated.subscriber.js.map +1 -0
- package/dist/finance.contract-activated.subscriber.spec.d.ts +2 -0
- package/dist/finance.contract-activated.subscriber.spec.d.ts.map +1 -0
- package/dist/finance.contract-activated.subscriber.spec.js +302 -0
- package/dist/finance.contract-activated.subscriber.spec.js.map +1 -0
- package/dist/finance.module.d.ts.map +1 -1
- package/dist/finance.module.js +6 -1
- package/dist/finance.module.js.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/_components/finance-layout.tsx.ejs +108 -0
- package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +91 -106
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +1306 -1145
- package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +288 -268
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +491 -351
- package/hedhog/frontend/app/administration/audit-logs/page.tsx.ejs +157 -173
- package/hedhog/frontend/app/administration/categories/page.tsx.ejs +44 -62
- package/hedhog/frontend/app/administration/cost-centers/page.tsx.ejs +62 -80
- package/hedhog/frontend/app/administration/period-close/page.tsx.ejs +151 -170
- package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +586 -224
- package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +204 -226
- package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +122 -140
- package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +32 -49
- package/hedhog/frontend/app/planning/cash-flow-forecast/page.tsx.ejs +84 -108
- package/hedhog/frontend/app/planning/receivables-calendar/page.tsx.ejs +53 -70
- package/hedhog/frontend/app/planning/scenarios/page.tsx.ejs +98 -95
- package/hedhog/frontend/app/reports/actual-vs-forecast/page.tsx.ejs +100 -125
- package/hedhog/frontend/app/reports/aging-default/page.tsx.ejs +77 -105
- package/hedhog/frontend/app/reports/cash-position/page.tsx.ejs +99 -134
- package/hedhog/frontend/app/reports/overview-results/page.tsx.ejs +147 -182
- package/hedhog/frontend/app/reports/top-customers/page.tsx.ejs +49 -61
- package/hedhog/frontend/app/reports/top-operational-expenses/page.tsx.ejs +49 -67
- package/hedhog/frontend/messages/en.json +224 -70
- package/hedhog/frontend/messages/pt.json +224 -70
- 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 +7 -6
- package/src/dto/create-bank-account.dto.ts +7 -1
- package/src/dto/update-bank-account.dto.ts +7 -1
- package/src/finance.contract-activated.subscriber.spec.ts +392 -0
- package/src/finance.contract-activated.subscriber.ts +780 -0
- package/src/finance.module.ts +6 -1
- package/src/finance.service.ts +4 -0
|
@@ -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',
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { WidgetWrapper } from '@/app/(app)/(libraries)/core/dashboard/components';
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
} from '@/components/ui/card';
|
|
11
|
+
import { Money } from '@/components/ui/money';
|
|
12
|
+
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
13
|
+
import { BarChart3 } from 'lucide-react';
|
|
14
|
+
import { useTranslations } from 'next-intl';
|
|
15
|
+
|
|
16
|
+
interface AgingWidgetProps {
|
|
17
|
+
widget?: { name?: string };
|
|
18
|
+
onRemove?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface AgingItem {
|
|
22
|
+
bucket0_30?: number | null;
|
|
23
|
+
bucket31_60?: number | null;
|
|
24
|
+
bucket61_90?: number | null;
|
|
25
|
+
bucket90plus?: number | null;
|
|
26
|
+
total?: number | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface FinanceData {
|
|
30
|
+
agingInadimplencia?: AgingItem[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default function ReceivableAgingAnalysis({
|
|
34
|
+
widget,
|
|
35
|
+
onRemove,
|
|
36
|
+
}: AgingWidgetProps) {
|
|
37
|
+
const t = useTranslations('finance.DashboardPage');
|
|
38
|
+
|
|
39
|
+
const { data, isLoading, isAccessDenied, isError } =
|
|
40
|
+
useWidgetData<FinanceData>({
|
|
41
|
+
endpoint: '/finance/data',
|
|
42
|
+
queryKey: 'finance-receivable-aging-analysis',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const agingItems = data?.agingInadimplencia || [];
|
|
46
|
+
|
|
47
|
+
const totals = {
|
|
48
|
+
bucket0to30: agingItems.reduce(
|
|
49
|
+
(acc, item) => acc + Number(item?.bucket0_30 || 0),
|
|
50
|
+
0
|
|
51
|
+
),
|
|
52
|
+
bucket31to60: agingItems.reduce(
|
|
53
|
+
(acc, item) => acc + Number(item?.bucket31_60 || 0),
|
|
54
|
+
0
|
|
55
|
+
),
|
|
56
|
+
bucket61to90: agingItems.reduce(
|
|
57
|
+
(acc, item) => acc + Number(item?.bucket61_90 || 0),
|
|
58
|
+
0
|
|
59
|
+
),
|
|
60
|
+
bucket90plus: agingItems.reduce(
|
|
61
|
+
(acc, item) => acc + Number(item?.bucket90plus || 0),
|
|
62
|
+
0
|
|
63
|
+
),
|
|
64
|
+
total: agingItems.reduce((acc, item) => acc + Number(item?.total || 0), 0),
|
|
65
|
+
clients: agingItems.length,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const buckets = [
|
|
69
|
+
{
|
|
70
|
+
label: t('aging.range0to30'),
|
|
71
|
+
value: totals.bucket0to30,
|
|
72
|
+
colorClass: 'bg-amber-400',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: t('aging.range31to60'),
|
|
76
|
+
value: totals.bucket31to60,
|
|
77
|
+
colorClass: 'bg-orange-400',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
label: t('aging.range61to90'),
|
|
81
|
+
value: totals.bucket61to90,
|
|
82
|
+
colorClass: 'bg-rose-400',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
label: t('aging.range90plus'),
|
|
86
|
+
value: totals.bucket90plus,
|
|
87
|
+
colorClass: 'bg-red-500',
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<WidgetWrapper
|
|
93
|
+
isLoading={isLoading}
|
|
94
|
+
isAccessDenied={isAccessDenied}
|
|
95
|
+
isError={isError}
|
|
96
|
+
widgetName={widget?.name || t('aging.title')}
|
|
97
|
+
onRemove={onRemove}
|
|
98
|
+
>
|
|
99
|
+
<Card className="flex h-full flex-col overflow-hidden">
|
|
100
|
+
<CardHeader className="pb-1.5">
|
|
101
|
+
<CardTitle className="flex items-center gap-2 text-sm font-semibold">
|
|
102
|
+
<BarChart3 className="h-4 w-4 text-rose-500" />
|
|
103
|
+
{t('aging.title')}
|
|
104
|
+
</CardTitle>
|
|
105
|
+
<CardDescription className="line-clamp-1 text-xs">
|
|
106
|
+
{t('aging.description')}
|
|
107
|
+
</CardDescription>
|
|
108
|
+
</CardHeader>
|
|
109
|
+
<CardContent className="flex flex-1 flex-col gap-2 overflow-hidden pt-0">
|
|
110
|
+
<div className="grid grid-cols-2 gap-2 rounded-lg bg-muted/40 p-2.5">
|
|
111
|
+
<div>
|
|
112
|
+
<p className="text-[10px] uppercase tracking-[0.16em] text-muted-foreground">
|
|
113
|
+
{t('aging.totalLabel')}
|
|
114
|
+
</p>
|
|
115
|
+
<p className="text-sm font-semibold text-foreground sm:text-base">
|
|
116
|
+
<Money value={totals.total} />
|
|
117
|
+
</p>
|
|
118
|
+
</div>
|
|
119
|
+
<div>
|
|
120
|
+
<p className="text-[10px] uppercase tracking-[0.16em] text-muted-foreground">
|
|
121
|
+
{t('aging.clientsLabel')}
|
|
122
|
+
</p>
|
|
123
|
+
<p className="text-sm font-semibold text-foreground sm:text-base">
|
|
124
|
+
{totals.clients}
|
|
125
|
+
</p>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div className="grid min-h-0 grid-cols-2 gap-2">
|
|
130
|
+
{buckets.map((bucket) => {
|
|
131
|
+
const percentage =
|
|
132
|
+
totals.total > 0 ? (bucket.value / totals.total) * 100 : 0;
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
key={bucket.label}
|
|
137
|
+
className="rounded-md border border-border/60 bg-background/70 p-2"
|
|
138
|
+
>
|
|
139
|
+
<div className="mb-1 flex items-start justify-between gap-2">
|
|
140
|
+
<span className="text-[11px] leading-4 text-muted-foreground">
|
|
141
|
+
{bucket.label}
|
|
142
|
+
</span>
|
|
143
|
+
<span className="shrink-0 text-[11px] font-medium text-foreground">
|
|
144
|
+
<Money value={bucket.value} />
|
|
145
|
+
</span>
|
|
146
|
+
</div>
|
|
147
|
+
<div className="h-1.5 overflow-hidden rounded-full bg-muted">
|
|
148
|
+
<div
|
|
149
|
+
className={`h-full rounded-full ${bucket.colorClass}`}
|
|
150
|
+
style={{
|
|
151
|
+
width: `${Math.max(percentage, bucket.value > 0 ? 8 : 0)}%`,
|
|
152
|
+
}}
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
})}
|
|
158
|
+
</div>
|
|
159
|
+
</CardContent>
|
|
160
|
+
</Card>
|
|
161
|
+
</WidgetWrapper>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -13,7 +13,7 @@ import { useWidgetData } from '@/hooks/use-widget-data';
|
|
|
13
13
|
import { ArrowDownRight } from 'lucide-react';
|
|
14
14
|
import { useTranslations } from 'next-intl';
|
|
15
15
|
import Link from 'next/link';
|
|
16
|
-
import { WidgetWrapper } from '
|
|
16
|
+
import { WidgetWrapper } from '@/app/(app)/(libraries)/core/dashboard/components';
|
|
17
17
|
|
|
18
18
|
interface FinanceData {
|
|
19
19
|
titulosPagar?: Array<{
|
|
@@ -13,7 +13,7 @@ import { useWidgetData } from '@/hooks/use-widget-data';
|
|
|
13
13
|
import { ArrowUpRight } from 'lucide-react';
|
|
14
14
|
import { useTranslations } from 'next-intl';
|
|
15
15
|
import Link from 'next/link';
|
|
16
|
-
import { WidgetWrapper } from '
|
|
16
|
+
import { WidgetWrapper } from '@/app/(app)/(libraries)/core/dashboard/components';
|
|
17
17
|
|
|
18
18
|
interface FinanceData {
|
|
19
19
|
titulosReceber?: Array<{
|
|
@@ -25,6 +25,13 @@ columns:
|
|
|
25
25
|
- name: account_type
|
|
26
26
|
type: enum
|
|
27
27
|
values: [checking, savings, investment, cash, other]
|
|
28
|
+
- name: logo_file_id
|
|
29
|
+
type: fk
|
|
30
|
+
isNullable: true
|
|
31
|
+
references:
|
|
32
|
+
table: file
|
|
33
|
+
column: id
|
|
34
|
+
onDelete: SET NULL
|
|
28
35
|
- name: status
|
|
29
36
|
type: enum
|
|
30
37
|
values: [active, inactive]
|
|
@@ -34,4 +41,5 @@ columns:
|
|
|
34
41
|
indices:
|
|
35
42
|
- columns: [code]
|
|
36
43
|
isUnique: true
|
|
44
|
+
- columns: [logo_file_id]
|
|
37
45
|
- columns: [status]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/finance",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.301",
|
|
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/api-locale": "0.0.14",
|
|
14
13
|
"@hed-hog/api-pagination": "0.0.7",
|
|
15
|
-
"@hed-hog/
|
|
16
|
-
"@hed-hog/core": "0.0.
|
|
17
|
-
"@hed-hog/contact": "0.0.299",
|
|
14
|
+
"@hed-hog/api-locale": "0.0.14",
|
|
15
|
+
"@hed-hog/core": "0.0.301",
|
|
18
16
|
"@hed-hog/api-prisma": "0.0.6",
|
|
19
|
-
"@hed-hog/
|
|
17
|
+
"@hed-hog/tag": "0.0.301",
|
|
18
|
+
"@hed-hog/api-types": "0.0.1",
|
|
19
|
+
"@hed-hog/contact": "0.0.301"
|
|
20
20
|
},
|
|
21
21
|
"exports": {
|
|
22
22
|
".": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
],
|
|
33
33
|
"scripts": {
|
|
34
34
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
|
|
35
|
+
"test": "jest --config jest.config.ts --runInBand",
|
|
35
36
|
"prebuild": "pnpm --dir ../.. exec ts-node ./scripts/build-dependencies.ts libraries/finance",
|
|
36
37
|
"build": "tsc --project tsconfig.production.json",
|
|
37
38
|
"patch": "pnpm exec ts-node ../../scripts/patch.ts libraries/finance",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getLocaleText } from '@hed-hog/api-locale';
|
|
2
|
-
import { IsNumber, IsOptional, IsString, Min } from 'class-validator';
|
|
2
|
+
import { IsInt, IsNumber, IsOptional, IsString, Min } from 'class-validator';
|
|
3
3
|
|
|
4
4
|
export class CreateBankAccountDto {
|
|
5
5
|
@IsString({
|
|
@@ -34,6 +34,12 @@ export class CreateBankAccountDto {
|
|
|
34
34
|
})
|
|
35
35
|
description?: string;
|
|
36
36
|
|
|
37
|
+
@IsOptional()
|
|
38
|
+
@IsInt({
|
|
39
|
+
message: (args) => getLocaleText('validation.idMustBeInteger', args.value),
|
|
40
|
+
})
|
|
41
|
+
logo_file_id?: number | null;
|
|
42
|
+
|
|
37
43
|
@IsOptional()
|
|
38
44
|
@IsNumber(
|
|
39
45
|
{},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getLocaleText } from '@hed-hog/api-locale';
|
|
2
|
-
import { IsOptional, IsString } from 'class-validator';
|
|
2
|
+
import { IsInt, IsOptional, IsString } from 'class-validator';
|
|
3
3
|
|
|
4
4
|
export class UpdateBankAccountDto {
|
|
5
5
|
@IsOptional()
|
|
@@ -36,6 +36,12 @@ export class UpdateBankAccountDto {
|
|
|
36
36
|
})
|
|
37
37
|
description?: string;
|
|
38
38
|
|
|
39
|
+
@IsOptional()
|
|
40
|
+
@IsInt({
|
|
41
|
+
message: (args) => getLocaleText('validation.idMustBeInteger', args.value),
|
|
42
|
+
})
|
|
43
|
+
logo_file_id?: number | null;
|
|
44
|
+
|
|
39
45
|
@IsOptional()
|
|
40
46
|
@IsString({
|
|
41
47
|
message: (args) =>
|