add filter per kolom user

This commit is contained in:
vergiLgood1 2025-02-28 22:36:31 +07:00
parent dd24481574
commit 0c16fc2be5
5 changed files with 433 additions and 36 deletions

View File

@ -370,11 +370,11 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use
</div>
</div>
<SheetFooter className="mt-6">
{/* <SheetFooter className="mt-6">
<Button variant="outline" onClick={() => onOpenChange(false)}>
Close
</Button>
</SheetFooter>
</SheetFooter> */}
</SheetContent>
</Sheet>
)

View File

@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useState, useMemo } from "react";
import {
PlusCircle,
Search,
@ -10,6 +10,14 @@ import {
ChevronDown,
UserPlus,
Mail,
SortAsc,
SortDesc,
Mail as MailIcon,
Phone,
Clock,
Calendar,
ShieldAlert,
ListFilter,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@ -18,7 +26,9 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuCheckboxItem,
} from "@/components/ui/dropdown-menu";
import { useQuery } from "@tanstack/react-query";
import { fetchUsers } from "@/app/protected/(admin)/dashboard/user-management/action";
@ -36,6 +46,21 @@ export default function UserManagement() {
const [isAddUserOpen, setIsAddUserOpen] = useState(false);
const [isInviteUserOpen, setIsInviteUserOpen] = useState(false);
// Filter states
const [filters, setFilters] = useState<{
email: string;
phone: string;
lastSignIn: string;
createdAt: string;
status: string[];
}>({
email: "",
phone: "",
lastSignIn: "",
createdAt: "",
status: [],
});
// Use React Query to fetch users
const {
data: users = [],
@ -63,21 +88,150 @@ export default function UserManagement() {
setIsSheetOpen(false);
};
const filteredUsers = users.filter((user) => {
if (!searchQuery) return true;
const filteredUsers = useMemo(() => {
return users.filter((user) => {
// Global search
if (searchQuery) {
const query = searchQuery.toLowerCase();
const matchesSearch =
user.email?.toLowerCase().includes(query) ||
user.phone?.toLowerCase().includes(query) ||
user.id.toLowerCase().includes(query);
const query = searchQuery.toLowerCase();
return (
user.email?.toLowerCase().includes(query) ||
user.phone?.toLowerCase().includes(query) ||
user.id.toLowerCase().includes(query)
);
});
if (!matchesSearch) return false;
}
// Email filter
if (
filters.email &&
!user.email?.toLowerCase().includes(filters.email.toLowerCase())
) {
return false;
}
// Phone filter
if (
filters.phone &&
!user.phone?.toLowerCase().includes(filters.phone.toLowerCase())
) {
return false;
}
// Last sign in filter
if (filters.lastSignIn) {
if (filters.lastSignIn === "never" && user.last_sign_in_at) {
return false;
} else if (filters.lastSignIn === "today") {
const today = new Date();
today.setHours(0, 0, 0, 0);
const signInDate = user.last_sign_in_at
? new Date(user.last_sign_in_at)
: null;
if (!signInDate || signInDate < today) return false;
} else if (filters.lastSignIn === "week") {
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
const signInDate = user.last_sign_in_at
? new Date(user.last_sign_in_at)
: null;
if (!signInDate || signInDate < weekAgo) return false;
} else if (filters.lastSignIn === "month") {
const monthAgo = new Date();
monthAgo.setMonth(monthAgo.getMonth() - 1);
const signInDate = user.last_sign_in_at
? new Date(user.last_sign_in_at)
: null;
if (!signInDate || signInDate < monthAgo) return false;
}
}
// Created at filter
if (filters.createdAt) {
if (filters.createdAt === "today") {
const today = new Date();
today.setHours(0, 0, 0, 0);
const createdAt = new Date(user.created_at);
if (createdAt < today) return false;
} else if (filters.createdAt === "week") {
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
const createdAt = new Date(user.created_at);
if (createdAt < weekAgo) return false;
} else if (filters.createdAt === "month") {
const monthAgo = new Date();
monthAgo.setMonth(monthAgo.getMonth() - 1);
const createdAt = new Date(user.created_at);
if (createdAt < monthAgo) return false;
}
}
// Status filter
if (filters.status.length > 0) {
const userStatus = user.banned_until
? "banned"
: !user.email_confirmed_at
? "unconfirmed"
: "active";
if (!filters.status.includes(userStatus)) {
return false;
}
}
return true;
});
}, [users, searchQuery, filters]);
const clearFilters = () => {
setFilters({
email: "",
phone: "",
lastSignIn: "",
createdAt: "",
status: [],
});
};
const activeFilterCount = Object.values(filters).filter(
(value) =>
(typeof value === "string" && value !== "") ||
(Array.isArray(value) && value.length > 0)
).length;
const columns = [
{
id: "email",
header: "Email",
header: ({ column }: any) => (
<div className="flex items-center gap-1">
<span>Email</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<div className="p-2">
<Input
placeholder="Filter by email..."
value={filters.email}
onChange={(e) =>
setFilters({ ...filters, email: e.target.value })
}
className="w-full"
/>
</div>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => setFilters({ ...filters, email: "" })}
>
Clear filter
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
),
cell: ({ row }: { row: { original: User } }) => (
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-medium">
@ -93,21 +247,109 @@ export default function UserManagement() {
</div>
</div>
),
filterFn: (row: any, id: string, value: string) => {
return row.original.email?.toLowerCase().includes(value.toLowerCase());
},
},
{
id: "phone",
header: "Phone",
header: ({ column }: any) => (
<div className="flex items-center gap-1">
<span>Phone</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<div className="p-2">
<Input
placeholder="Filter by phone..."
value={filters.phone}
onChange={(e) =>
setFilters({ ...filters, phone: e.target.value })
}
className="w-full"
/>
</div>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => setFilters({ ...filters, phone: "" })}
>
Clear filter
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
),
cell: ({ row }: { row: { original: User } }) => row.original.phone || "-",
filterFn: (row: any, id: string, value: string) => {
return row.original.phone?.toLowerCase().includes(value.toLowerCase());
},
},
{
id: "lastSignIn",
header: "Last Sign In",
header: ({ column }: any) => (
<div className="flex items-center gap-1">
<span>Last Sign In</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuCheckboxItem
checked={filters.lastSignIn === "today"}
onCheckedChange={() =>
setFilters({
...filters,
lastSignIn: filters.lastSignIn === "today" ? "" : "today",
})
}
>
Today
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={filters.lastSignIn === "week"}
onCheckedChange={() =>
setFilters({
...filters,
lastSignIn: filters.lastSignIn === "week" ? "" : "week",
})
}
>
Last 7 days
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={filters.lastSignIn === "month"}
onCheckedChange={() =>
setFilters({
...filters,
lastSignIn: filters.lastSignIn === "month" ? "" : "month",
})
}
>
Last 30 days
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={filters.lastSignIn === "never"}
onCheckedChange={() =>
setFilters({
...filters,
lastSignIn: filters.lastSignIn === "never" ? "" : "never",
})
}
>
Never
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => setFilters({ ...filters, lastSignIn: "" })}
>
Clear filter
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
),
cell: ({ row }: { row: { original: User } }) => {
return row.original.last_sign_in_at
? new Date(row.original.last_sign_in_at).toLocaleString()
@ -116,14 +358,129 @@ export default function UserManagement() {
},
{
id: "createdAt",
header: "Created At",
header: ({ column }: any) => (
<div className="flex items-center gap-1">
<span>Created At</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuCheckboxItem
checked={filters.createdAt === "today"}
onCheckedChange={() =>
setFilters({
...filters,
createdAt: filters.createdAt === "today" ? "" : "today",
})
}
>
Today
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={filters.createdAt === "week"}
onCheckedChange={() =>
setFilters({
...filters,
createdAt: filters.createdAt === "week" ? "" : "week",
})
}
>
Last 7 days
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={filters.createdAt === "month"}
onCheckedChange={() =>
setFilters({
...filters,
createdAt: filters.createdAt === "month" ? "" : "month",
})
}
>
Last 30 days
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => setFilters({ ...filters, createdAt: "" })}
>
Clear filter
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
),
cell: ({ row }: { row: { original: User } }) => {
return new Date(row.original.created_at).toLocaleString();
},
},
{
id: "status",
header: "Status",
header: ({ column }: any) => (
<div className="flex items-center gap-1">
<span>Status</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuCheckboxItem
checked={filters.status.includes("active")}
onCheckedChange={() => {
const newStatus = [...filters.status];
if (newStatus.includes("active")) {
newStatus.splice(newStatus.indexOf("active"), 1);
} else {
newStatus.push("active");
}
setFilters({ ...filters, status: newStatus });
}}
>
Active
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={filters.status.includes("unconfirmed")}
onCheckedChange={() => {
const newStatus = [...filters.status];
if (newStatus.includes("unconfirmed")) {
newStatus.splice(newStatus.indexOf("unconfirmed"), 1);
} else {
newStatus.push("unconfirmed");
}
setFilters({ ...filters, status: newStatus });
}}
>
Unconfirmed
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={filters.status.includes("banned")}
onCheckedChange={() => {
const newStatus = [...filters.status];
if (newStatus.includes("banned")) {
newStatus.splice(newStatus.indexOf("banned"), 1);
} else {
newStatus.push("banned");
}
setFilters({ ...filters, status: newStatus });
}}
>
Banned
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => setFilters({ ...filters, status: [] })}
>
Clear filter
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
),
cell: ({ row }: { row: { original: User } }) => {
if (row.original.banned_until) {
return <Badge variant="destructive">Banned</Badge>;
@ -133,14 +490,6 @@ export default function UserManagement() {
}
return <Badge variant="default">Active</Badge>;
},
filterFn: (row: any, id: string, value: string) => {
const status = row.original.banned_until
? "banned"
: !row.original.email_confirmed_at
? "unconfirmed"
: "active";
return status.includes(value.toLowerCase());
},
},
{
id: "actions",
@ -202,8 +551,19 @@ export default function UserManagement() {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" size="icon">
<Filter className="h-4 w-4" />
<Button
variant="outline"
size="icon"
className={activeFilterCount > 0 ? "relative" : ""}
onClick={clearFilters}
>
<ListFilter className="h-4 w-4" />
{activeFilterCount > 0 && (
<span className="absolute -top-1 -right-1 bg-primary text-primary-foreground rounded-full w-4 h-4 text-xs flex items-center justify-center">
{activeFilterCount}
</span>
)}
</Button>
</div>
</div>

View File

@ -9,8 +9,6 @@ const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
@ -30,4 +28,4 @@ const PopoverContent = React.forwardRef<
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
export { Popover, PopoverTrigger, PopoverContent }

View File

@ -14,6 +14,7 @@
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
@ -2044,6 +2045,43 @@
}
}
},
"node_modules/@radix-ui/react-popover": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.6.tgz",
"integrity": "sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.5",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.2",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-popper": "1.2.2",
"@radix-ui/react-portal": "1.1.4",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-slot": "1.1.2",
"@radix-ui/react-use-controllable-state": "1.1.0",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz",

View File

@ -19,6 +19,7 @@
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",