update popup notify. update new pagination

This commit is contained in:
2026-04-09 17:48:09 +07:00
parent b4a667e764
commit 28723997b5
5 changed files with 65 additions and 41 deletions

11
package-lock.json generated
View File

@@ -32,6 +32,7 @@
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"sweetalert2": "^11.26.24",
"swiper": "^11.2.10", "swiper": "^11.2.10",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
"yet-another-react-lightbox": "^3.30.1" "yet-another-react-lightbox": "^3.30.1"
@@ -8908,6 +8909,16 @@
"url": "https://opencollective.com/svgo" "url": "https://opencollective.com/svgo"
} }
}, },
"node_modules/sweetalert2": {
"version": "11.26.24",
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.26.24.tgz",
"integrity": "sha512-SLgukW4wicewpW5VOukSXY5Z6DL/z7HCOK2ODSjmQPiSphCN8gJAmh9npoceXOtBRNoDN0xIz+zHYthtfiHmjg==",
"license": "MIT",
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/limonte"
}
},
"node_modules/swiper": { "node_modules/swiper": {
"version": "11.2.10", "version": "11.2.10",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-11.2.10.tgz", "resolved": "https://registry.npmjs.org/swiper/-/swiper-11.2.10.tgz",

View File

@@ -33,6 +33,7 @@
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"sweetalert2": "^11.26.24",
"swiper": "^11.2.10", "swiper": "^11.2.10",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
"yet-another-react-lightbox": "^3.30.1" "yet-another-react-lightbox": "^3.30.1"

View File

@@ -1,10 +1,10 @@
"use client"; "use client";
import ComponentCard from "@/components/common/ComponentCard"; import ComponentCard from "@/components/common/ComponentCard";
import Swal from "sweetalert2";
import PageBreadcrumb from "@/components/common/PageBreadCrumb"; import PageBreadcrumb from "@/components/common/PageBreadCrumb";
import BasicTableOne from "@/components/tables/BasicTableOne"; import BasicTableOne from "@/components/tables/BasicTableOne";
import ChangeRoleModal from "@/components/tables/ChangeRoleModal"; import ChangeRoleModal from "@/components/tables/ChangeRoleModal";
import UserDetailModal from "@/components/tables/UserDetailModal"; import UserDetailModal from "@/components/tables/UserDetailModal";
import { Modal } from "@/components/ui/modal";
import { responseUserTable, getUserDto, fullDataUser } from "@/interface/admin"; import { responseUserTable, getUserDto, fullDataUser } from "@/interface/admin";
import { import {
apiDeleteUser, apiDeleteUser,
@@ -13,7 +13,7 @@ import {
apiRestoreUser, apiRestoreUser,
} from "@/service/adminService"; } from "@/service/adminService";
import { useEffect, useState, useCallback } from "react"; import { useEffect, useState, useCallback } from "react";
import { toast } from "sonner"; import Pagination from "@/components/tables/Pagination";
export type SortColumn = "created_at" | "updated_at" | "display_name" | "email"; export type SortColumn = "created_at" | "updated_at" | "display_name" | "email";
@@ -48,7 +48,6 @@ export default function UserTable() {
const fetchRoles = async () => { const fetchRoles = async () => {
try { try {
const res = await apiGetAllRole(); const res = await apiGetAllRole();
console.log("Danh sách role:", res);
if (res?.status) { if (res?.status) {
setRoles(res.data); setRoles(res.data);
} }
@@ -58,7 +57,7 @@ export default function UserTable() {
}; };
fetchRoles(); fetchRoles();
}, []); }, []);
console.log("Roles đã fetch:", roles);
useEffect(() => { useEffect(() => {
const handler = setTimeout(() => { const handler = setTimeout(() => {
setDebouncedParams({ setDebouncedParams({
@@ -124,36 +123,57 @@ export default function UserTable() {
}; };
const handleDelete = async (user: fullDataUser) => { const handleDelete = async (user: fullDataUser) => {
if ( const result = await Swal.fire({
window.confirm( title: "Xác nhận khóa?",
`Khóa người dùng ${user.profile?.display_name || user.email}?`, text: `Bạn có chắc muốn khóa người dùng ${user.profile?.display_name || user.email}?`,
) icon: "warning",
) { showCancelButton: true,
showCloseButton: true,
confirmButtonColor: "#d33",
cancelButtonColor: "#3085d6",
confirmButtonText: "Đồng ý, khóa!",
cancelButtonText: "Hủy",
reverseButtons: true,
});
if (result.isConfirmed) {
try { try {
await apiDeleteUser(user.id); await apiDeleteUser(user.id);
toast.success("Đã khóa người dùng thành công!"); Swal.fire(
"Đã khóa!",
"Người dùng đã bị tạm dừng hoạt động.",
"success",
);
fetchUsers(); fetchUsers();
} catch (err) { } catch (err) {
toast.error("Đã xảy ra lỗi khi xóa!"); Swal.fire("Lỗi!", "Không thể thực hiện thao tác này.", "error");
} }
} }
}; };
const handleRestore = async (user: fullDataUser) => { const handleRestore = async (user: fullDataUser) => {
if ( const result = await Swal.fire({
window.confirm( title: "Khôi phục tài khoản?",
`Khôi phục người dùng ${user.profile?.display_name || user.email}?`, text: `Khôi phục quyền truy cập cho ${user.profile?.display_name || user.email}?`,
) icon: "question",
) { showCancelButton: true,
showCloseButton: true,
confirmButtonColor: "#28a745",
confirmButtonText: "Xác nhận",
cancelButtonText: "Hủy",
});
if (result.isConfirmed) {
try { try {
await apiRestoreUser(user.id); await apiRestoreUser(user.id);
toast.success("Khôi phục người dùng thành công!"); Swal.fire("Thành công", "Tài khoản đã được khôi phục.", "success");
fetchUsers(); fetchUsers();
} catch (err) { } catch (err) {
toast.error("Đã xảy ra lỗi khi khôi phục!"); Swal.fire("Thất bại", "Vui lòng thử lại sau.", "error");
} }
} }
}; };
return ( return (
<div> <div>
<PageBreadcrumb pageTitle="Quản lý người dùng" /> <PageBreadcrumb pageTitle="Quản lý người dùng" />
@@ -171,7 +191,6 @@ export default function UserTable() {
/> />
</div> </div>
{/* Auth Provider */}
<div> <div>
<label className="block mb-2 text-sm font-medium"> <label className="block mb-2 text-sm font-medium">
Auth Provider Auth Provider
@@ -243,26 +262,19 @@ export default function UserTable() {
<div className="flex items-center justify-between mt-6"> <div className="flex items-center justify-between mt-6">
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Hiển thị trang {pagination?.current_page} /{" "} Hiển thị trang {pagination?.current_page || 1} /{" "}
{pagination?.total_pages} ({pagination?.total_records} kết quả) {pagination?.total_pages || 1} ({pagination?.total_records || 0} kết quả)
</p> </p>
<div className="flex gap-2">
<button {pagination && pagination.total_pages > 1 && (
disabled={page <= 1 || loading} <Pagination
onClick={() => setPage((p) => p - 1)} currentPage={pagination.current_page}
className="px-4 py-2 border rounded-md disabled:opacity-50 hover:bg-gray-100 dark:hover:bg-gray-700" totalPages={pagination.total_pages}
> onPageChange={(newPage) => setPage(newPage)}
Trước />
</button> )}
<button
disabled={page >= (pagination?.total_pages || 1) || loading}
onClick={() => setPage((p) => p + 1)}
className="px-4 py-2 border rounded-md disabled:opacity-50 hover:bg-gray-100 dark:hover:bg-gray-700"
>
Sau
</button>
</div>
</div> </div>
<UserDetailModal <UserDetailModal
isOpen={isModalOpen} isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)} onClose={() => setIsModalOpen(false)}

View File

@@ -54,7 +54,7 @@ export default function BasicTableOne({
return ( return (
<div className="flex flex-col ml-2 opacity-50 cursor-pointer hover:opacity-100"> <div className="flex flex-col ml-2 opacity-50 cursor-pointer hover:opacity-100">
<svg <svg
className={`w-3 h-3 ${isActive && sortOrder === "asc" ? "text-brand-500 opacity-100" : "text-gray-400"}`} className={`w-3 h-3 ${isActive && sortOrder === "asc" ? "text-blue-700 opacity-100" : "text-gray-400"}`}
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -67,7 +67,7 @@ export default function BasicTableOne({
/> />
</svg> </svg>
<svg <svg
className={`w-3 h-3 -mt-1 ${isActive && sortOrder === "desc" ? "text-brand-500 opacity-100" : "text-gray-400"}`} className={`w-3 h-3 -mt-1 ${isActive && sortOrder === "desc" ? "text-blue-700 opacity-100" : "text-gray-400"}`}
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"

View File

@@ -41,7 +41,7 @@ const AppHeader: React.FC = () => {
}, []); }, []);
return ( return (
<header className="sticky top-0 flex w-full bg-white border-gray-200 z-99999 dark:border-gray-800 dark:bg-gray-900 lg:border-b"> <header className="sticky top-0 flex w-full bg-white border-gray-200 z-99 dark:border-gray-800 dark:bg-gray-900 lg:border-b">
<div className="flex flex-col items-center justify-between grow lg:flex-row lg:px-6"> <div className="flex flex-col items-center justify-between grow lg:flex-row lg:px-6">
<div className="flex items-center justify-between w-full gap-2 px-3 py-3 border-b border-gray-200 dark:border-gray-800 sm:gap-4 lg:justify-normal lg:border-b-0 lg:px-0 lg:py-4"> <div className="flex items-center justify-between w-full gap-2 px-3 py-3 border-b border-gray-200 dark:border-gray-800 sm:gap-4 lg:justify-normal lg:border-b-0 lg:px-0 lg:py-4">
<button <button