From 268dd26d9a3cf10313d9d63fc8708fb884a60b8e Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Mon, 15 Jun 2026 17:35:45 +0330 Subject: [PATCH] feat(admin-dashboard): add full result drilldown modals --- src/lib/types.ts | 2 + src/views/AdminDashboard.tsx | 141 +++++++++++++++++++++++++++++++++-- 2 files changed, 137 insertions(+), 6 deletions(-) diff --git a/src/lib/types.ts b/src/lib/types.ts index ad107ee..a9fba66 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -760,6 +760,7 @@ export interface AnalyticsPointSchema { } export interface AnalyticsPointGroupSchema { + items: AnalyticsPointSchema[]; top_items: AnalyticsPointSchema[]; other_count: number; total_count: number; @@ -797,6 +798,7 @@ export interface AnalyticsPostPopularitySchema { } export interface AnalyticsPostPopularityGroupSchema { + items: AnalyticsPostPopularitySchema[]; top_items: AnalyticsPostPopularitySchema[]; other_count: number; total_count: number; diff --git a/src/views/AdminDashboard.tsx b/src/views/AdminDashboard.tsx index 3658c1e..12fb2f6 100644 --- a/src/views/AdminDashboard.tsx +++ b/src/views/AdminDashboard.tsx @@ -41,6 +41,8 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { api } from "@/lib/api"; @@ -152,6 +154,10 @@ function dataWithOtherNotice(group: AnalyticsPointGroupSchema) { return group.top_items; } +function fullGroupItems(group: AnalyticsPointGroupSchema) { + return group.items?.length ? group.items : group.top_items; +} + function StatCard({ title, value, @@ -309,13 +315,124 @@ function CustomValueTooltip({ ); } -function HighCardinalityNotice({ group }: { group: AnalyticsPointGroupSchema }) { +function FullResultsAction({ + group, + onOpen, +}: { + group: AnalyticsPointGroupSchema; + onOpen: () => void; +}) { if (group.total_count <= group.top_items.length) return null; return ( -

- نمایش {formatNumberPersian(group.top_items.length)} مورد برتر از {formatNumberPersian(group.total_count)} مورد؛{" "} - {formatNumberPersian(group.other_count)} مورد دیگر در نمودار فشرده نشده‌اند. -

+
+ + نمودار خلاصه {formatNumberPersian(group.top_items.length)} مورد اول را نشان می‌دهد؛ همه{" "} + {formatNumberPersian(group.total_count)} مورد در نمای کامل قابل بررسی است. + + +
+ ); +} + +function DashboardPointDetailModal({ + open, + onOpenChange, + title, + description, + data, + color, + unit, + formatter, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; + description: string; + data: AnalyticsPointSchema[]; + color: string; + unit?: string; + formatter?: (value: number) => string; +}) { + const [search, setSearch] = React.useState(""); + const [sortBy, setSortBy] = React.useState<"value" | "label">("value"); + const filteredData = React.useMemo(() => { + const normalizedSearch = search.trim().toLocaleLowerCase("fa-IR"); + const filtered = normalizedSearch + ? data.filter((item) => toPersianDigits(item.label).toLocaleLowerCase("fa-IR").includes(normalizedSearch)) + : data; + return [...filtered].sort((a, b) => { + if (sortBy === "label") return String(a.label).localeCompare(String(b.label), "fa"); + return Number(b.value) - Number(a.value) || String(a.label).localeCompare(String(b.label), "fa"); + }); + }, [data, search, sortBy]); + + return ( + + + + {title} +

{description}

+
+
+ setSearch(event.target.value)} + placeholder="جستجو در عنوان‌ها..." + className="text-right" + /> + + +
+ {!filteredData.length ? ( + + ) : ( + <> + + + + + formatter ? formatter(Number(value)) : formatNumberPersian(Number(value))} + /> + truncateLabel(String(value), 24)} + /> + } /> + + + + + + + )} +
+
); } @@ -365,6 +482,8 @@ function HorizontalBarCard({ valueFormatter?: (value: number) => string; }) { const data = dataWithOtherNotice(group); + const allData = fullGroupItems(group); + const [detailsOpen, setDetailsOpen] = React.useState(false); return ( @@ -407,8 +526,18 @@ function HorizontalBarCard({ - + setDetailsOpen(true)} /> + )}