listing-facebook/client/app/routes/users/list.tsx

371 lines
10 KiB
TypeScript

"use client";
import { useQuery } from "@tanstack/react-query";
import _ from "lodash";
import {
Archive,
Download,
Plus,
Share,
Star,
Trash2,
UserPlus,
} from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useSearchParams } from "react-router";
import { toast } from "sonner";
import { userApi } from "~/api/user-api.service";
import {
DataTable,
type Column,
type TableState,
} from "~/components/core/data-table";
import Loader from "~/components/loader";
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import { delay } from "~/features/delay";
import {
stateToURLQuery,
urlQueryToState,
} from "~/features/state-url-converter";
import { useAppSelector } from "~/hooks/use-app-dispatch";
import { useUsersContext } from "~/layouts/contexts/user-layout.context";
import type { RootState } from "~/store";
import UserModal, { useUserModal } from "./components/users/user-modal";
export default function List() {
const { setEdit } = useUserModal();
const { setTitle, setDescription, setAction } = useUsersContext();
const [searchParams, setSearchParams] = useSearchParams();
const prevStates = useRef<Record<string, any>>(null);
const [initStates, setInitStates] = useState<Record<string, any> | undefined>(
undefined
);
const [states, setStates] = useState<TableState<IUser> | undefined>(
undefined
);
const { user } = useAppSelector((state: RootState) => state.app);
const handleDelete = async (data: IUser) => {
const result = await userApi.delete(data);
if (!result) return;
refetch();
};
const handleBulkDelete = async (selectedUsers: IUser[]) => {
const result = await userApi.bulkDelete(selectedUsers);
if (!result) return;
refetch();
toast.info(`Đã xóa ${selectedUsers.length} người dùng thành công.`, {
duration: 3000,
description: "Các người dùng đã được xóa khỏi hệ thống.",
});
};
const columns: Column<IUser>[] = [
{
key: "email",
label: "Nhân viên",
displayType: "custom",
sortable: true,
render(value, row) {
return (
<div className="flex items-center space-x-3">
<Avatar className="h-9 w-9">
<AvatarImage src={row.avatar} alt={"employee.name"} />
<AvatarFallback className="text-sm">
{row.username.charAt(0)}
</AvatarFallback>
</Avatar>
<div>
<div className="font-medium">{row.username}</div>
<div className="text-sm text-gray-500">{row.email}</div>
</div>
</div>
);
},
},
{
key: "roles",
label: "Vai trò",
displayType: "custom",
render: (value, row) => {
if (!row.roles.length) {
return <Badge variant={"secondary"}>Chưa vai trò</Badge>;
}
return (
<div className="flex items-center gap-1 max-w-[200px] flex-wrap">
{row.roles.map((role) => {
return <Badge key={role.id}>{role.role_name}</Badge>;
})}
</div>
);
},
},
{
key: "active",
label: "Trạng thái",
displayType: "status",
sortable: true,
displayOptions: {
statusMap: {
true: {
label: "Hoạt động",
variant: "default",
color: "bg-green-100 text-green-800 border-green-200",
},
false: {
label: "Ngừng hoạt động",
variant: "secondary",
color: "bg-gray-100 text-gray-800 border-gray-200",
},
},
},
},
{
key: "is_login_enabled",
label: "Quyền truy cập",
displayType: "status",
sortable: true,
displayOptions: {
statusMap: {
true: {
label: "Có",
variant: "default",
color: "bg-green-100 text-green-800 border-green-200",
},
false: {
label: "Không",
variant: "secondary",
color: "bg-gray-100 text-gray-800 border-gray-200",
},
},
},
},
{
key: "created_at",
label: "Ngày tham gia",
displayType: "datetime",
sortable: true,
},
];
const filterOptions: FilterOption[] = [
{
key: "email",
label: "Email",
type: "text" as const,
},
{
key: "first_name",
label: "First name",
type: "text" as const,
},
{
key: "last_name",
label: "Last name",
type: "text" as const,
},
{
key: "created_at",
label: "Ngày tham gia",
type: "dateRange" as const,
},
];
const bulkActions = [
{
key: "delete",
label: "Xóa",
icon: <Trash2 className="h-4 w-4 text-destructive" />,
variant: "destructive" as const,
action: handleBulkDelete,
confirmMessage:
"Bạn có chắc chắn muốn xóa các người dùng đã chọn? Hành động này không thể hoàn tác.",
},
];
// 🎯 Custom Actions - Đa dạng và có điều kiện
const customActions = [
// 📧 Communication actions
// 📊 Export actions
{
key: "export-profile",
label: "Xuất hồ sơ",
icon: <Download className="h-4 w-4" />,
action: (employee: any) => {},
},
// 🌟 Rating actions
{
key: "add-review",
label: "Thêm đánh giá",
icon: <Star className="h-4 w-4" />,
action: (employee: any) => {},
show: (employee: any) =>
employee.status === "active" && !employee.isManager, // Chỉ đánh giá nhân viên, không đánh giá manager
confirmMessage:
"Bạn có chắc chắn muốn xóa các người dùng đã chọn? Hành động này không thể hoàn tác.",
},
// 👥 Management actions
{
key: "promote-to-manager",
label: "Thăng chức Manager",
icon: <UserPlus className="h-4 w-4" />,
variant: "default" as const,
action: (employee: any) => {},
show: (employee: any) =>
employee.status === "active" &&
!employee.isManager &&
(employee.level === "Senior" || employee.level === "Mid"), // Chỉ Senior/Mid mới có thể thăng chức
},
// 📤 Share actions
{
key: "share-profile",
label: "Chia sẻ hồ sơ",
icon: <Share className="h-4 w-4" />,
action: (employee: any) => {
const profileUrl = `${window.location.origin}/employees/${employee.id}`;
navigator.clipboard.writeText(profileUrl);
},
},
// 📁 Archive actions
{
key: "archive",
label: "Lưu trữ",
icon: <Archive className="h-4 w-4" />,
variant: "secondary" as const,
action: (employee: any) => {},
show: (employee: any) => employee.status === "terminated", // Chỉ lưu trữ nhân viên đã nghỉ
},
];
useEffect(() => {
setTitle("Danh sách nhân viên");
setDescription("Bạn có thể thêm, sửa, xóa và tìm kiếm nhân viên tại đây.");
setAction?.(
<UserModal onSubmited={refetch}>
<Button className="cursor-pointer">
<Plus className="mr-2 h-4 w-4" />
Thêm nhân viên
</Button>
</UserModal>
);
}, []);
// Init state từ URL
useEffect(() => {
const s = urlQueryToState(searchParams);
setInitStates(s);
setStates((prev) => (prev ? prev : s) as TableState<IUser>); // chỉ gán nếu chưa có state
}, []);
// Chuyển state sang query string + kiểm tra nếu khác với lần trước mới update
useEffect(() => {
if (!states) return;
const query = stateToURLQuery(states);
setSearchParams(query);
const { data, ...curStates } = states;
if (_.isEqual(curStates, prevStates.current)) return;
prevStates.current = curStates;
}, [states]);
// 👉 Ổn định queryKey để tránh refetch không cần thiết
const queryKey = useMemo(() => ["users", states], [states]);
const { data, isLoading, refetch } = useQuery({
queryKey,
queryFn: async () => {
if (!states) return [];
await delay(300); // Giả lập delay để thấy loading
const res = await userApi.index(states as TableState<IUser>);
return res;
},
enabled: !!states,
});
if (!initStates) return <Loader />;
return (
<div className="">
<DataTable
initialState={initStates}
customActions={customActions}
data={data?.data || []}
columns={columns}
searchKeys={["email"]}
filterOptions={filterOptions}
selectable={true}
bulkActions={bulkActions}
pagination={{
currentPage: data?.current_page || 1,
pageSize: data?.per_page || 10,
totalItems: data?.total || 0,
totalPages: data?.last_page || 0,
startIndex: data?.from || 0,
endIndex: data?.to || 0,
}}
loading={isLoading}
onPaginationChange={(p) => {
setStates(
(prev) =>
({
...prev,
pagination: {
...prev?.pagination,
currentPage: p.currentPage,
pageSize: p.pageSize,
},
} as TableState<IUser>)
);
}}
onFilterChange={({ search, ...filters }) => {
setStates(
(prev) =>
({
...prev,
filters,
search,
} as TableState<IUser>)
);
}}
onSortChange={(sort) => {
setStates(
(prev) =>
({
...prev,
sort,
} as TableState<IUser>)
);
}}
onDelete={handleDelete}
onEdit={setEdit}
options={{
disableDel(data) {
return data.id === user?.id;
},
}}
/>
</div>
);
}