diff --git a/api.ts b/api.ts index 4309a11..bedfaab 100644 --- a/api.ts +++ b/api.ts @@ -12,6 +12,7 @@ export const API = { PRESIGNED: `${API_URL_ROOT}/media/presigned` }, Auth : { + LOGOUT: `${API_URL_ROOT}/auth/logout`, SIGNUP: `${API_URL_ROOT}/auth/signup`, SIGNIN: `${API_URL_ROOT}/auth/signin`, CREATEOTP: `${API_URL_ROOT}/auth/token/create`, diff --git a/src/app/(admin)/(others-pages)/(tables)/user-table/page.tsx b/src/app/(admin)/(others-pages)/(tables)/user-table/page.tsx index b21d5b9..2b1ac05 100644 --- a/src/app/(admin)/(others-pages)/(tables)/user-table/page.tsx +++ b/src/app/(admin)/(others-pages)/(tables)/user-table/page.tsx @@ -2,62 +2,85 @@ import ComponentCard from "@/components/common/ComponentCard"; import PageBreadcrumb from "@/components/common/PageBreadCrumb"; import BasicTableOne from "@/components/tables/BasicTableOne"; -import { fullDataUser, getUserDto } from "@/interface/admin"; +import { responseUserTable, getUserDto } from "@/interface/admin"; import { apiGetListUser } from "@/service/adminService"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; -// Trích xuất type sort cho dễ tái sử dụng export type SortColumn = "created_at" | "updated_at" | "display_name" | "email"; export default function UserTable() { - const [users, setUsers] = useState([]); - const [loading, setLoading] = useState(true); + // --- States cho Pagination --- + const [limit, setLimit] = useState(5); // Default theo ảnh là 5 + const [limitInput, setLimitInput] = useState("5"); + const [page, setPage] = useState(1); + // --- States cho Filter (Theo ảnh Swagger) --- const [searchTerm, setSearchTerm] = useState(""); - const [debouncedSearch, setDebouncedSearch] = useState(""); + const [authProvider, setAuthProvider] = useState(""); + const [createdFrom, setCreatedFrom] = useState(""); + const [createdTo, setCreatedTo] = useState(""); + const [isDeleted, setIsDeleted] = useState(undefined); + + // Debounced states + const [debouncedParams, setDebouncedParams] = useState({ + search: "", + limit: 5, + authProvider: "", + }); + // --- States cho Table --- + const [tableData, setTableData] = useState(null); + const [loading, setLoading] = useState(true); const [sortBy, setSortBy] = useState(undefined); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); + // 1. Xử lý Debounce cho các ô input text useEffect(() => { const handler = setTimeout(() => { - setDebouncedSearch(searchTerm); - }, 500); - + setDebouncedParams({ + search: searchTerm, + limit: parseInt(limitInput) || 5, + authProvider: authProvider, + }); + setPage(1); // Reset về trang 1 khi filter thay đổi + }, 600); return () => clearTimeout(handler); - }, [searchTerm]); + }, [searchTerm, limitInput, authProvider]); + + // 2. Hàm fetch data + const fetchUsers = useCallback(async () => { + setLoading(true); + try { + const payload: getUserDto = { + page: page, + limit: debouncedParams.limit, + search: debouncedParams.search || undefined, + auth_provider: debouncedParams.authProvider || undefined, + created_from: createdFrom || undefined, + created_to: createdTo || undefined, + is_deleted: isDeleted, + sort: sortBy, + order: sortOrder, + }; + + const response = await apiGetListUser(payload); + if (response?.status) { + setTableData(response); + } + } catch (err) { + console.error("Fetch error:", err); + setTableData(null); + } finally { + setLoading(false); + } + }, [page, debouncedParams, createdFrom, createdTo, isDeleted, sortBy, sortOrder]); useEffect(() => { - const fetchUser = async () => { - setLoading(true); - try { - const payload: getUserDto = { limit: 10 }; - if (debouncedSearch) payload.search = debouncedSearch; - if (sortBy) { - payload.sort = sortBy; - payload.order = sortOrder; - } - - const response = await apiGetListUser(payload); - // console.log("Request Payload:", payload); - // console.log("API Response:", response); - if (response && response.data) { - setUsers(response.data); - } else { - setUsers([]); - } - } catch (err) { - console.error("Lỗi:", err); - setUsers([]); - } finally { - setLoading(false); - } - }; - - fetchUser(); - }, [debouncedSearch, sortBy, sortOrder]); + fetchUsers(); + }, [fetchUsers]); const handleSort = (column: SortColumn) => { + setPage(1); if (sortBy === column) { setSortOrder(sortOrder === "asc" ? "desc" : "asc"); } else { @@ -66,46 +89,105 @@ export default function UserTable() { } }; + const pagination = tableData?.pagination; + return (
- +
- - {/* Ô nhập tìm kiếm */} -
- setSearchTerm(e.target.value)} - className="w-full sm:w-1/3 px-4 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent dark:bg-gray-800 dark:border-gray-700 dark:text-white" - /> -
+ + {/* Grid Layout cho các Filter giống Swagger */} +
+ {/* Search */} +
+ + setSearchTerm(e.target.value)} + className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700" + /> +
-
+ {/* Auth Provider */} +
+ + setAuthProvider(e.target.value)} + className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700" + /> +
+ + +
+ + +
+ + {/* Limit */} +
+ + setLimitInput(e.target.value)} + className="w-full px-3 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700" + /> +
+
+ + + +
{loading && ( -
-
- {/* Spinner xoay tròn */} -
-

- Đang tải... -

-
+
+
)} - {/* Bảng vẫn hiển thị ở dưới (mờ đi) hoặc ẩn tùy bạn, - nhưng truyền data vào để tránh giật lag layout */} -
+ + {/* Phân trang sử dụng data từ API mới */} +
+

