MIF_E31221222/sigap-website/app/_components/admin/users/user-management.tsx

632 lines
20 KiB
TypeScript

"use client";
import { useState, useMemo, useEffect } from "react";
import {
PlusCircle,
Search,
Filter,
MoreHorizontal,
X,
ChevronDown,
UserPlus,
Mail,
SortAsc,
SortDesc,
Mail as MailIcon,
Phone,
Clock,
Calendar,
ShieldAlert,
ListFilter,
} from "lucide-react";
import { Button } from "@/app/_components/ui/button";
import { Input } from "@/app/_components/ui/input";
import { Badge } from "@/app/_components/ui/badge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuCheckboxItem,
} from "@/app/_components/ui/dropdown-menu";
import { useQuery } from "@tanstack/react-query";
import { fetchUsers } from "@/app/(protected)/(admin)/dashboard/user-management/action";
import { User } from "@/src/models/users/users.model";
import { toast } from "sonner";
import { DataTable } from "./data-table";
import { InviteUserDialog } from "./invite-user";
import { AddUserDialog } from "./add-user-dialog";
import { UserDetailsSheet } from "./sheet";
import { Avatar } from "@radix-ui/react-avatar";
import Image from "next/image";
export default function UserManagement() {
const [searchQuery, setSearchQuery] = useState("");
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [isSheetOpen, setIsSheetOpen] = useState(false);
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 [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const fetchedUsers = await fetchUsers();
setUsers(fetchedUsers);
} catch (error) {
toast.error("Failed to fetch users");
} finally {
setIsLoading(false);
}
};
fetchData();
}, []);
const refetch = async () => {
setIsLoading(true);
try {
const fetchedUsers = await fetchUsers();
setUsers(fetchedUsers);
} catch (error) {
toast.error("Failed to fetch users");
} finally {
setIsLoading(false);
}
};
const handleUserClick = (user: User) => {
setSelectedUser(user);
setIsSheetOpen(true);
};
const handleUserUpdate = () => {
refetch();
setIsSheetOpen(false);
};
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);
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 = user.created_at
? user.created_at
? new Date(user.created_at)
: new Date()
: new Date();
if (createdAt < today) return false;
} else if (filters.createdAt === "week") {
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
const createdAt = user.created_at
? new Date(user.created_at)
: new Date();
if (createdAt < weekAgo) return false;
} else if (filters.createdAt === "month") {
const monthAgo = new Date();
monthAgo.setMonth(monthAgo.getMonth() - 1);
const createdAt = user.created_at
? new Date(user.created_at)
: new Date();
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: ({ 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">
<Avatar className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-medium">
{row.original.profile?.avatar ? (
<Image
src={row.original.profile.avatar}
alt="Avatar"
className="w-full h-full rounded-full"
width={32}
height={32}
/>
) : (
row.original.email?.[0]?.toUpperCase() || "?"
)}
</Avatar>
<div>
<div className="font-medium">
{row.original.email || "No email"}
</div>
<div className="text-xs text-muted-foreground">
{row.original.id}
</div>
</div>
</div>
),
},
{
id: "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 || "-",
},
{
id: "lastSignIn",
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()
: "Never";
},
},
{
id: "createdAt",
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 row.original.created_at
? new Date(row.original.created_at).toLocaleString()
: "N/A";
},
},
{
id: "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>;
}
if (!row.original.email_confirmed_at) {
return <Badge variant="outline">Unconfirmed</Badge>;
}
return <Badge variant="default">Active</Badge>;
},
},
{
id: "actions",
header: "",
cell: ({ row }: { row: { original: User } }) => (
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
handleUserClick(row.original);
}}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
),
},
];
return (
<div className="space-y-4">
<div className="flex flex-col sm:flex-row gap-4 justify-between items-start sm:items-center">
<div className="relative w-full sm:w-72">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search users..."
className="pl-8"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
{searchQuery && (
<Button
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-9 w-9"
onClick={() => setSearchQuery("")}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
<div className="flex gap-2 w-full sm:w-auto">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="gap-1">
<PlusCircle className="h-4 w-4" />
Add User
<ChevronDown className="h-4 w-4 ml-1" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setIsAddUserOpen(true)}>
<UserPlus className="h-4 w-4 mr-2" />
Create new user
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsInviteUserOpen(true)}>
<Mail className="h-4 w-4 mr-2" />
Send invitation
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<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>
<DataTable
columns={columns}
data={filteredUsers}
loading={isLoading}
onRowClick={(user) => handleUserClick(user)}
/>
{selectedUser && (
<UserDetailsSheet
user={selectedUser}
open={isSheetOpen}
onOpenChange={setIsSheetOpen}
onUserUpdate={handleUserUpdate}
/>
)}
<AddUserDialog
open={isAddUserOpen}
onOpenChange={setIsAddUserOpen}
onUserAdded={() => refetch()}
/>
<InviteUserDialog
open={isInviteUserOpen}
onOpenChange={setIsInviteUserOpen}
onUserInvited={() => refetch()}
/>
</div>
);
}