371 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			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 có 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>
 | 
						|
  );
 | 
						|
}
 |