@hed-hog/finance 0.0.279 → 0.0.286
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/finance-report-query.dto.d.ts +16 -0
- package/dist/dto/finance-report-query.dto.d.ts.map +1 -0
- package/dist/dto/finance-report-query.dto.js +59 -0
- package/dist/dto/finance-report-query.dto.js.map +1 -0
- package/dist/finance-reports.controller.d.ts +71 -0
- package/dist/finance-reports.controller.d.ts.map +1 -0
- package/dist/finance-reports.controller.js +61 -0
- package/dist/finance-reports.controller.js.map +1 -0
- package/dist/finance.module.d.ts.map +1 -1
- package/dist/finance.module.js +2 -0
- package/dist/finance.module.js.map +1 -1
- package/dist/finance.service.d.ts +93 -0
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +456 -0
- package/dist/finance.service.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/hedhog/data/route.yaml +27 -0
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +158 -125
- package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +102 -88
- package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +113 -89
- package/hedhog/frontend/app/reports/_lib/use-finance-reports.ts.ejs +238 -0
- package/hedhog/frontend/app/reports/overview-results/page.tsx.ejs +96 -78
- package/hedhog/frontend/app/reports/top-customers/page.tsx.ejs +239 -130
- package/hedhog/frontend/app/reports/top-operational-expenses/page.tsx.ejs +242 -135
- package/hedhog/frontend/messages/en.json +33 -2
- package/hedhog/frontend/messages/pt.json +33 -2
- package/package.json +7 -7
- package/src/dto/finance-report-query.dto.ts +49 -0
- package/src/finance-reports.controller.ts +28 -0
- package/src/finance.module.ts +2 -0
- package/src/finance.service.ts +645 -10
- package/src/index.ts +1 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Page, PageHeader } from '@/components/entity-list';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
4
5
|
import {
|
|
5
6
|
Card,
|
|
6
7
|
CardContent,
|
|
@@ -9,7 +10,6 @@ import {
|
|
|
9
10
|
CardTitle,
|
|
10
11
|
} from '@/components/ui/card';
|
|
11
12
|
import { Input } from '@/components/ui/input';
|
|
12
|
-
import { Label } from '@/components/ui/label';
|
|
13
13
|
import { Money } from '@/components/ui/money';
|
|
14
14
|
import {
|
|
15
15
|
Select,
|
|
@@ -28,11 +28,14 @@ import {
|
|
|
28
28
|
} from '@/components/ui/table';
|
|
29
29
|
import { Crown, PieChartIcon, Users } from 'lucide-react';
|
|
30
30
|
import { useTranslations } from 'next-intl';
|
|
31
|
-
import {
|
|
31
|
+
import { type FormEvent, useState } from 'react';
|
|
32
|
+
import type { TooltipProps } from 'recharts';
|
|
32
33
|
import {
|
|
33
34
|
Bar,
|
|
34
35
|
BarChart,
|
|
36
|
+
CartesianGrid,
|
|
35
37
|
Cell,
|
|
38
|
+
Legend,
|
|
36
39
|
Pie,
|
|
37
40
|
PieChart,
|
|
38
41
|
ResponsiveContainer,
|
|
@@ -41,43 +44,86 @@ import {
|
|
|
41
44
|
YAxis,
|
|
42
45
|
} from 'recharts';
|
|
43
46
|
import {
|
|
44
|
-
aggregateTopCustomers,
|
|
45
47
|
getDefaultDateRange,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
type GroupBy,
|
|
49
|
+
useTopCustomersReport,
|
|
50
|
+
} from '../_lib/use-finance-reports';
|
|
48
51
|
|
|
49
|
-
const
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
'#
|
|
56
|
-
'#
|
|
57
|
-
'#
|
|
58
|
-
'#
|
|
59
|
-
'#
|
|
52
|
+
const rankingColors = [
|
|
53
|
+
'#2563EB',
|
|
54
|
+
'#DC2626',
|
|
55
|
+
'#059669',
|
|
56
|
+
'#D97706',
|
|
57
|
+
'#7C3AED',
|
|
58
|
+
'#0F766E',
|
|
59
|
+
'#BE123C',
|
|
60
|
+
'#1D4ED8',
|
|
61
|
+
'#15803D',
|
|
62
|
+
'#B45309',
|
|
60
63
|
];
|
|
61
64
|
|
|
65
|
+
const currencyFormatter = new Intl.NumberFormat('pt-BR', {
|
|
66
|
+
style: 'currency',
|
|
67
|
+
currency: 'BRL',
|
|
68
|
+
});
|
|
69
|
+
|
|
62
70
|
export default function TopCustomersReportPage() {
|
|
63
71
|
const t = useTranslations('finance.TopCustomersReportPage');
|
|
64
72
|
const defaults = getDefaultDateRange();
|
|
65
|
-
const [
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
const [filters, setFilters] = useState({
|
|
74
|
+
from: defaults.from,
|
|
75
|
+
to: defaults.to,
|
|
76
|
+
groupBy: 'year' as GroupBy,
|
|
77
|
+
search: '',
|
|
78
|
+
});
|
|
79
|
+
const [appliedFilters, setAppliedFilters] = useState(filters);
|
|
80
|
+
const { data: viewData, isFetching } = useTopCustomersReport({
|
|
81
|
+
from: appliedFilters.from,
|
|
82
|
+
to: appliedFilters.to,
|
|
83
|
+
groupBy: appliedFilters.groupBy,
|
|
84
|
+
search: appliedFilters.search,
|
|
85
|
+
topN: 20,
|
|
86
|
+
});
|
|
68
87
|
|
|
69
|
-
const
|
|
70
|
-
()
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
to,
|
|
74
|
-
groupBy,
|
|
75
|
-
topN: 20,
|
|
76
|
-
}),
|
|
77
|
-
[from, to, groupBy]
|
|
78
|
-
);
|
|
88
|
+
const handleSubmitFilters = (event: FormEvent<HTMLFormElement>) => {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
setAppliedFilters(filters);
|
|
91
|
+
};
|
|
79
92
|
|
|
80
|
-
const
|
|
93
|
+
const renderSolidTooltip = ({
|
|
94
|
+
active,
|
|
95
|
+
payload,
|
|
96
|
+
label,
|
|
97
|
+
}: TooltipProps<string | number, string>) => {
|
|
98
|
+
if (!active || !payload || payload.length === 0) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div className="min-w-52 rounded-md border bg-popover px-3 py-2 shadow-xl">
|
|
104
|
+
<p className="mb-1 text-sm font-semibold text-popover-foreground">
|
|
105
|
+
{String(label ?? payload[0]?.name ?? '')}
|
|
106
|
+
</p>
|
|
107
|
+
{payload.map((row, index) => (
|
|
108
|
+
<div
|
|
109
|
+
key={`${String(row.dataKey ?? row.name ?? 'item')}-${index}`}
|
|
110
|
+
className="flex items-center justify-between gap-3 text-sm"
|
|
111
|
+
>
|
|
112
|
+
<div className="flex items-center gap-2 text-popover-foreground">
|
|
113
|
+
<span
|
|
114
|
+
className="inline-block h-2.5 w-2.5 rounded-full"
|
|
115
|
+
style={{ backgroundColor: row.color || 'hsl(var(--chart-1))' }}
|
|
116
|
+
/>
|
|
117
|
+
<span>{row.name ?? row.dataKey ?? 'Valor'}</span>
|
|
118
|
+
</div>
|
|
119
|
+
<span className="font-semibold text-popover-foreground">
|
|
120
|
+
{currencyFormatter.format(Number(row.value || 0))}
|
|
121
|
+
</span>
|
|
122
|
+
</div>
|
|
123
|
+
))}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
81
127
|
|
|
82
128
|
return (
|
|
83
129
|
<Page>
|
|
@@ -91,164 +137,225 @@ export default function TopCustomersReportPage() {
|
|
|
91
137
|
]}
|
|
92
138
|
/>
|
|
93
139
|
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
<form
|
|
141
|
+
onSubmit={handleSubmitFilters}
|
|
142
|
+
className="flex w-full flex-col gap-3 lg:flex-row"
|
|
143
|
+
>
|
|
144
|
+
<Input
|
|
145
|
+
type="search"
|
|
146
|
+
value={filters.search}
|
|
147
|
+
onChange={(event) =>
|
|
148
|
+
setFilters((current) => ({
|
|
149
|
+
...current,
|
|
150
|
+
search: event.target.value,
|
|
151
|
+
}))
|
|
152
|
+
}
|
|
153
|
+
placeholder={t('filters.searchPlaceholder')}
|
|
154
|
+
className="w-full lg:flex-[2.4]"
|
|
155
|
+
aria-label={t('filters.searchAria')}
|
|
156
|
+
/>
|
|
157
|
+
<Input
|
|
158
|
+
id="from"
|
|
159
|
+
type="date"
|
|
160
|
+
value={filters.from}
|
|
161
|
+
onChange={(event) =>
|
|
162
|
+
setFilters((current) => ({
|
|
163
|
+
...current,
|
|
164
|
+
from: event.target.value,
|
|
165
|
+
}))
|
|
166
|
+
}
|
|
167
|
+
max={filters.to}
|
|
168
|
+
className="w-full lg:w-[170px]"
|
|
169
|
+
aria-label={t('filters.fromAria')}
|
|
170
|
+
placeholder={t('filters.fromPlaceholder')}
|
|
171
|
+
/>
|
|
172
|
+
<Input
|
|
173
|
+
id="to"
|
|
174
|
+
type="date"
|
|
175
|
+
value={filters.to}
|
|
176
|
+
onChange={(event) =>
|
|
177
|
+
setFilters((current) => ({
|
|
178
|
+
...current,
|
|
179
|
+
to: event.target.value,
|
|
180
|
+
}))
|
|
181
|
+
}
|
|
182
|
+
min={filters.from}
|
|
183
|
+
className="w-full lg:w-[170px]"
|
|
184
|
+
aria-label={t('filters.toAria')}
|
|
185
|
+
placeholder={t('filters.toPlaceholder')}
|
|
186
|
+
/>
|
|
187
|
+
<Select
|
|
188
|
+
value={filters.groupBy}
|
|
189
|
+
onValueChange={(value) =>
|
|
190
|
+
setFilters((current) => ({
|
|
191
|
+
...current,
|
|
192
|
+
groupBy: value as GroupBy,
|
|
193
|
+
}))
|
|
194
|
+
}
|
|
195
|
+
>
|
|
196
|
+
<SelectTrigger
|
|
197
|
+
className="w-full lg:w-[150px]"
|
|
198
|
+
aria-label={t('filters.groupByAria')}
|
|
199
|
+
>
|
|
200
|
+
<SelectValue placeholder={t('filters.groupByPlaceholder')} />
|
|
201
|
+
</SelectTrigger>
|
|
202
|
+
<SelectContent>
|
|
203
|
+
<SelectItem value="day">{t('groupBy.day')}</SelectItem>
|
|
204
|
+
<SelectItem value="week">{t('groupBy.week')}</SelectItem>
|
|
205
|
+
<SelectItem value="month">{t('groupBy.month')}</SelectItem>
|
|
206
|
+
<SelectItem value="year">{t('groupBy.year')}</SelectItem>
|
|
207
|
+
</SelectContent>
|
|
208
|
+
</Select>
|
|
209
|
+
<Button
|
|
210
|
+
type="submit"
|
|
211
|
+
className="w-full lg:w-auto"
|
|
212
|
+
disabled={isFetching}
|
|
213
|
+
>
|
|
214
|
+
{t('filters.submit')}
|
|
215
|
+
</Button>
|
|
216
|
+
</form>
|
|
139
217
|
|
|
140
|
-
<div className="grid gap-
|
|
218
|
+
<div className="grid gap-3 md:grid-cols-3">
|
|
141
219
|
<Card>
|
|
142
|
-
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-
|
|
143
|
-
<CardTitle className="text-
|
|
220
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1 pt-3 px-4">
|
|
221
|
+
<CardTitle className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
|
144
222
|
{t('cards.total')}
|
|
145
223
|
</CardTitle>
|
|
146
|
-
<
|
|
224
|
+
<div className="rounded-full bg-blue-500/10 p-1.5">
|
|
225
|
+
<Users className="h-4 w-4 text-blue-600" />
|
|
226
|
+
</div>
|
|
147
227
|
</CardHeader>
|
|
148
|
-
<CardContent>
|
|
149
|
-
<div className="text-
|
|
150
|
-
<Money value={
|
|
228
|
+
<CardContent className="pb-3 pt-0 px-4">
|
|
229
|
+
<div className="text-xl font-bold">
|
|
230
|
+
<Money value={viewData.total} />
|
|
151
231
|
</div>
|
|
152
232
|
</CardContent>
|
|
153
233
|
</Card>
|
|
154
234
|
<Card>
|
|
155
|
-
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-
|
|
156
|
-
<CardTitle className="text-
|
|
235
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1 pt-3 px-4">
|
|
236
|
+
<CardTitle className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
|
157
237
|
{t('cards.top5Concentration')}
|
|
158
238
|
</CardTitle>
|
|
159
|
-
<
|
|
239
|
+
<div className="rounded-full bg-purple-500/10 p-1.5">
|
|
240
|
+
<PieChartIcon className="h-4 w-4 text-purple-600" />
|
|
241
|
+
</div>
|
|
160
242
|
</CardHeader>
|
|
161
|
-
<CardContent>
|
|
162
|
-
<div className="text-
|
|
163
|
-
{
|
|
243
|
+
<CardContent className="pb-3 pt-0 px-4">
|
|
244
|
+
<div className="text-xl font-bold">
|
|
245
|
+
{viewData.top5Percent.toFixed(1)}%
|
|
164
246
|
</div>
|
|
165
247
|
</CardContent>
|
|
166
248
|
</Card>
|
|
167
249
|
<Card>
|
|
168
|
-
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-
|
|
169
|
-
<CardTitle className="text-
|
|
250
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1 pt-3 px-4">
|
|
251
|
+
<CardTitle className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
|
170
252
|
{t('cards.leader')}
|
|
171
253
|
</CardTitle>
|
|
172
|
-
<
|
|
254
|
+
<div className="rounded-full bg-amber-500/10 p-1.5">
|
|
255
|
+
<Crown className="h-4 w-4 text-amber-500" />
|
|
256
|
+
</div>
|
|
173
257
|
</CardHeader>
|
|
174
|
-
<CardContent>
|
|
175
|
-
<div className="text-
|
|
176
|
-
{leader?.customer || '-'}
|
|
258
|
+
<CardContent className="pb-3 pt-0 px-4">
|
|
259
|
+
<div className="text-sm font-medium">
|
|
260
|
+
{viewData.leader?.customer || '-'}
|
|
177
261
|
</div>
|
|
178
|
-
<div className="text-muted-foreground
|
|
179
|
-
{leader ? <Money value={leader.value} /> : '-'}
|
|
262
|
+
<div className="text-xs text-muted-foreground">
|
|
263
|
+
{viewData.leader ? <Money value={viewData.leader.value} /> : '-'}
|
|
180
264
|
</div>
|
|
181
265
|
</CardContent>
|
|
182
266
|
</Card>
|
|
183
267
|
</div>
|
|
184
268
|
|
|
185
|
-
<div className="grid gap-
|
|
269
|
+
<div className="grid gap-3 xl:grid-cols-5">
|
|
186
270
|
<Card className="xl:col-span-3">
|
|
187
|
-
<CardHeader>
|
|
271
|
+
<CardHeader className="pb-3">
|
|
188
272
|
<CardTitle>{t('bars.title')}</CardTitle>
|
|
189
273
|
<CardDescription>{t('bars.description')}</CardDescription>
|
|
190
274
|
</CardHeader>
|
|
191
|
-
<CardContent>
|
|
192
|
-
<ResponsiveContainer width="100%" height={
|
|
275
|
+
<CardContent className="pb-4 pt-0">
|
|
276
|
+
<ResponsiveContainer width="100%" height={360}>
|
|
193
277
|
<BarChart
|
|
194
|
-
data={
|
|
278
|
+
data={viewData.topCustomers}
|
|
195
279
|
layout="vertical"
|
|
196
280
|
margin={{ left: 60, right: 16 }}
|
|
197
281
|
>
|
|
282
|
+
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
|
198
283
|
<XAxis
|
|
199
284
|
type="number"
|
|
200
285
|
tickFormatter={(value) => `${(value / 1000).toFixed(0)}k`}
|
|
201
286
|
/>
|
|
202
287
|
<YAxis dataKey="customer" type="category" width={190} />
|
|
203
288
|
<Tooltip
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
style: 'currency',
|
|
207
|
-
currency: 'BRL',
|
|
208
|
-
}).format(value)
|
|
209
|
-
}
|
|
210
|
-
/>
|
|
211
|
-
<Bar
|
|
212
|
-
dataKey="value"
|
|
213
|
-
radius={[0, 6, 6, 0]}
|
|
214
|
-
fill="hsl(var(--chart-2))"
|
|
289
|
+
cursor={{ fill: 'hsl(var(--muted) / 0.35)' }}
|
|
290
|
+
content={renderSolidTooltip}
|
|
215
291
|
/>
|
|
292
|
+
<Bar dataKey="value" radius={[0, 6, 6, 0]}>
|
|
293
|
+
{viewData.topCustomers.map((entry, index) => (
|
|
294
|
+
<Cell
|
|
295
|
+
key={entry.customer}
|
|
296
|
+
fill={rankingColors[index % rankingColors.length]}
|
|
297
|
+
stroke="hsl(var(--background))"
|
|
298
|
+
strokeWidth={1}
|
|
299
|
+
/>
|
|
300
|
+
))}
|
|
301
|
+
</Bar>
|
|
216
302
|
</BarChart>
|
|
217
303
|
</ResponsiveContainer>
|
|
304
|
+
<div className="mt-4 grid grid-cols-2 gap-2 text-sm sm:grid-cols-3">
|
|
305
|
+
{viewData.topCustomers.slice(0, 6).map((item, index) => (
|
|
306
|
+
<div key={item.customer} className="flex items-center gap-2">
|
|
307
|
+
<span
|
|
308
|
+
className="inline-block h-2.5 w-2.5 rounded-full"
|
|
309
|
+
style={{
|
|
310
|
+
backgroundColor:
|
|
311
|
+
rankingColors[index % rankingColors.length],
|
|
312
|
+
}}
|
|
313
|
+
/>
|
|
314
|
+
<span className="truncate text-foreground/90">
|
|
315
|
+
#{index + 1} {item.customer}
|
|
316
|
+
</span>
|
|
317
|
+
</div>
|
|
318
|
+
))}
|
|
319
|
+
</div>
|
|
218
320
|
</CardContent>
|
|
219
321
|
</Card>
|
|
220
322
|
|
|
221
323
|
<Card className="xl:col-span-2">
|
|
222
|
-
<CardHeader>
|
|
324
|
+
<CardHeader className="pb-3">
|
|
223
325
|
<CardTitle>{t('pie.title')}</CardTitle>
|
|
224
326
|
<CardDescription>{t('pie.description')}</CardDescription>
|
|
225
327
|
</CardHeader>
|
|
226
|
-
<CardContent>
|
|
227
|
-
<ResponsiveContainer width="100%" height={
|
|
328
|
+
<CardContent className="pb-4 pt-0">
|
|
329
|
+
<ResponsiveContainer width="100%" height={340}>
|
|
228
330
|
<PieChart>
|
|
229
331
|
<Pie
|
|
230
|
-
data={
|
|
332
|
+
data={viewData.pieData}
|
|
231
333
|
dataKey="value"
|
|
232
334
|
nameKey="customer"
|
|
233
|
-
innerRadius={
|
|
234
|
-
outerRadius={
|
|
335
|
+
innerRadius={55}
|
|
336
|
+
outerRadius={100}
|
|
235
337
|
paddingAngle={2}
|
|
338
|
+
cy="42%"
|
|
236
339
|
>
|
|
237
|
-
{
|
|
340
|
+
{viewData.pieData.map((entry, index) => (
|
|
238
341
|
<Cell
|
|
239
342
|
key={entry.customer}
|
|
240
|
-
fill={
|
|
343
|
+
fill={rankingColors[index % rankingColors.length]}
|
|
344
|
+
stroke="hsl(var(--background))"
|
|
345
|
+
strokeWidth={1}
|
|
241
346
|
/>
|
|
242
347
|
))}
|
|
243
348
|
</Pie>
|
|
244
|
-
<
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
349
|
+
<Legend
|
|
350
|
+
iconType="circle"
|
|
351
|
+
verticalAlign="bottom"
|
|
352
|
+
wrapperStyle={{
|
|
353
|
+
fontSize: 12,
|
|
354
|
+
lineHeight: '20px',
|
|
355
|
+
paddingTop: 12,
|
|
356
|
+
}}
|
|
251
357
|
/>
|
|
358
|
+
<Tooltip cursor={false} content={renderSolidTooltip} />
|
|
252
359
|
</PieChart>
|
|
253
360
|
</ResponsiveContainer>
|
|
254
361
|
</CardContent>
|
|
@@ -274,7 +381,7 @@ export default function TopCustomersReportPage() {
|
|
|
274
381
|
</TableRow>
|
|
275
382
|
</TableHeader>
|
|
276
383
|
<TableBody>
|
|
277
|
-
{
|
|
384
|
+
{viewData.topCustomers.length === 0 ? (
|
|
278
385
|
<TableRow>
|
|
279
386
|
<TableCell
|
|
280
387
|
colSpan={3}
|
|
@@ -284,9 +391,11 @@ export default function TopCustomersReportPage() {
|
|
|
284
391
|
</TableCell>
|
|
285
392
|
</TableRow>
|
|
286
393
|
) : (
|
|
287
|
-
|
|
394
|
+
viewData.topCustomers.map((item) => {
|
|
288
395
|
const participation =
|
|
289
|
-
|
|
396
|
+
viewData.total > 0
|
|
397
|
+
? (item.value / viewData.total) * 100
|
|
398
|
+
: 0;
|
|
290
399
|
|
|
291
400
|
return (
|
|
292
401
|
<TableRow key={item.customer}>
|