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