mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useUsers } from "@/contexts/AdminUserContext";
|
||||
import { House, RefreshCw, ChevronLeft, ChevronRight, ExternalLink, CalendarIcon } from "lucide-react";
|
||||
import { House, RefreshCw, ChevronLeft, ChevronRight, ExternalLink } from "lucide-react";
|
||||
|
||||
import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -9,9 +9,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { DatePickerButton } from "@/components/generic/DatePickerButton";
|
||||
|
||||
import { ACTION_CONFIG, getActionBadge } from "@/data/activity.data";
|
||||
import { timeAgo } from "@/utils/timestamp.util";
|
||||
@@ -195,57 +194,6 @@ export default function ActivityFeed() {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── DatePickerButton ─────────────────────────────────────────────────────────
|
||||
function DatePickerButton({ value, onChange, placeholder, disabled }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { fmtDate } = useDateFormat();
|
||||
|
||||
const label = value ? fmtDate(value) : placeholder;
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={`h-8 text-sm w-[150px] justify-start font-normal gap-2 ${!value ? "text-muted-foreground" : ""}`}
|
||||
>
|
||||
<CalendarIcon className="size-3.5 shrink-0" />
|
||||
<span className="truncate">{label}</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={value}
|
||||
onSelect={(d) => { onChange(d ?? null); setOpen(false); }}
|
||||
disabled={disabled}
|
||||
initialFocus
|
||||
/>
|
||||
<div className="border-t px-3 py-2 flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-xs"
|
||||
onClick={() => { onChange(new Date()); setOpen(false); }}
|
||||
>
|
||||
Today
|
||||
</Button>
|
||||
{value && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-xs text-muted-foreground"
|
||||
onClick={() => { onChange(null); setOpen(false); }}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Row ──────────────────────────────────────────────────────────────────────
|
||||
function initials(name, email) {
|
||||
if (name) return name.split(" ").map((n) => n[0]).slice(0, 2).join("").toUpperCase();
|
||||
@@ -293,10 +241,10 @@ function ActivityRow({ row, onViewUser }) {
|
||||
{ts ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-muted-foreground cursor-default">{timeAgo(ts)}</span>
|
||||
<span className="text-muted-foreground cursor-default">{fmtDateTime(ts)}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
{fmtDateTime(ts)}
|
||||
{timeAgo(ts)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
|
||||
@@ -8,13 +8,20 @@ import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { DatePickerButton } from "@/components/generic/DatePickerButton";
|
||||
|
||||
import { ACTION_CONFIG, getActionBadge } from "@/data/activity.data";
|
||||
import { timeAgo } from "@/utils/timestamp.util";
|
||||
import { fmtISO } from "@/utils/datetime.util";
|
||||
import { useDateFormat } from "@/hooks/useDateFormat";
|
||||
|
||||
const LIMIT = 20;
|
||||
|
||||
function toDateStr(d) {
|
||||
if (!d) return undefined;
|
||||
return fmtISO(d);
|
||||
}
|
||||
|
||||
export default function UserActivityPage() {
|
||||
const { userId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
@@ -22,6 +29,8 @@ export default function UserActivityPage() {
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [action, setAction] = useState("all");
|
||||
const [from, setFrom] = useState(null);
|
||||
const [to, setTo] = useState(null);
|
||||
|
||||
const load = useCallback(
|
||||
(p = 1) => {
|
||||
@@ -29,14 +38,16 @@ export default function UserActivityPage() {
|
||||
page: p,
|
||||
limit: LIMIT,
|
||||
action: action === "all" ? undefined : action,
|
||||
from: toDateStr(from),
|
||||
to: toDateStr(to),
|
||||
});
|
||||
setPage(p);
|
||||
},
|
||||
[fetchUserActivity, userId, action]
|
||||
[fetchUserActivity, userId, action, from, to]
|
||||
);
|
||||
|
||||
useEffect(() => { fetchUser(userId); }, [userId]);
|
||||
useEffect(() => { load(1); }, [action, userId]);
|
||||
useEffect(() => { load(1); }, [action, from, to, userId]);
|
||||
|
||||
const displayName = user?.personal_info?.name?.full_name ?? user?.email ?? `User #${userId}`;
|
||||
|
||||
@@ -76,7 +87,7 @@ export default function UserActivityPage() {
|
||||
</div>
|
||||
|
||||
{/* ─── Filter ──────────────────────────────────────────────── */}
|
||||
<div className="flex items-center gap-3 bg-card border rounded-lg p-4">
|
||||
<div className="flex flex-wrap items-end gap-3 bg-card border rounded-lg p-4">
|
||||
<div className="flex flex-col gap-1 min-w-[180px]">
|
||||
<span className="text-xs text-muted-foreground">Filter by action</span>
|
||||
<Select value={action} onValueChange={setAction}>
|
||||
@@ -91,10 +102,26 @@ export default function UserActivityPage() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{action !== "all" && (
|
||||
<div className="flex items-end pt-5">
|
||||
<Button variant="ghost" size="sm" className="h-8 text-xs" onClick={() => setAction("all")}>
|
||||
Clear
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted-foreground">From</span>
|
||||
<DatePickerButton value={from} onChange={setFrom} placeholder="Start date" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted-foreground">To</span>
|
||||
<DatePickerButton value={to} onChange={setTo} placeholder="End date" disabled={from ? { before: from } : undefined} />
|
||||
</div>
|
||||
|
||||
{(action !== "all" || from || to) && (
|
||||
<div className="flex items-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 text-xs"
|
||||
onClick={() => { setAction("all"); setFrom(null); setTo(null); }}
|
||||
>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -187,10 +214,10 @@ function ActivityItem({ row }) {
|
||||
{ts ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-muted-foreground cursor-default">{timeAgo(ts)}</span>
|
||||
<span className="text-muted-foreground cursor-default">{fmtDateTime(ts)}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
{fmtDateTime(ts)}
|
||||
{timeAgo(ts)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user