User Management for Super Admin - Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build User Management page for Super Admin with full CRUD on all users across all organizations.
Architecture:
- New
/usersroute accessible only to super admin - Main list page with table, filters, pagination
- Slide-over panel for create/edit/view actions
- New service methods for user CRUD + suspend/activate
- No organization service exists - create one for listing organizations
Tech Stack: React 19, Ant Design 6, TanStack Query v5, Zustand
File Structure
frontend/src/
├── pages/users/
│ ├── UsersPage.tsx # Main list page
│ └── UsersPage.less
├── components/users/
│ ├── UserTable.tsx # Table with data + actions
│ ├── UserFilters.tsx # Org dropdown + search
│ ├── UserSlideOver.tsx # Slide-over panel
│ ├── UserForm.tsx # Create/Edit form
│ ├── DeleteConfirmModal.tsx # Delete confirmation
│ └── UserStatusBadge.tsx # Status badge
├── services/user.service.ts # Add new methods
├── services/organization.service.ts # Create - for org listing
├── hooks/useUsers.ts # Create - TanStack Query hooks
├── schemas/user.schema.ts # Create - Zod schema for form
├── types/user.types.ts # Create - TypeScript types
└── lib/api.ts # May need adjustment if base URL changes
backend/src/users/
├── users.service.ts # May need minor adjustments
frontend/src/components/layouts/SidebarMenu.tsx # Add nav item
frontend/src/App.tsx # Add /users route
Task 1: Create TypeScript types for User Management
Files:
Create:
frontend/src/types/user.types.tsStep 1: Create types file
export interface User {
id: string;
email: string;
fullName: string;
phone: string | null;
avatarUrl: string | null;
status: 'active' | 'inactive' | 'suspended';
organizationId: string | null;
organization: { id: string; name: string } | null;
roles: { id: string; name: string }[];
createdAt: string;
updatedAt: string;
}
export interface UserFilters {
page?: number;
limit?: number;
search?: string;
orgId?: string;
status?: string;
}
export interface UserListResponse {
data: User[];
meta: { total: number; page: number; limit: number; totalPages: number };
}
export interface CreateUserData {
email: string;
password: string;
fullName: string;
roleIds: string[];
organizationId: string;
phone?: string;
}
export interface UpdateUserData {
fullName?: string;
phone?: string;
status?: 'active' | 'inactive' | 'suspended';
roleIds?: string[];
}
- Step 2: Commit
git add frontend/src/types/user.types.ts
git commit -m "feat(frontend): add user types for user management"
Task 2: Create Organization service (needed for org dropdown)
Files:
Create:
frontend/src/services/organization.service.tsStep 1: Create organization service
import { api } from '@/lib/api';
export interface Organization {
id: string;
name: string;
code: string;
status: string;
}
export const organizationService = {
async getAll(): Promise<{ data: Organization[] }> {
const response = await api.get<{ data: Organization[] }>('/organizations');
return response.data;
},
};
- Step 2: Commit
git add frontend/src/services/organization.service.ts
git commit -m "feat(frontend): add organization service for listing orgs"
Task 3: Add new methods to user service
Files:
Modify:
frontend/src/services/user.service.tsStep 1: Add new methods to user service
import { api } from '@/lib/api';
import type { User, UserFilters, UserListResponse, CreateUserData, UpdateUserData } from '@/types/user.types';
export interface OrgUser {
id: string;
fullName: string;
email: string;
status: 'active' | 'inactive' | 'suspended';
roles?: { id: string; name: string }[];
}
export interface OrgUsersResponse {
data: OrgUser[];
meta: { total: number; page: number; limit: number; totalPages: number };
}
export const userService = {
async getOrgUsers(params?: {
page?: number;
limit?: number;
search?: string;
}): Promise<OrgUsersResponse> {
const response = await api.get<OrgUsersResponse>('/users', { params });
return response.data;
},
async getUsers(params?: UserFilters): Promise<UserListResponse> {
const response = await api.get<UserListResponse>('/users', { params });
return response.data;
},
async getUserById(id: string): Promise<User> {
const response = await api.get<User>(`/users/${id}`);
return response.data;
},
async createUser(data: CreateUserData): Promise<User> {
const response = await api.post<User>('/users', data);
return response.data;
},
async updateUser(id: string, data: UpdateUserData): Promise<User> {
const response = await api.put<User>(`/users/${id}`, data);
return response.data;
},
async deleteUser(id: string): Promise<void> {
await api.delete(`/users/${id}`);
},
async suspendUser(id: string): Promise<User> {
const response = await api.post<User>(`/users/${id}/suspend`);
return response.data;
},
async activateUser(id: string): Promise<User> {
const response = await api.post<User>(`/users/${id}/activate`);
return response.data;
},
};
- Step 2: Commit
git add frontend/src/services/user.service.ts
git commit -m "feat(frontend): add user service methods for CRUD + suspend/activate"
Task 4: Create Zod schema for user form
Files:
Create:
frontend/src/schemas/user.schema.tsStep 1: Create user schema
import { z } from 'zod';
export const createUserSchema = z.object({
email: z.string().email('Email không hợp lệ').min(1, 'Email là bắt buộc'),
password: z.string().min(8, 'Mật khẩu phải có ít nhất 8 ký tự'),
confirmPassword: z.string().min(8, 'Xác nhận mật khẩu phải có ít nhất 8 ký tự'),
fullName: z.string().min(2, 'Họ tên phải có ít nhất 2 ký tự'),
roleIds: z.array(z.string()).min(1, 'Vai trò là bắt buộc'),
organizationId: z.string().min(1, 'Tổ chức là bắt buộc'),
phone: z.string().optional(),
}).refine((data) => data.password === data.confirmPassword, {
message: 'Mật khẩu xác nhận không khớp',
path: ['confirmPassword'],
});
export const updateUserSchema = z.object({
fullName: z.string().min(2, 'Họ tên phải có ít nhất 2 ký tự').optional(),
phone: z.string().optional(),
status: z.enum(['active', 'inactive', 'suspended']).optional(),
roleIds: z.array(z.string()).optional(),
});
export type CreateUserInput = z.infer<typeof createUserSchema>;
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
- Step 2: Commit
git add frontend/src/schemas/user.schema.ts
git commit -m "feat(frontend): add user schema for form validation"
Task 5: Create TanStack Query hooks for users
Files:
Create:
frontend/src/hooks/useUsers.tsStep 1: Create useUsers hook
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { userService } from '@/services/user.service';
import type { UserFilters, CreateUserData, UpdateUserData } from '@/types/user.types';
import { message } from 'antd';
export function useUsers(filters: UserFilters) {
return useQuery({
queryKey: ['users', filters],
queryFn: () => userService.getUsers(filters),
});
}
export function useUser(id: string) {
return useQuery({
queryKey: ['user', id],
queryFn: () => userService.getUserById(id),
enabled: !!id,
});
}
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateUserData) => userService.createUser(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
message.success('Tạo người dùng thành công');
},
onError: () => {
message.error('Tạo người dùng thất bại');
},
});
}
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateUserData }) =>
userService.updateUser(id, data),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
message.success('Cập nhật người dùng thành công');
},
onError: () => {
message.error('Cập nhật người dùng thất bại');
},
});
}
export function useDeleteUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => userService.deleteUser(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
message.success('Xóa người dùng thành công');
},
onError: () => {
message.error('Xóa người dùng thất bại');
},
});
}
export function useSuspendUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => userService.suspendUser(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
message.success('Tạm ngưng người dùng thành công');
},
onError: () => {
message.error('Tạm ngưng người dùng thất bại');
},
});
}
export function useActivateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => userService.activateUser(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
message.success('Kích hoạt người dùng thành công');
},
onError: () => {
message.error('Kích hoạt người dùng thất bại');
},
});
}
- Step 2: Commit
git add frontend/src/hooks/useUsers.ts
git commit -m "feat(frontend): add useUsers hook for TanStack Query"
Task 6: Create UserStatusBadge component
Files:
Create:
frontend/src/components/users/UserStatusBadge.tsxStep 1: Create UserStatusBadge component
import type React from 'react';
import { Tag } from 'antd';
interface UserStatusBadgeProps {
status: 'active' | 'inactive' | 'suspended';
}
export const UserStatusBadge: React.FC<UserStatusBadgeProps> = ({ status }) => {
const config = {
active: { color: 'success', text: 'Hoạt động' },
inactive: { color: 'default', text: 'Không hoạt động' },
suspended: { color: 'error', text: 'Tạm ngưng' },
};
const { color, text } = config[status] || config.inactive;
return <Tag color={color}>{text}</Tag>;
};
- Step 2: Commit
git add frontend/src/components/users/UserStatusBadge.tsx
git commit -m "feat(frontend): add UserStatusBadge component"
Task 7: Create DeleteConfirmModal component
Files:
Create:
frontend/src/components/users/DeleteConfirmModal.tsxStep 1: Create DeleteConfirmModal component
import { Button, Modal, Typography } from 'antd';
import type React from 'react';
const { Text } = Typography;
interface DeleteConfirmModalProps {
open: boolean;
userName: string;
onConfirm: () => void;
onCancel: () => void;
loading?: boolean;
}
export const DeleteConfirmModal: React.FC<DeleteConfirmModalProps> = ({
open,
userName,
onConfirm,
onCancel,
loading,
}) => {
return (
<Modal
open={open}
title="Xác nhận xóa"
onCancel={onCancel}
footer={[
<Button key="cancel" onClick={onCancel}>
Hủy
</Button>,
<Button key="delete" type="primary" danger onClick={onConfirm} loading={loading}>
Xóa
</Button>,
]}
>
<Text>Xóa người dùng "{userName}"? Hành động này không thể hoàn tác.</Text>
</Modal>
);
};
- Step 2: Commit
git add frontend/src/components/users/DeleteConfirmModal.tsx
git commit -m "feat(frontend): add DeleteConfirmModal component"
Task 8: Create UserFilters component
Files:
Create:
frontend/src/components/users/UserFilters.tsxStep 1: Create UserFilters component
import { Input, Select, Space } from 'antd';
import type React from 'react';
import { SearchOutlined } from '@ant-design/icons';
import type { Organization } from '@/services/organization.service';
const { Search } = Input;
interface UserFiltersProps {
organizations: Organization[];
selectedOrgId: string | undefined;
searchValue: string;
onOrgChange: (value: string | undefined) => void;
onSearch: (value: string) => void;
loading?: boolean;
}
export const UserFilters: React.FC<UserFiltersProps> = ({
organizations,
selectedOrgId,
searchValue,
onOrgChange,
onSearch,
loading,
}) => {
const orgOptions = [
{ label: 'Tất cả tổ chức', value: '' },
...organizations.map((org) => ({ label: org.name, value: org.id })),
];
return (
<Space wrap>
<Select
placeholder="Chọn tổ chức"
value={selectedOrgId || ''}
onChange={(value) => onOrgChange(value || undefined)}
options={orgOptions}
style={{ minWidth: 200 }}
allowClear
loading={loading}
/>
<Search
placeholder="Tìm theo tên hoặc email..."
value={searchValue}
onSearch={onSearch}
onChange={(e) => onSearch(e.target.value)}
prefix={<SearchOutlined />}
style={{ width: 300 }}
allowClear
/>
</Space>
);
};
- Step 2: Commit
git add frontend/src/components/users/UserFilters.tsx
git commit -m "feat(frontend): add UserFilters component"
Task 9: Create UserForm component
Files:
Create:
frontend/src/components/users/UserForm.tsxStep 1: Create UserForm component
import { Button, Form, Input, Select, Space } from 'antd';
import type React from 'react';
import { zodToAntdRules } from '@/lib/zodToAntdRules';
import { createUserSchema, updateUserSchema } from '@/schemas/user.schema';
import type { Organization } from '@/services/organization.service';
import type { User } from '@/types/user.types';
interface UserFormProps {
mode: 'create' | 'edit';
user?: User;
organizations: Organization[];
roles: { id: string; name: string }[];
onSubmit: (values: unknown) => void;
onCancel: () => void;
loading?: boolean;
}
const ROLE_OPTIONS = [
{ label: 'Super Admin', value: 'superadmin' },
{ label: 'Admin', value: 'admin' },
{ label: 'Owner', value: 'owner' },
{ label: 'Staff', value: 'staff' },
{ label: 'Technical', value: 'technical' },
];
export const UserForm: React.FC<UserFormProps> = ({
mode,
user,
organizations,
roles,
onSubmit,
onCancel,
loading,
}) => {
const [form] = Form.useForm();
const schema = mode === 'create' ? createUserSchema : updateUserSchema;
const orgOptions = organizations.map((org) => ({
label: org.name,
value: org.id,
}));
const roleOptions = roles.length > 0
? roles.map((role) => ({ label: role.name, value: role.id }))
: ROLE_OPTIONS;
return (
<Form
form={form}
layout="vertical"
onFinish={onSubmit}
initialValues={mode === 'edit' && user ? {
email: user.email,
fullName: user.fullName,
phone: user.phone,
roleIds: user.roles?.map((r) => r.id),
status: user.status,
} : undefined}
>
<Form.Item label="Email" name="email" rules={zodToAntdRules(schema.shape.email)}>
{mode === 'create' ? (
<Input placeholder="email@example.com" />
) : (
<Input disabled />
)}
</Form.Item>
{mode === 'create' && (
<>
<Form.Item label="Mật khẩu" name="password" rules={zodToAntdRules(schema.shape.password)}>
<Input.Password placeholder="Ít nhất 8 ký tự" />
</Form.Item>
<Form.Item
label="Xác nhận mật khẩu"
name="confirmPassword"
rules={zodToAntdRules(schema.shape.confirmPassword)}
>
<Input.Password placeholder="Nhập lại mật khẩu" />
</Form.Item>
</>
)}
<Form.Item label="Họ tên" name="fullName" rules={zodToAntdRules(schema.shape.fullName || createUserSchema.shape.fullName)}>
<Input placeholder="Nguyễn Văn A" />
</Form.Item>
{mode === 'create' && (
<>
<Form.Item
label="Vai trò"
name="roleIds"
rules={zodToAntdRules(createUserSchema.shape.roleIds)}
>
<Select
mode="multiple"
placeholder="Chọn vai trò"
options={roleOptions}
/>
</Form.Item>
<Form.Item
label="Tổ chức"
name="organizationId"
rules={zodToAntdRules(createUserSchema.shape.organizationId)}
>
<Select placeholder="Chọn tổ chức" options={orgOptions} />
</Form.Item>
</>
)}
{mode === 'edit' && (
<>
<Form.Item label="Tổ chức">
<Input disabled value={user?.organization?.name || 'N/A'} />
</Form.Item>
<Form.Item label="Trạng thái" name="status">
<Select
options={[
{ label: 'Hoạt động', value: 'active' },
{ label: 'Tạm ngưng', value: 'suspended' },
]}
/>
</Form.Item>
</>
)}
<Form.Item label="Số điện thoại" name="phone">
<Input placeholder="+84..." />
</Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={onCancel}>Hủy</Button>
<Button type="primary" htmlType="submit" loading={loading}>
{mode === 'create' ? 'Tạo' : 'Lưu'}
</Button>
</Space>
</Form>
);
};
- Step 2: Commit
git add frontend/src/components/users/UserForm.tsx
git commit -m "feat(frontend): add UserForm component"
Task 10: Create UserSlideOver component
Files:
Create:
frontend/src/components/users/UserSlideOver.tsxStep 1: Create UserSlideOver component
import { Drawer, Spin } from 'antd';
import type React from 'react';
import { UserForm } from './UserForm';
import type { Organization } from '@/services/organization.service';
import type { User } from '@/types/user.types';
interface UserSlideOverProps {
open: boolean;
mode: 'create' | 'edit' | 'view';
user?: User;
organizations: Organization[];
roles: { id: string; name: string }[];
onSubmit: (values: unknown) => void;
onClose: () => void;
loading?: boolean;
}
export const UserSlideOver: React.FC<UserSlideOverProps> = ({
open,
mode,
user,
organizations,
roles,
onSubmit,
onClose,
loading,
}) => {
const title = mode === 'create' ? 'Tạo người dùng' : mode === 'edit' ? `Sửa: ${user?.fullName}` : `Chi tiết: ${user?.fullName}`;
return (
<Drawer
open={open}
title={title}
onClose={onClose}
width={480}
destroyOnClose
>
<Spin spinning={loading}>
{(mode === 'create' || mode === 'edit') ? (
<UserForm
mode={mode}
user={user}
organizations={organizations}
roles={roles}
onSubmit={onSubmit}
onCancel={onClose}
loading={loading}
/>
) : (
<div style={{ padding: '16px 0' }}>
<p><strong>Email:</strong> {user?.email}</p>
<p><strong>Họ tên:</strong> {user?.fullName}</p>
<p><strong>Số điện thoại:</strong> {user?.phone || 'N/A'}</p>
<p><strong>Tổ chức:</strong> {user?.organization?.name || 'N/A'}</p>
<p><strong>Vai trò:</strong> {user?.roles?.map(r => r.name).join(', ') || 'N/A'}</p>
<p><strong>Trạng thái:</strong> {user?.status}</p>
</div>
)}
</Spin>
</Drawer>
);
};
- Step 2: Commit
git add frontend/src/components/users/UserSlideOver.tsx
git commit -m "feat(frontend): add UserSlideOver component"
Task 11: Create UserTable component
Files:
Create:
frontend/src/components/users/UserTable.tsxStep 1: Create UserTable component
import { Avatar, Button, Dropdown, Space, Tag, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import type React from 'react';
import { MoreOutlined, EditOutlined, EyeOutlined, DeleteOutlined, StopOutlined, CheckCircleOutlined } from '@ant-design/icons';
import type { MenuProps } from 'antd';
import { UserStatusBadge } from './UserStatusBadge';
import type { User } from '@/types/user.types';
const { Text } = Typography;
interface UserTableProps {
data: User[];
loading?: boolean;
onEdit: (user: User) => void;
onView: (user: User) => void;
onDelete: (user: User) => void;
onSuspend: (user: User) => void;
onActivate: (user: User) => void;
}
export const UserTable: React.FC<UserTableProps> = ({
data,
onEdit,
onView,
onDelete,
onSuspend,
onActivate,
}) => {
const getActionMenu = (user: User): MenuProps['items'] => {
const items: MenuProps['items'] = [
{
key: 'view',
icon: <EyeOutlined />,
label: 'Xem chi tiết',
onClick: () => onView(user),
},
{
key: 'edit',
icon: <EditOutlined />,
label: 'Sửa',
onClick: () => onEdit(user),
},
{ type: 'divider' },
];
if (user.status === 'suspended') {
items.push({
key: 'activate',
icon: <CheckCircleOutlined />,
label: 'Kích hoạt',
onClick: () => onActivate(user),
});
} else {
items.push({
key: 'suspend',
icon: <StopOutlined />,
label: 'Tạm ngưng',
onClick: () => onSuspend(user),
});
}
items.push({
key: 'delete',
icon: <DeleteOutlined />,
label: 'Xóa',
danger: true,
onClick: () => onDelete(user),
});
return items;
};
const columns: ColumnsType<User> = [
{
title: 'Người dùng',
key: 'user',
render: (_, record) => (
<Space>
<Avatar style={{ backgroundColor: '#1890ff' }}>
{record.fullName.charAt(0).toUpperCase()}
</Avatar>
<Text strong>{record.fullName}</Text>
</Space>
),
},
{
title: 'Email',
dataIndex: 'email',
key: 'email',
},
{
title: 'Vai trò',
key: 'roles',
render: (_, record) => (
record.roles?.map((role) => (
<Tag key={role.id} color="blue">{role.name}</Tag>
)) || 'N/A'
),
},
{
title: 'Tổ chức',
key: 'organization',
render: (_, record) => record.organization?.name || 'N/A',
},
{
title: 'Trạng thái',
dataIndex: 'status',
key: 'status',
render: (status) => <UserStatusBadge status={status} />,
},
{
title: '',
key: 'actions',
width: 50,
render: (_, record) => (
<Dropdown menu={{ items: getActionMenu(record) }} trigger={['click']}>
<Button type="text" icon={<MoreOutlined />} />
</Dropdown>
),
},
];
return (
<DataTable
dataSource={data}
columns={columns}
rowKey="id"
loading={loading}
pagination={false}
/>
);
};
- Step 2: Commit
git add frontend/src/components/users/UserTable.tsx
git commit -m "feat(frontend): add UserTable component"
Task 12: Create UsersPage main page
Files:
Create:
frontend/src/pages/users/UsersPage.tsxCreate:
frontend/src/pages/users/UsersPage.lessStep 1: Create UsersPage
import { PlusOutlined } from '@ant-design/icons';
import { Button, Card, Pagination, Row, Space } from 'antd';
import { useState } from 'react';
import { EmptyState } from '@/components/common/EmptyState';
import { LoadingSkeleton } from '@/components/common/LoadingSkeleton';
import { PageHeader } from '@/components/common/PageHeader';
import { DeleteConfirmModal } from '@/components/users/DeleteConfirmModal';
import { UserFilters } from '@/components/users/UserFilters';
import { UserSlideOver } from '@/components/users/UserSlideOver';
import { UserTable } from '@/components/users/UserTable';
import { useActivateUser, useCreateUser, useDeleteUser, useSuspendUser, useUpdateUser, useUsers } from '@/hooks/useUsers';
import { organizationService } from '@/services/organization.service';
import type { User } from '@/types/user.types';
import { useQuery } from '@tanstack/react-query';
export default function UsersPage() {
const [page, setPage] = useState(1);
const [limit, setLimit] = useState(20);
const [search, setSearch] = useState('');
const [orgId, setOrgId] = useState<string | undefined>(undefined);
const [slideOverOpen, setSlideOverOpen] = useState(false);
const [slideOverMode, setSlideOverMode] = useState<'create' | 'edit' | 'view'>('create');
const [selectedUser, setSelectedUser] = useState<User | undefined>();
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [userToDelete, setUserToDelete] = useState<User | undefined>();
const { data: usersData, isLoading, isError, refetch } = useUsers({
page,
limit,
search: search || undefined,
orgId,
});
const { data: orgsData } = useQuery({
queryKey: ['organizations'],
queryFn: () => organizationService.getAll(),
});
const createUser = useCreateUser();
const updateUser = useUpdateUser();
const deleteUser = useDeleteUser();
const suspendUser = useSuspendUser();
const activateUser = useActivateUser();
const organizations = orgsData?.data || [];
const handleCreate = () => {
setSelectedUser(undefined);
setSlideOverMode('create');
setSlideOverOpen(true);
};
const handleEdit = (user: User) => {
setSelectedUser(user);
setSlideOverMode('edit');
setSlideOverOpen(true);
};
const handleView = (user: User) => {
setSelectedUser(user);
setSlideOverMode('view');
setSlideOverOpen(true);
};
const handleDelete = (user: User) => {
setUserToDelete(user);
setDeleteModalOpen(true);
};
const handleConfirmDelete = async () => {
if (userToDelete) {
await deleteUser.mutateAsync(userToDelete.id);
setDeleteModalOpen(false);
setUserToDelete(undefined);
}
};
const handleSuspend = async (user: User) => {
await suspendUser.mutateAsync(user.id);
};
const handleActivate = async (user: User) => {
await activateUser.mutateAsync(user.id);
};
const handleSlideOverClose = () => {
setSlideOverOpen(false);
setSelectedUser(undefined);
};
const handleFormSubmit = async (values: unknown) => {
if (slideOverMode === 'create') {
await createUser.mutateAsync(values as Record<string, unknown>);
} else if (slideOverMode === 'edit' && selectedUser) {
await updateUser.mutateAsync({ id: selectedUser.id, data: values as Record<string, unknown> });
}
handleSlideOverClose();
};
const handleSearch = (value: string) => {
setSearch(value);
setPage(1);
};
const handleOrgChange = (value: string | undefined) => {
setOrgId(value);
setPage(1);
};
if (isLoading) return <LoadingSkeleton type="card" />;
if (isError) return (
<EmptyState
title="Lỗi tải dữ liệu"
description="Không thể tải danh sách người dùng"
action={{ label: 'Thử lại', onClick: () => refetch() }}
/>
);
const users = usersData?.data || [];
const meta = usersData?.meta;
return (
<div>
<PageHeader
title="Quản lý người dùng"
subtitle="Quản lý tất cả người dùng trong hệ thống"
extra={
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
Tạo người dùng
</Button>
}
/>
<Card style={{ marginBottom: 16 }}>
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<UserFilters
organizations={organizations}
selectedOrgId={orgId}
searchValue={search}
onOrgChange={handleOrgChange}
onSearch={handleSearch}
/>
</Row>
{users.length === 0 ? (
<EmptyState
title="Chưa có người dùng"
description="Không tìm thấy người dùng nào"
/>
) : (
<UserTable
data={users}
onEdit={handleEdit}
onView={handleView}
onDelete={handleDelete}
onSuspend={handleSuspend}
onActivate={handleActivate}
/>
)}
</Card>
{meta && meta.totalPages > 1 && (
<Row justify="end">
<Pagination
current={page}
pageSize={limit}
total={meta.total}
onChange={(p, size) => {
setPage(p);
setLimit(size);
}}
showSizeChanger
/>
</Row>
)}
<UserSlideOver
open={slideOverOpen}
mode={slideOverMode}
user={selectedUser}
organizations={organizations}
roles={selectedUser?.roles || []}
onSubmit={handleFormSubmit}
onClose={handleSlideOverClose}
loading={createUser.isPending || updateUser.isPending}
/>
<DeleteConfirmModal
open={deleteModalOpen}
userName={userToDelete?.fullName || ''}
onConfirm={handleConfirmDelete}
onCancel={() => setDeleteModalOpen(false)}
loading={deleteUser.isPending}
/>
</div>
);
}
- Step 2: Create LESS file (leave empty for now)
// frontend/src/pages/users/UsersPage.less
// Styles for UsersPage if needed
- Step 3: Commit
git add frontend/src/pages/users/UsersPage.tsx frontend/src/pages/users/UsersPage.less
git commit -m "feat(frontend): add UsersPage with full CRUD UI"
Task 13: Add Users route to App.tsx
Files:
Modify:
frontend/src/App.tsxStep 1: Add UsersPage import and route
Add import:
import { UsersPage } from '@/pages/users/UsersPage';
Add route inside the <Route element={<DashboardLayout />}> block:
<Route path="/users" element={<UsersPage />} />
- Step 2: Commit
git add frontend/src/App.tsx
git commit -m "feat(frontend): add /users route for user management"
Task 14: Add Users nav item to SidebarMenu
Files:
Modify:
frontend/src/components/layouts/SidebarMenu.tsxStep 1: Add TeamOutlined import and Users nav item
Add import:
import { TeamOutlined } from '@ant-design/icons';
Add nav item to NAV_ITEMS array (add at top level, before ‘farms’):
{
key: 'users',
icon: <TeamOutlined />,
label: 'Người dùng',
path: '/users',
requiredPermission: { resource: 'user', action: 'read' },
},
- Step 2: Commit
git add frontend/src/components/layouts/SidebarMenu.tsx
git commit -m "feat(frontend): add Users menu item to sidebar for super admin"
Task 15: Verify with lint check
Files:
Run:
bun run lint:fixfrom frontend directoryStep 1: Run lint check
cd frontend && bun run lint:fix
Fix any issues reported.
- Step 2: Commit any lint fixes
git add -A && git commit -m "fix(frontend): lint fixes for user management"
Spec Coverage Check
- Super admin can view all users across all organizations - Task 12 (UsersPage + useUsers)
- Super admin can filter users by organization - Task 8 (UserFilters) + Task 12
- Super admin can search users by name or email - Task 8 (UserFilters) + Task 12
- Super admin can create new user with email, password, role, and organization - Task 9 (UserForm) + Task 10 (UserSlideOver) + Task 12
- Super admin can edit user’s role and status - Task 9 (UserForm) + Task 10 (UserSlideOver) + Task 12
- Super admin can suspend/activate user - Task 5 (hooks) + Task 11 (UserTable actions) + Task 12
- Super admin can delete user with confirmation - Task 7 (DeleteConfirmModal) + Task 12
- Slide-over panel slides from right with form - Task 10 (UserSlideOver)
- Delete requires confirmation modal - Task 7 (DeleteConfirmModal)
- All actions show loading state and handle errors gracefully - useMutation with onError message
Plan complete and saved to docs/superpowers/plans/2026-04-22-user-management-plan.md. Two execution options:
1. Subagent-Driven (recommended) - I dispatch a fresh subagent per task, review between tasks, fast iteration
2. Inline Execution - Execute tasks in this session using executing-plans, batch execution with checkpoints
Which approach?