From 0c16fc2be547869d214077fd51a619dadf3d5a04 Mon Sep 17 00:00:00 2001 From: vergiLgood1 Date: Fri, 28 Feb 2025 22:36:31 +0700 Subject: [PATCH] add filter per kolom user --- .../components/admin/users/sheet.tsx | 4 +- .../admin/users/user-management.tsx | 422 ++++++++++++++++-- sigap-website/components/ui/popover.tsx | 4 +- sigap-website/package-lock.json | 38 ++ sigap-website/package.json | 1 + 5 files changed, 433 insertions(+), 36 deletions(-) diff --git a/sigap-website/components/admin/users/sheet.tsx b/sigap-website/components/admin/users/sheet.tsx index 7cc0438..7f998f1 100644 --- a/sigap-website/components/admin/users/sheet.tsx +++ b/sigap-website/components/admin/users/sheet.tsx @@ -370,11 +370,11 @@ export function UserDetailsSheet({ open, onOpenChange, user, onUserUpdate }: Use - + {/* - + */} ) diff --git a/sigap-website/components/admin/users/user-management.tsx b/sigap-website/components/admin/users/user-management.tsx index 89c04be..bf78c06 100644 --- a/sigap-website/components/admin/users/user-management.tsx +++ b/sigap-website/components/admin/users/user-management.tsx @@ -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) => ( +
+ + Email + + + + + +
+ + setFilters({ ...filters, email: e.target.value }) + } + className="w-full" + /> +
+ + setFilters({ ...filters, email: "" })} + > + Clear filter + +
+
+
+ ), cell: ({ row }: { row: { original: User } }) => (
@@ -93,21 +247,109 @@ export default function UserManagement() {
), - filterFn: (row: any, id: string, value: string) => { - return row.original.email?.toLowerCase().includes(value.toLowerCase()); - }, }, { id: "phone", - header: "Phone", + header: ({ column }: any) => ( +
+ + Phone + + + + + +
+ + setFilters({ ...filters, phone: e.target.value }) + } + className="w-full" + /> +
+ + setFilters({ ...filters, phone: "" })} + > + Clear filter + +
+
+
+ ), 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) => ( +
+ + Last Sign In + + + + + + + setFilters({ + ...filters, + lastSignIn: filters.lastSignIn === "today" ? "" : "today", + }) + } + > + Today + + + setFilters({ + ...filters, + lastSignIn: filters.lastSignIn === "week" ? "" : "week", + }) + } + > + Last 7 days + + + setFilters({ + ...filters, + lastSignIn: filters.lastSignIn === "month" ? "" : "month", + }) + } + > + Last 30 days + + + setFilters({ + ...filters, + lastSignIn: filters.lastSignIn === "never" ? "" : "never", + }) + } + > + Never + + + setFilters({ ...filters, lastSignIn: "" })} + > + Clear filter + + + +
+ ), 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) => ( +
+ + Created At + + + + + + + setFilters({ + ...filters, + createdAt: filters.createdAt === "today" ? "" : "today", + }) + } + > + Today + + + setFilters({ + ...filters, + createdAt: filters.createdAt === "week" ? "" : "week", + }) + } + > + Last 7 days + + + setFilters({ + ...filters, + createdAt: filters.createdAt === "month" ? "" : "month", + }) + } + > + Last 30 days + + + setFilters({ ...filters, createdAt: "" })} + > + Clear filter + + + +
+ ), cell: ({ row }: { row: { original: User } }) => { return new Date(row.original.created_at).toLocaleString(); }, }, { id: "status", - header: "Status", + header: ({ column }: any) => ( +
+ + Status + + + + + + { + const newStatus = [...filters.status]; + if (newStatus.includes("active")) { + newStatus.splice(newStatus.indexOf("active"), 1); + } else { + newStatus.push("active"); + } + setFilters({ ...filters, status: newStatus }); + }} + > + Active + + { + const newStatus = [...filters.status]; + if (newStatus.includes("unconfirmed")) { + newStatus.splice(newStatus.indexOf("unconfirmed"), 1); + } else { + newStatus.push("unconfirmed"); + } + setFilters({ ...filters, status: newStatus }); + }} + > + Unconfirmed + + { + const newStatus = [...filters.status]; + if (newStatus.includes("banned")) { + newStatus.splice(newStatus.indexOf("banned"), 1); + } else { + newStatus.push("banned"); + } + setFilters({ ...filters, status: newStatus }); + }} + > + Banned + + + setFilters({ ...filters, status: [] })} + > + Clear filter + + + +
+ ), cell: ({ row }: { row: { original: User } }) => { if (row.original.banned_until) { return Banned; @@ -133,14 +490,6 @@ export default function UserManagement() { } return Active; }, - 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() { - diff --git a/sigap-website/components/ui/popover.tsx b/sigap-website/components/ui/popover.tsx index 29c7bd2..a0ec48b 100644 --- a/sigap-website/components/ui/popover.tsx +++ b/sigap-website/components/ui/popover.tsx @@ -9,8 +9,6 @@ const Popover = PopoverPrimitive.Root const PopoverTrigger = PopoverPrimitive.Trigger -const PopoverAnchor = PopoverPrimitive.Anchor - const PopoverContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef @@ -30,4 +28,4 @@ const PopoverContent = React.forwardRef< )) PopoverContent.displayName = PopoverPrimitive.Content.displayName -export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } +export { Popover, PopoverTrigger, PopoverContent } diff --git a/sigap-website/package-lock.json b/sigap-website/package-lock.json index 369da81..7d88891 100644 --- a/sigap-website/package-lock.json +++ b/sigap-website/package-lock.json @@ -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", diff --git a/sigap-website/package.json b/sigap-website/package.json index 3580eb3..7de0b6e 100644 --- a/sigap-website/package.json +++ b/sigap-website/package.json @@ -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",