diff --git a/apps/web/src/apps/main/index.tsx b/apps/web/src/apps/main/index.tsx
index 355df58..5bb896b 100644
--- a/apps/web/src/apps/main/index.tsx
+++ b/apps/web/src/apps/main/index.tsx
@@ -8,6 +8,7 @@ const SystemSetting = lazy(() => import('./modules/system/setting'));
const SystemInformation = lazy(() => import('./modules/system/information'));
const SystemNotification = lazy(() => import('./modules/system/notification'));
const PrivilegesModule = lazy(() => import('./modules/system/privileges/presentation/factory'));
+const UsersModule = lazy(() => import('./modules/system/users/presentation/factory'));
const ConfigurationModule = lazy(() => import('./modules/configuration'));
const SalesModule = lazy(() => import('./modules/sales'));
const LogisticsFieldModule = lazy(() => import('./modules/field/logistics'));
@@ -22,6 +23,7 @@ export default function AppModule() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/apps/web/src/apps/main/layouts/data/menu.data.ts b/apps/web/src/apps/main/layouts/data/menu.data.ts
index 6162863..61b1e57 100644
--- a/apps/web/src/apps/main/layouts/data/menu.data.ts
+++ b/apps/web/src/apps/main/layouts/data/menu.data.ts
@@ -44,44 +44,44 @@ export const MENU_ITEMS: MenuItemType[] = [
icon: LayoutDashboard,
path: '/app/dashboard',
},
- {
- key: 'crm',
- label: 'nav:crm',
- icon: Users,
- path: '/app/crm',
- children: [
- {
- key: 'crm-leads',
- label: 'nav:crm-leads',
- icon: Briefcase,
- path: '/app/crm/leads',
- },
- {
- key: 'crm-pipelines',
- label: 'nav:crm-pipelines',
- icon: Activity,
- path: '/app/crm/pipelines',
- },
- {
- key: 'crm-contacts',
- label: 'nav:crm-contacts',
- icon: Phone,
- path: '/app/crm/contacts',
- },
- ],
- },
+ // {
+ // key: 'crm',
+ // label: 'nav:crm',
+ // icon: Users,
+ // path: '/app/crm',
+ // children: [
+ // {
+ // key: 'crm-leads',
+ // label: 'nav:crm-leads',
+ // icon: Briefcase,
+ // path: '/app/crm/leads',
+ // },
+ // {
+ // key: 'crm-pipelines',
+ // label: 'nav:crm-pipelines',
+ // icon: Activity,
+ // path: '/app/crm/pipelines',
+ // },
+ // {
+ // key: 'crm-contacts',
+ // label: 'nav:crm-contacts',
+ // icon: Phone,
+ // path: '/app/crm/contacts',
+ // },
+ // ],
+ // },
{
key: 'sales',
label: 'nav:sales',
icon: ShoppingCart,
path: '/app/sales',
children: [
- {
- key: 'sales-quotations',
- label: 'nav:sales-quotations',
- icon: FileText,
- path: '/app/sales/quotations',
- },
+ // {
+ // key: 'sales-quotations',
+ // label: 'nav:sales-quotations',
+ // icon: FileText,
+ // path: '/app/sales/quotations',
+ // },
{
key: 'sales-requests',
label: 'nav:sales-requests',
@@ -140,150 +140,150 @@ export const MENU_ITEMS: MenuItemType[] = [
},
],
},
- {
- key: 'supply-chain',
- label: 'nav:supply-chain',
- icon: Truck,
- path: '/app/supply-chain',
- children: [
- {
- key: 'sc-inventory',
- label: 'nav:sc-inventory',
- icon: Box,
- path: '/app/supply-chain/inventory',
- children: [
- {
- key: 'sc-inventory-products',
- label: 'nav:sc-inventory-products',
- icon: Layers,
- path: '/app/supply-chain/inventory/products',
- },
- {
- key: 'sc-inventory-categories',
- label: 'nav:sc-inventory-categories',
- icon: Globe,
- path: '/app/supply-chain/inventory/categories',
- },
- {
- key: 'sc-inventory-adjustments',
- label: 'nav:sc-inventory-adjustments',
- icon: FileSearch,
- path: '/app/supply-chain/inventory/adjustments',
- },
- ],
- },
- {
- key: 'sc-warehouses',
- label: 'nav:sc-warehouses',
- icon: Warehouse,
- path: '/app/supply-chain/warehouses',
- },
- {
- key: 'sc-logistics',
- label: 'nav:sc-logistics',
- icon: Globe,
- path: '/app/supply-chain/logistics',
- },
- ],
- },
- {
- key: 'manufacturing',
- label: 'nav:manufacturing',
- icon: Factory,
- path: '/app/manufacturing',
- children: [
- {
- key: 'mfg-bom',
- label: 'nav:mfg-bom',
- icon: Layers,
- path: '/app/manufacturing/bom',
- },
- {
- key: 'mfg-work-orders',
- label: 'nav:mfg-work-orders',
- icon: HardHat,
- path: '/app/manufacturing/work-orders',
- },
- ],
- },
- {
- key: 'hris',
- label: 'nav:hris',
- icon: Briefcase,
- path: '/app/hris',
- children: [
- {
- key: 'hris-employees',
- label: 'nav:hris-employees',
- icon: Users,
- path: '/app/hris/employees',
- },
- {
- key: 'hris-attendance',
- label: 'nav:hris-attendance',
- icon: Clock,
- path: '/app/hris/attendance',
- },
- {
- key: 'hris-payroll',
- label: 'nav:hris-payroll',
- icon: CreditCard,
- path: '/app/hris/payroll',
- },
- {
- key: 'hris-calendar',
- label: 'nav:hris-calendar',
- icon: Calendar,
- path: '/app/hris/calendar',
- },
- ],
- },
- {
- key: 'accounting',
- label: 'nav:accounting',
- icon: Calculator,
- path: '/app/accounting',
- children: [
- {
- key: 'acc-gl',
- label: 'nav:acc-gl',
- icon: Database,
- path: '/app/accounting/general-ledger',
- },
- {
- key: 'acc-taxes',
- label: 'nav:acc-taxes',
- icon: PiggyBank,
- path: '/app/accounting/taxes',
- },
- ],
- },
- {
- key: 'settings',
- label: 'nav:settings',
- icon: Settings,
- path: '/app/settings',
- children: [
- {
- key: 'settings-general',
- label: 'nav:settings-general',
- icon: Settings,
- path: '/app/settings/general',
- },
- {
- key: 'settings-security',
- label: 'nav:settings-security',
- icon: Shield,
- path: '/app/settings/security',
- },
- {
- key: 'long-text-2',
- label: 'nav:long-text-2',
- icon: FileText,
- path: '/app/settings/long-menu-test',
- },
- ],
- },
+ // {
+ // key: 'supply-chain',
+ // label: 'nav:supply-chain',
+ // icon: Truck,
+ // path: '/app/supply-chain',
+ // children: [
+ // {
+ // key: 'sc-inventory',
+ // label: 'nav:sc-inventory',
+ // icon: Box,
+ // path: '/app/supply-chain/inventory',
+ // children: [
+ // {
+ // key: 'sc-inventory-products',
+ // label: 'nav:sc-inventory-products',
+ // icon: Layers,
+ // path: '/app/supply-chain/inventory/products',
+ // },
+ // {
+ // key: 'sc-inventory-categories',
+ // label: 'nav:sc-inventory-categories',
+ // icon: Globe,
+ // path: '/app/supply-chain/inventory/categories',
+ // },
+ // {
+ // key: 'sc-inventory-adjustments',
+ // label: 'nav:sc-inventory-adjustments',
+ // icon: FileSearch,
+ // path: '/app/supply-chain/inventory/adjustments',
+ // },
+ // ],
+ // },
+ // {
+ // key: 'sc-warehouses',
+ // label: 'nav:sc-warehouses',
+ // icon: Warehouse,
+ // path: '/app/supply-chain/warehouses',
+ // },
+ // {
+ // key: 'sc-logistics',
+ // label: 'nav:sc-logistics',
+ // icon: Globe,
+ // path: '/app/supply-chain/logistics',
+ // },
+ // ],
+ // },
+ // {
+ // key: 'manufacturing',
+ // label: 'nav:manufacturing',
+ // icon: Factory,
+ // path: '/app/manufacturing',
+ // children: [
+ // {
+ // key: 'mfg-bom',
+ // label: 'nav:mfg-bom',
+ // icon: Layers,
+ // path: '/app/manufacturing/bom',
+ // },
+ // {
+ // key: 'mfg-work-orders',
+ // label: 'nav:mfg-work-orders',
+ // icon: HardHat,
+ // path: '/app/manufacturing/work-orders',
+ // },
+ // ],
+ // },
+ // {
+ // key: 'hris',
+ // label: 'nav:hris',
+ // icon: Briefcase,
+ // path: '/app/hris',
+ // children: [
+ // {
+ // key: 'hris-employees',
+ // label: 'nav:hris-employees',
+ // icon: Users,
+ // path: '/app/hris/employees',
+ // },
+ // {
+ // key: 'hris-attendance',
+ // label: 'nav:hris-attendance',
+ // icon: Clock,
+ // path: '/app/hris/attendance',
+ // },
+ // {
+ // key: 'hris-payroll',
+ // label: 'nav:hris-payroll',
+ // icon: CreditCard,
+ // path: '/app/hris/payroll',
+ // },
+ // {
+ // key: 'hris-calendar',
+ // label: 'nav:hris-calendar',
+ // icon: Calendar,
+ // path: '/app/hris/calendar',
+ // },
+ // ],
+ // },
+ // {
+ // key: 'accounting',
+ // label: 'nav:accounting',
+ // icon: Calculator,
+ // path: '/app/accounting',
+ // children: [
+ // {
+ // key: 'acc-gl',
+ // label: 'nav:acc-gl',
+ // icon: Database,
+ // path: '/app/accounting/general-ledger',
+ // },
+ // {
+ // key: 'acc-taxes',
+ // label: 'nav:acc-taxes',
+ // icon: PiggyBank,
+ // path: '/app/accounting/taxes',
+ // },
+ // ],
+ // },
+ // {
+ // key: 'settings',
+ // label: 'nav:settings',
+ // icon: Settings,
+ // path: '/app/settings',
+ // children: [
+ // {
+ // key: 'settings-general',
+ // label: 'nav:settings-general',
+ // icon: Settings,
+ // path: '/app/settings/general',
+ // },
+ // {
+ // key: 'settings-security',
+ // label: 'nav:settings-security',
+ // icon: Shield,
+ // path: '/app/settings/security',
+ // },
+ // {
+ // key: 'long-text-2',
+ // label: 'nav:long-text-2',
+ // icon: FileText,
+ // path: '/app/settings/long-menu-test',
+ // },
+ // ],
+ // },
{
key: 'configuration',
label: 'nav:configuration',
@@ -340,27 +340,34 @@ export const MENU_ITEMS: MenuItemType[] = [
path: '/app/system/privileges/index',
moduleKey: 'PRIVILEGES',
},
- ],
- },
- {
- key: 'example-module',
- label: 'nav:example-module',
- icon: Database,
- path: '/app/example-module',
- children: [
{
- key: 'example-full-page',
- label: 'nav:example-full-page',
- icon: LayoutDashboard,
- path: '/app/example/full-page/index',
- moduleKey: 'EXAMPLE_FULL_PAGE',
+ key: 'system-users',
+ label: 'nav:system-users',
+ icon: Users,
+ path: '/app/system/users/index',
+ moduleKey: 'USERS',
},
- // {
- // key: 'example-single-page',
- // label: 'nav:example-single-page',
- // icon: FileText,
- // path: '/app/example/single-page/index',
- // },
],
},
+ // {
+ // key: 'example-module',
+ // label: 'nav:example-module',
+ // icon: Database,
+ // path: '/app/example-module',
+ // children: [
+ // {
+ // key: 'example-full-page',
+ // label: 'nav:example-full-page',
+ // icon: LayoutDashboard,
+ // path: '/app/example/full-page/index',
+ // moduleKey: 'EXAMPLE_FULL_PAGE',
+ // },
+ // // {
+ // // key: 'example-single-page',
+ // // label: 'nav:example-single-page',
+ // // icon: FileText,
+ // // path: '/app/example/single-page/index',
+ // // },
+ // ],
+ // },
];
diff --git a/apps/web/src/apps/main/layouts/languages/en/nav.json b/apps/web/src/apps/main/layouts/languages/en/nav.json
index 8d5e7ca..0a0047d 100644
--- a/apps/web/src/apps/main/layouts/languages/en/nav.json
+++ b/apps/web/src/apps/main/layouts/languages/en/nav.json
@@ -36,6 +36,7 @@
"example-single-page": "Example Single Page",
"system": "System",
"system-privileges": "Privileges",
+ "system-users": "Users",
"configuration": "Configuration",
"configuration-divisions": "Divisions",
"configuration-branches": "Branches",
diff --git a/apps/web/src/apps/main/layouts/languages/id/nav.json b/apps/web/src/apps/main/layouts/languages/id/nav.json
index 04247d9..af189d4 100644
--- a/apps/web/src/apps/main/layouts/languages/id/nav.json
+++ b/apps/web/src/apps/main/layouts/languages/id/nav.json
@@ -36,6 +36,7 @@
"example-single-page": "Contoh Halaman Tunggal",
"system": "Sistem",
"system-privileges": "Hak Akses",
+ "system-users": "Pengguna",
"configuration": "Konfigurasi",
"configuration-divisions": "Divisi",
"configuration-branches": "Cabang",
diff --git a/apps/web/src/apps/main/modules/system/users/data/user.remote.service.test.ts b/apps/web/src/apps/main/modules/system/users/data/user.remote.service.test.ts
new file mode 100644
index 0000000..a95e9e5
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/data/user.remote.service.test.ts
@@ -0,0 +1,65 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import type { AxiosInstance } from '@repo/core-api/http-client';
+import { UsersRemoteDataServices } from './user.remote.service';
+import { UsersRemoteDataTransformer } from '../domain/transformers/user.remote.transformer';
+
+function createMockHttpClient(): AxiosInstance {
+ return {
+ request: vi.fn().mockResolvedValue({ data: {}, status: 200 }),
+ defaults: {} as AxiosInstance['defaults'],
+ interceptors: {
+ request: { use: vi.fn(), eject: vi.fn(), clear: vi.fn() },
+ response: { use: vi.fn(), eject: vi.fn(), clear: vi.fn() },
+ },
+ getUri: vi.fn(),
+ get: vi.fn(),
+ delete: vi.fn(),
+ head: vi.fn(),
+ options: vi.fn(),
+ post: vi.fn(),
+ put: vi.fn(),
+ patch: vi.fn(),
+ postForm: vi.fn(),
+ putForm: vi.fn(),
+ patchForm: vi.fn(),
+ } as unknown as AxiosInstance;
+}
+
+describe('UsersRemoteDataServices', () => {
+ let httpClient: AxiosInstance;
+ let service: UsersRemoteDataServices;
+
+ beforeEach(() => {
+ httpClient = createMockHttpClient();
+ service = new UsersRemoteDataServices(httpClient, {
+ apiUrl: '/users',
+ moduleKey: 'USERS',
+ transformer: new UsersRemoteDataTransformer(),
+ });
+ });
+
+ it('uses PATCH when editing a user', async () => {
+ await service.edit('user-1', { username: 'alice' } as any);
+ expect(httpClient.request).toHaveBeenCalledWith(
+ expect.objectContaining({ url: '/users/user-1', method: 'PATCH' }),
+ );
+ });
+
+ it('bulk-deletes via POST /users/bulk-delete', async () => {
+ await service.batchDelete(['user-1']);
+ expect(httpClient.request).toHaveBeenCalledWith(
+ expect.objectContaining({ url: '/users/bulk-delete', method: 'POST' }),
+ );
+ });
+
+ it('activates via PATCH /users/:id/status with status active', async () => {
+ await service.activate('user-1');
+ expect(httpClient.request).toHaveBeenCalledWith(
+ expect.objectContaining({
+ url: '/users/user-1/status',
+ method: 'PATCH',
+ data: { status: 'active' },
+ }),
+ );
+ });
+});
diff --git a/apps/web/src/apps/main/modules/system/users/data/user.remote.service.ts b/apps/web/src/apps/main/modules/system/users/data/user.remote.service.ts
new file mode 100644
index 0000000..ff62135
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/data/user.remote.service.ts
@@ -0,0 +1,13 @@
+import type { AxiosInstance } from '@repo/core-api/http-client';
+import type { DataServicesConfig } from '@repo/core-api/data-services';
+import { TrackGoRemoteDataServices } from '../../../../../../core/lib/trackgo-remote-data-services';
+import type { UserEntity } from '../domain/entities';
+
+export class UsersRemoteDataServices extends TrackGoRemoteDataServices {
+ constructor(httpClient: AxiosInstance, config: DataServicesConfig) {
+ super(httpClient, {
+ ...config,
+ apiUrl: config.apiUrl ?? '/users',
+ });
+ }
+}
diff --git a/apps/web/src/apps/main/modules/system/users/domain/constants/index.ts b/apps/web/src/apps/main/modules/system/users/domain/constants/index.ts
new file mode 100644
index 0000000..1ea0d40
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/domain/constants/index.ts
@@ -0,0 +1 @@
+export * from './user.constants';
diff --git a/apps/web/src/apps/main/modules/system/users/domain/constants/user.constants.ts b/apps/web/src/apps/main/modules/system/users/domain/constants/user.constants.ts
new file mode 100644
index 0000000..a187cb5
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/domain/constants/user.constants.ts
@@ -0,0 +1,11 @@
+import { ModuleConfigEntity } from '@repo/ui/foundations';
+import type { UserEntity } from '../entities';
+
+export const usersModuleConfig: ModuleConfigEntity = {
+ moduleKey: 'USERS',
+ translationNamespace: 'USERS',
+ apiUrl: '/users',
+ webUrl: '/app/system/users',
+ moduleCategory: 'FULL_PAGE',
+ moduleType: 'MASTER_DATA',
+} as const;
diff --git a/apps/web/src/apps/main/modules/system/users/domain/entities/index.ts b/apps/web/src/apps/main/modules/system/users/domain/entities/index.ts
new file mode 100644
index 0000000..e4aa507
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/domain/entities/index.ts
@@ -0,0 +1 @@
+export * from './user.entity';
diff --git a/apps/web/src/apps/main/modules/system/users/domain/entities/user.entity.ts b/apps/web/src/apps/main/modules/system/users/domain/entities/user.entity.ts
new file mode 100644
index 0000000..cb898b3
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/domain/entities/user.entity.ts
@@ -0,0 +1,31 @@
+import { BaseEntity } from '@repo/core-api/data-services';
+import type { ConfigurationStatus } from '../../../../configuration/divisions/domain/entities';
+import type { RelationRef } from '../../../../../../../core/domain/relation-ref';
+
+export interface UserEntity extends BaseEntity {
+ username: string;
+ password?: string;
+ isSuperadmin?: boolean;
+ privilegeId?: string | null;
+ privilege?: RelationRef | null;
+ employee?: RelationRef | null;
+ status?: ConfigurationStatus;
+ createdAt?: number;
+ updatedAt?: number;
+ createdBy?: string;
+ updatedBy?: string;
+}
+
+export interface UserDto {
+ id?: string;
+ username: string;
+ isSuperadmin?: boolean;
+ privilegeId?: string | null;
+ privilege?: RelationRef | null;
+ employee?: RelationRef | null;
+ status?: ConfigurationStatus;
+ createdAt?: number;
+ updatedAt?: number;
+ createdBy?: string | { id: string; username: string };
+ updatedBy?: string | { id: string; username: string };
+}
diff --git a/apps/web/src/apps/main/modules/system/users/domain/factories/index.ts b/apps/web/src/apps/main/modules/system/users/domain/factories/index.ts
new file mode 100644
index 0000000..8c2fa73
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/domain/factories/index.ts
@@ -0,0 +1,12 @@
+import { apiClient } from '../../../../../../../core/lib/api-client';
+import { UsersRemoteDataServices } from '../../data/user.remote.service';
+import { usersModuleConfig } from '../constants/user.constants';
+import { UsersRemoteDataTransformer } from '../transformers/user.remote.transformer';
+
+export const usersDataTransformer = new UsersRemoteDataTransformer();
+
+export const usersDataService = new UsersRemoteDataServices(apiClient, {
+ apiUrl: usersModuleConfig.apiUrl,
+ moduleKey: usersModuleConfig.moduleKey,
+ transformer: usersDataTransformer,
+});
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
new file mode 100644
index 0000000..e8af1e1
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/domain/transformers/user.remote.transformer.test.ts
@@ -0,0 +1,123 @@
+import { describe, expect, it } from 'vitest';
+import { UsersRemoteDataTransformer } from './user.remote.transformer';
+import type { UserEntity } from '../entities';
+
+const transformer = new UsersRemoteDataTransformer();
+
+const dto = {
+ id: 'user-1',
+ username: 'alice',
+ isSuperadmin: false,
+ privilege: { id: 'priv-1', code: 'SALES_STAFF', name: 'Sales Staff' },
+ employee: { id: 'emp-1', code: 'EMP_01', name: 'Ada Lovelace' },
+ status: 'active' as const,
+ createdAt: 1,
+ updatedAt: 2,
+ createdBy: { id: 'u1', username: 'admin' },
+ updatedBy: { id: 'u2', username: 'bob' },
+};
+
+describe('UsersRemoteDataTransformer', () => {
+ it('maps dto fields onto the entity and flattens privilegeId and audit usernames', () => {
+ const entity = transformer.transformToEntity(dto);
+ expect(entity).toMatchObject({
+ id: 'user-1',
+ username: 'alice',
+ isSuperadmin: false,
+ privilegeId: 'priv-1',
+ privilege: { id: 'priv-1', code: 'SALES_STAFF', name: 'Sales Staff' },
+ employee: { id: 'emp-1', code: 'EMP_01', name: 'Ada Lovelace' },
+ createdBy: 'admin',
+ updatedBy: 'bob',
+ });
+ });
+
+ it('builds create payload with username, password, and privilegeId, without status', () => {
+ const entity: UserEntity = {
+ id: dto.id,
+ username: dto.username,
+ password: 'password123',
+ isSuperadmin: dto.isSuperadmin,
+ privilegeId: 'priv-1',
+ privilege: dto.privilege,
+ employee: dto.employee,
+ status: 'draft',
+ };
+ const payload = transformer.transformCreatePayload(entity);
+ expect(payload).toEqual({
+ username: 'alice',
+ password: 'password123',
+ privilegeId: 'priv-1',
+ });
+ expect(payload).not.toHaveProperty('status');
+ expect(payload).not.toHaveProperty('isSuperadmin');
+ expect(payload).not.toHaveProperty('employee');
+ });
+
+ it('resolves privilegeId from nested privilege on create', () => {
+ const payload = transformer.transformCreatePayload({
+ username: 'alice',
+ password: 'password123',
+ privilege: { id: 'priv-1', code: 'SALES_STAFF', name: 'Sales Staff' },
+ });
+ expect(payload).toEqual({
+ username: 'alice',
+ password: 'password123',
+ privilegeId: 'priv-1',
+ });
+ });
+
+ it('builds edit payload without status, empty password, or audit fields', () => {
+ const payload = transformer.transformEditPayload({
+ username: dto.username,
+ password: '',
+ privilegeId: 'priv-1',
+ status: dto.status,
+ });
+ expect(payload).toEqual({
+ username: 'alice',
+ privilegeId: 'priv-1',
+ });
+ expect(payload).not.toHaveProperty('password');
+ expect(payload).not.toHaveProperty('status');
+ expect(payload).not.toHaveProperty('id');
+ });
+
+ it('includes password on edit only when it is non-empty', () => {
+ const payload = transformer.transformEditPayload({
+ username: 'alice',
+ password: 'newpassword',
+ privilegeId: 'priv-1',
+ });
+ expect(payload).toEqual({
+ username: 'alice',
+ password: 'newpassword',
+ privilegeId: 'priv-1',
+ });
+ });
+
+ it('sends privilegeId null when privilege is cleared on edit', () => {
+ const payload = transformer.transformEditPayload({
+ username: 'alice',
+ privilege: null,
+ privilegeId: '',
+ });
+ expect(payload).toEqual({
+ username: 'alice',
+ privilegeId: null,
+ });
+ });
+
+ it('maps privilege object to privilegeId on filter payload', () => {
+ const payload = transformer.transformPayloadFilter({
+ username: 'alice',
+ status: 'active',
+ privilege: { id: 'priv-1', code: 'SALES_STAFF', name: 'Sales Staff' },
+ });
+ expect(payload).toEqual({
+ username: 'alice',
+ status: 'active',
+ privilegeId: 'priv-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
new file mode 100644
index 0000000..44c137f
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/domain/transformers/user.remote.transformer.ts
@@ -0,0 +1,71 @@
+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;
+ return id == null ? null : String(id);
+ }
+ return entity.privilegeId;
+}
+
+function flattenActor(value: unknown): string | undefined {
+ if (typeof value === 'string') return value;
+ if (value && typeof value === 'object' && 'username' in value) {
+ return String((value as { username: unknown }).username);
+ }
+ return undefined;
+}
+
+export class UsersRemoteDataTransformer extends BaseDataTransformer {
+ transformToEntity(dto: UserDto | UserEntity): UserEntity {
+ const privilegeId = 'privilegeId' in dto ? dto.privilegeId : (dto as UserEntity).privilegeId;
+ const privilege = (dto as UserEntity).privilege ?? null;
+ return {
+ id: dto.id,
+ username: dto.username,
+ isSuperadmin: dto.isSuperadmin,
+ privilegeId: privilegeId ?? privilege?.id ?? null,
+ privilege,
+ employee: (dto as UserEntity).employee ?? null,
+ status: dto.status,
+ createdAt: dto.createdAt,
+ updatedAt: dto.updatedAt,
+ createdBy: flattenActor(dto.createdBy),
+ updatedBy: flattenActor(dto.updatedBy),
+ };
+ }
+
+ transformToDTO(entity: UserEntity): UserEntity {
+ return { ...entity };
+ }
+
+ transformCreatePayload(entity: Partial): Partial {
+ return omitEmptyFields({
+ username: entity.username,
+ password: entity.password,
+ privilegeId: resolvePrivilegeId(entity),
+ });
+ }
+
+ transformEditPayload(entity: Partial): Partial {
+ const payload: Record = {
+ username: entity.username,
+ privilegeId: emptyToNull(resolvePrivilegeId(entity)) as string | null,
+ };
+ if (entity.password) {
+ return { ...payload, password: entity.password };
+ }
+ return payload;
+ }
+
+ transformPayloadFilter(filter: Record): Record {
+ const next = { ...filter };
+ if (next.privilege && typeof next.privilege === 'object') {
+ next.privilegeId = next.privilege.id;
+ delete next.privilege;
+ }
+ 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
new file mode 100644
index 0000000..d3c5584
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/domain/validators/user.validator.test.ts
@@ -0,0 +1,50 @@
+import { describe, expect, it } from 'vitest';
+import { createUserSchema } from './user.validator';
+
+describe('createUserSchema', () => {
+ const t = (key: string) => key;
+ const valid = {
+ username: 'alice',
+ password: 'password123',
+ };
+
+ it('accepts a complete create payload', () => {
+ const schema = createUserSchema(t, { requirePassword: true });
+ expect(schema.safeParse(valid).success).toBe(true);
+ });
+
+ it('rejects a username shorter than 3 characters', () => {
+ const schema = createUserSchema(t, { requirePassword: true });
+ expect(schema.safeParse({ ...valid, username: 'ab' }).success).toBe(false);
+ });
+
+ it('rejects a username longer than 32 characters', () => {
+ const schema = createUserSchema(t, { requirePassword: true });
+ expect(schema.safeParse({ ...valid, username: 'a'.repeat(33) }).success).toBe(false);
+ });
+
+ it('rejects a username with characters other than letters, digits, or underscore', () => {
+ const schema = createUserSchema(t, { requirePassword: true });
+ expect(schema.safeParse({ ...valid, username: 'alice-bob' }).success).toBe(false);
+ });
+
+ it('rejects a password shorter than 8 characters when required', () => {
+ const schema = createUserSchema(t, { requirePassword: true });
+ expect(schema.safeParse({ ...valid, password: 'short' }).success).toBe(false);
+ });
+
+ it('rejects an empty password when required', () => {
+ const schema = createUserSchema(t, { requirePassword: true });
+ expect(schema.safeParse({ ...valid, password: '' }).success).toBe(false);
+ });
+
+ it('allows an empty password when not required', () => {
+ const schema = createUserSchema(t, { requirePassword: false });
+ expect(schema.safeParse({ username: 'alice', password: '' }).success).toBe(true);
+ });
+
+ it('rejects a short password when not required but provided', () => {
+ const schema = createUserSchema(t, { requirePassword: false });
+ expect(schema.safeParse({ username: 'alice', password: 'short' }).success).toBe(false);
+ });
+});
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
new file mode 100644
index 0000000..463a425
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/domain/validators/user.validator.ts
@@ -0,0 +1,29 @@
+import { z } from 'zod';
+import { compose, required, rangeLength } from '@repo/ui/validators';
+
+const USERNAME_PATTERN = /^[a-zA-Z0-9_]+$/;
+
+export const createUserSchema = (t: (key: string) => string, options?: { requirePassword?: boolean }) => {
+ const requirePassword = options?.requirePassword ?? true;
+ const passwordLabel = t('common:fields.password');
+ const usernameLabel = t('common:fields.username');
+
+ const password = requirePassword
+ ? compose(z.string(), required(passwordLabel), rangeLength(8, 72, passwordLabel))
+ : z.union([z.literal(''), compose(z.string(), rangeLength(8, 72, passwordLabel))]).optional();
+
+ return z.object({
+ username: compose(
+ z.string(),
+ required(usernameLabel),
+ rangeLength(3, 32, usernameLabel),
+ (schema: z.ZodString) =>
+ schema.regex(USERNAME_PATTERN, {
+ message: JSON.stringify({ key: 'validation:invalid_format', values: { field: usernameLabel } }),
+ }),
+ ),
+ password,
+ privilege: z.any().nullable().optional(),
+ privilegeId: z.string().nullable().optional(),
+ });
+};
diff --git a/apps/web/src/apps/main/modules/system/users/presentation/components/detail-component/detail-general.tsx b/apps/web/src/apps/main/modules/system/users/presentation/components/detail-component/detail-general.tsx
new file mode 100644
index 0000000..ca1e216
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/presentation/components/detail-component/detail-general.tsx
@@ -0,0 +1,41 @@
+import { Box, Paper, SimpleGrid, FieldValue, RenderDate, Text, StatusBadge } from '@repo/ui/components';
+import { useDetailPageContext, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
+import { relationLabel } from '../../../../../field/shared/relation-label';
+import type { UserEntity } from '../../../domain/entities';
+
+export function DetailGeneral() {
+ const { detailData } = useDetailPageContext();
+ const { t } = useEnterpriseModuleTranslationContext();
+ const data = detailData;
+
+ return (
+
+
+ {t('section_general')}
+
+
+
+
+
+
+
+ }
+ />
+ }
+ />
+ }
+ />
+
+
+
+ );
+}
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
new file mode 100644
index 0000000..d9e87b7
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/presentation/components/form-component/form-general.tsx
@@ -0,0 +1,65 @@
+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 { privilegesDataService } from '../../../../privileges/domain/factories';
+import type { PrivilegeEntity } from '../../../../privileges/domain/entities';
+
+export function FormGeneral({ requirePassword }: { requirePassword: boolean }) {
+ const { formControl } = useFormPageContext();
+ const { t } = useEnterpriseModuleTranslationContext();
+ const privilege = formControl.watch('privilege');
+
+ 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]);
+
+ return (
+
+
+ {t('section_general')}
+
+
+
+
+
+
+ control={formControl.control}
+ name="privilege"
+ label={t('common:fields.privilege')}
+ placeholder={t('common:fields.privilege')}
+ valueKey="id"
+ labelKey="name"
+ clearable
+ searchable
+ loadOptions={loadPrivilegeOptions}
+ defaultOptions={privilege ? [privilege] : []}
+ renderLabel={(item) => `${item.code} - ${item.name}`}
+ />
+
+
+
+ );
+}
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
new file mode 100644
index 0000000..67229ab
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/presentation/components/index-component/filter-content.tsx
@@ -0,0 +1,39 @@
+import { SimpleGrid } from '@repo/ui/components';
+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 { relationLabel } from '../../../../../field/shared/relation-label';
+import type { PrivilegeEntity } from '../../../../privileges/domain/entities';
+
+export const FilterFormContent = ({ form, t }: { form: UseFormReturn; t: (key: string) => string }) => {
+ return (
+
+
+
+ control={form.control}
+ name="privilege"
+ label={t('common:fields.privilege')}
+ placeholder={t('common:fields.privilege')}
+ valueKey="id"
+ labelKey="name"
+ clearable
+ searchable
+ loadOptions={loadPrivilegeOptions}
+ renderLabel={(item) => relationLabel(item)}
+ />
+
+
+ );
+};
diff --git a/apps/web/src/apps/main/modules/system/users/presentation/factory/index.tsx b/apps/web/src/apps/main/modules/system/users/presentation/factory/index.tsx
new file mode 100644
index 0000000..7e2013c
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/presentation/factory/index.tsx
@@ -0,0 +1,40 @@
+import { lazy } from 'react';
+import { Navigate, Route, Routes } from 'react-router-dom';
+import { EnterpriseModuleProvider } from '@repo/ui/foundations';
+import { registerModuleNamespace } from '@repo/core-i18n';
+import { usersModuleConfig } from '../../domain/constants';
+import { usersDataService } from '../../domain/factories';
+import { UserEntity } from '../../domain/entities';
+import { usersStore } from '../store';
+
+import usersId from '../languages/id/users.json';
+import usersEn from '../languages/en/users.json';
+
+const IndexPage = lazy(() => import('../pages/user.page.index'));
+const FormPage = lazy(() => import('../pages/user.page.form'));
+const DetailPage = lazy(() => import('../pages/user.page.detail'));
+
+registerModuleNamespace(usersModuleConfig.translationNamespace, {
+ id: usersId,
+ en: usersEn,
+});
+
+export default function UsersModule() {
+ return (
+
+ config={usersModuleConfig}
+ dataServices={usersDataService}
+ store={usersStore}
+ >
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ );
+}
diff --git a/apps/web/src/apps/main/modules/system/users/presentation/languages/en/users.json b/apps/web/src/apps/main/modules/system/users/presentation/languages/en/users.json
new file mode 100644
index 0000000..8c50468
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/presentation/languages/en/users.json
@@ -0,0 +1,19 @@
+{
+ "title": "Users",
+ "detail_page_title": "User Detail",
+ "create_page_title": "New User",
+ "edit_page_title": "Edit User",
+ "duplicate_page_title": "Duplicate User",
+ "description": "Manage application <1>users1>, 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.",
+ "duplicate_page_description": "Copy an existing user to create a new account.",
+ "section_general": "General",
+ "status_draft": "Draft",
+ "status_active": "Active",
+ "status_archived": "Archived",
+ "yes": "Yes",
+ "no": "No",
+ "password_keep_hint": "Leave blank to keep the current password"
+}
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
new file mode 100644
index 0000000..e62c898
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/presentation/languages/id/users.json
@@ -0,0 +1,19 @@
+{
+ "title": "Pengguna",
+ "detail_page_title": "Detail Pengguna",
+ "create_page_title": "Pengguna Baru",
+ "edit_page_title": "Ubah Pengguna",
+ "duplicate_page_title": "Duplikat Pengguna",
+ "description": "Kelola <1>pengguna1> 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.",
+ "duplicate_page_description": "Salin pengguna yang ada untuk membuat akun baru.",
+ "section_general": "Umum",
+ "status_draft": "Draft",
+ "status_active": "Aktif",
+ "status_archived": "Diarsipkan",
+ "yes": "Ya",
+ "no": "Tidak",
+ "password_keep_hint": "Kosongkan untuk tetap memakai kata sandi saat ini"
+}
diff --git a/apps/web/src/apps/main/modules/system/users/presentation/load-privilege-options.ts b/apps/web/src/apps/main/modules/system/users/presentation/load-privilege-options.ts
new file mode 100644
index 0000000..c53c4c4
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/presentation/load-privilege-options.ts
@@ -0,0 +1,12 @@
+import type { LoadOptionsFn } from '@repo/ui/form';
+import { privilegesDataService } from '../../privileges/domain/factories';
+import type { PrivilegeEntity } from '../../privileges/domain/entities';
+
+export const loadPrivilegeOptions: LoadOptionsFn = async (search, page) => {
+ const result = await privilegesDataService.getMany({
+ params: { search, page, limit: 20, status: 'active' },
+ });
+ const rows = (result.data as { data?: PrivilegeEntity[]; 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
new file mode 100644
index 0000000..75d64ee
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/presentation/pages/user.page.detail.tsx
@@ -0,0 +1,23 @@
+import { EnterpriseDetailPageProvider, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
+import { usersModuleConfig } from '../../domain/constants';
+import { DetailGeneral } from '../components/detail-component/detail-general';
+
+export default function UserPageDetail() {
+ const { t } = useEnterpriseModuleTranslationContext();
+
+ return (
+
+
+
+ );
+}
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
new file mode 100644
index 0000000..02199b6
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/presentation/pages/user.page.form.tsx
@@ -0,0 +1,48 @@
+import { useMemo } from 'react';
+import { useEnterpriseModuleTranslationContext, EnterpriseFormPageProvider, FormPageType } from '@repo/ui/foundations';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { usersModuleConfig } from '../../domain/constants';
+import { createUserSchema } from '../../domain/validators/user.validator';
+import { FormGeneral } from '../components/form-component/form-general';
+
+export default function UserPageForm({ formPageType }: { formPageType: FormPageType }) {
+ const { t } = useEnterpriseModuleTranslationContext();
+ const requirePassword = formPageType !== 'EDIT';
+
+ const title = useMemo(() => {
+ if (formPageType === 'CREATE') {
+ return { title: t('create_page_title'), description: t('create_page_description') };
+ }
+ if (formPageType === 'EDIT') {
+ return { title: t('edit_page_title'), description: t('edit_page_description') };
+ }
+ if (formPageType === 'DUPLICATE') {
+ return { title: t('duplicate_page_title'), description: t('duplicate_page_description') };
+ }
+ return { title: '', description: '' };
+ }, [formPageType, t]);
+
+ const validator = useMemo(() => createUserSchema(t, { requirePassword }), [t, requirePassword]);
+ const formControl = useForm({ resolver: zodResolver(validator) });
+
+ return (
+
+
+
+ );
+}
diff --git a/apps/web/src/apps/main/modules/system/users/presentation/pages/user.page.index.tsx b/apps/web/src/apps/main/modules/system/users/presentation/pages/user.page.index.tsx
new file mode 100644
index 0000000..d17ba1a
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/presentation/pages/user.page.index.tsx
@@ -0,0 +1,67 @@
+import { useMemo } from 'react';
+import {
+ EnterpriseIndexPageProvider,
+ useEnterpriseModuleTranslationContext,
+ EnterpriseDataTable,
+} from '@repo/ui/foundations';
+import { ColDef, Text } from '@repo/ui/components';
+import { Trans } from '@repo/core-i18n';
+import { Users } from 'lucide-react';
+import { FilterFormContent } from '../components/index-component/filter-content';
+import { relationLabel } from '../../../../field/shared/relation-label';
+import type { UserEntity } from '../../domain/entities';
+
+export default function UserPageIndex() {
+ const { t } = useEnterpriseModuleTranslationContext();
+
+ const columnDefs: ColDef[] = useMemo(() => {
+ return [
+ { field: 'username', headerName: t('common:fields.username'), minWidth: 160 },
+ {
+ field: 'privilege',
+ headerName: t('common:fields.privilege'),
+ minWidth: 180,
+ valueGetter: ({ data }) => relationLabel(data?.privilege),
+ },
+ {
+ field: 'employee',
+ headerName: t('common:fields.employee'),
+ minWidth: 180,
+ valueGetter: ({ data }) => relationLabel(data?.employee),
+ },
+ {
+ field: 'isSuperadmin',
+ headerName: t('common:fields.isSuperadmin'),
+ minWidth: 140,
+ valueFormatter: ({ value }) => t(value ? 'yes' : 'no'),
+ },
+ ];
+ }, [t]);
+
+ const filterConfig = useMemo(() => {
+ return {
+ renderBody: (form: any) => {
+ if (!form) return null;
+ return ;
+ },
+ };
+ }, [t]);
+
+ return (
+ }} />
+ ),
+ icon: Users,
+ breadcrumbs: [
+ { label: t('nav:system'), type: 'text' },
+ { label: t('nav:system-users'), type: 'text' },
+ ],
+ }}
+ >
+
+
+ );
+}
diff --git a/apps/web/src/apps/main/modules/system/users/presentation/store/index.ts b/apps/web/src/apps/main/modules/system/users/presentation/store/index.ts
new file mode 100644
index 0000000..e7972d0
--- /dev/null
+++ b/apps/web/src/apps/main/modules/system/users/presentation/store/index.ts
@@ -0,0 +1,22 @@
+import { create } from 'zustand';
+import { EnterpriseModuleState } from '@repo/ui/foundations';
+import { UserEntity } from '../../domain/entities';
+
+export interface UsersStoreState extends EnterpriseModuleState {}
+
+export const usersStore = create((set) => ({
+ metaData: { limit: 15 },
+ setMetaData: (data) => set({ metaData: data }),
+
+ filterData: {},
+ setFilterData: (data) => set({ filterData: data }),
+
+ selectedRows: [],
+ setSelectedRows: (rows) => set({ selectedRows: rows }),
+
+ privileges: [],
+ setPrivileges: (privileges) => set({ privileges }),
+
+ tableConfig: null,
+ setTableConfig: (config) => set({ tableConfig: config }),
+}));
diff --git a/apps/web/src/core/constants/api-url.ts b/apps/web/src/core/constants/api-url.ts
index 06f3f23..b1f5307 100644
--- a/apps/web/src/core/constants/api-url.ts
+++ b/apps/web/src/core/constants/api-url.ts
@@ -4,6 +4,7 @@ export const API_URL = {
AUTH_REFRESH: '/auth/refresh',
AUTH_REVOKE: '/auth/revoke',
AUTH_ME: '/auth/me',
+ USERS: '/users',
DIVISIONS: '/divisions',
BRANCHES: '/branches',
CUSTOMERS: '/customers',
diff --git a/apps/web/src/core/constants/module-key.ts b/apps/web/src/core/constants/module-key.ts
index f091968..1cb9bdc 100644
--- a/apps/web/src/core/constants/module-key.ts
+++ b/apps/web/src/core/constants/module-key.ts
@@ -1,5 +1,6 @@
export const MODULE_KEY = {
PRIVILEGES: 'PRIVILEGES',
+ USERS: 'USERS',
CONFIGURATION_DIVISION: 'CONFIGURATION.DIVISION',
CONFIGURATION_BRANCH: 'CONFIGURATION.BRANCH',
CONFIGURATION_CUSTOMER: 'CONFIGURATION.CUSTOMER',
diff --git a/apps/web/src/core/constants/web-url.ts b/apps/web/src/core/constants/web-url.ts
index 099e6ae..0c0ac68 100644
--- a/apps/web/src/core/constants/web-url.ts
+++ b/apps/web/src/core/constants/web-url.ts
@@ -1,4 +1,5 @@
export const WEB_URL = {
+ USERS: '/app/system/users',
DIVISIONS: '/app/configuration/divisions',
BRANCHES: '/app/configuration/branches',
CUSTOMERS: '/app/configuration/customers',
diff --git a/packages/core-i18n/src/languages/en/common.json b/packages/core-i18n/src/languages/en/common.json
index 815fd20..f8fa573 100644
--- a/packages/core-i18n/src/languages/en/common.json
+++ b/packages/core-i18n/src/languages/en/common.json
@@ -198,6 +198,10 @@
"jobTitle": "Job Title",
"mobilePhone": "Mobile Phone",
"user": "User",
+ "username": "Username",
+ "password": "Password",
+ "privilege": "Privilege",
+ "isSuperadmin": "Superadmin",
"role": "Role",
"notes": "Notes",
"isActive": "Active",
diff --git a/packages/core-i18n/src/languages/id/common.json b/packages/core-i18n/src/languages/id/common.json
index 88fc443..172c555 100644
--- a/packages/core-i18n/src/languages/id/common.json
+++ b/packages/core-i18n/src/languages/id/common.json
@@ -198,6 +198,10 @@
"jobTitle": "Jabatan",
"mobilePhone": "No. HP",
"user": "Pengguna",
+ "username": "Nama pengguna",
+ "password": "Kata sandi",
+ "privilege": "Hak akses",
+ "isSuperadmin": "Superadmin",
"role": "Peran",
"notes": "Catatan",
"isActive": "Aktif",