"use client";
import * as React from "react";
import { useQuery } from "@tanstack/react-query";
import DateObject from "react-date-object";
import persian from "react-date-object/calendars/persian";
import persian_fa from "react-date-object/locales/persian_fa";
import DatePicker from "react-multi-date-picker";
import {
Activity,
BarChart3,
BookOpen,
CalendarDays,
GraduationCap,
Heart,
LibraryBig,
LineChart as LineChartIcon,
MessageCircle,
Save,
Tags,
UsersRound,
WalletCards,
} from "lucide-react";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
Line,
LineChart,
Scatter,
ScatterChart,
XAxis,
YAxis,
ZAxis,
} from "recharts";
import AsyncSearchableCombobox from "@/components/AsyncSearchableCombobox";
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 { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/lib/api";
import type {
AnalyticsPointGroupSchema,
AnalyticsPointSchema,
AnalyticsPostPopularitySchema,
AnalyticsTrendPointSchema,
BlogAnalyticsSchema,
EventAnalyticsSchema,
UserAnalyticsSchema,
} from "@/lib/types";
import {
cn,
formatJalaliDate,
formatNumberPersian,
formatToman,
resolveErrorMessage,
toPersianDigits,
} from "@/lib/utils";
const PALETTE = {
teal: "#2dd4bf",
cyan: "#38bdf8",
amber: "#fbbf24",
rose: "#fb7185",
violet: "#a78bfa",
emerald: "#34d399",
slate: "#94a3b8",
};
const STATUS_COLORS = [PALETTE.teal, PALETTE.cyan, PALETTE.amber, PALETTE.rose, PALETTE.violet, PALETTE.emerald];
type DateRangeState = {
from: string;
to: string;
};
type SectionState = DateRangeState & {
eventId?: string | null;
};
function toApiDate(date: DateObject | null) {
if (!date) return "";
const gregorian = date.toDate();
const year = gregorian.getFullYear();
const month = String(gregorian.getMonth() + 1).padStart(2, "0");
const day = String(gregorian.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function fromApiDate(value?: string | null) {
if (!value) return null;
const [year, month, day] = value.split("-").map(Number);
if (!year || !month || !day) return null;
return new DateObject({
date: new Date(year, month - 1, day),
calendar: persian,
locale: persian_fa,
});
}
function formatJalaliTick(value?: string | number) {
if (!value) return "";
const raw = String(value);
const date = new Date(raw.length === 10 ? `${raw}T00:00:00` : raw);
if (Number.isNaN(date.getTime())) return toPersianDigits(raw);
const locale = Intl.DateTimeFormat.supportedLocalesOf(["fa-IR-u-ca-persian"]).length
? "fa-IR-u-ca-persian"
: "fa-IR";
return new Intl.DateTimeFormat(locale, { month: "short", day: "numeric" }).format(date);
}
function truncateLabel(value: string, max = 18) {
const normalized = toPersianDigits(value);
return normalized.length > max ? `${normalized.slice(0, max - 1)}…` : normalized;
}
function chartHeight(count: number, min = 280) {
return Math.max(min, count * 38 + 90);
}
function axisWidth(items: AnalyticsPointSchema[]) {
const maxLength = Math.max(...items.map((item) => String(item.label).length), 10);
return Math.min(190, Math.max(90, maxLength * 7));
}
function dataWithOtherNotice(group: AnalyticsPointGroupSchema) {
return group.top_items;
}
function StatCard({
title,
value,
description,
icon: Icon,
tone = "teal",
}: {
title: string;
value: string;
description: string;
icon: React.ComponentType<{ className?: string }>;
tone?: keyof typeof PALETTE;
}) {
return (
{title}
{value}
{description}
);
}
function SectionError({ error }: { error: unknown }) {
return (
{resolveErrorMessage(error)}
);
}
function SectionLoading() {
return (
در حال بارگذاری دادههای داشبورد...
);
}
function EmptyChart({ label = "دادهای برای نمایش وجود ندارد." }: { label?: string }) {
return (
{label}
);
}
function DateRangeFilter({
value,
onChange,
onReset,
}: {
value: DateRangeState;
onChange: (next: DateRangeState) => void;
onReset: () => void;
}) {
return (
از تاریخ
onChange({ ...value, from: toApiDate(next instanceof DateObject ? next : null) })}
calendar={persian}
locale={persian_fa}
calendarPosition="bottom-right"
inputClass="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-right ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
placeholder="تاریخ شروع"
containerClassName="w-full"
/>
تا تاریخ
onChange({ ...value, to: toApiDate(next instanceof DateObject ? next : null) })}
calendar={persian}
locale={persian_fa}
calendarPosition="bottom-right"
inputClass="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-right ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
placeholder="تاریخ پایان"
containerClassName="w-full"
/>
پاککردن
);
}
function FilterCard({
title,
description,
children,
}: {
title: string;
description: string;
children: React.ReactNode;
}) {
return (
{title}
{description}
{children}
);
}
function ChartViewport({ children, minWidth = 560 }: { children: React.ReactNode; minWidth?: number }) {
return (
);
}
function CustomValueTooltip({
active,
payload,
unit,
formatter,
}: {
active?: boolean;
payload?: Array<{ payload?: AnalyticsPointSchema; value?: number }>;
unit?: string;
formatter?: (value: number) => string;
}) {
if (!active || !payload?.length || !payload[0].payload) return null;
const item = payload[0].payload;
return (
{toPersianDigits(item.label)}
{formatter ? formatter(Number(item.value)) : formatNumberPersian(item.value)}
{unit ? ` ${unit}` : ""}
);
}
function HighCardinalityNotice({ group }: { group: AnalyticsPointGroupSchema }) {
if (group.total_count <= group.top_items.length) return null;
return (
نمایش {formatNumberPersian(group.top_items.length)} مورد برتر از {formatNumberPersian(group.total_count)} مورد؛{" "}
{formatNumberPersian(group.other_count)} مورد دیگر در نمودار فشرده نشدهاند.
);
}
function ValuesTable({
data,
unit,
formatter,
}: {
data: AnalyticsPointSchema[];
unit?: string;
formatter?: (value: number) => string;
}) {
if (!data.length) return null;
return (
{data.map((item, index) => (
{toPersianDigits(index + 1)}
{toPersianDigits(item.label)}
{formatter ? formatter(Number(item.value)) : formatNumberPersian(item.value)}
{unit ? ` ${unit}` : ""}
))}
);
}
function HorizontalBarCard({
title,
description,
group,
color = PALETTE.teal,
unit,
valueFormatter,
}: {
title: string;
description: string;
group: AnalyticsPointGroupSchema;
color?: string;
unit?: string;
valueFormatter?: (value: number) => string;
}) {
const data = dataWithOtherNotice(group);
return (
{title}
{description}
{!data.length ? (
) : (
<>
valueFormatter ? valueFormatter(Number(value)) : formatNumberPersian(Number(value))}
/>
truncateLabel(String(value))}
/>
} />
>
)}
);
}
function TrendLineCard({
title,
description,
data,
color = PALETTE.teal,
valueFormatter = formatNumberPersian,
}: {
title: string;
description: string;
data: AnalyticsTrendPointSchema[];
color?: string;
valueFormatter?: (value: number) => string;
}) {
return (
{title}
{description}
{!data.length ? (
) : (
valueFormatter(Number(value))}
/>
{
if (!active || !payload?.length) return null;
const item = payload[0].payload as AnalyticsTrendPointSchema;
return (
{formatJalaliDate(item.date)}
{valueFormatter(Number(item.value))}
);
}}
/>
)}
);
}
function StatusChartCard({
title,
description,
data,
}: {
title: string;
description: string;
data: Array<{ status: string; label: string; value: number }>;
}) {
return (
{title}
{description}
{!data.length ? (
) : (
<>
formatNumberPersian(Number(value))}
/>
truncateLabel(String(value), 14)}
/>
} />
{data.map((_, index) => (
|
))}
({ label: item.label, value: item.value }))} />
>
)}
);
}
function ActivityTrendCard({ data }: { data: BlogAnalyticsSchema["activity_trend"] }) {
return (
روند تعاملات بلاگ
لایک، ذخیره و کامنت در بازه انتخابی
{!data.length ? (
) : (
formatNumberPersian(Number(value))}
/>
} />
)}
);
}
function BlogScatterCard({ group }: { group: BlogAnalyticsSchema["post_popularity"] }) {
const data = group.top_items;
return (
محبوبیت نوشتهها
لایک در برابر ذخیره؛ اندازه نقطه بر اساس تعداد کامنت
{!data.length ? (
) : (
<>
formatNumberPersian(Number(value))}
/>
formatNumberPersian(Number(value))}
/>
{
if (!active || !payload?.length) return null;
const item = payload[0].payload as AnalyticsPostPopularitySchema;
return (
{item.title}
لایک: {formatNumberPersian(item.likes)}
ذخیره: {formatNumberPersian(item.saves)}
کامنت: {formatNumberPersian(item.comments)}
);
}}
/>
{group.total_count > data.length ? (
نمایش {formatNumberPersian(data.length)} نوشته برتر از {formatNumberPersian(group.total_count)} نوشته دارای تعامل.
) : null}
>
)}
);
}
function TopEventsCard({ group }: { group: EventAnalyticsSchema["top_events"] }) {
return (
رویدادهای برتر
بر اساس شرکتکننده تاییدشده، درآمد و زمان برگزاری
{group.top_items.length ? (
group.top_items.map((event, index) => (
{event.title}
{formatNumberPersian(event.attendees)} نفر
{event.fill_rate != null ? ` · ${formatNumberPersian(event.fill_rate)}٪ ظرفیت` : ""}
{event.revenue ? ` · ${formatToman(event.revenue)}` : ""}
{toPersianDigits(index + 1)}
))
) : (
دادهای وجود ندارد.
)}
{group.total_count > group.top_items.length ? (
{formatNumberPersian(group.other_count)} رویداد دیگر در این لیست خلاصه نشدهاند.
) : null}
);
}
function TopPostsCard({ posts }: { posts: BlogAnalyticsSchema["top_posts"] }) {
return (
نوشتههای برتر
بر اساس جمع لایک، ذخیره و کامنت
{posts.length ? (
posts.map((post, index) => (
{post.title}
{formatNumberPersian(post.likes)} لایک · {formatNumberPersian(post.saves)} ذخیره ·{" "}
{formatNumberPersian(post.comments)} کامنت
{toPersianDigits(index + 1)}
))
) : (
دادهای وجود ندارد.
)}
);
}
function UsersSection() {
const [filters, setFilters] = React.useState({ from: "", to: "" });
const query = useQuery({
queryKey: ["admin", "analytics", "users", filters],
queryFn: () => api.getAdminUserAnalytics({ date_from: filters.from || undefined, date_to: filters.to || undefined }),
});
return (
setFilters({ from: "", to: "" })} />
{query.isLoading ? : null}
{query.isError ? : null}
{query.data ? : null}
);
}
function UsersContent({ data }: { data: UserAnalyticsSchema }) {
return (
<>
>
);
}
function EventsSection() {
const [filters, setFilters] = React.useState({ from: "", to: "", eventId: null });
const query = useQuery({
queryKey: ["admin", "analytics", "events", filters],
queryFn: () =>
api.getAdminEventAnalytics({
date_from: filters.from || undefined,
date_to: filters.to || undefined,
event_id: filters.eventId ? Number(filters.eventId) : undefined,
}),
});
const reset = () => setFilters({ from: "", to: "", eventId: null });
return (
setFilters((current) => ({ ...current, ...next }))}
onReset={reset}
/>
رویداد
setFilters((current) => ({ ...current, eventId }))}
loadOptions={async ({ search, limit, offset }) => {
const data = await api.getAdminDashboardEventOptions({ search, limit, offset });
return {
count: data.count,
results: data.results.map((item) => ({
...item,
description: item.description ?? undefined,
})),
};
}}
placeholder="همه رویدادها"
searchPlaceholder="جستجوی رویداد..."
emptyText="رویدادی پیدا نشد."
/>
پاککردن
{query.isLoading ?
: null}
{query.isError ?
: null}
{query.data ?
: null}
);
}
function EventsContent({ data }: { data: EventAnalyticsSchema }) {
return (
<>
>
);
}
function BlogSection() {
const [filters, setFilters] = React.useState({ from: "", to: "" });
const query = useQuery({
queryKey: ["admin", "analytics", "blog", filters],
queryFn: () => api.getAdminBlogAnalytics({ date_from: filters.from || undefined, date_to: filters.to || undefined }),
});
return (
setFilters({ from: "", to: "" })} />
{query.isLoading ? : null}
{query.isError ? : null}
{query.data ? : null}
);
}
function BlogContent({ data }: { data: BlogAnalyticsSchema }) {
return (
<>
>
);
}
export default function AdminDashboard() {
return (
داشبورد دستاوردها
گزارشهای جداگانه برای کاربران، رویدادها و بلاگ با فیلترهای مستقل و خوانا
کاربران
رویدادها
بلاگ
);
}