From e02564b5cdbef441b260e412308b75f30882fe6b Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 20 Jan 2026 22:21:06 +0700 Subject: [PATCH 1/3] Added User List table --- messages/en.json | 47 ++++- messages/vi.json | 45 +++- prisma/seed.ts | 6 +- src/components/audit/audit-columns.tsx | 105 +--------- src/components/audit/view-detail-dialog.tsx | 115 +++++++++++ src/components/form/admin-ban-user-form.tsx | 193 ++++++++++++++++++ .../form/admin-set-password-form.tsx | 124 +++++++++++ .../form/admin-set-user-role-form.tsx | 146 +++++++++++++ .../form/admin-update-user-info-form.tsx | 123 +++++++++++ src/components/form/change-password-form.tsx | 2 +- src/components/form/profile-form.tsx | 5 +- src/components/form/settings-form.tsx | 8 + src/components/form/signin-form.tsx | 1 + src/components/form/user-settings-form.tsx | 8 + src/components/sidebar/app-sidebar.tsx | 15 +- src/components/sidebar/nav-main.tsx | 17 +- src/components/sidebar/nav-user.tsx | 3 +- src/components/ui/alert.tsx | 78 +++++++ src/components/ui/dialog.tsx | 71 ++++--- src/components/ui/input.tsx | 6 +- src/components/ui/search-input.tsx | 43 ++++ src/components/user/change-role-dialog.tsx | 67 ++++++ .../user/change-user-status-dialog.tsx | 67 ++++++ src/components/user/edit-user-dialog.tsx | 66 ++++++ src/components/user/set-password-dialog.tsx | 67 ++++++ src/components/user/user-column.tsx | 74 +++++++ src/hooks/use-prevent-auto-focus.ts | 14 ++ src/lib/auth/permissions.ts | 18 +- src/routeTree.gen.ts | 154 ++++++++++---- src/routes/(app)/(auth)/account/index.tsx | 10 +- src/routes/(app)/(auth)/kanri/index.tsx | 15 ++ src/routes/(app)/(auth)/{ => kanri}/logs.tsx | 52 +---- src/routes/(app)/(auth)/kanri/route.tsx | 10 + .../(app)/(auth)/{ => kanri}/settings.tsx | 2 +- src/routes/(app)/(auth)/kanri/users.tsx | 81 ++++++++ src/service/audit.api.ts | 4 +- src/service/audit.schema.ts | 2 - src/service/queries.ts | 10 + src/service/repository.ts | 41 +++- src/service/setting.api.ts | 30 +-- src/service/user.api.ts | 133 ++++++++++++ src/service/user.schema.ts | 45 ++++ src/types/common.d.ts | 4 + src/types/db.d.ts | 15 +- src/utils/{help.ts => helper.ts} | 16 ++ 45 files changed, 1866 insertions(+), 292 deletions(-) create mode 100644 src/components/audit/view-detail-dialog.tsx create mode 100644 src/components/form/admin-ban-user-form.tsx create mode 100644 src/components/form/admin-set-password-form.tsx create mode 100644 src/components/form/admin-set-user-role-form.tsx create mode 100644 src/components/form/admin-update-user-info-form.tsx create mode 100644 src/components/ui/alert.tsx create mode 100644 src/components/ui/search-input.tsx create mode 100644 src/components/user/change-role-dialog.tsx create mode 100644 src/components/user/change-user-status-dialog.tsx create mode 100644 src/components/user/edit-user-dialog.tsx create mode 100644 src/components/user/set-password-dialog.tsx create mode 100644 src/components/user/user-column.tsx create mode 100644 src/hooks/use-prevent-auto-focus.ts create mode 100644 src/routes/(app)/(auth)/kanri/index.tsx rename src/routes/(app)/(auth)/{ => kanri}/logs.tsx (64%) create mode 100644 src/routes/(app)/(auth)/kanri/route.tsx rename src/routes/(app)/(auth)/{ => kanri}/settings.tsx (90%) create mode 100644 src/routes/(app)/(auth)/kanri/users.tsx create mode 100644 src/service/user.api.ts create mode 100644 src/service/user.schema.ts create mode 100644 src/types/common.d.ts rename src/utils/{help.ts => helper.ts} (61%) diff --git a/messages/en.json b/messages/en.json index 0a48f32..66ff78b 100644 --- a/messages/en.json +++ b/messages/en.json @@ -24,6 +24,19 @@ } } ], + "exp_time": [ + { + "match": { + "time=1d": "1 day", + "time=7d": "7 days", + "time=15d": "15 days", + "time=1m": "1 month", + "time=6m": "6 month", + "time=1y": "1 year", + "time=0": "Forever" + } + } + ], "ui_login_btn": "Sign in", "ui_logout_btn": "Sign out", "ui_cancel_btn": "Cancel", @@ -31,11 +44,14 @@ "ui_confirm_btn": "Confirm", "ui_signup_btn": "Sign up", "ui_view_btn": "View", - "ui_save_btn": "Save", + "ui_save_btn": "Save changes", "ui_update_btn": "Update", "ui_delete_btn": "Delete", "ui_ban_btn": "Lock", "ui_unban_btn": "Unlock", + "ui_update_password_btn": "Set password", + "ui_change_role_btn": "Set role", + "ui_edit_user_btn": "Edit User", "ui_dialog_view_title": "View {type} details", "ui_view_all_notifications": "View All Notifications", "ui_label_notifications": "Notifications", @@ -46,7 +62,8 @@ "nav_add_new": "Add new", "nav_edit": "Edit", "nav_change_password": "Change password", - "nav_log": "Logs", + "nav_logs": "Logs", + "nav_users": "Người dùng", "nav_roles": "Vai trò & quyền hạn", "nav_box": "Box", "nav_account": "Account", @@ -84,6 +101,8 @@ "logs_page_ui_table_header_table": "Table", "logs_page_ui_table_header_action": "Action", "logs_page_ui_table_header_create_at": "Create date", + "logs_page_ui_table_header_old_value": "Old value", + "logs_page_ui_table_header_new_value": "New value", "logs_page_ui_badge_action": [ { "match": { @@ -98,6 +117,28 @@ } } ], + "users_page_ui_title": "Users", + "users_page_ui_table_header_name": "Name", + "users_page_ui_table_header_email": "Email", + "users_page_ui_table_header_role": "Role", + "users_page_ui_table_header_banned": "Banned?", + "users_page_ui_table_header_created_at": "Create date", + "users_page_message_user_not_found": "User not found!", + "users_page_message_user_min": "Password must be at least 5 characters long!", + "users_page_message_contain_uppercase": "Password must contain at least one UPPERCASE letter!", + "users_page_message_contain_lowercase": "Password must contain at least one lowercase letter!", + "users_page_message_contain_number": "Password must contain at least one number!", + "users_page_message_contain_special": "Password must contain at least one special character!", + "users_page_message_set_password_success": "Set password successfully!", + "users_page_message_update_info_success": "Edited user information successfully!", + "users_page_message_set_role_success": "Modified user role successfully!", + "users_page_message_banned_success": "Banned {name} successfully!", + "users_page_message_select_min_one_day": "Please select expiration at least 1 day!", + "users_page_ui_form_ban_reason": "Ban reason", + "users_page_ui_form_ban_exp": "Ban expiration", + "users_page_ui_select_placeholder_language": "Select language", + "users_page_ui_select_placeholder_ban_exp": "Select time", "backend_INVALID_EMAIL_OR_PASSWORD": "Email or password incorrect!", - "backend_INVALID_PASSWORD": "Password incorrect!" + "backend_INVALID_PASSWORD": "Password incorrect!", + "backend_YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role" } diff --git a/messages/vi.json b/messages/vi.json index 3329bda..4c94b84 100644 --- a/messages/vi.json +++ b/messages/vi.json @@ -24,6 +24,19 @@ } } ], + "exp_time": [ + { + "match": { + "time=1d": "1 ngày", + "time=7d": "7 ngày", + "time=15d": "15 ngày", + "time=1m": "1 tháng", + "time=6m": "6 tháng", + "time=1y": "1 năm", + "time=0": "Vĩnh viễn" + } + } + ], "ui_login_btn": "Đăng nhập", "ui_logout_btn": "Đăng xuất", "ui_cancel_btn": "Hủy", @@ -31,11 +44,14 @@ "ui_confirm_btn": "Xác nhận", "ui_signup_btn": "Đăng ký", "ui_view_btn": "Xem", - "ui_save_btn": "Lưu", + "ui_save_btn": "Lưu thay đổi", "ui_update_btn": "Cập nhật", "ui_delete_btn": "Xóa", "ui_ban_btn": "Khóa", "ui_unban_btn": "Mở khóa", + "ui_update_password_btn": "Đặt lại mật khẩu", + "ui_change_role_btn": "Đặt lại quyền hạn", + "ui_edit_user_btn": "Chỉnh sửa người dùng", "ui_dialog_view_title": "Xem chi tiết {type}", "ui_view_all_notifications": "Xem tất cả thông báo", "ui_label_notifications": "Thông báo", @@ -46,7 +62,8 @@ "nav_add_new": "Thêm mới", "nav_edit": "Chỉnh sửa", "nav_change_password": "Đổi mật khẩu", - "nav_log": "Lịch sử", + "nav_logs": "Lịch sử", + "nav_users": "Người dùng", "nav_roles": "Vai trò & quyền hạn", "nav_box": "Hộp chứa", "nav_account": "Tài khoản", @@ -100,6 +117,28 @@ } } ], + "users_page_ui_title": "Người dùng", + "users_page_ui_table_header_name": "Tên", + "users_page_ui_table_header_email": "Email", + "users_page_ui_table_header_role": "Quyền hạn", + "users_page_ui_table_header_banned": "Đã khóa?", + "users_page_ui_table_header_created_at": "Ngày tạo", + "users_page_message_user_not_found": "Không tìm thấy người dùng này!", + "users_page_message_user_min": "Mật khẩu phải có ít nhất 5 ký tự!", + "users_page_message_contain_uppercase": "Mật khẩu phải có ít nhất 1 ký tự viết HOA!", + "users_page_message_contain_lowercase": "Mật khẩu phải có ít nhất 1 ký tự viết thường!", + "users_page_message_contain_number": "Mật khẩu phải có ít nhất 1 chữ số!", + "users_page_message_contain_special": "Mật khẩu phải có ít nhất 1 ký tự đặc biệt!", + "users_page_message_set_password_success": "Đặt lại mật khẩu thành công!", + "users_page_message_update_info_success": "Chỉnh sửa thông tin người dùng thành công!", + "users_page_message_set_role_success": "Chỉnh sửa quyền hạn người dùng thành công!", + "users_page_message_banned_success": "Người dùng {name} bị cấm thành công!", + "users_page_message_select_min_one_day": "Chọn ít nhất là 1 ngày!", + "users_page_ui_form_ban_reason": "Lý do cấm", + "users_page_ui_form_ban_exp": "Thời gian cấm", + "users_page_ui_select_placeholder_language": "Hãy chọn ngôn ngữ", + "users_page_ui_select_placeholder_ban_exp": "Hãy chọn thời gian cấm", "backend_INVALID_EMAIL_OR_PASSWORD": "Email hoặc mật khẩu không đúng!", - "backend_INVALID_PASSWORD": "Mật khẩu hiện tại không đúng!" + "backend_INVALID_PASSWORD": "Mật khẩu hiện tại không đúng!", + "backend_YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "Bạn không đủ quyền để chỉnh sửa quyền hạn người dùng!" } diff --git a/prisma/seed.ts b/prisma/seed.ts index a4f34eb..c7bb214 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -25,7 +25,7 @@ async function main() { body: { email: 'luu.dat.tham@gmail.com', password: 'Th@m!S@m!040390', - name: 'Sam', + name: 'Sam Liu', role: 'admin', }, }); @@ -38,8 +38,8 @@ async function main() { ...settingsData, { key: admin ? (admin?.user?.id as string) : (mailExists?.id as string), - value: '{ "language": "en" }', - description: 'User Settings', + value: '{"language": "en"}', + description: 'User settings', relation: 'user', }, ]; diff --git a/src/components/audit/audit-columns.tsx b/src/components/audit/audit-columns.tsx index ed6341c..6aa7650 100644 --- a/src/components/audit/audit-columns.tsx +++ b/src/components/audit/audit-columns.tsx @@ -1,24 +1,14 @@ import { m } from '@/paraglide/messages'; import { formatters } from '@/utils/formatters'; -import { jsonSupport } from '@/utils/help'; -import { EyeIcon } from '@phosphor-icons/react'; import { ColumnDef } from '@tanstack/react-table'; import { Badge } from '../ui/badge'; -import { Button } from '../ui/button'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '../ui/dialog'; -import { Label } from '../ui/label'; -import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; -import ActionBadge, { UserActionType } from './action-badge'; -export const logColumns: ColumnDef[] = [ +import ActionBadge, { UserActionType } from './action-badge'; +import ViewDetail from './view-detail-dialog'; + +export const logColumns: ColumnDef[] = [ { - accessorKey: 'user.name', + accessorFn: (row) => row.user?.name ?? '', header: m.logs_page_ui_table_header_username(), meta: { thClass: 'w-1/6', @@ -72,88 +62,3 @@ export const logColumns: ColumnDef[] = [ ), }, ]; - -type ViewDetailProps = { - data: AuditLog; -}; - -const ViewDetail = ({ data }: ViewDetailProps) => { - return ( - - - - - - - - - - - - - - - {m.ui_dialog_view_title({ type: m.nav_log() })} - - -
-
- - {m.logs_page_ui_table_header_username()}: - - -
-
- - {m.logs_page_ui_table_header_table()}: - - - {data.tableName} - -
-
- - {m.logs_page_ui_table_header_action()}: - - -
- {data.oldValue && ( -
- - {m.logs_page_ui_table_header_old_value()}: - -
-                {jsonSupport(data.oldValue)}
-              
-
- )} -
- - {m.logs_page_ui_table_header_new_value()}: - -
-              {data.newValue ? jsonSupport(data.newValue) : ''}
-            
-
-
- - {m.logs_page_ui_table_header_create_at()}: - - {formatters.dateTime(new Date(data.createdAt))} -
-
-
-
- ); -}; diff --git a/src/components/audit/view-detail-dialog.tsx b/src/components/audit/view-detail-dialog.tsx new file mode 100644 index 0000000..e001f14 --- /dev/null +++ b/src/components/audit/view-detail-dialog.tsx @@ -0,0 +1,115 @@ +import usePreventAutoFocus from '@/hooks/use-prevent-auto-focus'; +import { m } from '@/paraglide/messages'; +import { formatters } from '@/utils/formatters'; +import { jsonSupport } from '@/utils/helper'; +import { EyeIcon } from '@phosphor-icons/react'; +import { Badge } from '../ui/badge'; +import { Button } from '../ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '../ui/dialog'; +import { Label } from '../ui/label'; +import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; +import ActionBadge, { UserActionType } from './action-badge'; + +type ViewDetailProps = { + data: AuditWithUser; +}; + +const ViewDetail = ({ data }: ViewDetailProps) => { + const prevent = usePreventAutoFocus(); + + return ( + + + + + + + + + + + + e.preventDefault()} + > + + + + {m.ui_dialog_view_title({ type: m.nav_logs() })} + + + {m.ui_dialog_view_title({ type: m.nav_logs() })} + + +
+
+ + {m.logs_page_ui_table_header_username()}: + + +
+
+ + {m.logs_page_ui_table_header_table()}: + + + {data.tableName} + +
+
+ + {m.logs_page_ui_table_header_action()}: + + +
+ {data.oldValue && ( +
+ + {m.logs_page_ui_table_header_old_value()}: + +
+                {jsonSupport(data.oldValue)}
+              
+
+ )} +
+ + {m.logs_page_ui_table_header_new_value()}: + +
+              {data.newValue ? jsonSupport(data.newValue) : ''}
+            
+
+
+ + {m.logs_page_ui_table_header_create_at()}: + + {formatters.dateTime(new Date(data.createdAt))} +
+
+
+
+ ); +}; + +export default ViewDetail; diff --git a/src/components/form/admin-ban-user-form.tsx b/src/components/form/admin-ban-user-form.tsx new file mode 100644 index 0000000..f9ad033 --- /dev/null +++ b/src/components/form/admin-ban-user-form.tsx @@ -0,0 +1,193 @@ +import { m } from '@/paraglide/messages'; +import { usersQueries } from '@/service/queries'; +import { banUser } from '@/service/user.api'; +import { userBanSchema } from '@/service/user.schema'; +import { ReturnError } from '@/types/common'; +import { WarningIcon } from '@phosphor-icons/react'; +import { useForm } from '@tanstack/react-form'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { UserWithRole } from 'better-auth/plugins'; +import { toast } from 'sonner'; +import { Alert, AlertDescription, AlertTitle } from '../ui/alert'; +import { Button } from '../ui/button'; +import { DialogClose, DialogFooter } from '../ui/dialog'; +import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field'; +import { Input } from '../ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '../ui/select'; +import { Textarea } from '../ui/textarea'; + +type FormProps = { + data: UserWithRole; + onSubmit: (open: boolean) => void; +}; + +const BanUserForm = ({ data, onSubmit }: FormProps) => { + const queryClient = useQueryClient(); + + const banUserMutation = useMutation({ + mutationFn: banUser, + onSuccess: () => { + queryClient.refetchQueries({ + queryKey: usersQueries.all, + }); + onSubmit(false); + toast.success(m.users_page_message_banned_success({ name: data.name }), { + richColors: true, + }); + }, + onError: (error: ReturnError) => { + console.error(error); + toast.error( + (m[`backend_${error.code}` as keyof typeof m] as () => string)(), + { richColors: true }, + ); + }, + }); + + const form = useForm({ + defaultValues: { + id: data.id, + banReason: '', + banExp: 0, + }, + validators: { + onChange: userBanSchema, + onSubmit: userBanSchema, + }, + onSubmit: async ({ value }) => { + banUserMutation.mutate({ data: value }); + }, + }); + + return ( +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + > + + + + + {m.profile_form_name()}: {data.name} + + adá + + { + const isInvalid = + field.state.meta.isTouched && !field.state.meta.isValid; + return ( + + + {isInvalid && } + + ); + }} + /> + { + const isInvalid = + field.state.meta.isTouched && !field.state.meta.isValid; + return ( + + + {m.users_page_ui_form_ban_reason()}: + +