@hed-hog/catalog 0.0.276
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/catalog-resource.config.d.ts +12 -0
- package/dist/catalog-resource.config.d.ts.map +1 -0
- package/dist/catalog-resource.config.js +209 -0
- package/dist/catalog-resource.config.js.map +1 -0
- package/dist/catalog.controller.d.ts +29 -0
- package/dist/catalog.controller.d.ts.map +1 -0
- package/dist/catalog.controller.js +117 -0
- package/dist/catalog.controller.js.map +1 -0
- package/dist/catalog.module.d.ts +3 -0
- package/dist/catalog.module.d.ts.map +1 -0
- package/dist/catalog.module.js +25 -0
- package/dist/catalog.module.js.map +1 -0
- package/dist/catalog.service.d.ts +36 -0
- package/dist/catalog.service.d.ts.map +1 -0
- package/dist/catalog.service.js +142 -0
- package/dist/catalog.service.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/hedhog/data/menu.yaml +233 -0
- package/hedhog/data/role.yaml +7 -0
- package/hedhog/data/route.yaml +56 -0
- package/hedhog/frontend/app/[resource]/page.tsx.ejs +684 -0
- package/hedhog/frontend/app/_components/catalog-nav.tsx.ejs +85 -0
- package/hedhog/frontend/app/_lib/catalog-resources.tsx.ejs +569 -0
- package/hedhog/frontend/app/dashboard/page.tsx.ejs +566 -0
- package/hedhog/frontend/app/page.tsx.ejs +5 -0
- package/hedhog/frontend/messages/en.json +293 -0
- package/hedhog/frontend/messages/pt.json +293 -0
- package/hedhog/table/catalog_affiliate_program.yaml +41 -0
- package/hedhog/table/catalog_attribute.yaml +53 -0
- package/hedhog/table/catalog_attribute_group.yaml +18 -0
- package/hedhog/table/catalog_brand.yaml +34 -0
- package/hedhog/table/catalog_category_attribute.yaml +36 -0
- package/hedhog/table/catalog_click_event.yaml +50 -0
- package/hedhog/table/catalog_comparison.yaml +65 -0
- package/hedhog/table/catalog_comparison_highlight.yaml +39 -0
- package/hedhog/table/catalog_comparison_item.yaml +30 -0
- package/hedhog/table/catalog_content_relation.yaml +42 -0
- package/hedhog/table/catalog_import_run.yaml +33 -0
- package/hedhog/table/catalog_import_source.yaml +24 -0
- package/hedhog/table/catalog_merchant.yaml +29 -0
- package/hedhog/table/catalog_offer.yaml +83 -0
- package/hedhog/table/catalog_price_history.yaml +34 -0
- package/hedhog/table/catalog_product.yaml +76 -0
- package/hedhog/table/catalog_product_attribute_value.yaml +60 -0
- package/hedhog/table/catalog_product_category.yaml +26 -0
- package/hedhog/table/catalog_product_image.yaml +34 -0
- package/hedhog/table/catalog_product_score.yaml +38 -0
- package/hedhog/table/catalog_product_site.yaml +47 -0
- package/hedhog/table/catalog_product_tag.yaml +19 -0
- package/hedhog/table/catalog_score_criterion.yaml +37 -0
- package/hedhog/table/catalog_seo_page_rule.yaml +51 -0
- package/hedhog/table/catalog_similarity_rule.yaml +28 -0
- package/hedhog/table/catalog_site.yaml +40 -0
- package/hedhog/table/catalog_site_category.yaml +26 -0
- package/package.json +40 -0
- package/src/catalog-resource.config.ts +218 -0
- package/src/catalog.controller.ts +82 -0
- package/src/catalog.module.ts +12 -0
- package/src/catalog.service.ts +167 -0
- package/src/index.ts +1 -0
- package/src/language/en.json +4 -0
- package/src/language/pt.json +4 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CatalogNav } from '../_components/catalog-nav';
|
|
4
|
+
import {
|
|
5
|
+
catalogKpiResources,
|
|
6
|
+
catalogQuickActionResources,
|
|
7
|
+
catalogResources,
|
|
8
|
+
} from '../_lib/catalog-resources';
|
|
9
|
+
import { Page, PageHeader } from '@/components/entity-list';
|
|
10
|
+
import { Badge } from '@/components/ui/badge';
|
|
11
|
+
import { Button } from '@/components/ui/button';
|
|
12
|
+
import {
|
|
13
|
+
Card,
|
|
14
|
+
CardContent,
|
|
15
|
+
CardDescription,
|
|
16
|
+
CardHeader,
|
|
17
|
+
CardTitle,
|
|
18
|
+
} from '@/components/ui/card';
|
|
19
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
20
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
21
|
+
import {
|
|
22
|
+
Activity,
|
|
23
|
+
ArrowUpRight,
|
|
24
|
+
Boxes,
|
|
25
|
+
ChartNoAxesCombined,
|
|
26
|
+
RefreshCcw,
|
|
27
|
+
ShieldCheck,
|
|
28
|
+
Sparkles,
|
|
29
|
+
} from 'lucide-react';
|
|
30
|
+
import Link from 'next/link';
|
|
31
|
+
import { useTranslations } from 'next-intl';
|
|
32
|
+
import {
|
|
33
|
+
Bar,
|
|
34
|
+
BarChart,
|
|
35
|
+
CartesianGrid,
|
|
36
|
+
Cell,
|
|
37
|
+
Pie,
|
|
38
|
+
PieChart,
|
|
39
|
+
ResponsiveContainer,
|
|
40
|
+
Tooltip,
|
|
41
|
+
XAxis,
|
|
42
|
+
YAxis,
|
|
43
|
+
} from 'recharts';
|
|
44
|
+
|
|
45
|
+
type CatalogStats = {
|
|
46
|
+
resource: string;
|
|
47
|
+
total: number;
|
|
48
|
+
active?: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const chartPalette = [
|
|
52
|
+
'#f97316',
|
|
53
|
+
'#14b8a6',
|
|
54
|
+
'#0ea5e9',
|
|
55
|
+
'#84cc16',
|
|
56
|
+
'#f59e0b',
|
|
57
|
+
'#ef4444',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const chartTooltipStyle = {
|
|
61
|
+
backgroundColor: 'hsl(var(--card))',
|
|
62
|
+
border: '1px solid hsl(var(--border))',
|
|
63
|
+
borderRadius: '12px',
|
|
64
|
+
fontSize: '12px',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default function CatalogDashboardPage() {
|
|
68
|
+
const t = useTranslations('catalog');
|
|
69
|
+
const { request } = useApp();
|
|
70
|
+
|
|
71
|
+
const { data, isLoading, refetch } = useQuery<CatalogStats[]>({
|
|
72
|
+
queryKey: ['catalog-dashboard-stats'],
|
|
73
|
+
queryFn: async () => {
|
|
74
|
+
const responses = await Promise.allSettled(
|
|
75
|
+
catalogResources.map(async (resource) => {
|
|
76
|
+
const response = await request({
|
|
77
|
+
url: `/catalog/${resource.resource}/stats`,
|
|
78
|
+
});
|
|
79
|
+
const statsData = response.data as {
|
|
80
|
+
total?: number;
|
|
81
|
+
active?: number;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
resource: resource.resource,
|
|
86
|
+
total: Number(statsData.total ?? 0),
|
|
87
|
+
active:
|
|
88
|
+
statsData.active !== undefined
|
|
89
|
+
? Number(statsData.active)
|
|
90
|
+
: undefined,
|
|
91
|
+
} satisfies CatalogStats;
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return responses.map((response, index) =>
|
|
96
|
+
response.status === 'fulfilled'
|
|
97
|
+
? response.value
|
|
98
|
+
: {
|
|
99
|
+
resource:
|
|
100
|
+
catalogResources[index]?.resource ?? `fallback-${index}`,
|
|
101
|
+
total: 0,
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
initialData: [],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const dashboardStats = data as CatalogStats[];
|
|
109
|
+
|
|
110
|
+
const statsMap = new Map(dashboardStats.map((item) => [item.resource, item]));
|
|
111
|
+
const totalRecords = dashboardStats.reduce(
|
|
112
|
+
(sum, item) => sum + item.total,
|
|
113
|
+
0
|
|
114
|
+
);
|
|
115
|
+
const totalActive = dashboardStats.reduce(
|
|
116
|
+
(sum, item) => sum + (typeof item.active === 'number' ? item.active : 0),
|
|
117
|
+
0
|
|
118
|
+
);
|
|
119
|
+
const monitoredResources = dashboardStats.filter(
|
|
120
|
+
(item) => item.total > 0
|
|
121
|
+
).length;
|
|
122
|
+
const statusAwareResources = dashboardStats.filter(
|
|
123
|
+
(item) => typeof item.active === 'number'
|
|
124
|
+
).length;
|
|
125
|
+
|
|
126
|
+
const kpiCards = catalogKpiResources.map((resourceKey) => {
|
|
127
|
+
const resource = catalogResources.find(
|
|
128
|
+
(item) => item.resource === resourceKey
|
|
129
|
+
)!;
|
|
130
|
+
const stats = statsMap.get(resourceKey);
|
|
131
|
+
return { resource, total: stats?.total ?? 0, active: stats?.active };
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const volumeChartData = [...catalogResources]
|
|
135
|
+
.map((resource) => ({
|
|
136
|
+
name: t(`resources.${resource.translationKey}.shortTitle`),
|
|
137
|
+
total: statsMap.get(resource.resource)?.total ?? 0,
|
|
138
|
+
}))
|
|
139
|
+
.sort((left, right) => right.total - left.total)
|
|
140
|
+
.slice(0, 6);
|
|
141
|
+
|
|
142
|
+
const statusChartData = catalogResources
|
|
143
|
+
.filter((resource) => resource.hasActiveStats)
|
|
144
|
+
.map((resource) => {
|
|
145
|
+
const stats = statsMap.get(resource.resource);
|
|
146
|
+
const active = Number(stats?.active ?? 0);
|
|
147
|
+
const total = Number(stats?.total ?? 0);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
name: t(`resources.${resource.translationKey}.shortTitle`),
|
|
151
|
+
active,
|
|
152
|
+
inactive: Math.max(total - active, 0),
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const distributionChartData = volumeChartData.map((item, index) => ({
|
|
157
|
+
...item,
|
|
158
|
+
fill: chartPalette[index % chartPalette.length],
|
|
159
|
+
}));
|
|
160
|
+
|
|
161
|
+
const leaderboard = [...catalogResources]
|
|
162
|
+
.map((resource) => ({
|
|
163
|
+
resource,
|
|
164
|
+
total: statsMap.get(resource.resource)?.total ?? 0,
|
|
165
|
+
active: statsMap.get(resource.resource)?.active,
|
|
166
|
+
}))
|
|
167
|
+
.sort((left, right) => right.total - left.total)
|
|
168
|
+
.slice(0, 5);
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<Page>
|
|
172
|
+
<PageHeader
|
|
173
|
+
title={t('dashboard.title')}
|
|
174
|
+
description={t('dashboard.subtitle')}
|
|
175
|
+
breadcrumbs={[
|
|
176
|
+
{ label: t('breadcrumbs.home'), href: '/' },
|
|
177
|
+
{ label: t('breadcrumbs.catalog') },
|
|
178
|
+
]}
|
|
179
|
+
actions={[
|
|
180
|
+
{
|
|
181
|
+
label: t('refresh'),
|
|
182
|
+
onClick: () => {
|
|
183
|
+
void refetch();
|
|
184
|
+
},
|
|
185
|
+
variant: 'outline',
|
|
186
|
+
icon: <RefreshCcw className="size-4" />,
|
|
187
|
+
},
|
|
188
|
+
]}
|
|
189
|
+
/>
|
|
190
|
+
|
|
191
|
+
<div className="min-w-0 space-y-6 overflow-x-hidden">
|
|
192
|
+
<Card className="overflow-hidden border-orange-200/70 bg-gradient-to-br from-orange-50 via-background to-amber-50 py-0">
|
|
193
|
+
<CardContent className="grid min-w-0 gap-6 px-6 py-6 lg:grid-cols-[minmax(0,1.4fr)_minmax(280px,0.9fr)]">
|
|
194
|
+
<div className="min-w-0 space-y-4">
|
|
195
|
+
<Badge className="w-fit rounded-full bg-orange-500/10 px-3 py-1 text-orange-700 hover:bg-orange-500/10">
|
|
196
|
+
<Sparkles className="mr-2 size-3.5" />
|
|
197
|
+
{t('dashboard.heroBadge')}
|
|
198
|
+
</Badge>
|
|
199
|
+
<div className="space-y-2">
|
|
200
|
+
<h2 className="text-3xl font-semibold tracking-tight text-balance">
|
|
201
|
+
{t('dashboard.heroTitle')}
|
|
202
|
+
</h2>
|
|
203
|
+
<p className="max-w-2xl text-sm leading-6 text-muted-foreground">
|
|
204
|
+
{t('dashboard.heroDescription')}
|
|
205
|
+
</p>
|
|
206
|
+
</div>
|
|
207
|
+
<div className="flex flex-wrap gap-3">
|
|
208
|
+
<Button asChild>
|
|
209
|
+
<Link href="/catalog/products">
|
|
210
|
+
{t('dashboard.primaryAction')}
|
|
211
|
+
</Link>
|
|
212
|
+
</Button>
|
|
213
|
+
<Button asChild variant="outline">
|
|
214
|
+
<Link href="/catalog/import-sources">
|
|
215
|
+
{t('dashboard.secondaryAction')}
|
|
216
|
+
</Link>
|
|
217
|
+
</Button>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<div className="grid gap-3 sm:grid-cols-3 lg:grid-cols-1">
|
|
222
|
+
{[
|
|
223
|
+
{
|
|
224
|
+
icon: Boxes,
|
|
225
|
+
label: t('dashboard.summary.totalRecords'),
|
|
226
|
+
value: totalRecords,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
icon: ShieldCheck,
|
|
230
|
+
label: t('dashboard.summary.activeRecords'),
|
|
231
|
+
value: totalActive,
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
icon: Activity,
|
|
235
|
+
label: t('dashboard.summary.monitoredResources'),
|
|
236
|
+
value: monitoredResources,
|
|
237
|
+
},
|
|
238
|
+
].map((item) => (
|
|
239
|
+
<div
|
|
240
|
+
key={item.label}
|
|
241
|
+
className="rounded-2xl border border-white/70 bg-white/80 p-4 shadow-sm backdrop-blur"
|
|
242
|
+
>
|
|
243
|
+
<div className="mb-3 flex items-center gap-2 text-muted-foreground">
|
|
244
|
+
<item.icon className="size-4" />
|
|
245
|
+
<span className="text-xs uppercase tracking-[0.2em]">
|
|
246
|
+
{item.label}
|
|
247
|
+
</span>
|
|
248
|
+
</div>
|
|
249
|
+
<div className="text-3xl font-semibold tracking-tight">
|
|
250
|
+
{item.value}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
))}
|
|
254
|
+
</div>
|
|
255
|
+
</CardContent>
|
|
256
|
+
</Card>
|
|
257
|
+
|
|
258
|
+
<CatalogNav currentHref="/catalog/dashboard" />
|
|
259
|
+
|
|
260
|
+
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
261
|
+
{kpiCards.map(({ resource, total, active }) => {
|
|
262
|
+
const Icon = resource.icon;
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<Card
|
|
266
|
+
key={resource.resource}
|
|
267
|
+
className="overflow-hidden border-border/70 py-0"
|
|
268
|
+
>
|
|
269
|
+
<div
|
|
270
|
+
className={`h-1 w-full bg-gradient-to-r ${resource.colorClass}`}
|
|
271
|
+
/>
|
|
272
|
+
<CardContent className="space-y-4 px-6 py-5">
|
|
273
|
+
<div className="flex items-start justify-between gap-3">
|
|
274
|
+
<div>
|
|
275
|
+
<p className="text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
|
276
|
+
{t(`resources.${resource.translationKey}.shortTitle`)}
|
|
277
|
+
</p>
|
|
278
|
+
<p className="mt-2 text-3xl font-semibold tracking-tight">
|
|
279
|
+
{isLoading ? '-' : total}
|
|
280
|
+
</p>
|
|
281
|
+
</div>
|
|
282
|
+
<div className={`rounded-2xl p-3 ${resource.glowClass}`}>
|
|
283
|
+
<Icon className="size-5" />
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
287
|
+
<span>{t('dashboard.card.totalLabel')}</span>
|
|
288
|
+
<span>
|
|
289
|
+
{typeof active === 'number'
|
|
290
|
+
? t('dashboard.card.activeLabel', { count: active })
|
|
291
|
+
: t('dashboard.card.noActiveLabel')}
|
|
292
|
+
</span>
|
|
293
|
+
</div>
|
|
294
|
+
</CardContent>
|
|
295
|
+
</Card>
|
|
296
|
+
);
|
|
297
|
+
})}
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<div className="grid min-w-0 gap-6 xl:grid-cols-[minmax(0,1.3fr)_minmax(320px,0.9fr)]">
|
|
301
|
+
<Card className="min-w-0">
|
|
302
|
+
<CardHeader>
|
|
303
|
+
<CardTitle>{t('dashboard.charts.volume.title')}</CardTitle>
|
|
304
|
+
<CardDescription>
|
|
305
|
+
{t('dashboard.charts.volume.description')}
|
|
306
|
+
</CardDescription>
|
|
307
|
+
</CardHeader>
|
|
308
|
+
<CardContent className="h-[320px]">
|
|
309
|
+
{isLoading ? (
|
|
310
|
+
<Skeleton className="h-full w-full rounded-xl" />
|
|
311
|
+
) : (
|
|
312
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
313
|
+
<BarChart data={volumeChartData}>
|
|
314
|
+
<CartesianGrid
|
|
315
|
+
strokeDasharray="3 3"
|
|
316
|
+
stroke="hsl(var(--border))"
|
|
317
|
+
vertical={false}
|
|
318
|
+
/>
|
|
319
|
+
<XAxis
|
|
320
|
+
dataKey="name"
|
|
321
|
+
tickLine={false}
|
|
322
|
+
axisLine={false}
|
|
323
|
+
fontSize={12}
|
|
324
|
+
/>
|
|
325
|
+
<YAxis tickLine={false} axisLine={false} fontSize={12} />
|
|
326
|
+
<Tooltip contentStyle={chartTooltipStyle} />
|
|
327
|
+
<Bar dataKey="total" radius={[8, 8, 0, 0]}>
|
|
328
|
+
{volumeChartData.map((entry, index) => (
|
|
329
|
+
<Cell
|
|
330
|
+
key={`${entry.name}-${index}`}
|
|
331
|
+
fill={chartPalette[index % chartPalette.length]}
|
|
332
|
+
/>
|
|
333
|
+
))}
|
|
334
|
+
</Bar>
|
|
335
|
+
</BarChart>
|
|
336
|
+
</ResponsiveContainer>
|
|
337
|
+
)}
|
|
338
|
+
</CardContent>
|
|
339
|
+
</Card>
|
|
340
|
+
|
|
341
|
+
<Card className="min-w-0">
|
|
342
|
+
<CardHeader>
|
|
343
|
+
<CardTitle>{t('dashboard.ranking.title')}</CardTitle>
|
|
344
|
+
<CardDescription>
|
|
345
|
+
{t('dashboard.ranking.description')}
|
|
346
|
+
</CardDescription>
|
|
347
|
+
</CardHeader>
|
|
348
|
+
<CardContent className="space-y-3">
|
|
349
|
+
{isLoading
|
|
350
|
+
? Array.from({ length: 5 }).map((_, index) => (
|
|
351
|
+
<Skeleton
|
|
352
|
+
key={index}
|
|
353
|
+
className="h-[72px] w-full rounded-2xl"
|
|
354
|
+
/>
|
|
355
|
+
))
|
|
356
|
+
: leaderboard.map((item, index) => {
|
|
357
|
+
const Icon = item.resource.icon;
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<Link
|
|
361
|
+
key={item.resource.resource}
|
|
362
|
+
href={item.resource.href}
|
|
363
|
+
className="flex min-w-0 items-center justify-between gap-3 rounded-2xl border border-border/70 px-4 py-3 transition-colors hover:bg-muted/40"
|
|
364
|
+
>
|
|
365
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
366
|
+
<div className="flex size-10 items-center justify-center rounded-2xl bg-muted text-muted-foreground">
|
|
367
|
+
<Icon className="size-4" />
|
|
368
|
+
</div>
|
|
369
|
+
<div className="min-w-0">
|
|
370
|
+
<p className="truncate font-medium">
|
|
371
|
+
{index + 1}.{' '}
|
|
372
|
+
{t(
|
|
373
|
+
`resources.${item.resource.translationKey}.title`
|
|
374
|
+
)}
|
|
375
|
+
</p>
|
|
376
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
377
|
+
{typeof item.active === 'number'
|
|
378
|
+
? t('dashboard.ranking.activeCount', {
|
|
379
|
+
count: item.active,
|
|
380
|
+
})
|
|
381
|
+
: t('dashboard.card.noActiveLabel')}
|
|
382
|
+
</p>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
<div className="shrink-0 text-right">
|
|
386
|
+
<p className="text-2xl font-semibold">{item.total}</p>
|
|
387
|
+
<p className="text-xs text-muted-foreground">
|
|
388
|
+
{t('dashboard.card.totalLabel')}
|
|
389
|
+
</p>
|
|
390
|
+
</div>
|
|
391
|
+
</Link>
|
|
392
|
+
);
|
|
393
|
+
})}
|
|
394
|
+
</CardContent>
|
|
395
|
+
</Card>
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
<div className="grid gap-6 xl:grid-cols-2">
|
|
399
|
+
<Card className="min-w-0">
|
|
400
|
+
<CardHeader>
|
|
401
|
+
<CardTitle>{t('dashboard.charts.status.title')}</CardTitle>
|
|
402
|
+
<CardDescription>
|
|
403
|
+
{t('dashboard.charts.status.description', {
|
|
404
|
+
count: statusAwareResources,
|
|
405
|
+
})}
|
|
406
|
+
</CardDescription>
|
|
407
|
+
</CardHeader>
|
|
408
|
+
<CardContent className="h-[320px]">
|
|
409
|
+
{isLoading ? (
|
|
410
|
+
<Skeleton className="h-full w-full rounded-xl" />
|
|
411
|
+
) : (
|
|
412
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
413
|
+
<BarChart data={statusChartData}>
|
|
414
|
+
<CartesianGrid
|
|
415
|
+
strokeDasharray="3 3"
|
|
416
|
+
stroke="hsl(var(--border))"
|
|
417
|
+
vertical={false}
|
|
418
|
+
/>
|
|
419
|
+
<XAxis
|
|
420
|
+
dataKey="name"
|
|
421
|
+
tickLine={false}
|
|
422
|
+
axisLine={false}
|
|
423
|
+
fontSize={12}
|
|
424
|
+
/>
|
|
425
|
+
<YAxis tickLine={false} axisLine={false} fontSize={12} />
|
|
426
|
+
<Tooltip contentStyle={chartTooltipStyle} />
|
|
427
|
+
<Bar
|
|
428
|
+
dataKey="active"
|
|
429
|
+
stackId="catalog-status"
|
|
430
|
+
fill="#f97316"
|
|
431
|
+
radius={[8, 8, 0, 0]}
|
|
432
|
+
/>
|
|
433
|
+
<Bar
|
|
434
|
+
dataKey="inactive"
|
|
435
|
+
stackId="catalog-status"
|
|
436
|
+
fill="#fed7aa"
|
|
437
|
+
radius={[8, 8, 0, 0]}
|
|
438
|
+
/>
|
|
439
|
+
</BarChart>
|
|
440
|
+
</ResponsiveContainer>
|
|
441
|
+
)}
|
|
442
|
+
</CardContent>
|
|
443
|
+
</Card>
|
|
444
|
+
|
|
445
|
+
<Card className="min-w-0">
|
|
446
|
+
<CardHeader>
|
|
447
|
+
<CardTitle>{t('dashboard.charts.distribution.title')}</CardTitle>
|
|
448
|
+
<CardDescription>
|
|
449
|
+
{t('dashboard.charts.distribution.description')}
|
|
450
|
+
</CardDescription>
|
|
451
|
+
</CardHeader>
|
|
452
|
+
<CardContent className="grid min-w-0 h-auto gap-4 lg:h-[320px] lg:grid-cols-[minmax(220px,0.9fr)_minmax(0,1.1fr)]">
|
|
453
|
+
{isLoading ? (
|
|
454
|
+
<>
|
|
455
|
+
<Skeleton className="h-full w-full rounded-full" />
|
|
456
|
+
<Skeleton className="h-full w-full rounded-2xl" />
|
|
457
|
+
</>
|
|
458
|
+
) : (
|
|
459
|
+
<>
|
|
460
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
461
|
+
<PieChart>
|
|
462
|
+
<Pie
|
|
463
|
+
data={distributionChartData}
|
|
464
|
+
dataKey="total"
|
|
465
|
+
nameKey="name"
|
|
466
|
+
innerRadius={58}
|
|
467
|
+
outerRadius={96}
|
|
468
|
+
paddingAngle={2}
|
|
469
|
+
>
|
|
470
|
+
{distributionChartData.map((entry) => (
|
|
471
|
+
<Cell key={entry.name} fill={entry.fill} />
|
|
472
|
+
))}
|
|
473
|
+
</Pie>
|
|
474
|
+
<Tooltip contentStyle={chartTooltipStyle} />
|
|
475
|
+
</PieChart>
|
|
476
|
+
</ResponsiveContainer>
|
|
477
|
+
<div className="min-w-0 space-y-3">
|
|
478
|
+
{distributionChartData.map((item) => (
|
|
479
|
+
<div
|
|
480
|
+
key={item.name}
|
|
481
|
+
className="flex min-w-0 items-center justify-between gap-3 rounded-2xl border border-border/70 px-4 py-3"
|
|
482
|
+
>
|
|
483
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
484
|
+
<span
|
|
485
|
+
className="inline-block size-3 rounded-full"
|
|
486
|
+
style={{ backgroundColor: item.fill }}
|
|
487
|
+
/>
|
|
488
|
+
<span className="truncate font-medium">{item.name}</span>
|
|
489
|
+
</div>
|
|
490
|
+
<span className="shrink-0 text-sm text-muted-foreground">
|
|
491
|
+
{item.total}
|
|
492
|
+
</span>
|
|
493
|
+
</div>
|
|
494
|
+
))}
|
|
495
|
+
</div>
|
|
496
|
+
</>
|
|
497
|
+
)}
|
|
498
|
+
</CardContent>
|
|
499
|
+
</Card>
|
|
500
|
+
</div>
|
|
501
|
+
|
|
502
|
+
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
503
|
+
{catalogQuickActionResources.map((resource) => {
|
|
504
|
+
const Icon = resource.icon;
|
|
505
|
+
|
|
506
|
+
return (
|
|
507
|
+
<Card key={resource.resource} className="min-w-0 overflow-hidden py-0">
|
|
508
|
+
<div
|
|
509
|
+
className={`h-full bg-gradient-to-br ${resource.colorClass} px-6 py-5`}
|
|
510
|
+
>
|
|
511
|
+
<div className="min-w-0 rounded-2xl border border-white/70 bg-background/90 p-5 shadow-sm backdrop-blur">
|
|
512
|
+
<div className="mb-4 flex items-center justify-between">
|
|
513
|
+
<div className={`rounded-2xl p-3 ${resource.glowClass}`}>
|
|
514
|
+
<Icon className="size-5" />
|
|
515
|
+
</div>
|
|
516
|
+
<ArrowUpRight className="size-4 text-muted-foreground" />
|
|
517
|
+
</div>
|
|
518
|
+
<h3 className="text-lg font-semibold">
|
|
519
|
+
{t(`resources.${resource.translationKey}.title`)}
|
|
520
|
+
</h3>
|
|
521
|
+
<p className="mt-2 min-h-10 break-words text-sm text-muted-foreground">
|
|
522
|
+
{t(`resources.${resource.translationKey}.description`)}
|
|
523
|
+
</p>
|
|
524
|
+
<div className="mt-5 flex items-center justify-between">
|
|
525
|
+
<span className="text-sm font-medium text-muted-foreground">
|
|
526
|
+
{t('dashboard.quickActions.itemHint')}
|
|
527
|
+
</span>
|
|
528
|
+
<Button asChild size="sm">
|
|
529
|
+
<Link href={resource.href}>{t('openResource')}</Link>
|
|
530
|
+
</Button>
|
|
531
|
+
</div>
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
534
|
+
</Card>
|
|
535
|
+
);
|
|
536
|
+
})}
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
<Card className="min-w-0 border-dashed">
|
|
540
|
+
<CardContent className="flex flex-wrap items-center justify-between gap-4 px-6 py-4">
|
|
541
|
+
<div className="space-y-1">
|
|
542
|
+
<div className="flex items-center gap-2 text-sm font-medium">
|
|
543
|
+
<ChartNoAxesCombined className="size-4 text-orange-600" />
|
|
544
|
+
{t('dashboard.footer.title')}
|
|
545
|
+
</div>
|
|
546
|
+
<p className="text-sm text-muted-foreground">
|
|
547
|
+
{t('dashboard.footer.description')}
|
|
548
|
+
</p>
|
|
549
|
+
</div>
|
|
550
|
+
<div className="flex flex-wrap gap-2">
|
|
551
|
+
<Badge variant="secondary">
|
|
552
|
+
{t('dashboard.footer.realData')}
|
|
553
|
+
</Badge>
|
|
554
|
+
<Badge variant="outline">{t('dashboard.footer.multiView')}</Badge>
|
|
555
|
+
<Badge variant="outline">
|
|
556
|
+
{t('dashboard.footer.resourceCount', {
|
|
557
|
+
count: catalogResources.length,
|
|
558
|
+
})}
|
|
559
|
+
</Badge>
|
|
560
|
+
</div>
|
|
561
|
+
</CardContent>
|
|
562
|
+
</Card>
|
|
563
|
+
</div>
|
|
564
|
+
</Page>
|
|
565
|
+
);
|
|
566
|
+
}
|