From 105cf3030acb1472bbaa55e4722230ea33abac9a Mon Sep 17 00:00:00 2001 From: shancheas Date: Thu, 27 Aug 2026 12:22:27 +0700 Subject: [PATCH] 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. --- .../domain/privilege.matrix-rows.test.ts | 58 ++++++++++++++ .../domain/privilege.matrix-rows.ts | 29 +++++++ .../detail-component/detail-permissions.tsx | 59 +------------- .../privilege-permissions-table.tsx | 53 +++++++++++++ .../user.remote.transformer.test.ts | 47 ++++++++++- .../transformers/user.remote.transformer.ts | 37 ++++++--- .../domain/validators/user.validator.test.ts | 15 ++++ .../users/domain/validators/user.validator.ts | 2 + .../detail-component/detail-privilege.tsx | 79 +++++++++++++++++++ .../form-component/form-general.tsx | 55 ++++++++++--- .../index-component/filter-content.tsx | 14 ++++ .../presentation/languages/en/users.json | 15 +++- .../presentation/languages/id/users.json | 15 +++- .../presentation/load-employee-options.ts | 12 +++ .../presentation/pages/user.page.detail.tsx | 7 +- .../presentation/pages/user.page.form.tsx | 4 +- .../presentation/pages/user.page.index.tsx | 1 + .../components/data-table/index.tsx | 3 +- 18 files changed, 416 insertions(+), 89 deletions(-) create mode 100644 apps/web/src/apps/main/modules/system/privileges/domain/privilege.matrix-rows.test.ts create mode 100644 apps/web/src/apps/main/modules/system/privileges/domain/privilege.matrix-rows.ts create mode 100644 apps/web/src/apps/main/modules/system/privileges/presentation/components/privilege-permissions-table.tsx create mode 100644 apps/web/src/apps/main/modules/system/users/presentation/components/detail-component/detail-privilege.tsx create mode 100644 apps/web/src/apps/main/modules/system/users/presentation/load-employee-options.ts diff --git a/apps/web/src/apps/main/modules/system/privileges/domain/privilege.matrix-rows.test.ts b/apps/web/src/apps/main/modules/system/privileges/domain/privilege.matrix-rows.test.ts new file mode 100644 index 0000000..93da36c --- /dev/null +++ b/apps/web/src/apps/main/modules/system/privileges/domain/privilege.matrix-rows.test.ts @@ -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, + }); + }); +}); diff --git a/apps/web/src/apps/main/modules/system/privileges/domain/privilege.matrix-rows.ts b/apps/web/src/apps/main/modules/system/privileges/domain/privilege.matrix-rows.ts new file mode 100644 index 0000000..0d9fd91 --- /dev/null +++ b/apps/web/src/apps/main/modules/system/privileges/domain/privilege.matrix-rows.ts @@ -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 | 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)); +} diff --git a/apps/web/src/apps/main/modules/system/privileges/presentation/components/detail-component/detail-permissions.tsx b/apps/web/src/apps/main/modules/system/privileges/presentation/components/detail-component/detail-permissions.tsx index 0dc3294..c2645ac 100644 --- a/apps/web/src/apps/main/modules/system/privileges/presentation/components/detail-component/detail-permissions.tsx +++ b/apps/web/src/apps/main/modules/system/privileges/presentation/components/detail-component/detail-permissions.tsx @@ -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(); 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 ( - - - {t('section_permissions')} - - - - - - {t('matrix_module')} - {PRIVILEGE_ACTIONS.map((action) => ( - - {t(`matrix_${action}`)} - - ))} - - - - {rows.map((row) => ( - - - {row.label} - - {row.code} - - - {PRIVILEGE_ACTIONS.map((action) => ( - - {row.cell[action] ? : } - - ))} - - ))} - -
-
-
- ); + return ; } diff --git a/apps/web/src/apps/main/modules/system/privileges/presentation/components/privilege-permissions-table.tsx b/apps/web/src/apps/main/modules/system/privileges/presentation/components/privilege-permissions-table.tsx new file mode 100644 index 0000000..034282a --- /dev/null +++ b/apps/web/src/apps/main/modules/system/privileges/presentation/components/privilege-permissions-table.tsx @@ -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 | null | undefined; + t: (key: string) => string; +}) { + const rows = buildPrivilegeMatrixRows(privilege); + + return ( + + + {t('section_permissions')} + + + + + + {t('matrix_module')} + {PRIVILEGE_ACTIONS.map((action) => ( + + {t(`matrix_${action}`)} + + ))} + + + + {rows.map((row) => ( + + + {row.label} + + {row.code} + + + {PRIVILEGE_ACTIONS.map((action) => ( + + {row.cell[action] ? : } + + ))} + + ))} + +
+
+
+ ); +} diff --git a/apps/web/src/apps/main/modules/system/users/domain/transformers/user.remote.transformer.test.ts b/apps/web/src/apps/main/modules/system/users/domain/transformers/user.remote.transformer.test.ts index 0809ec0..a800792 100644 --- a/apps/web/src/apps/main/modules/system/users/domain/transformers/user.remote.transformer.test.ts +++ b/apps/web/src/apps/main/modules/system/users/domain/transformers/user.remote.transformer.test.ts @@ -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', + }); + }); }); diff --git a/apps/web/src/apps/main/modules/system/users/domain/transformers/user.remote.transformer.ts b/apps/web/src/apps/main/modules/system/users/domain/transformers/user.remote.transformer.ts index d8a2092..da58663 100644 --- a/apps/web/src/apps/main/modules/system/users/domain/transformers/user.remote.transformer.ts +++ b/apps/web/src/apps/main/modules/system/users/domain/transformers/user.remote.transformer.ts @@ -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): 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, 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 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): Partial { const payload: Record = { 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 transformPayloadFilter(filter: Record): Record { 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); } } diff --git a/apps/web/src/apps/main/modules/system/users/domain/validators/user.validator.test.ts b/apps/web/src/apps/main/modules/system/users/domain/validators/user.validator.test.ts index d3c5584..7e0e06b 100644 --- a/apps/web/src/apps/main/modules/system/users/domain/validators/user.validator.test.ts +++ b/apps/web/src/apps/main/modules/system/users/domain/validators/user.validator.test.ts @@ -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); + }); }); diff --git a/apps/web/src/apps/main/modules/system/users/domain/validators/user.validator.ts b/apps/web/src/apps/main/modules/system/users/domain/validators/user.validator.ts index 463a425..149e084 100644 --- a/apps/web/src/apps/main/modules/system/users/domain/validators/user.validator.ts +++ b/apps/web/src/apps/main/modules/system/users/domain/validators/user.validator.ts @@ -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(), }); }; diff --git a/apps/web/src/apps/main/modules/system/users/presentation/components/detail-component/detail-privilege.tsx b/apps/web/src/apps/main/modules/system/users/presentation/components/detail-component/detail-privilege.tsx new file mode 100644 index 0000000..a370753 --- /dev/null +++ b/apps/web/src/apps/main/modules/system/users/presentation/components/detail-component/detail-privilege.tsx @@ -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(); + const { t } = useEnterpriseModuleTranslationContext(); + const privilegeId = resolvePrivilegeId(detailData); + const [privilege, setPrivilege] = useState(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 ( + + + {t('section_privilege')} + + + {t('privilege_empty')} + + + ); + } + + return ( + + + + {t('section_privilege')} + + + + + + (val ? : '-')} + /> + + + + {privilege ? : null} + + ); +} diff --git a/apps/web/src/apps/main/modules/system/users/presentation/components/form-component/form-general.tsx b/apps/web/src/apps/main/modules/system/users/presentation/components/form-component/form-general.tsx index d9e87b7..c8ebede 100644 --- a/apps/web/src/apps/main/modules/system/users/presentation/components/form-component/form-general.tsx +++ b/apps/web/src/apps/main/modules/system/users/presentation/components/form-component/form-general.tsx @@ -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 ( @@ -58,6 +82,19 @@ export function FormGeneral({ requirePassword }: { requirePassword: boolean }) { defaultOptions={privilege ? [privilege] : []} renderLabel={(item) => `${item.code} - ${item.name}`} /> + + 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} + /> diff --git a/apps/web/src/apps/main/modules/system/users/presentation/components/index-component/filter-content.tsx b/apps/web/src/apps/main/modules/system/users/presentation/components/index-component/filter-content.tsx index 67229ab..91fd595 100644 --- a/apps/web/src/apps/main/modules/system/users/presentation/components/index-component/filter-content.tsx +++ b/apps/web/src/apps/main/modules/system/users/presentation/components/index-component/filter-content.tsx @@ -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; t: (key: string) => string }) => { return ( @@ -27,6 +29,18 @@ export const FilterFormContent = ({ form, t }: { form: UseFormReturn; t: (k loadOptions={loadPrivilegeOptions} renderLabel={(item) => relationLabel(item)} /> + + 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)} + /> users, 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", diff --git a/apps/web/src/apps/main/modules/system/users/presentation/languages/id/users.json b/apps/web/src/apps/main/modules/system/users/presentation/languages/id/users.json index e62c898..3b20dd4 100644 --- a/apps/web/src/apps/main/modules/system/users/presentation/languages/id/users.json +++ b/apps/web/src/apps/main/modules/system/users/presentation/languages/id/users.json @@ -5,11 +5,20 @@ "edit_page_title": "Ubah Pengguna", "duplicate_page_title": "Duplikat Pengguna", "description": "Kelola <1>pengguna 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", diff --git a/apps/web/src/apps/main/modules/system/users/presentation/load-employee-options.ts b/apps/web/src/apps/main/modules/system/users/presentation/load-employee-options.ts new file mode 100644 index 0000000..f5b0888 --- /dev/null +++ b/apps/web/src/apps/main/modules/system/users/presentation/load-employee-options.ts @@ -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 = 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 }; +}; diff --git a/apps/web/src/apps/main/modules/system/users/presentation/pages/user.page.detail.tsx b/apps/web/src/apps/main/modules/system/users/presentation/pages/user.page.detail.tsx index 75d64ee..8780d5c 100644 --- a/apps/web/src/apps/main/modules/system/users/presentation/pages/user.page.detail.tsx +++ b/apps/web/src/apps/main/modules/system/users/presentation/pages/user.page.detail.tsx @@ -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() { ], }} > - + + + + ); } diff --git a/apps/web/src/apps/main/modules/system/users/presentation/pages/user.page.form.tsx b/apps/web/src/apps/main/modules/system/users/presentation/pages/user.page.form.tsx index 02199b6..9ca8972 100644 --- a/apps/web/src/apps/main/modules/system/users/presentation/pages/user.page.form.tsx +++ b/apps/web/src/apps/main/modules/system/users/presentation/pages/user.page.form.tsx @@ -30,8 +30,8 @@ export default function UserPageForm({ formPageType }: { formPageType: FormPageT { diff --git a/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx b/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx index d6abe1b..4f49218 100644 --- a/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx +++ b/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx @@ -680,7 +680,8 @@ export function EnterpriseDataTable(props: EnterpriseDataT // Define the default Status column const statusColumn: ColDef = { colId: 'status', - maxWidth: 130, + minWidth: 100, + maxWidth: 100, suppressHeaderMenuButton: true, // Suppress menu to keep the header clean