+ Hiển thị trang {pagination?.current_page} / {pagination?.total_pages} ({pagination?.total_records} kết quả) +

+
+ + +
+
); -} +} \ No newline at end of file diff --git a/src/app/(admin)/(others-pages)/profile/page.tsx b/src/app/(admin)/(others-pages)/profile/page.tsx index 258cf44..bbb803b 100644 --- a/src/app/(admin)/(others-pages)/profile/page.tsx +++ b/src/app/(admin)/(others-pages)/profile/page.tsx @@ -15,23 +15,22 @@ export default function Profile() { const [mediaData, setMediaData] = useState(null); const [loading, setLoading] = useState(true); - useEffect(() => { - const fetchUser = async () => { - try { - const userData = await apiGetCurrentUser(); - const mediaResponse = await apiGetCurrentUserMedia(); // Giả sử hàm này return {status, data, message} - - setMediaData(mediaResponse); - setUser(userData); - } catch (err) { - console.error("Lỗi:", err); - } finally { - setLoading(false); - } - }; - fetchUser(); -}, []); + useEffect(() => { + const fetchUser = async () => { + try { + const userData = await apiGetCurrentUser(); + const mediaResponse = await apiGetCurrentUserMedia(); + setMediaData(mediaResponse); + setUser(userData); + } catch (err) { + console.error("Lỗi:", err); + } finally { + setLoading(false); + } + }; + fetchUser(); + }, []); return (
@@ -41,8 +40,8 @@ export default function Profile() {
+ {(mediaData?.data?.length ?? 0) > 0 && } -
diff --git a/src/components/header/UserDropdown.tsx b/src/components/header/UserDropdown.tsx index 3b5479e..59832e7 100644 --- a/src/components/header/UserDropdown.tsx +++ b/src/components/header/UserDropdown.tsx @@ -6,9 +6,12 @@ import { Dropdown } from "../ui/dropdown/Dropdown"; import { DropdownItem } from "../ui/dropdown/DropdownItem"; import { fullDataUser } from "@/interface/admin"; import { UserMetaCardProps } from "@/interface/user"; -import { apiGetCurrentUser } from "@/service/auth"; +import { apiGetCurrentUser, apiLogout } from "@/service/auth"; +import { useRouter } from "next/navigation"; export default function UserDropdown() { + const router = useRouter(); + const [isOpen, setIsOpen] = useState(false); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); @@ -25,7 +28,7 @@ export default function UserDropdown() { const fetchUser = async () => { try { const userData = await apiGetCurrentUser(); - console.log("User data in dropdown:", userData); + // console.log("User data in dropdown:", userData); setUser(userData); } catch (err) { console.error("Lỗi:", err); @@ -35,7 +38,27 @@ export default function UserDropdown() { }; fetchUser(); }, []); - + + const handleLogout = async (e: React.MouseEvent) => { + e.preventDefault(); + + try { + await apiLogout(); + } catch (error) { + console.error("Logout failed", error); + } finally { + localStorage.clear(); + sessionStorage.clear(); + + document.cookie.split(";").forEach((c) => { + document.cookie = c + .replace(/^ +/, "") + .replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/"); + }); + + window.location.href = "/signin"; + } + }; return (
@@ -168,6 +191,7 @@ export default function UserDropdown() { diff --git a/src/components/tables/BasicTableOne.tsx b/src/components/tables/BasicTableOne.tsx index ce239d8..422e84f 100644 --- a/src/components/tables/BasicTableOne.tsx +++ b/src/components/tables/BasicTableOne.tsx @@ -11,10 +11,9 @@ import Badge from "../ui/badge/Badge"; import Image from "next/image"; import { fullDataUser } from "@/interface/admin"; -// Kiểu dữ liệu sort +// Kiểu dữ liệu sort dựa trên UserTable type SortColumn = "created_at" | "updated_at" | "display_name" | "email"; -// Định nghĩa kiểu dữ liệu cho props truyền từ cha xuống interface BasicTableOneProps { data: fullDataUser[]; onSort: (column: SortColumn) => void; @@ -22,10 +21,15 @@ interface BasicTableOneProps { sortOrder?: "asc" | "desc"; } -export default function BasicTableOne({ data, onSort, sortBy, sortOrder }: BasicTableOneProps) { - // Hàm phụ trợ để format lại ngày tháng năm +export default function BasicTableOne({ + data, + onSort, + sortBy, + sortOrder, +}: BasicTableOneProps) { + // Format ngày tháng: 08 Apr 2026 const formatDate = (dateString: string) => { - if (!dateString) return ""; + if (!dateString) return "-"; const date = new Date(dateString); return date.toLocaleDateString("en-GB", { day: "2-digit", @@ -34,22 +38,38 @@ export default function BasicTableOne({ data, onSort, sortBy, sortOrder }: Basic }); }; - // Component phụ trợ để vẽ icon mũi tên Sort + // Component icon sắp xếp const SortIcon = ({ column }: { column: SortColumn }) => { const isActive = sortBy === column; return (
- + - +
); @@ -58,127 +78,167 @@ export default function BasicTableOne({ data, onSort, sortBy, sortOrder }: Basic return (
-
+ {/* Đảm bảo bảng có chiều rộng tối thiểu để không bị nát layout trên mobile */} +
- {/* Table Header */} - -
onSort("display_name")}> + +
onSort("display_name")} + > Người dùng
- - -
onSort("email")}> + + +
onSort("email")} + > Email
- - - Vai trò (Role) + + + Vai trò - + + Trạng thái - - -
onSort("created_at")}> + + +
onSort("created_at")} + > Ngày tham gia
- {/* Thêm cột Ngày cập nhật */} - -
onSort("updated_at")}> - Cập nhật lần cuối + +
onSort("updated_at")} + > + Cập nhật
- {/* Table Body */} - {data.map((user) => ( - - - {/* Cột User */} - -
-
- {user.profile?.avatar_url ? ( - - ) : ( - - {user.profile?.display_name?.charAt(0) || "U"} + {data.length > 0 ? ( + data.map((user) => ( + + {/* Cột Tên + Avatar */} + +
+
+ {user.profile?.avatar_url ? ( + + ) : ( + + {user.profile?.display_name?.charAt(0) || "U"} + + )} +
+
+ + {user.profile?.display_name || "N/A"} - )} -
-
- - {user.profile?.display_name || "Chưa cập nhật tên"} - - {user.profile?.phone && ( - {user.profile.phone} + ID: {user.id.slice(0, 8)}... +
+
+
+ + {/* Cột Email */} + + {user.email} + + + {/* Cột Roles (Badge list) */} + +
+ {user.roles && user.roles.length > 0 ? ( + user.roles.map((role) => ( + + {role.name} + + )) + ) : ( + No Role )}
-
- + - {/* Cột Email */} - - {user.email} - + {/* Cột Trạng thái (Sử dụng Badge component) */} + + + {user.is_deleted ? "Bị khóa" : "Hoạt động"} + + - {/* Cột Roles */} - - {user.roles && user.roles.length > 0 ? ( - user.roles.map((role: any) => ( - - {role.name} - - )) - ) : ( - Chưa cấp quyền - )} - - - {/* Cột Trạng thái */} - - - {user.is_deleted ? "Bị khóa" : "Hoạt động"} - - - - {/* Cột Ngày tham gia */} - - {formatDate(user.created_at)} - - - {/* Cột Ngày cập nhật */} - - {formatDate(user.updated_at)} - + {/* Cột Ngày tham gia */} + + {formatDate(user.created_at)} + + {/* Cột Ngày cập nhật */} + + {formatDate(user.updated_at)} + + + )) + ) : ( + + - ))} - + )} +
); -} \ No newline at end of file +} diff --git a/src/components/user-profile/AccountDetails.tsx b/src/components/user-profile/AccountDetails.tsx index d9feff2..3b8ad63 100644 --- a/src/components/user-profile/AccountDetails.tsx +++ b/src/components/user-profile/AccountDetails.tsx @@ -80,7 +80,7 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) { return ( <> -
+

@@ -108,7 +108,7 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) { -

diff --git a/src/components/user-profile/Media.tsx b/src/components/user-profile/Media.tsx index 5c73246..b9a1a3b 100644 --- a/src/components/user-profile/Media.tsx +++ b/src/components/user-profile/Media.tsx @@ -19,8 +19,8 @@ export default function MediaCard({ data }: { data: MediaDto }) { })); return ( -
-

Media Assets

+
+

Media Assets

{listMedia.map((item, idx) => ( diff --git a/src/interface/admin.ts b/src/interface/admin.ts index 5f6659b..6456b94 100644 --- a/src/interface/admin.ts +++ b/src/interface/admin.ts @@ -1,13 +1,17 @@ import { Profile, UserRole } from "@/interface/user"; export interface getUserDto { - cursor?: string; + page?: number; // Thay cursor bằng page theo Swagger limit: number; is_deleted?: boolean; order?: "asc" | "desc"; role_ids?: string[]; - search?: string; + search?: string; sort?: "created_at" | "updated_at" | "display_name" | "email"; + // Thêm các trường mới từ ảnh Swagger + auth_provider?: string; + created_from?: string; + created_to?: string; } export interface fullDataUser { @@ -23,4 +27,16 @@ export interface fullDataUser { roles: UserRole[]; profile: Profile; token_version: number; +} + +export interface responseUserTable { + data: fullDataUser[]; + status: boolean; + message?: string; + pagination?: { + current_page: number; + page_size: number; + total_records: number; + total_pages: number; + }; } \ No newline at end of file diff --git a/src/service/auth.ts b/src/service/auth.ts index bdbe132..7e51ca9 100644 --- a/src/service/auth.ts +++ b/src/service/auth.ts @@ -21,6 +21,11 @@ export const apiSignUp = async (payload: any) => { return response.data; }; +export const apiLogout = async () => { + const response = await api.post(API.Auth.LOGOUT); + return response.data; +}; + export const apiSignIn = async (payload: any) => { const response = await api.post(API.Auth.SIGNIN, payload); return response.data;