fix(admin): prioritize follow-up tickets (#18)

* 🐛 fix(admin): prioritize follow-up tickets by updated time

*  feat(dashboard): show node/user traffic ranks side-by-side
This commit is contained in:
web@ppanel 2026-02-27 07:01:11 +08:00 committed by shanshanzhong
parent 370d59d5ad
commit 116f6e5360
2 changed files with 137 additions and 127 deletions

View File

@ -13,13 +13,7 @@ import {
ChartTooltip,
ChartTooltipContent,
} from "@workspace/ui/components/chart";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@workspace/ui/components/select";
// (Select imports removed)
import { Separator } from "@workspace/ui/components/separator";
import { Tabs, TabsList, TabsTrigger } from "@workspace/ui/components/tabs";
import Empty from "@workspace/ui/composed/empty";
@ -62,7 +56,6 @@ export default function Statistics() {
},
});
const [dataType, setDataType] = useState<string | "nodes" | "users">("nodes");
const [timeFrame, setTimeFrame] = useState<string | "today" | "yesterday">(
"today"
);
@ -93,10 +86,112 @@ export default function Statistics() {
})) || [],
},
};
const currentData =
trafficData[dataType as "nodes" | "users"][
timeFrame as "today" | "yesterday"
];
const TrafficRankCard = ({ type }: { type: "nodes" | "users" }) => {
const currentData = trafficData[type][timeFrame as "today" | "yesterday"];
return (
<Card>
<CardHeader className="!flex-row flex items-center justify-between">
<CardTitle>
{type === "nodes"
? t("nodeTraffic", "Node Traffic")
: t("userTraffic", "User Traffic")}
</CardTitle>
<Tabs onValueChange={setTimeFrame} value={timeFrame}>
<TabsList>
<TabsTrigger value="today">{t("today", "Today")}</TabsTrigger>
<TabsTrigger value="yesterday">
{t("yesterday", "Yesterday")}
</TabsTrigger>
</TabsList>
</Tabs>
</CardHeader>
<CardContent className="h-80">
{currentData.length > 0 ? (
<ChartContainer
className="max-h-80"
config={{
traffic: {
label: t("traffic", "Traffic"),
color: "var(--primary)",
},
type: {
label: t("type", "Type"),
color: "var(--muted-foreground)",
},
label: {
color: "var(--foreground)",
},
}}
>
<BarChart data={currentData} height={400} layout="vertical">
<CartesianGrid strokeDasharray="3 3" />
<XAxis
axisLine={false}
tickFormatter={(value) => formatBytes(value || 0)}
tickLine={false}
type="number"
/>
<YAxis
axisLine={false}
dataKey="name"
interval={0}
tickFormatter={(_value, index) => String(index + 1)}
tickLine={false}
tickMargin={0}
type="category"
width={15}
/>
<ChartTooltip
content={
<ChartTooltipContent
formatter={(value) => formatBytes(Number(value) || 0)}
label={true}
labelFormatter={(label, [payload]) =>
type === "nodes" ? (
`${t("nodes", "Nodes")}: ${label}`
) : (
<>
<div className="w-80">
<UserSubscribeDetail
enabled={true}
id={payload?.payload.name}
/>
</div>
<Separator className="my-2" />
<div>{`${t("users", "Users")}: ${label}`}</div>
</>
)
}
/>
}
trigger="hover"
/>
<Bar
dataKey="traffic"
fill="var(--primary)"
radius={[0, 4, 4, 0]}
>
<LabelList
className="fill-foreground"
dataKey="name"
fontSize={12}
offset={8}
position="insideLeft"
/>
</Bar>
</BarChart>
</ChartContainer>
) : (
<div className="flex h-full items-center justify-center">
<Empty />
</div>
)}
</CardContent>
</Card>
);
};
return (
<>
@ -189,122 +284,14 @@ export default function Statistics() {
))}
<SystemVersionCard />
</div>
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-2">
<RevenueStatisticsCard />
<UserStatisticsCard />
<Card>
<CardHeader className="!flex-row flex items-center justify-between">
<CardTitle>{t("trafficRank", "Traffic Rank")}</CardTitle>
<Tabs onValueChange={setTimeFrame} value={timeFrame}>
<TabsList>
<TabsTrigger value="today">{t("today", "Today")}</TabsTrigger>
<TabsTrigger value="yesterday">
{t("yesterday", "Yesterday")}
</TabsTrigger>
</TabsList>
</Tabs>
</CardHeader>
<CardContent className="h-80">
<div className="mb-6 flex items-center justify-between">
<h4 className="font-semibold">
{dataType === "nodes"
? t("nodeTraffic", "Node Traffic")
: t("userTraffic", "User Traffic")}
</h4>
<Select defaultValue="nodes" onValueChange={setDataType}>
<SelectTrigger className="w-28">
<SelectValue
placeholder={t("selectTypePlaceholder", "Select Type")}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="nodes">{t("nodes", "Nodes")}</SelectItem>
<SelectItem value="users">{t("users", "Users")}</SelectItem>
</SelectContent>
</Select>
</div>
{currentData.length > 0 ? (
<ChartContainer
className="max-h-80"
config={{
traffic: {
label: t("traffic", "Traffic"),
color: "var(--primary)",
},
type: {
label: t("type", "Type"),
color: "var(--muted-foreground)",
},
label: {
color: "var(--foreground)",
},
}}
>
<BarChart data={currentData} height={400} layout="vertical">
<CartesianGrid strokeDasharray="3 3" />
<XAxis
axisLine={false}
tickFormatter={(value) => formatBytes(value || 0)}
tickLine={false}
type="number"
/>
<YAxis
axisLine={false}
dataKey="name"
interval={0}
tickFormatter={(_value, index) => String(index + 1)}
tickLine={false}
tickMargin={0}
type="category"
width={15}
/>
<ChartTooltip
content={
<ChartTooltipContent
formatter={(value) => formatBytes(Number(value) || 0)}
label={true}
labelFormatter={(label, [payload]) =>
dataType === "nodes" ? (
`${t("nodes", "Nodes")}: ${label}`
) : (
<>
<div className="w-80">
<UserSubscribeDetail
enabled={true}
id={payload?.payload.name}
/>
</div>
<Separator className="my-2" />
<div>{`${t("users", "Users")}: ${label}`}</div>
</>
)
}
/>
}
trigger="hover"
/>
<Bar
dataKey="traffic"
fill="var(--primary)"
radius={[0, 4, 4, 0]}
>
<LabelList
className="fill-foreground"
dataKey="name"
fontSize={12}
offset={8}
position="insideLeft"
/>
</Bar>
</BarChart>
</ChartContainer>
) : (
<div className="flex h-full items-center justify-center">
<Empty />
</div>
)}
</CardContent>
</Card>
</div>
<div className="grid gap-3 md:grid-cols-2">
<TrafficRankCard type="nodes" />
<TrafficRankCard type="users" />
</div>
</>
);

View File

@ -168,8 +168,31 @@ export default function Page() {
...pagination,
...filters,
});
const list = (data.data?.list || []) as API.Ticket[];
// Client-side ordering to improve triage efficiency:
// - Put "Pending Follow-up" (status=1) before "Pending Reply" (status=2)
// - Within each group, sort by updated_at desc
const statusPriority = (status: number) => {
if (status === 1) return 0;
if (status === 2) return 1;
return 2;
};
const toTime = (value: any) => {
const t = new Date(value).getTime();
return Number.isFinite(t) ? t : 0;
};
list.sort((a, b) => {
const pa = statusPriority(a.status);
const pb = statusPriority(b.status);
if (pa !== pb) return pa - pb;
return toTime(b.updated_at) - toTime(a.updated_at);
});
return {
list: data.data?.list || [],
list,
total: data.data?.total || 0,
};
}}