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:
parent
370d59d5ad
commit
116f6e5360
@ -13,13 +13,7 @@ import {
|
|||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
} from "@workspace/ui/components/chart";
|
} from "@workspace/ui/components/chart";
|
||||||
import {
|
// (Select imports removed)
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@workspace/ui/components/select";
|
|
||||||
import { Separator } from "@workspace/ui/components/separator";
|
import { Separator } from "@workspace/ui/components/separator";
|
||||||
import { Tabs, TabsList, TabsTrigger } from "@workspace/ui/components/tabs";
|
import { Tabs, TabsList, TabsTrigger } from "@workspace/ui/components/tabs";
|
||||||
import Empty from "@workspace/ui/composed/empty";
|
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">(
|
const [timeFrame, setTimeFrame] = useState<string | "today" | "yesterday">(
|
||||||
"today"
|
"today"
|
||||||
);
|
);
|
||||||
@ -93,10 +86,112 @@ export default function Statistics() {
|
|||||||
})) || [],
|
})) || [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const currentData =
|
|
||||||
trafficData[dataType as "nodes" | "users"][
|
const TrafficRankCard = ({ type }: { type: "nodes" | "users" }) => {
|
||||||
timeFrame as "today" | "yesterday"
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -189,122 +284,14 @@ export default function Statistics() {
|
|||||||
))}
|
))}
|
||||||
<SystemVersionCard />
|
<SystemVersionCard />
|
||||||
</div>
|
</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 />
|
<RevenueStatisticsCard />
|
||||||
<UserStatisticsCard />
|
<UserStatisticsCard />
|
||||||
<Card>
|
</div>
|
||||||
<CardHeader className="!flex-row flex items-center justify-between">
|
|
||||||
<CardTitle>{t("trafficRank", "Traffic Rank")}</CardTitle>
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
<Tabs onValueChange={setTimeFrame} value={timeFrame}>
|
<TrafficRankCard type="nodes" />
|
||||||
<TabsList>
|
<TrafficRankCard type="users" />
|
||||||
<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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -168,8 +168,31 @@ export default function Page() {
|
|||||||
...pagination,
|
...pagination,
|
||||||
...filters,
|
...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 {
|
return {
|
||||||
list: data.data?.list || [],
|
list,
|
||||||
total: data.data?.total || 0,
|
total: data.data?.total || 0,
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user