change role

This commit is contained in:
2026-04-08 22:36:49 +07:00
parent 30a2286f52
commit c5cf156338
6 changed files with 47 additions and 16 deletions

View File

@@ -72,6 +72,7 @@ export default function UserTable() {
}; };
const response = await apiGetListUser(payload); const response = await apiGetListUser(payload);
if (response?.status) { if (response?.status) {
setTableData(response); setTableData(response);
} }

View File

@@ -5,6 +5,7 @@
@theme { @theme {
--font-*: initial; --font-*: initial;
--font-outfit: Outfit, sans-serif; --font-outfit: Outfit, sans-serif;
--font-inter: "Inter", ui-sans-serif, system-ui, sans-serif;
--breakpoint-*: initial; --breakpoint-*: initial;
--breakpoint-2xsm: 375px; --breakpoint-2xsm: 375px;
@@ -188,7 +189,7 @@
cursor: pointer; cursor: pointer;
} }
body { body {
@apply relative font-normal font-outfit z-1 bg-gray-50; @apply relative font-normal font-inter z-1 bg-gray-50;
} }
} }

View File

@@ -1,4 +1,4 @@
import { Outfit } from 'next/font/google'; import { Inter } from 'next/font/google';
import './globals.css'; import './globals.css';
import "flatpickr/dist/flatpickr.css"; import "flatpickr/dist/flatpickr.css";
import { SidebarProvider } from '@/context/SidebarContext'; import { SidebarProvider } from '@/context/SidebarContext';
@@ -6,7 +6,7 @@ import { ThemeProvider } from '@/context/ThemeContext';
import { Toaster } from 'sonner'; import { Toaster } from 'sonner';
import StoreProvider from '@/store/StoreProvider'; import StoreProvider from '@/store/StoreProvider';
const outfit = Outfit({ const inter = Inter({
subsets: ["latin"], subsets: ["latin"],
}); });
@@ -17,7 +17,7 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en">
<body className={`${outfit.className} dark:bg-gray-900`}> <body className={`${inter.className} dark:bg-gray-900`}>
<StoreProvider> <StoreProvider>
<ThemeProvider> <ThemeProvider>
<SidebarProvider>{children} <Toaster closeButton richColors position="top-right" /> </SidebarProvider> <SidebarProvider>{children} <Toaster closeButton richColors position="top-right" /> </SidebarProvider>

View File

