diff --git a/api.ts b/api.ts index 004fdd5..d77eb87 100644 --- a/api.ts +++ b/api.ts @@ -35,8 +35,9 @@ export const API = { RESTORE_USER: (Id: number | string) => `${API_URL_ROOT}/users/${Id}/restore`, GET_USER_MEDIA: (Id: number | string) => `${API_URL_ROOT}/users/${Id}/media`, GET_ALL_ROLE: `${API_URL_ROOT}/roles`, - UPDATE_APPLICATION_STATUS: (Id: number | string) => `${API_URL_ROOT}/historian/application/${Id}/status` - + UPDATE_APPLICATION_STATUS: (Id: number | string) => `${API_URL_ROOT}/historian/application/${Id}/status`, + RESET_PASSWORD: (Id: number | string) => `${API_URL_ROOT}/users/${Id}/password`, + CREATE_USER: `${API_URL_ROOT}/users`, }, Historian:{ CREATE_CV: `${API_URL_ROOT}/historian/application`, diff --git a/constant.ts b/constant.ts index 191828f..1376588 100644 --- a/constant.ts +++ b/constant.ts @@ -1,2 +1,4 @@ export const LIMIT_ITEM_TABLE = 5; -export const INITIAL_LIMIT = 16; \ No newline at end of file +export const INITIAL_LIMIT = 16; +export const IS_SEND_EMAIL = true; +export const DEFAULT_ROLE_NAME = "USER"; \ No newline at end of file diff --git a/src/app/(admin)/(others-pages)/(management)/user-information/page.tsx b/src/app/(admin)/(others-pages)/(management)/user-information/page.tsx index b7000cb..ce850c6 100644 --- a/src/app/(admin)/(others-pages)/(management)/user-information/page.tsx +++ b/src/app/(admin)/(others-pages)/(management)/user-information/page.tsx @@ -5,17 +5,20 @@ import PageBreadcrumb from "@/components/common/PageBreadCrumb"; import BasicTableOne from "@/components/tables/BasicTableOne"; import ChangeRoleModal from "@/components/tables/ChangeRoleModal"; import UserDetailModal from "@/components/tables/UserDetailModal"; -import { responseUserTable, getUserDto, fullDataUser } from "@/interface/admin"; +import { responseUserTable, getUserDto, fullDataUser, createUser } from "@/interface/admin"; import { apiDeleteUser, apiGetAllRole, apiGetListUser, apiRestoreUser, + apiCreateUser, + apiResetPassword, } from "@/service/adminService"; import { useEffect, useState, useCallback } from "react"; import Pagination from "@/components/tables/Pagination"; -import { LIMIT_ITEM_TABLE } from "../../../../../../constant"; +import { LIMIT_ITEM_TABLE, IS_SEND_EMAIL } from "../../../../../../constant"; import CustomDateRangePicker from "@/components/common/CustomDateRangePicker"; +import Input from "@/components/form/input/InputField"; export type SortColumn = "created_at" | "updated_at" | "display_name" | "email"; @@ -52,6 +55,21 @@ export default function UserTable() { const [roleUser, setRoleUser] = useState(null); const [isRoleModalOpen, setIsRoleModalOpen] = useState(false); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [createPayload, setCreatePayload] = useState({ + email: "", + display_name: "", + password: "", + role_ids: [], + }); + + const [isResetModalOpen, setIsResetModalOpen] = useState(false); + const [resetUser, setResetUser] = useState(null); + const [resetPayload, setResetPayload] = useState({ + new_password: "", + is_send_email: IS_SEND_EMAIL, + }); + const [debouncedParams, setDebouncedParams] = useState({ search: "", limit: 5, @@ -194,6 +212,74 @@ export default function UserTable() { setIsModalOpen(true); }; + const generateRandomPassword = () => { + const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const lowercase = "abcdefghijklmnopqrstuvwxyz"; + const numbers = "0123456789"; + const specials = "!@#$%^&*()_+~`|}{[]:;?><,./-="; + + let password = ""; + password += uppercase[Math.floor(Math.random() * uppercase.length)]; + password += numbers[Math.floor(Math.random() * numbers.length)]; + password += specials[Math.floor(Math.random() * specials.length)]; + + const allChars = lowercase + uppercase + numbers + specials; + for (let i = 0; i < 5; i++) { + password += allChars[Math.floor(Math.random() * allChars.length)]; + } + + return password.split('').sort(() => 0.5 - Math.random()).join(''); + }; + + const handleOpenCreateModal = () => { + setCreatePayload({ + email: "", + display_name: "", + password: generateRandomPassword(), + role_ids: [], + }); + setIsCreateModalOpen(true); + }; + + const handleCreateUser = async () => { + try { + const res = await apiCreateUser(createPayload as any); + if (res?.status) { + Swal.fire("Thành công", "Tạo tài khoản thành công.", "success"); + setIsCreateModalOpen(false); + fetchUsers(); + } else { + Swal.fire("Thất bại", res?.message || "Không thể tạo tài khoản.", "error"); + } + } catch (err: any) { + Swal.fire("Lỗi!", err?.response?.data?.message || "Có lỗi xảy ra.", "error"); + } + }; + + const handleOpenResetModal = (user: fullDataUser) => { + setResetUser(user); + setResetPayload({ + new_password: generateRandomPassword(), + is_send_email: IS_SEND_EMAIL, + }); + setIsResetModalOpen(true); + }; + + const handleResetPassword = async () => { + if (!resetUser) return; + try { + const res = await apiResetPassword(resetUser.id, resetPayload as any); + if (res?.status) { + Swal.fire("Thành công", "Đặt lại mật khẩu thành công.", "success"); + setIsResetModalOpen(false); + } else { + Swal.fire("Thất bại", res?.message || "Không thể đặt lại mật khẩu.", "error"); + } + } catch (err: any) { + Swal.fire("Lỗi!", err?.response?.data?.message || "Có lỗi xảy ra.", "error"); + } + }; + const handleOpenRoleModal = (user: fullDataUser) => { setRoleUser(user); setIsRoleModalOpen(true); @@ -353,7 +439,20 @@ export default function UserTable() { - + + + + + Tạo tài khoản + + } + >
{loading && (
@@ -394,6 +493,10 @@ export default function UserTable() { onClose={() => setIsModalOpen(false)} user={selectedUser} onChangeRole={handleOpenRoleModal} + onResetPassword={(u) => { + handleOpenResetModal(u); + setIsModalOpen(false); + }} onDelete={(u) => { handleDelete(u); setIsModalOpen(false); @@ -410,6 +513,123 @@ export default function UserTable() { user={roleUser} onSuccess={fetchUsers} /> + + {/* Modal Tạo Tài Khoản */} + {isCreateModalOpen && ( +
+
+
+

Tạo tài khoản mới

+ +
+
+
+ + setCreatePayload({...createPayload, display_name: e.target.value})} + className="w-full px-4 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 outline-none focus:ring-2 focus:ring-brand-500" + placeholder="Nhập tên hiển thị..." + /> +
+
+ + setCreatePayload({...createPayload, email: e.target.value})} + className="w-full px-4 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 outline-none focus:ring-2 focus:ring-brand-500" + placeholder="Nhập email..." + /> +
+
+ +
+ {roles.map((role) => ( + + ))} +
+
+
+ +
+ setCreatePayload({...createPayload, password: e.target.value})} + className="w-full px-4 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 outline-none focus:ring-2 focus:ring-brand-500" + /> + +
+
+
+
+ + +
+
+
+ )} + + {isResetModalOpen && ( +
+
+
+

Reset mật khẩu

+ +
+
+
+ +
+ setResetPayload({...resetPayload, new_password: e.target.value})} + className="w-full px-4 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 outline-none focus:ring-2 focus:ring-brand-500" + /> + +
+
+
+ setResetPayload({...resetPayload, is_send_email: e.target.checked})} className="w-4 h-4 text-brand-500 border-gray-300 rounded focus:ring-brand-500 cursor-pointer" /> + +
+
+
+ + +
+
+
+ )}
diff --git a/src/components/tables/ChangeRoleModal.tsx b/src/components/tables/ChangeRoleModal.tsx index 9b37043..aa672b2 100644 --- a/src/components/tables/ChangeRoleModal.tsx +++ b/src/components/tables/ChangeRoleModal.tsx @@ -5,6 +5,7 @@ import Button from "../ui/button/Button"; import { fullDataUser } from "@/interface/admin"; import { apiGetAllRole, apiChangeRole } from "@/service/adminService"; import { toast } from "sonner"; +import { DEFAULT_ROLE_NAME } from "../../../constant"; interface Role { id: string; @@ -18,7 +19,6 @@ interface ChangeRoleModalProps { onSuccess: () => void; } -const DEFAULT_ROLE_NAME = "USER"; export default function ChangeRoleModal({ isOpen, onClose, user, onSuccess }: ChangeRoleModalProps) { const [roles, setRoles] = useState([]); diff --git a/src/components/tables/UserDetailModal.tsx b/src/components/tables/UserDetailModal.tsx index ca81329..c135d1b 100644 --- a/src/components/tables/UserDetailModal.tsx +++ b/src/components/tables/UserDetailModal.tsx @@ -15,6 +15,7 @@ interface UserDetailModalProps { onChangeRole: (user: fullDataUser) => void; onDelete: (user: fullDataUser) => void; onRestore: (user: fullDataUser) => void; + onResetPassword: (user: fullDataUser) => void; } export default function UserDetailModal({ @@ -24,6 +25,7 @@ export default function UserDetailModal({ onChangeRole, onDelete, onRestore, + onResetPassword, }: UserDetailModalProps) { const [mediaData, setMediaData] = useState(null); const [loading, setLoading] = useState(true); @@ -96,6 +98,12 @@ export default function UserDetailModal({ Thao tác quản trị viên
+