feat: implement privilege permissions table and enhance user detail views

- Added a new PrivilegePermissionsTable component to display permissions associated with user privileges in a structured format.
- Refactored DetailPermissions and UserPageDetail components to utilize the new PrivilegePermissionsTable for better organization of privilege information.
- Introduced employeeId support in user management, allowing for enhanced user detail views and improved data handling.
- Updated validation schemas and unit tests to accommodate new employee and privilege relationships.

These changes improve the user management experience by providing clearer visibility into user privileges and associated employee details.
This commit is contained in:
shancheas
2026-08-27 12:22:27 +07:00
parent eadd4e3c81
commit 105cf3030a
18 changed files with 416 additions and 89 deletions
@@ -0,0 +1,58 @@
import { describe, expect, it } from 'vitest';
import { EMPTY_MATRIX_CELL } from './entities';
import { buildPrivilegeMatrixRows } from './privilege.matrix-rows';
describe('buildPrivilegeMatrixRows', () => {
it('returns an empty list when privilege is missing', () => {
expect(buildPrivilegeMatrixRows(null)).toEqual([]);
expect(buildPrivilegeMatrixRows(undefined)).toEqual([]);
});
it('maps matrix entries onto labeled rows sorted by sortOrder then code', () => {
const rows = buildPrivilegeMatrixRows({
matrix: {
'key-2': { ...EMPTY_MATRIX_CELL, view: true },
'key-1': { ...EMPTY_MATRIX_CELL, create: true },
},
details: [
{
privilegeKeyId: 'key-2',
keyCode: 'USERS',
keyLabel: 'Users',
sortOrder: 2,
action: 'view',
value: true,
},
{
privilegeKeyId: 'key-1',
keyCode: 'BRANCHES',
keyLabel: 'Branches',
sortOrder: 1,
action: 'create',
value: true,
},
],
});
expect(rows.map((row) => row.keyId)).toEqual(['key-1', 'key-2']);
expect(rows[0]).toMatchObject({
code: 'BRANCHES',
label: 'Branches',
cell: { ...EMPTY_MATRIX_CELL, create: true },
});
});
it('falls back to the key id when details are missing', () => {
const rows = buildPrivilegeMatrixRows({
matrix: { 'key-9': { ...EMPTY_MATRIX_CELL, view: true } },
details: [],
});
expect(rows[0]).toMatchObject({
keyId: 'key-9',
code: 'key-9',
label: 'key-9',
sortOrder: 0,
});
});
});
@@ -0,0 +1,29 @@
import { EMPTY_MATRIX_CELL, type PrivilegeEntity, type PrivilegeMatrixCell } from './entities';
export type PrivilegeMatrixRow = {
keyId: string;
code: string;
label: string;
sortOrder: number;
cell: PrivilegeMatrixCell;
};
export function buildPrivilegeMatrixRows(
privilege: Pick<PrivilegeEntity, 'matrix' | 'details'> | null | undefined,
): PrivilegeMatrixRow[] {
const matrix = privilege?.matrix ?? {};
const details = privilege?.details ?? [];
return Object.entries(matrix)
.map(([keyId, cell]) => {
const sample = details.find((detail) => detail.privilegeKeyId === keyId);
return {
keyId,
code: sample?.keyCode ?? keyId,
label: sample?.keyLabel ?? keyId,
sortOrder: sample?.sortOrder ?? 0,
cell: cell ?? EMPTY_MATRIX_CELL,
};
})
.sort((a, b) => a.sortOrder - b.sortOrder || a.code.localeCompare(b.code));
}
@@ -1,63 +1,10 @@
import { Box, Paper, Table, Text } from '@repo/ui/components';
import { Check, Minus } from 'lucide-react';
import { useDetailPageContext, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
import { EMPTY_MATRIX_CELL, PRIVILEGE_ACTIONS, type PrivilegeEntity } from '../../../domain/entities';
import type { PrivilegeEntity } from '../../../domain/entities';
import { PrivilegePermissionsTable } from '../privilege-permissions-table';
export function DetailPermissions() {
const { detailData } = useDetailPageContext<PrivilegeEntity>();
const { t } = useEnterpriseModuleTranslationContext();
const matrix = detailData?.matrix ?? {};
const details = detailData?.details ?? [];
const rows = Object.entries(matrix)
.map(([keyId, cell]) => {
const sample = details.find((detail) => detail.privilegeKeyId === keyId);
return {
keyId,
code: sample?.keyCode ?? keyId,
label: sample?.keyLabel ?? keyId,
sortOrder: sample?.sortOrder ?? 0,
cell: cell ?? EMPTY_MATRIX_CELL,
};
})
.sort((a, b) => a.sortOrder - b.sortOrder || a.code.localeCompare(b.code));
return (
<Paper withBorder shadow="sm" radius="md" p="xl">
<Text fw={600} mb="md">
{t('section_permissions')}
</Text>
<Box style={{ overflowX: 'auto' }}>
<Table striped highlightOnHover withTableBorder withColumnBorders>
<Table.Thead>
<Table.Tr>
<Table.Th>{t('matrix_module')}</Table.Th>
{PRIVILEGE_ACTIONS.map((action) => (
<Table.Th key={action} ta="center">
{t(`matrix_${action}`)}
</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{rows.map((row) => (
<Table.Tr key={row.keyId}>
<Table.Td>
<Text size="sm">{row.label}</Text>
<Text size="xs" c="dimmed">
{row.code}
</Text>
</Table.Td>
{PRIVILEGE_ACTIONS.map((action) => (
<Table.Td key={action} ta="center">
{row.cell[action] ? <Check size={16} /> : <Minus size={16} />}
</Table.Td>
))}
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Box>
</Paper>
);
return <PrivilegePermissionsTable privilege={detailData} t={t} />;
}
@@ -0,0 +1,53 @@
import { Box, Paper, Table, Text } from '@repo/ui/components';
import { Check, Minus } from 'lucide-react';
import { PRIVILEGE_ACTIONS, type PrivilegeEntity } from '../../domain/entities';
import { buildPrivilegeMatrixRows } from '../../domain/privilege.matrix-rows';
export function PrivilegePermissionsTable({
privilege,
t,
}: {
privilege: Pick<PrivilegeEntity, 'matrix' | 'details'> | null | undefined;
t: (key: string) => string;
}) {
const rows = buildPrivilegeMatrixRows(privilege);
return (
<Paper withBorder shadow="sm" radius="md" p="xl">
<Text fw={600} mb="md">
{t('section_permissions')}
</Text>
<Box style={{ overflowX: 'auto' }}>
<Table striped highlightOnHover withTableBorder withColumnBorders>
<Table.Thead>
<Table.Tr>
<Table.Th>{t('matrix_module')}</Table.Th>
{PRIVILEGE_ACTIONS.map((action) => (
<Table.Th key={action} ta="center">
{t(`matrix_${action}`)}
</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{rows.map((row) => (
<Table.Tr key={row.keyId}>
<Table.Td>
<Text size="sm">{row.label}</Text>
<Text size="xs" c="dimmed">
{row.code}
</Text>
</Table.Td>
{PRIVILEGE_ACTIONS.map((action) => (
<Table.Td key={action} ta="center">
{row.cell[action] ? <Check size={16} /> : <Minus size={16} />}
</Table.Td>
))}
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Box>
</Paper>
);
}
@@ -43,7 +43,7 @@ describe('UsersRemoteDataTransformer', () => {
expect(entity.updatedBy).toBe('u-99');
});
it('builds create payload with username, password, and privilegeId, without status', () => {
it('builds create payload with username, password, privilegeId, and employeeId, without status', () => {
const entity: UserEntity = {
id: dto.id,
username: dto.username,
@@ -59,6 +59,7 @@ describe('UsersRemoteDataTransformer', () => {
username: 'alice',
password: 'password123',
privilegeId: 'priv-1',
employeeId: 'emp-1',
});
expect(payload).not.toHaveProperty('status');
expect(payload).not.toHaveProperty('isSuperadmin');
@@ -78,20 +79,36 @@ describe('UsersRemoteDataTransformer', () => {
});
});
it('resolves employeeId from nested employee on create', () => {
const payload = transformer.transformCreatePayload({
username: 'alice',
password: 'password123',
employee: { id: 'emp-1', code: 'EMP_01', name: 'Ada Lovelace' },
});
expect(payload).toEqual({
username: 'alice',
password: 'password123',
employeeId: 'emp-1',
});
});
it('builds edit payload without status, empty password, or audit fields', () => {
const payload = transformer.transformEditPayload({
username: dto.username,
password: '',
privilegeId: 'priv-1',
employee: dto.employee,
status: dto.status,
});
expect(payload).toEqual({
username: 'alice',
privilegeId: 'priv-1',
employeeId: 'emp-1',
});
expect(payload).not.toHaveProperty('password');
expect(payload).not.toHaveProperty('status');
expect(payload).not.toHaveProperty('id');
expect(payload).not.toHaveProperty('employee');
});
it('includes password on edit only when it is non-empty', () => {
@@ -99,11 +116,13 @@ describe('UsersRemoteDataTransformer', () => {
username: 'alice',
password: 'newpassword',
privilegeId: 'priv-1',
employeeId: 'emp-1',
});
expect(payload).toEqual({
username: 'alice',
password: 'newpassword',
privilegeId: 'priv-1',
employeeId: 'emp-1',
});
});
@@ -116,6 +135,21 @@ describe('UsersRemoteDataTransformer', () => {
expect(payload).toEqual({
username: 'alice',
privilegeId: null,
employeeId: null,
});
});
it('sends employeeId null when employee is cleared on edit', () => {
const payload = transformer.transformEditPayload({
username: 'alice',
privilegeId: 'priv-1',
employee: null,
employeeId: '',
});
expect(payload).toEqual({
username: 'alice',
privilegeId: 'priv-1',
employeeId: null,
});
});
@@ -142,4 +176,15 @@ describe('UsersRemoteDataTransformer', () => {
privilegeId: 'priv-1',
});
});
it('maps employee object to employeeId on filter payload', () => {
const payload = transformer.transformPayloadFilter({
username: 'alice',
employee: { id: 'emp-1', code: 'EMP_01', name: 'Ada Lovelace' },
});
expect(payload).toEqual({
username: 'alice',
employeeId: 'emp-1',
});
});
});
@@ -2,12 +2,26 @@ import { BaseDataTransformer } from '@repo/core-api/data-services';
import { emptyToNull, omitEmptyFields } from '../../../../../../../core/domain/configuration-field-validators';
import type { UserDto, UserEntity } from '../entities';
function resolvePrivilegeId(entity: Partial<UserEntity>): string | null | undefined {
if (entity.privilege && typeof entity.privilege === 'object') {
const id = entity.privilege.id;
function resolveRelationId(
relation: { id?: unknown } | null | undefined,
fallbackId?: string | null,
): string | null | undefined {
if (relation && typeof relation === 'object') {
const id = relation.id;
return id == null ? null : String(id);
}
return entity.privilegeId;
return fallbackId;
}
function flattenRelationFilter(filter: Record<string, any>, objectKey: string, idKey: string) {
const value = filter[objectKey];
if (value && typeof value === 'object') {
filter[idKey] = value.id;
delete filter[objectKey];
} else if (typeof value === 'string') {
filter[idKey] = value;
delete filter[objectKey];
}
}
function flattenActor(value: unknown): string | undefined {
@@ -51,14 +65,16 @@ export class UsersRemoteDataTransformer extends BaseDataTransformer<UserEntity>
return omitEmptyFields({
username: entity.username,
password: entity.password,
privilegeId: resolvePrivilegeId(entity),
privilegeId: resolveRelationId(entity.privilege, entity.privilegeId),
employeeId: resolveRelationId(entity.employee, entity.employeeId),
});
}
transformEditPayload(entity: Partial<UserEntity>): Partial<UserEntity> {
const payload: Record<string, unknown> = {
username: entity.username,
privilegeId: emptyToNull(resolvePrivilegeId(entity)) as string | null,
privilegeId: emptyToNull(resolveRelationId(entity.privilege, entity.privilegeId)) as string | null,
employeeId: emptyToNull(resolveRelationId(entity.employee, entity.employeeId)) as string | null,
};
if (entity.password) {
return { ...payload, password: entity.password };
@@ -68,13 +84,8 @@ export class UsersRemoteDataTransformer extends BaseDataTransformer<UserEntity>
transformPayloadFilter(filter: Record<string, any>): Record<string, any> {
const next = { ...filter };
if (next.privilege && typeof next.privilege === 'object') {
next.privilegeId = next.privilege.id;
delete next.privilege;
} else if (typeof next.privilege === 'string') {
next.privilegeId = next.privilege;
delete next.privilege;
}
flattenRelationFilter(next, 'privilege', 'privilegeId');
flattenRelationFilter(next, 'employee', 'employeeId');
return omitEmptyFields(next);
}
}
@@ -47,4 +47,19 @@ describe('createUserSchema', () => {
const schema = createUserSchema(t, { requirePassword: false });
expect(schema.safeParse({ username: 'alice', password: 'short' }).success).toBe(false);
});
it('accepts an optional employee relation on create', () => {
const schema = createUserSchema(t, { requirePassword: true });
expect(
schema.safeParse({
...valid,
employee: { id: 'emp-1', code: 'EMP_01', name: 'Ada Lovelace' },
}).success,
).toBe(true);
});
it('accepts a null employee', () => {
const schema = createUserSchema(t, { requirePassword: true });
expect(schema.safeParse({ ...valid, employee: null }).success).toBe(true);
});
});
@@ -25,5 +25,7 @@ export const createUserSchema = (t: (key: string) => string, options?: { require
password,
privilege: z.any().nullable().optional(),
privilegeId: z.string().nullable().optional(),
employee: z.any().nullable().optional(),
employeeId: z.string().nullable().optional(),
});
};
@@ -0,0 +1,79 @@
import { useEffect, useState } from 'react';
import { Box, Paper, SimpleGrid, FieldValue, Stack, Text, StatusBadge } from '@repo/ui/components';
import { useDetailPageContext, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
import { privilegesDataService } from '../../../../privileges/domain/factories';
import { PrivilegePermissionsTable } from '../../../../privileges/presentation/components/privilege-permissions-table';
import type { PrivilegeEntity } from '../../../../privileges/domain/entities';
import type { UserEntity } from '../../../domain/entities';
function resolvePrivilegeId(user: UserEntity | null | undefined): string | undefined {
const fromRelation = user?.privilege?.id;
const fromId = user?.privilegeId;
const value = fromRelation || fromId;
return value ? String(value) : undefined;
}
export function DetailPrivilege() {
const { detailData } = useDetailPageContext<UserEntity>();
const { t } = useEnterpriseModuleTranslationContext();
const privilegeId = resolvePrivilegeId(detailData);
const [privilege, setPrivilege] = useState<PrivilegeEntity | null>(null);
useEffect(() => {
if (!privilegeId) {
setPrivilege(null);
return;
}
let cancelled = false;
void privilegesDataService
.getOne(String(privilegeId))
.then((result) => {
if (cancelled) return;
const entity = (result.data as { data?: PrivilegeEntity } | undefined)?.data;
setPrivilege(entity ?? null);
})
.catch(() => {
if (!cancelled) setPrivilege(null);
});
return () => {
cancelled = true;
};
}, [privilegeId]);
if (!privilegeId) {
return (
<Paper withBorder shadow="sm" radius="md" p="xl">
<Text fw={600} mb="md">
{t('section_privilege')}
</Text>
<Text size="sm" c="dimmed">
{t('privilege_empty')}
</Text>
</Paper>
);
}
return (
<Stack gap="md">
<Paper withBorder shadow="sm" radius="md" p="xl">
<Text fw={600} mb="md">
{t('section_privilege')}
</Text>
<Box>
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="md" verticalSpacing="xl">
<FieldValue label={t('common:fields.code')} value={privilege?.code ?? detailData?.privilege?.code} />
<FieldValue label={t('common:fields.name')} value={privilege?.name ?? detailData?.privilege?.name} />
<FieldValue
label={t('common:fields.status')}
value={privilege?.status}
render={(val) => (val ? <StatusBadge status={String(val)} /> : '-')}
/>
</SimpleGrid>
</Box>
</Paper>
{privilege ? <PrivilegePermissionsTable privilege={privilege} t={t} /> : null}
</Stack>
);
}
@@ -2,24 +2,48 @@ import { useEffect } from 'react';
import { Box, FieldTextInput, FieldPasswordInput, FieldAsyncSelect, Paper, SimpleGrid, Text } from '@repo/ui/components';
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
import { loadPrivilegeOptions } from '../../load-privilege-options';
import { loadEmployeeOptions } from '../../load-employee-options';
import { privilegesDataService } from '../../../../privileges/domain/factories';
import { employeesDataService } from '../../../../../configuration/employees/domain/factories';
import { relationLabel } from '../../../../../field/shared/relation-label';
import type { PrivilegeEntity } from '../../../../privileges/domain/entities';
import type { EmployeeEntity } from '../../../../../configuration/employees/domain/entities';
function hydrateRelation(
formControl: { getValues: (name: string) => unknown; setValue: (name: string, value: unknown) => void },
objectKey: 'privilege' | 'employee',
idKey: 'privilegeId' | 'employeeId',
current: { id?: string; code?: string; name?: string } | null | undefined,
getOne: (id: string) => Promise<{ data?: { data?: unknown } }>,
) {
const id = current?.id ?? formControl.getValues(idKey);
if (!id) return;
if (current?.code && current.name && current.name !== String(current.id)) return;
void getOne(String(id))
.then((result) => {
const entity = result.data?.data;
if (entity) formControl.setValue(objectKey, entity);
})
.catch(() => undefined);
}
export function FormGeneral({ requirePassword }: { requirePassword: boolean }) {
const { formControl } = useFormPageContext();
const { t } = useEnterpriseModuleTranslationContext();
const privilege = formControl.watch('privilege');
const employee = formControl.watch('employee');
useEffect(() => {
const current = formControl.getValues('privilege') as PrivilegeEntity | null | undefined;
const id = current?.id ?? formControl.getValues('privilegeId');
if (!id) return;
if (current?.code && current.name && current.name !== String(current.id)) return;
void privilegesDataService.getOne(String(id)).then((result) => {
const entity = (result.data as { data?: PrivilegeEntity } | undefined)?.data;
if (entity) formControl.setValue('privilege', entity);
});
}, [formControl]);
hydrateRelation(formControl, 'privilege', 'privilegeId', privilege, (id) =>
privilegesDataService.getOne(id),
);
}, [privilege, formControl]);
useEffect(() => {
hydrateRelation(formControl, 'employee', 'employeeId', employee, (id) =>
employeesDataService.getOne(id),
);
}, [employee, formControl]);
return (
<Paper withBorder shadow="sm" radius="md" p="xl">
@@ -58,6 +82,19 @@ export function FormGeneral({ requirePassword }: { requirePassword: boolean }) {
defaultOptions={privilege ? [privilege] : []}
renderLabel={(item) => `${item.code} - ${item.name}`}
/>
<FieldAsyncSelect<EmployeeEntity>
control={formControl.control}
name="employee"
label={t('common:fields.employee')}
placeholder={t('common:fields.employee')}
valueKey="id"
labelKey="name"
clearable
searchable
loadOptions={loadEmployeeOptions}
defaultOptions={employee ? [employee] : []}
renderLabel={relationLabel}
/>
</SimpleGrid>
</Box>
</Paper>
@@ -3,8 +3,10 @@ import { FieldTextInput, FieldSelect, FieldAsyncSelect } from '@repo/ui/form';
import { UseFormReturn } from 'react-hook-form';
import { statusFilterOptions } from '../../../../../configuration/shared/status-filter-options';
import { loadPrivilegeOptions } from '../../load-privilege-options';
import { loadEmployeeOptions } from '../../load-employee-options';
import { relationLabel } from '../../../../../field/shared/relation-label';
import type { PrivilegeEntity } from '../../../../privileges/domain/entities';
import type { EmployeeEntity } from '../../../../../configuration/employees/domain/entities';
export const FilterFormContent = ({ form, t }: { form: UseFormReturn<any>; t: (key: string) => string }) => {
return (
@@ -27,6 +29,18 @@ export const FilterFormContent = ({ form, t }: { form: UseFormReturn<any>; t: (k
loadOptions={loadPrivilegeOptions}
renderLabel={(item) => relationLabel(item)}
/>
<FieldAsyncSelect<EmployeeEntity>
control={form.control}
name="employee"
label={t('common:fields.employee')}
placeholder={t('common:fields.employee')}
valueKey="id"
labelKey="name"
clearable
searchable
loadOptions={loadEmployeeOptions}
renderLabel={(item) => relationLabel(item)}
/>
<FieldSelect
control={form.control}
name="status"
@@ -5,11 +5,20 @@
"edit_page_title": "Edit User",
"duplicate_page_title": "Duplicate User",
"description": "Manage application <1>users</1>, passwords, and privilege assignment.",
"detail_page_description": "Review username, privilege, employee link, and account status.",
"create_page_description": "Create a user with a username, password, and optional privilege.",
"edit_page_description": "Update username, password, and assigned privilege.",
"detail_page_description": "Review username, assigned privilege details, employee link, and account status.",
"create_page_description": "Create a user with a username, password, optional privilege, and optional employee.",
"edit_page_description": "Update username, password, assigned privilege, and employee.",
"duplicate_page_description": "Copy an existing user to create a new account.",
"section_general": "General",
"section_privilege": "Privilege",
"section_permissions": "Permissions",
"matrix_module": "Module",
"matrix_view": "View",
"matrix_create": "Create",
"matrix_update": "Update",
"matrix_delete": "Delete",
"matrix_import": "Import",
"privilege_empty": "No privilege assigned",
"status_draft": "Draft",
"status_active": "Active",
"status_archived": "Archived",
@@ -5,11 +5,20 @@
"edit_page_title": "Ubah Pengguna",
"duplicate_page_title": "Duplikat Pengguna",
"description": "Kelola <1>pengguna</1> aplikasi, kata sandi, dan penetapan hak akses.",
"detail_page_description": "Tinjau nama pengguna, hak akses, tautan karyawan, dan status akun.",
"create_page_description": "Buat pengguna dengan nama pengguna, kata sandi, dan hak akses opsional.",
"edit_page_description": "Perbarui nama pengguna, kata sandi, dan hak akses yang ditetapkan.",
"detail_page_description": "Tinjau nama pengguna, detail hak akses, tautan karyawan, dan status akun.",
"create_page_description": "Buat pengguna dengan nama pengguna, kata sandi, hak akses opsional, dan karyawan opsional.",
"edit_page_description": "Perbarui nama pengguna, kata sandi, hak akses, dan karyawan yang ditetapkan.",
"duplicate_page_description": "Salin pengguna yang ada untuk membuat akun baru.",
"section_general": "Umum",
"section_privilege": "Hak akses",
"section_permissions": "Izin",
"matrix_module": "Modul",
"matrix_view": "Lihat",
"matrix_create": "Buat",
"matrix_update": "Ubah",
"matrix_delete": "Hapus",
"matrix_import": "Impor",
"privilege_empty": "Tidak ada hak akses yang ditetapkan",
"status_draft": "Draft",
"status_active": "Aktif",
"status_archived": "Diarsipkan",
@@ -0,0 +1,12 @@
import type { LoadOptionsFn } from '@repo/ui/form';
import { employeesDataService } from '../../../configuration/employees/domain/factories';
import type { EmployeeEntity } from '../../../configuration/employees/domain/entities';
export const loadEmployeeOptions: LoadOptionsFn<EmployeeEntity> = async (search, page) => {
const result = await employeesDataService.getMany({
params: { search, page, limit: 20, status: 'active' },
});
const rows = (result.data as { data?: EmployeeEntity[]; meta?: { totalPages?: number } })?.data ?? [];
const totalPages = (result.data as { meta?: { totalPages?: number } })?.meta?.totalPages ?? 1;
return { options: rows, hasMore: page < totalPages };
};
@@ -1,6 +1,8 @@
import { Stack } from '@repo/ui/components';
import { EnterpriseDetailPageProvider, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
import { usersModuleConfig } from '../../domain/constants';
import { DetailGeneral } from '../components/detail-component/detail-general';
import { DetailPrivilege } from '../components/detail-component/detail-privilege';
export default function UserPageDetail() {
const { t } = useEnterpriseModuleTranslationContext();
@@ -17,7 +19,10 @@ export default function UserPageDetail() {
],
}}
>
<DetailGeneral />
<Stack gap="md">
<DetailGeneral />
<DetailPrivilege />
</Stack>
</EnterpriseDetailPageProvider>
);
}
@@ -30,8 +30,8 @@ export default function UserPageForm({ formPageType }: { formPageType: FormPageT
<EnterpriseFormPageProvider
formControl={formControl}
formPageType={formPageType}
ignoreKeyDuplicate={['username', 'password']}
ignoreKeyUpdate={['status', 'isSuperadmin', 'employee', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy', 'id']}
ignoreKeyDuplicate={['username', 'password', 'employee', 'employeeId']}
ignoreKeyUpdate={['status', 'isSuperadmin', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy', 'id']}
highlightDataKey="username"
pageHeaderProps={{
title: title?.title,
@@ -43,6 +43,7 @@ export default function UserPageIndex() {
defaultValues: {
username: '',
privilege: null,
employee: null,
status: '',
},
renderBody: (form: any) => {