@@ -18,6 +18,8 @@ interface ChangeRoleModalProps {
onSuccess: () => void; onSuccess: () => void;
} }
const DEFAULT_ROLE_NAME = "USER";
export default function ChangeRoleModal({ isOpen, onClose, user, onSuccess }: ChangeRoleModalProps) { export default function ChangeRoleModal({ isOpen, onClose, user, onSuccess }: ChangeRoleModalProps) {
const [roles, setRoles] = useState<Role[]>([]); const [roles, setRoles] = useState<Role[]>([]);
const [selectedRoleIds, setSelectedRoleIds] = useState<string[]>([]); const [selectedRoleIds, setSelectedRoleIds] = useState<string[]>([]);
@@ -42,9 +44,13 @@ export default function ChangeRoleModal({ isOpen, onClose, user, onSuccess }: Ch
} }
}, [isOpen, user]); }, [isOpen, user]);
const handleToggleRole = (roleId: string) => { const handleToggleRole = (roleId: string, isDefault: boolean) => {
if (isDefault) return;
setSelectedRoleIds((prev) => setSelectedRoleIds((prev) =>
prev.includes(roleId) ? prev.filter((id) => id !== roleId) : [...prev, roleId] prev.includes(roleId)
? prev.filter((id) => id !== roleId)
: [...prev, roleId]
); );
}; };
@@ -54,11 +60,12 @@ export default function ChangeRoleModal({ isOpen, onClose, user, onSuccess }: Ch
try { try {
setLoading(true); setLoading(true);
const payload = { const payload = {
role_ids: selectedRoleIds, role_ids: selectedRoleIds,
user_id: user.id, user_id: user.id,
}; };
console.log("Payload gửi lên API:", payload);
await apiChangeRole(user.id, payload); await apiChangeRole(user.id, payload);
toast.success("Cập nhật vai trò thành công!"); toast.success("Cập nhật vai trò thành công!");
onSuccess(); onSuccess();
@@ -95,20 +102,42 @@ export default function ChangeRoleModal({ isOpen, onClose, user, onSuccess }: Ch
<div className="flex flex-col space-y-1 max-h-[300px] overflow-y-auto custom-scrollbar -mx-2 px-2"> <div className="flex flex-col space-y-1 max-h-[300px] overflow-y-auto custom-scrollbar -mx-2 px-2">
{roles.map((role) => { {roles.map((role) => {
const isSelected = selectedRoleIds.includes(role.id); const isSelected = selectedRoleIds.includes(role.id);
const isDefault = role.name === DEFAULT_ROLE_NAME;
return ( return (
<label <label
key={role.id} key={role.id}
className="flex items-center gap-3 p-2.5 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors" className={`flex items-center gap-3 p-2.5 rounded-lg transition-colors ${
isDefault
? "opacity-40 cursor-not-allowed bg-gray-50 dark:bg-gray-800/30"
: "cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50"
}`}
> >
<input <input
type="checkbox" type="checkbox"
checked={isSelected} checked={isDefault ? true : isSelected}
onChange={() => handleToggleRole(role.id)} onChange={() => handleToggleRole(role.id, isDefault)}
className="w-4 h-4 text-brand-500 border-gray-300 rounded focus:ring-brand-500 dark:border-gray-600 dark:bg-gray-700" disabled={isDefault}
className={`w-4 h-4 rounded border-gray-300 focus:ring-brand-500 dark:border-gray-600 dark:bg-gray-700 ${
isDefault ? "text-gray-400" : "text-brand-500"
}`}
/> />
<span className={`text-sm ${isSelected ? "text-gray-900 dark:text-white font-medium" : "text-gray-700 dark:text-gray-300"}`}> <div className="flex flex-col">
<span className={`text-sm ${
isDefault
? "text-gray-500 font-normal"
: isSelected
? "text-gray-900 dark:text-white font-medium"
: "text-gray-700 dark:text-gray-300"
}`}>
{role.name} {role.name}
</span> </span>
{isDefault && (
<span className="text-[10px] text-gray-400 italic">
Mặc đnh không thể xóa
</span>
)}
</div>
</label> </label>
); );
})} })}

View File

@@ -33,7 +33,7 @@ export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
"No roles available"} "No roles available"}
</p> </p>
<div className="hidden h-3.5 w-px bg-gray-300 dark:bg-gray-700 xl:block"></div> <div className="hidden h-3.5 w-px bg-gray-300 dark:bg-gray-700 xl:block"></div>
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400 max-w-[450px] truncate">
{data.data?.profile?.bio || "No bio available"} {data.data?.profile?.bio || "No bio available"}
</p> </p>
<div className="hidden h-3.5 w-px bg-gray-300 dark:bg-gray-700 xl:block"></div> <div className="hidden h-3.5 w-px bg-gray-300 dark:bg-gray-700 xl:block"></div>

View File

@@ -11,7 +11,7 @@ export const apiGetListUser = async (payload: getUserDto) => {
export const apiChangeRole = async (id: string, payload: any) => { export const apiChangeRole = async (id: string, payload: any) => {
const response = await api.patch(API.Admin.CHANGE_ROLE(id), payload); const response = await api.patch(API.Admin.CHANGE_ROLE(id), payload);
console.log("Response từ API sau khi đổi role:", response); // console.log("Response từ API sau khi đổi role:", response);
return response?.data; return response?.data;
}; };
export const apiDeleteUser = async (id: string) => { export const apiDeleteUser = async (id: string) => {