update table

This commit is contained in:
2026-04-09 15:36:16 +07:00
parent 430063e913
commit b4a667e764
4 changed files with 159 additions and 102 deletions

View File

@@ -8,6 +8,7 @@ import { Modal } from "@/components/ui/modal";
import { responseUserTable, getUserDto, fullDataUser } from "@/interface/admin"; import { responseUserTable, getUserDto, fullDataUser } from "@/interface/admin";
import { import {
apiDeleteUser, apiDeleteUser,
apiGetAllRole,
apiGetListUser, apiGetListUser,
apiRestoreUser, apiRestoreUser,
} from "@/service/adminService"; } from "@/service/adminService";
@@ -17,19 +18,18 @@ import { toast } from "sonner";
export type SortColumn = "created_at" | "updated_at" | "display_name" | "email"; export type SortColumn = "created_at" | "updated_at" | "display_name" | "email";
export default function UserTable() { export default function UserTable() {
const [limit, setLimit] = useState<number>(5);
const [limitInput, setLimitInput] = useState<string>("5");
const [page, setPage] = useState<number>(1); const [page, setPage] = useState<number>(1);
const [limitInput, setLimitInput] = useState<string>("5");
const [selectedRole, setSelectedRole] = useState<string>("");
const [roles, setRoles] = useState<{ id: string; name: string }[]>([]);
const [searchTerm, setSearchTerm] = useState<string>(""); const [searchTerm, setSearchTerm] = useState<string>("");
const [authProvider, setAuthProvider] = useState<string>(""); const [authProvider, setAuthProvider] = useState<string>("");
const [createdFrom, setCreatedFrom] = useState<string>("");
const [createdTo, setCreatedTo] = useState<string>("");
const [isDeleted, setIsDeleted] = useState<boolean | undefined>(undefined); const [isDeleted, setIsDeleted] = useState<boolean | undefined>(undefined);
const [selectedUser, setSelectedUser] = useState<fullDataUser | null>(null); const [selectedUser, setSelectedUser] = useState<fullDataUser | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [roleUser, setRoleUser] = useState<fullDataUser | null>(null); const [roleUser, setRoleUser] = useState<fullDataUser | null>(null);
const [isRoleModalOpen, setIsRoleModalOpen] = useState(false); const [isRoleModalOpen, setIsRoleModalOpen] = useState(false);
@@ -44,6 +44,21 @@ export default function UserTable() {
const [sortBy, setSortBy] = useState<SortColumn | undefined>(undefined); const [sortBy, setSortBy] = useState<SortColumn | undefined>(undefined);
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
useEffect(() => {
const fetchRoles = async () => {
try {
const res = await apiGetAllRole();
console.log("Danh sách role:", res);
if (res?.status) {
setRoles(res.data);
}
} catch (err) {
console.error("Lỗi lấy danh sách role:", err);
}
};
fetchRoles();
}, []);
console.log("Roles đã fetch:", roles);
useEffect(() => { useEffect(() => {
const handler = setTimeout(() => { const handler = setTimeout(() => {
setDebouncedParams({ setDebouncedParams({
@@ -64,15 +79,13 @@ export default function UserTable() {
limit: debouncedParams.limit, limit: debouncedParams.limit,
search: debouncedParams.search || undefined, search: debouncedParams.search || undefined,
auth_provider: debouncedParams.authProvider || undefined, auth_provider: debouncedParams.authProvider || undefined,
created_from: createdFrom || undefined,
created_to: createdTo || undefined,
is_deleted: isDeleted, is_deleted: isDeleted,
sort: sortBy, sort: sortBy,
order: sortOrder, order: sortOrder,
role_ids: selectedRole ? [selectedRole] : undefined,
}; };
const response = await apiGetListUser(payload); const response = await apiGetListUser(payload);
if (response?.status) { if (response?.status) {
setTableData(response); setTableData(response);
} }
@@ -82,15 +95,7 @@ export default function UserTable() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [ }, [page, debouncedParams, isDeleted, sortBy, sortOrder, selectedRole]);
page,
debouncedParams,
createdFrom,
createdTo,
isDeleted,
sortBy,
sortOrder,
]);
useEffect(() => { useEffect(() => {
fetchUsers(); fetchUsers();
@@ -112,34 +117,39 @@ export default function UserTable() {
setSelectedUser(user); setSelectedUser(user);
setIsModalOpen(true); setIsModalOpen(true);
}; };
const handleOpenRoleModal = (user: fullDataUser) => { const handleOpenRoleModal = (user: fullDataUser) => {
setRoleUser(user); setRoleUser(user);
setIsRoleModalOpen(true); setIsRoleModalOpen(true);
}; };
const handleDelete = async (user: fullDataUser) => { const handleDelete = async (user: fullDataUser) => {
const confirmMessage = `Bạn có chắc chắn muốn khóa/xóa người dùng ${user.profile?.display_name || user.email}?`; if (
if (window.confirm(confirmMessage)) { window.confirm(
`Khóa người dùng ${user.profile?.display_name || user.email}?`,
)
) {
try { try {
await apiDeleteUser(user.id); await apiDeleteUser(user.id);
toast.success("Đã khóa người dùng thành công!"); toast.success("Đã khóa người dùng thành công!");
fetchUsers(); fetchUsers();
} catch (err) { } catch (err) {
console.error(err);
toast.error("Đã xảy ra lỗi khi xóa!"); toast.error("Đã xảy ra lỗi khi xóa!");
} }
} }
}; };
const handleRestore = async (user: fullDataUser) => { const handleRestore = async (user: fullDataUser) => {
const confirmMessage = `Bạn có chắc chắn muốn khôi phục người dùng ${user.profile?.display_name || user.email}?`; if (
if (window.confirm(confirmMessage)) { window.confirm(
`Khôi phục người dùng ${user.profile?.display_name || user.email}?`,
)
) {
try { try {
await apiRestoreUser(user.id); await apiRestoreUser(user.id);
toast.success("Khôi phục người dùng thành công!"); toast.success("Khôi phục người dùng thành công!");
fetchUsers(); fetchUsers();
} catch (err) { } catch (err) {
console.error(err);
toast.error("Đã xảy ra lỗi khi khôi phục!"); toast.error("Đã xảy ra lỗi khi khôi phục!");
} }
} }
@@ -149,9 +159,7 @@ export default function UserTable() {
<PageBreadcrumb pageTitle="Quản lý người dùng" /> <PageBreadcrumb pageTitle="Quản lý người dùng" />
<div className="space-y-6"> <div className="space-y-6">
<ComponentCard title="Bộ lọc tìm kiếm"> <ComponentCard title="Bộ lọc tìm kiếm">
{/* Grid Layout cho các Filter giống Swagger */}
<div className="grid grid-cols-1 gap-4 mb-6 md:grid-cols-3 lg:grid-cols-4"> <div className="grid grid-cols-1 gap-4 mb-6 md:grid-cols-3 lg:grid-cols-4">
{/* Search */}
<div> <div>
<label className="block mb-2 text-sm font-medium">Search</label> <label className="block mb-2 text-sm font-medium">Search</label>
<input <input
@@ -168,13 +176,14 @@ export default function UserTable() {
<label className="block mb-2 text-sm font-medium"> <label className="block mb-2 text-sm font-medium">
Auth Provider Auth Provider
</label> </label>
<input <select
type="text"
placeholder="google"
value={authProvider} value={authProvider}
onChange={(e) => setAuthProvider(e.target.value)} onChange={(e) => setAuthProvider(e.target.value)}
className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700" className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 cursor-pointer bg-white"
/> >
<option value="">Tất cả</option>
<option value="google">Google</option>
</select>
</div> </div>
<div> <div>
@@ -226,6 +235,9 @@ export default function UserTable() {
sortBy={sortBy} sortBy={sortBy}
sortOrder={sortOrder} sortOrder={sortOrder}
onViewDetail={handleOpenDetail} onViewDetail={handleOpenDetail}
roles={roles}
selectedRole={selectedRole}
onFilterRole={(role) => setSelectedRole(role)}
/> />
</div> </div>

View File

@@ -13,12 +13,20 @@ import { fullDataUser } from "@/interface/admin";
type SortColumn = "created_at" | "updated_at" | "display_name" | "email"; type SortColumn = "created_at" | "updated_at" | "display_name" | "email";
interface Role {
id: string;
name: string;
}
interface BasicTableOneProps { interface BasicTableOneProps {
data: fullDataUser[]; data: fullDataUser[];
onSort: (column: SortColumn) => void; onSort: (column: SortColumn) => void;
onViewDetail: (user: fullDataUser) => void; onViewDetail: (user: fullDataUser) => void;
sortBy?: SortColumn; sortBy?: SortColumn;
sortOrder?: "asc" | "desc"; sortOrder?: "asc" | "desc";
onFilterRole?: (role: string) => void;
selectedRole?: string;
roles?: Role[];
} }
export default function BasicTableOne({ export default function BasicTableOne({
@@ -27,8 +35,10 @@ export default function BasicTableOne({
onViewDetail, onViewDetail,
sortBy, sortBy,
sortOrder, sortOrder,
onFilterRole,
selectedRole,
roles = [],
}: BasicTableOneProps) { }: BasicTableOneProps) {
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
if (!dateString) return "-"; if (!dateString) return "-";
const date = new Date(dateString); const date = new Date(dateString);
@@ -48,7 +58,6 @@ export default function BasicTableOne({
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
> >
<path <path
strokeLinecap="round" strokeLinecap="round"
@@ -62,7 +71,6 @@ export default function BasicTableOne({
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
> >
<path <path
strokeLinecap="round" strokeLinecap="round"
@@ -84,27 +92,58 @@ export default function BasicTableOne({
<TableRow> <TableRow>
<TableCell <TableCell
isHeader isHeader
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400" className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400 min-w-[265px] max-w-[265px]"
> >
<div <div
className="flex items-center cursor-pointer select-none" className="flex items-center cursor-pointer select-none"
onClick={() => onSort("display_name")} onClick={() => onSort("display_name")}
> >
Người dùng Người dùng <SortIcon column="display_name" />
<SortIcon column="display_name" />
</div> </div>
</TableCell> </TableCell>
<TableCell <TableCell
isHeader isHeader
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400" className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400 min-w-[337px] max-w-[337px]"
> >
<div <div
className="flex items-center cursor-pointer select-none" className="flex items-center cursor-pointer select-none"
onClick={() => onSort("email")} onClick={() => onSort("email")}
> >
Email Email <SortIcon column="email" />
<SortIcon column="email" /> </div>
</TableCell>
<TableCell
isHeader
className="px-5 py-3 font-medium text-start text-theme-xs min-w-[188px] max-w-[188px]"
>
<div className="relative inline-flex items-center group">
<select
value={selectedRole}
onChange={(e) => onFilterRole?.(e.target.value)}
className="bg-transparent border-none outline-none cursor-pointer appearance-none text-gray-500 dark:text-gray-400 pr-5 hover:text-brand-500 transition-colors font-medium"
>
<option value="">Vai trò (Tất cả)</option>
{roles.map((role) => (
<option key={role.id} value={role.id}>
{role.name}
</option>
))}
</select>
<svg
className="w-3 h-3 absolute right-0 text-gray-400 pointer-events-none group-hover:text-brand-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M19 9l-7 7-7-7"
/>
</svg>
</div> </div>
</TableCell> </TableCell>
@@ -112,11 +151,6 @@ export default function BasicTableOne({
isHeader isHeader
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400" className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
> >
Vai trò
</TableCell>
<TableCell isHeader className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400">
Trạng thái Trạng thái
</TableCell> </TableCell>
@@ -128,8 +162,7 @@ export default function BasicTableOne({
className="flex items-center cursor-pointer select-none" className="flex items-center cursor-pointer select-none"
onClick={() => onSort("created_at")} onClick={() => onSort("created_at")}
> >
Ngày tham gia Ngày tham gia <SortIcon column="created_at" />
<SortIcon column="created_at" />
</div> </div>
</TableCell> </TableCell>
@@ -141,11 +174,14 @@ export default function BasicTableOne({
className="flex items-center cursor-pointer select-none" className="flex items-center cursor-pointer select-none"
onClick={() => onSort("updated_at")} onClick={() => onSort("updated_at")}
> >
Cập nhật Cập nhật <SortIcon column="updated_at" />
<SortIcon column="updated_at" />
</div> </div>
</TableCell> </TableCell>
<TableCell isHeader className="px-5 py-3 font-medium text-gray-500 text-center text-theme-xs dark:text-gray-400">
<TableCell
isHeader
className="px-5 py-3 font-medium text-gray-500 text-center text-theme-xs dark:text-gray-400"
>
Thao tác Thao tác
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -166,7 +202,7 @@ export default function BasicTableOne({
width={40} width={40}
height={40} height={40}
src={user.profile.avatar_url} src={user.profile.avatar_url}
alt={user.profile.display_name || "Avatar"} alt="Avatar"
className="object-cover w-full h-full" className="object-cover w-full h-full"
/> />
) : ( ) : (
@@ -192,17 +228,17 @@ export default function BasicTableOne({
<TableCell className="px-5 py-4 text-start text-theme-sm"> <TableCell className="px-5 py-4 text-start text-theme-sm">
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{user.roles && user.roles.length > 0 ? ( {user.roles?.map((role) => (
user.roles.map((role) => (
<span <span
key={role.id} key={role.id}
className="px-2 py-0.5 rounded-md bg-brand-50 text-brand-600 dark:bg-brand-500/10 dark:text-brand-400 text-[10px] font-normal uppercase tracking-wider" className="px-2 py-0.5 rounded-md bg-brand-50 text-brand-600 dark:bg-brand-500/10 dark:text-brand-400 text-[10px] uppercase"
> >
{role.name} {role.name}
</span> </span>
)) )) || (
) : ( <span className="text-gray-400 italic text-[10px]">
<span className="text-gray-400 italic">No Role</span> No Role
</span>
)} )}
</div> </div>
</TableCell> </TableCell>
@@ -220,7 +256,6 @@ export default function BasicTableOne({
<TableCell className="px-5 py-4 text-gray-600 text-theme-sm dark:text-gray-400"> <TableCell className="px-5 py-4 text-gray-600 text-theme-sm dark:text-gray-400">
{formatDate(user.created_at)} {formatDate(user.created_at)}
</TableCell> </TableCell>
<TableCell className="px-5 py-4 text-gray-600 text-theme-sm dark:text-gray-400"> <TableCell className="px-5 py-4 text-gray-600 text-theme-sm dark:text-gray-400">
{formatDate(user.updated_at)} {formatDate(user.updated_at)}
</TableCell> </TableCell>
@@ -228,7 +263,7 @@ export default function BasicTableOne({
<TableCell className="px-5 py-4 text-center"> <TableCell className="px-5 py-4 text-center">
<button <button
onClick={() => onViewDetail(user)} onClick={() => onViewDetail(user)}
className="text-brand-500 hover:text-brand-600 font-medium text-theme-sm transition-colors" className="text-brand-500 hover:text-brand-600 font-medium text-theme-sm"
> >
Chi tiết Chi tiết
</button> </button>
@@ -237,7 +272,16 @@ export default function BasicTableOne({
)) ))
) : ( ) : (
<TableRow> <TableRow>
<TableCell className="px-5 py-4 text-center text-gray-500 italic"> </TableCell> <TableCell
colSpan={7}
className="px-5 py-24 text-center text-gray-500 italic"
>
<div className="flex flex-col items-center justify-center gap-2">
<p className="text-theme-sm">
Không tìm thấy dữ liệu người dùng
</p>
</div>
</TableCell>
</TableRow> </TableRow>
)} )}
</TableBody> </TableBody>

View File

@@ -1,54 +1,49 @@
import React, { ReactNode } from "react"; import React, { ReactNode, TdHTMLAttributes, HTMLAttributes } from "react";
// Props for Table // Props for Table
interface TableProps { interface TableProps extends HTMLAttributes<HTMLTableElement> {
children: ReactNode; // Table content (thead, tbody, etc.) children: ReactNode;
className?: string; // Optional className for styling
} }
// Props for TableHeader // Props for TableHeader
interface TableHeaderProps { interface TableHeaderProps extends HTMLAttributes<HTMLTableSectionElement> {
children: ReactNode; // Header row(s) children: ReactNode;
className?: string; // Optional className for styling
} }
// Props for TableBody // Props for TableBody
interface TableBodyProps { interface TableBodyProps extends HTMLAttributes<HTMLTableSectionElement> {
children: ReactNode; // Body row(s) children: ReactNode;
className?: string; // Optional className for styling
} }
// Props for TableRow // Props for TableRow
interface TableRowProps { interface TableRowProps extends HTMLAttributes<HTMLTableRowElement> {
children: ReactNode; // Cells (th or td) children: ReactNode;
className?: string; // Optional className for styling
} }
// Props for TableCell // Props for TableCell - Hỗ trợ colSpan, rowSpan...
interface TableCellProps { interface TableCellProps extends TdHTMLAttributes<HTMLTableCellElement> {
children: ReactNode; // Cell content children: ReactNode;
isHeader?: boolean; // If true, renders as <th>, otherwise <td> isHeader?: boolean;
className?: string; // Optional className for styling
} }
// Table Component // Table Component
const Table: React.FC<TableProps> = ({ children, className }) => { const Table: React.FC<TableProps> = ({ children, className, ...props }) => {
return <table className={`min-w-full ${className}`}>{children}</table>; return <table className={`min-w-full ${className}`} {...props}>{children}</table>;
}; };
// TableHeader Component // TableHeader Component
const TableHeader: React.FC<TableHeaderProps> = ({ children, className }) => { const TableHeader: React.FC<TableHeaderProps> = ({ children, className, ...props }) => {
return <thead className={className}>{children}</thead>; return <thead className={className} {...props}>{children}</thead>;
}; };
// TableBody Component // TableBody Component
const TableBody: React.FC<TableBodyProps> = ({ children, className }) => { const TableBody: React.FC<TableBodyProps> = ({ children, className, ...props }) => {
return <tbody className={className}>{children}</tbody>; return <tbody className={className} {...props}>{children}</tbody>;
}; };
// TableRow Component // TableRow Component
const TableRow: React.FC<TableRowProps> = ({ children, className }) => { const TableRow: React.FC<TableRowProps> = ({ children, className, ...props }) => {
return <tr className={className}>{children}</tr>; return <tr className={className} {...props}>{children}</tr>;
}; };
// TableCell Component // TableCell Component
@@ -56,9 +51,14 @@ const TableCell: React.FC<TableCellProps> = ({
children, children,
isHeader = false, isHeader = false,
className, className,
...props
}) => { }) => {
const CellTag = isHeader ? "th" : "td"; const CellTag = isHeader ? "th" : "td";
return <CellTag className={` ${className}`}>{children}</CellTag>; return (
<CellTag className={className} {...(props as any)}>
{children}
</CellTag>
);
}; };
export { Table, TableHeader, TableBody, TableRow, TableCell }; export { Table, TableHeader, TableBody, TableRow, TableCell };

View File

@@ -22,6 +22,7 @@ export const apiRestoreUser = async (id: string) => {
const response = await api.patch(API.Admin.RESTORE_USER(id)); const response = await api.patch(API.Admin.RESTORE_USER(id));
return response?.data; return response?.data;
}; };
export const apiGetAllRole = async () => { export const apiGetAllRole = async () => {
const response = await api.get(API.Admin.GET_ALL_ROLE); const response = await api.get(API.Admin.GET_ALL_ROLE);
return response?.data; return response?.data;