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 /users route 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.ts

  • Step 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.ts

  • Step 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.ts

  • Step 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.ts

  • Step 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.ts

  • Step 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.tsx

  • Step 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.tsx

  • Step 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.tsx

  • Step 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.tsx

  • Step 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.tsx

  • Step 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.tsx

  • Step 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.tsx

  • Create: frontend/src/pages/users/UsersPage.less

  • Step 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.tsx

  • Step 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.tsx

  • Step 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:fix from frontend directory

  • Step 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?