mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 23:28:11 +00:00
Merge pull request #67 from Tria-plc/freight/feature/edr_org_seeder
Freight/feature/edr org seeder
This commit is contained in:
@@ -21,6 +21,10 @@ import { OtpModule } from './modules/otp/otp.module';
|
|||||||
import { RuleEngineModule } from "./modules/rule-engine/rule-engine.module";
|
import { RuleEngineModule } from "./modules/rule-engine/rule-engine.module";
|
||||||
import { BackofficeModule } from "./modules/backoffice/backoffice.module";
|
import { BackofficeModule } from "./modules/backoffice/backoffice.module";
|
||||||
import { DemoPermissionsModule } from "./modules/demo-permissions/demo-permissions.module";
|
import { DemoPermissionsModule } from "./modules/demo-permissions/demo-permissions.module";
|
||||||
|
import {
|
||||||
|
EDR_FREIGHT_APPLICATION,
|
||||||
|
EDR_FREIGHT_PERMISSIONS,
|
||||||
|
} from "./seed/edr-freight.seed";
|
||||||
import { EdrOrgSeeder } from "./seed/edr-org.seeder";
|
import { EdrOrgSeeder } from "./seed/edr-org.seeder";
|
||||||
import { DemoUsersSeeder } from "./seed/demo-users.seeder";
|
import { DemoUsersSeeder } from "./seed/demo-users.seeder";
|
||||||
|
|
||||||
@@ -36,7 +40,10 @@ import { DemoUsersSeeder } from "./seed/demo-users.seeder";
|
|||||||
config.get<TypeOrmModuleOptions>("database")!,
|
config.get<TypeOrmModuleOptions>("database")!,
|
||||||
}),
|
}),
|
||||||
SharedAuthModule,
|
SharedAuthModule,
|
||||||
IamModule.forRoot(),
|
IamModule.forRoot({
|
||||||
|
applications: [EDR_FREIGHT_APPLICATION],
|
||||||
|
permissions: EDR_FREIGHT_PERMISSIONS,
|
||||||
|
}),
|
||||||
BookingsModule,
|
BookingsModule,
|
||||||
FilesModule,
|
FilesModule,
|
||||||
ConsignmentsModule,
|
ConsignmentsModule,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Param,
|
Param,
|
||||||
ParseUUIDPipe,
|
ParseUUIDPipe,
|
||||||
Post,
|
Post,
|
||||||
|
Query,
|
||||||
Put,
|
Put,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
@@ -27,6 +28,19 @@ export class BackofficeController {
|
|||||||
return this.backofficeService.createOrganizationUser(organizationId, dto);
|
return this.backofficeService.createOrganizationUser(organizationId, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("organizations/:orgId/employees")
|
||||||
|
@ApiOperation({ summary: "Get deduplicated organization employees for backoffice" })
|
||||||
|
getOrganizationEmployees(
|
||||||
|
@Param("orgId", ParseUUIDPipe) organizationId: string,
|
||||||
|
@Query("skip") skip?: string,
|
||||||
|
@Query("take") take?: string,
|
||||||
|
) {
|
||||||
|
return this.backofficeService.getOrganizationEmployees(organizationId, {
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Get("organizations/:orgId/employee-users/:userId/roles")
|
@Get("organizations/:orgId/employee-users/:userId/roles")
|
||||||
@ApiOperation({ summary: "Get org-scoped roles assigned to an employee user" })
|
@ApiOperation({ summary: "Get org-scoped roles assigned to an employee user" })
|
||||||
getEmployeeUserRoles(
|
getEmployeeUserRoles(
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
import { InjectRepository } from "@nestjs/typeorm";
|
import { InjectRepository } from "@nestjs/typeorm";
|
||||||
import { hashPassword } from "@tria-plc/api-common/utils/argon";
|
import { hashPassword } from "@tria-plc/api-common/utils/argon";
|
||||||
import { EUserStatus } from "@tria-plc/api-common/utils/enums/user.enum";
|
import { EUserStatus } from "@tria-plc/api-common/utils/enums/user.enum";
|
||||||
import { DataSource, In, IsNull, Repository } from "typeorm";
|
import { DataSource, EntityManager, In, IsNull, Repository } from "typeorm";
|
||||||
|
|
||||||
import { Employee, Organization, UserCredential } from "@tria-plc/iamapi-common";
|
import { Employee, Organization, UserCredential } from "@tria-plc/iamapi-common";
|
||||||
import { Role } from "@tria-plc/iamapi-common/entities/iam/user/role.entity";
|
import { Role } from "@tria-plc/iamapi-common/entities/iam/user/role.entity";
|
||||||
@@ -21,10 +21,14 @@ const RESERVED_ROLE_KEYS = new Set([
|
|||||||
"unit_admin",
|
"unit_admin",
|
||||||
]);
|
]);
|
||||||
const DEFAULT_USER_PASSWORD = "12345678";
|
const DEFAULT_USER_PASSWORD = "12345678";
|
||||||
|
const ORGANIZATION_ADMIN_ROLE_KEY = "organization_admin";
|
||||||
|
const EDR_ORG_MANAGER_ROLE_KEY = "edr_org_manager";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BackofficeService {
|
export class BackofficeService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@InjectRepository(Employee)
|
||||||
|
private readonly employeeRepository: Repository<Employee>,
|
||||||
@InjectRepository(Organization)
|
@InjectRepository(Organization)
|
||||||
private readonly organizationRepository: Repository<Organization>,
|
private readonly organizationRepository: Repository<Organization>,
|
||||||
@InjectRepository(Role)
|
@InjectRepository(Role)
|
||||||
@@ -51,6 +55,7 @@ export class BackofficeService {
|
|||||||
const email = dto.email.trim().toLowerCase();
|
const email = dto.email.trim().toLowerCase();
|
||||||
const username = dto.username.trim().toLowerCase();
|
const username = dto.username.trim().toLowerCase();
|
||||||
const phoneNumber = dto.phoneNumber?.trim() || undefined;
|
const phoneNumber = dto.phoneNumber?.trim() || undefined;
|
||||||
|
const assignOrganizationAdmin = dto.assignOrganizationAdmin === true;
|
||||||
const name = {
|
const name = {
|
||||||
en: dto.name.en.trim(),
|
en: dto.name.en.trim(),
|
||||||
...(dto.name.am?.trim() ? { am: dto.name.am.trim() } : {}),
|
...(dto.name.am?.trim() ? { am: dto.name.am.trim() } : {}),
|
||||||
@@ -168,6 +173,16 @@ export class BackofficeService {
|
|||||||
throw new NotFoundException("employee_create_failed");
|
throw new NotFoundException("employee_create_failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userId = user.id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new NotFoundException("user_create_failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assignOrganizationAdmin) {
|
||||||
|
await this.ensureOrganizationAdminAccess(manager, organizationId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
return employee;
|
return employee;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -201,6 +216,45 @@ export class BackofficeService {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getOrganizationEmployees(
|
||||||
|
organizationId: string,
|
||||||
|
query: { skip?: string; take?: string },
|
||||||
|
) {
|
||||||
|
const organizationExists = await this.organizationRepository.exists({
|
||||||
|
where: { id: organizationId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!organizationExists) {
|
||||||
|
throw new NotFoundException("organization_not_found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const take = Number.parseInt(query.take ?? "1000", 10);
|
||||||
|
const skip = Number.parseInt(query.skip ?? "0", 10);
|
||||||
|
|
||||||
|
const employees = await this.employeeRepository.find({
|
||||||
|
where: {
|
||||||
|
organizationId,
|
||||||
|
isCurrent: true,
|
||||||
|
},
|
||||||
|
relations: {
|
||||||
|
user: true,
|
||||||
|
employeePositions: {
|
||||||
|
position: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
createdAt: "DESC",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deduplicated = this.mergeEmployeesByUser(employees);
|
||||||
|
|
||||||
|
return {
|
||||||
|
count: deduplicated.length,
|
||||||
|
items: deduplicated.slice(skip, skip + take),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async replaceEmployeeUserRoles(
|
async replaceEmployeeUserRoles(
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
@@ -268,4 +322,104 @@ export class BackofficeService {
|
|||||||
throw new NotFoundException("user_not_found_in_organization");
|
throw new NotFoundException("user_not_found_in_organization");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private mergeEmployeesByUser(employees: Employee[]) {
|
||||||
|
const employeesByUserId = new Map<string, Employee>();
|
||||||
|
|
||||||
|
for (const employee of employees) {
|
||||||
|
const userId = employee.userId;
|
||||||
|
const employeeId = employee.id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
if (employeeId) {
|
||||||
|
employeesByUserId.set(employeeId, employee);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = employeesByUserId.get(userId);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
employeesByUserId.set(userId, employee);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingPositions = existing.employeePositions ?? [];
|
||||||
|
const nextPositions = employee.employeePositions ?? [];
|
||||||
|
const mergedEmployeePositions = Array.from(
|
||||||
|
new Map(
|
||||||
|
[...existingPositions, ...nextPositions].map((employeePosition) => [
|
||||||
|
employeePosition.id,
|
||||||
|
employeePosition,
|
||||||
|
]),
|
||||||
|
).values(),
|
||||||
|
);
|
||||||
|
|
||||||
|
employeesByUserId.set(userId, {
|
||||||
|
...existing,
|
||||||
|
...employee,
|
||||||
|
id: existing.id,
|
||||||
|
user: existing.user ?? employee.user,
|
||||||
|
userId,
|
||||||
|
name: existing.name ?? employee.name,
|
||||||
|
status: existing.status ?? employee.status,
|
||||||
|
employeePositions: mergedEmployeePositions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...employeesByUserId.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureOrganizationAdminAccess(
|
||||||
|
manager: EntityManager,
|
||||||
|
organizationId: string,
|
||||||
|
userId: string,
|
||||||
|
) {
|
||||||
|
const roles = await manager.getRepository(Role).find({
|
||||||
|
where: [
|
||||||
|
{ key: ORGANIZATION_ADMIN_ROLE_KEY },
|
||||||
|
{ key: EDR_ORG_MANAGER_ROLE_KEY },
|
||||||
|
],
|
||||||
|
select: { id: true, key: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const requiredRoles = [ORGANIZATION_ADMIN_ROLE_KEY, EDR_ORG_MANAGER_ROLE_KEY].map((key) => {
|
||||||
|
const role = roles.find((item) => item.key === key);
|
||||||
|
|
||||||
|
if (!role?.id) {
|
||||||
|
throw new NotFoundException(`required_role_not_seeded:${key}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: role.id,
|
||||||
|
key: role.key,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingRoleIds = new Set(
|
||||||
|
(
|
||||||
|
await manager.getRepository(UserRole).find({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
organizationId,
|
||||||
|
},
|
||||||
|
select: { roleId: true },
|
||||||
|
})
|
||||||
|
).map((userRole) => userRole.roleId),
|
||||||
|
);
|
||||||
|
|
||||||
|
const rolesToInsert = requiredRoles
|
||||||
|
.filter((role) => !existingRoleIds.has(role.id))
|
||||||
|
.map((role) => ({
|
||||||
|
userId,
|
||||||
|
roleId: role.id,
|
||||||
|
organizationId,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!rolesToInsert.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await manager.getRepository(UserRole).insert(rolesToInsert);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { IsEmail, IsObject, IsOptional, IsString, MinLength } from "class-validator";
|
import { IsBoolean, IsEmail, IsObject, IsOptional, IsString, MinLength } from "class-validator";
|
||||||
|
|
||||||
class CreateOrganizationUserNameDto {
|
class CreateOrganizationUserNameDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@@ -31,4 +31,9 @@ export class CreateOrganizationUserDto {
|
|||||||
@ApiProperty({ type: CreateOrganizationUserNameDto })
|
@ApiProperty({ type: CreateOrganizationUserNameDto })
|
||||||
@IsObject()
|
@IsObject()
|
||||||
name!: CreateOrganizationUserNameDto;
|
name!: CreateOrganizationUserNameDto;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, default: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
assignOrganizationAdmin?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
229
apps/edr-freight-api/src/seed/edr-freight.seed.ts
Normal file
229
apps/edr-freight-api/src/seed/edr-freight.seed.ts
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
export type FreightSeedRole = {
|
||||||
|
key: string;
|
||||||
|
name: { en: string };
|
||||||
|
permissionKeys: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const IAM_PERMISSION_KEYS = {
|
||||||
|
activateEmployee: "can:activateEmployee",
|
||||||
|
activateUser: "can:activateUser",
|
||||||
|
createEmployee: "can:createEmployee",
|
||||||
|
createPositionPermission: "can:create:position_permission",
|
||||||
|
createUnit: "can:create:unit",
|
||||||
|
createUserRole: "can:create:user_role",
|
||||||
|
deactivateEmployee: "can:deactivateEmployee",
|
||||||
|
deletePositionPermission: "can:delete:position_permission",
|
||||||
|
deleteUnit: "can:delete:unit",
|
||||||
|
deleteUserRole: "can:delete:user_role",
|
||||||
|
findAllOrganization: "can:find_all:organization",
|
||||||
|
manageOrganizationAdmin: "manage:organizationAdmin",
|
||||||
|
manageUnitAdmin: "manage:unitAdmin",
|
||||||
|
updateUnit: "can:update:unit",
|
||||||
|
viewPositionPermission: "can:view:position_permission",
|
||||||
|
viewUserRole: "can:view:user_role",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const EDR_FREIGHT_APPLICATION = {
|
||||||
|
id: "7f5a2175-c270-495b-bec9-d59ddbdab5d1",
|
||||||
|
key: "edr_freight_app",
|
||||||
|
name: {
|
||||||
|
am: "EDR Freight App",
|
||||||
|
en: "EDR Freight App",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const EMPLOYEE_REGISTRATION_PERMISSIONS = [
|
||||||
|
{
|
||||||
|
id: "62b5aa2d-4ef6-474d-913a-994568dce1c8",
|
||||||
|
key: "edr_freight_app:employee_registration:view",
|
||||||
|
name: { am: "View employee registration", en: "View employee registration" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "8072204d-26de-4e62-88aa-74afd916a0cb",
|
||||||
|
key: "edr_freight_app:employee_registration:create",
|
||||||
|
name: { am: "Create employee registration", en: "Create employee registration" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b7dc55a6-ae7c-4558-8c4e-7d8ce5c7fa08",
|
||||||
|
key: "edr_freight_app:employee_registration:update",
|
||||||
|
name: { am: "Update employee registration", en: "Update employee registration" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "7ef06121-bd31-4c0d-b36d-5401b4bfd05c",
|
||||||
|
key: "edr_freight_app:employee_registration:activate",
|
||||||
|
name: { am: "Activate employee registration", en: "Activate employee registration" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2688e144-7f0c-4704-8d59-e92b0c08117a",
|
||||||
|
key: "edr_freight_app:employee_registration:deactivate",
|
||||||
|
name: { am: "Deactivate employee registration", en: "Deactivate employee registration" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const ROLE_ASSIGNMENT_PERMISSIONS = [
|
||||||
|
{
|
||||||
|
id: "4de87873-e00d-4330-9b4f-f4fb065f49e0",
|
||||||
|
key: "edr_freight_app:role_assignment:view",
|
||||||
|
name: { am: "View role assignment", en: "View role assignment" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "36f022b4-4b94-4220-a46c-df7bd1a1b184",
|
||||||
|
key: "edr_freight_app:role_assignment:assign",
|
||||||
|
name: { am: "Assign role", en: "Assign role" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "c1f34177-a0ae-4a46-a24a-3281b9137bab",
|
||||||
|
key: "edr_freight_app:role_assignment:replace",
|
||||||
|
name: { am: "Replace role assignment", en: "Replace role assignment" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const HIERARCHY_UNIT_PERMISSIONS = [
|
||||||
|
{
|
||||||
|
id: "2bfa2428-ec40-4588-9b01-dfacce6a2b82",
|
||||||
|
key: "edr_freight_app:hierarchy_units:view",
|
||||||
|
name: { am: "View hierarchy units", en: "View hierarchy units" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "1e92daff-9cc7-4a67-9994-879f34bfda16",
|
||||||
|
key: "edr_freight_app:hierarchy_units:create",
|
||||||
|
name: { am: "Create hierarchy unit", en: "Create hierarchy unit" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4ef2d8ad-c627-4448-b4b6-dd6b8b602dc1",
|
||||||
|
key: "edr_freight_app:hierarchy_units:update",
|
||||||
|
name: { am: "Update hierarchy unit", en: "Update hierarchy unit" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "15353ac5-246b-42e6-9ac3-eb61c4f1cd22",
|
||||||
|
key: "edr_freight_app:hierarchy_units:delete",
|
||||||
|
name: { am: "Delete hierarchy unit", en: "Delete hierarchy unit" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const HIERARCHY_POSITION_PERMISSIONS = [
|
||||||
|
{
|
||||||
|
id: "37ff6f5b-9fb0-4139-af99-22fe54703029",
|
||||||
|
key: "edr_freight_app:hierarchy_positions:view",
|
||||||
|
name: { am: "View hierarchy positions", en: "View hierarchy positions" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "af6c091a-6448-4459-a635-c2181efd1de0",
|
||||||
|
key: "edr_freight_app:hierarchy_positions:create",
|
||||||
|
name: { am: "Create hierarchy position", en: "Create hierarchy position" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "e78f624d-b570-4cd6-8f16-12090a4a9d31",
|
||||||
|
key: "edr_freight_app:hierarchy_positions:update",
|
||||||
|
name: { am: "Update hierarchy position", en: "Update hierarchy position" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "7fba7887-a365-4281-96ea-fb14582b047e",
|
||||||
|
key: "edr_freight_app:hierarchy_positions:delete",
|
||||||
|
name: { am: "Delete hierarchy position", en: "Delete hierarchy position" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "a33905ff-f2b8-40b9-a8cf-e2968f6f46fb",
|
||||||
|
key: "edr_freight_app:hierarchy_positions:change_parent",
|
||||||
|
name: { am: "Change hierarchy position parent", en: "Change hierarchy position parent" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const HIERARCHY_EMPLOYEE_ASSIGNMENT_PERMISSIONS = [
|
||||||
|
{
|
||||||
|
id: "b6ca90ff-3e95-4af2-bac8-fb298ca62080",
|
||||||
|
key: "edr_freight_app:hierarchy_employee_assignment:view",
|
||||||
|
name: { am: "View hierarchy employee assignment", en: "View hierarchy employee assignment" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "0637472f-d6b7-4332-85bb-eaa6a02205c1",
|
||||||
|
key: "edr_freight_app:hierarchy_employee_assignment:invite",
|
||||||
|
name: { am: "Invite hierarchy employee assignment", en: "Invite hierarchy employee assignment" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "de366c81-b6d1-4cf9-a5f1-a5c8a6fb5e7b",
|
||||||
|
key: "edr_freight_app:hierarchy_employee_assignment:assign",
|
||||||
|
name: { am: "Assign hierarchy employee assignment", en: "Assign hierarchy employee assignment" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const POSITION_TYPE_PERMISSIONS = [
|
||||||
|
{
|
||||||
|
id: "f258fb51-2890-4c93-b024-271b09d705d0",
|
||||||
|
key: "edr_freight_app:position_types:view",
|
||||||
|
name: { am: "View position types", en: "View position types" },
|
||||||
|
applicationKey: EDR_FREIGHT_APPLICATION.key,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const EDR_FREIGHT_PERMISSIONS = [
|
||||||
|
...EMPLOYEE_REGISTRATION_PERMISSIONS,
|
||||||
|
...ROLE_ASSIGNMENT_PERMISSIONS,
|
||||||
|
...HIERARCHY_UNIT_PERMISSIONS,
|
||||||
|
...HIERARCHY_POSITION_PERMISSIONS,
|
||||||
|
...HIERARCHY_EMPLOYEE_ASSIGNMENT_PERMISSIONS,
|
||||||
|
...POSITION_TYPE_PERMISSIONS,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const EDR_FREIGHT_ROLES: FreightSeedRole[] = [
|
||||||
|
{
|
||||||
|
key: "edr_employee",
|
||||||
|
name: { en: "EDR Employee" },
|
||||||
|
permissionKeys: [
|
||||||
|
"edr_freight_app:employee_registration:view",
|
||||||
|
"edr_freight_app:role_assignment:view",
|
||||||
|
"edr_freight_app:hierarchy_units:view",
|
||||||
|
"edr_freight_app:hierarchy_positions:view",
|
||||||
|
"edr_freight_app:hierarchy_employee_assignment:view",
|
||||||
|
"edr_freight_app:position_types:view",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "edr_org_manager",
|
||||||
|
name: { en: "EDR Org Manager" },
|
||||||
|
permissionKeys: [
|
||||||
|
...EDR_FREIGHT_PERMISSIONS.map((permission) => permission.key),
|
||||||
|
IAM_PERMISSION_KEYS.createEmployee,
|
||||||
|
IAM_PERMISSION_KEYS.deactivateEmployee,
|
||||||
|
IAM_PERMISSION_KEYS.activateEmployee,
|
||||||
|
IAM_PERMISSION_KEYS.activateUser,
|
||||||
|
IAM_PERMISSION_KEYS.createUserRole,
|
||||||
|
IAM_PERMISSION_KEYS.deleteUserRole,
|
||||||
|
IAM_PERMISSION_KEYS.viewUserRole,
|
||||||
|
IAM_PERMISSION_KEYS.manageOrganizationAdmin,
|
||||||
|
IAM_PERMISSION_KEYS.manageUnitAdmin,
|
||||||
|
IAM_PERMISSION_KEYS.createUnit,
|
||||||
|
IAM_PERMISSION_KEYS.updateUnit,
|
||||||
|
IAM_PERMISSION_KEYS.deleteUnit,
|
||||||
|
IAM_PERMISSION_KEYS.createPositionPermission,
|
||||||
|
IAM_PERMISSION_KEYS.deletePositionPermission,
|
||||||
|
IAM_PERMISSION_KEYS.viewPositionPermission,
|
||||||
|
IAM_PERMISSION_KEYS.findAllOrganization,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "edr_customer",
|
||||||
|
name: { en: "EDR Customer" },
|
||||||
|
permissionKeys: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -8,45 +8,17 @@ import {
|
|||||||
} from "@tria-plc/iamapi-common";
|
} from "@tria-plc/iamapi-common";
|
||||||
import { DataSource, EntityManager, In } from "typeorm";
|
import { DataSource, EntityManager, In } from "typeorm";
|
||||||
|
|
||||||
|
import { EDR_FREIGHT_ROLES, type FreightSeedRole } from "./edr-freight.seed";
|
||||||
|
|
||||||
const EDR_ORG_KEY = "edr_freight";
|
const EDR_ORG_KEY = "edr_freight";
|
||||||
const EDR_ORG_NAME = { en: "EDR Freight" };
|
const EDR_ORG_NAME = { en: "EDR Freight" };
|
||||||
const SEED_FLAG = "SEED_EDR_ORG";
|
const SEED_FLAG = "SEED_EDR_ORG";
|
||||||
|
|
||||||
type SeedPermission = {
|
|
||||||
key: string;
|
|
||||||
name: { en: string };
|
|
||||||
};
|
|
||||||
|
|
||||||
type SeedRole = {
|
|
||||||
key: string;
|
|
||||||
name: { en: string };
|
|
||||||
permissions: SeedPermission[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type SeedOrganization = {
|
type SeedOrganization = {
|
||||||
id: string;
|
id: string;
|
||||||
key: string;
|
key: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SEED_ROLES: SeedRole[] = [
|
|
||||||
{
|
|
||||||
key: "edr_employee",
|
|
||||||
name: { en: "EDR Employee" },
|
|
||||||
permissions: [
|
|
||||||
// { key: "permission:key", name: { en: "Permission Name" } },
|
|
||||||
{ key: "permission:key", name: { en: "Permission Name" } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "edr_customer",
|
|
||||||
name: { en: "EDR Customer" },
|
|
||||||
permissions: [
|
|
||||||
// { key: "permission:key", name: { en: "Permission Name" } },
|
|
||||||
{ key: "permission:key", name: { en: "Permission Name" } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EdrOrgSeeder {
|
export class EdrOrgSeeder {
|
||||||
private readonly logger = new Logger(EdrOrgSeeder.name);
|
private readonly logger = new Logger(EdrOrgSeeder.name);
|
||||||
@@ -63,9 +35,8 @@ export class EdrOrgSeeder {
|
|||||||
const organization = await this.ensureOrganization(manager);
|
const organization = await this.ensureOrganization(manager);
|
||||||
|
|
||||||
await this.ensureOrganizationConfiguration(manager, organization.id);
|
await this.ensureOrganizationConfiguration(manager, organization.id);
|
||||||
await this.ensurePermissions(manager, SEED_ROLES);
|
await this.ensureRoles(manager, EDR_FREIGHT_ROLES);
|
||||||
await this.ensureRoles(manager, SEED_ROLES);
|
await this.ensureRolePermissions(manager, EDR_FREIGHT_ROLES);
|
||||||
await this.ensureRolePermissions(manager, SEED_ROLES);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(`Ensured EDR organization seed for '${EDR_ORG_KEY}'`);
|
this.logger.log(`Ensured EDR organization seed for '${EDR_ORG_KEY}'`);
|
||||||
@@ -127,34 +98,7 @@ export class EdrOrgSeeder {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private collectPermissions(seedRoles: SeedRole[]) {
|
private async ensureRoles(manager: EntityManager, seedRoles: FreightSeedRole[]) {
|
||||||
const permissionByKey = new Map<string, SeedPermission>();
|
|
||||||
|
|
||||||
for (const role of seedRoles) {
|
|
||||||
for (const permission of role.permissions) {
|
|
||||||
permissionByKey.set(permission.key, permission);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...permissionByKey.values()];
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ensurePermissions(manager: EntityManager, seedRoles: SeedRole[]) {
|
|
||||||
const permissions = this.collectPermissions(seedRoles);
|
|
||||||
|
|
||||||
if (!permissions.length) {
|
|
||||||
this.logger.log("No EDR role permissions configured; skipping permission seed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await manager.getRepository(Permission).upsert(permissions, {
|
|
||||||
conflictPaths: { key: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.log(`Ensured ${permissions.length} EDR permissions`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ensureRoles(manager: EntityManager, seedRoles: SeedRole[]) {
|
|
||||||
await manager.getRepository(Role).upsert(
|
await manager.getRepository(Role).upsert(
|
||||||
seedRoles.map(({ key, name }) => ({ key, name })),
|
seedRoles.map(({ key, name }) => ({ key, name })),
|
||||||
{
|
{
|
||||||
@@ -169,24 +113,24 @@ export class EdrOrgSeeder {
|
|||||||
|
|
||||||
private async ensureRolePermissions(
|
private async ensureRolePermissions(
|
||||||
manager: EntityManager,
|
manager: EntityManager,
|
||||||
seedRoles: SeedRole[],
|
seedRoles: FreightSeedRole[],
|
||||||
) {
|
) {
|
||||||
const permissions = this.collectPermissions(seedRoles);
|
const permissionKeys = [...new Set(seedRoles.flatMap((role) => role.permissionKeys))];
|
||||||
|
|
||||||
if (!permissions.length) {
|
if (!permissionKeys.length) {
|
||||||
|
this.logger.log("No EDR role permissions configured; skipping role-permission links");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleRepository = manager.getRepository(Role);
|
const roleRepository = manager.getRepository(Role);
|
||||||
const permissionRepository = manager.getRepository(Permission);
|
|
||||||
const rolePermissionRepository = manager.getRepository(RolePermission);
|
const rolePermissionRepository = manager.getRepository(RolePermission);
|
||||||
|
|
||||||
const roles = await roleRepository.find({
|
const roles = await roleRepository.find({
|
||||||
where: { key: In(seedRoles.map((role) => role.key)) },
|
where: { key: In(seedRoles.map((role) => role.key)) },
|
||||||
select: { id: true, key: true },
|
select: { id: true, key: true },
|
||||||
});
|
});
|
||||||
const seededPermissions = await permissionRepository.find({
|
const seededPermissions = await manager.getRepository(Permission).find({
|
||||||
where: { key: In(permissions.map((permission) => permission.key)) },
|
where: { key: In(permissionKeys) },
|
||||||
select: { id: true, key: true },
|
select: { id: true, key: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -202,11 +146,11 @@ export class EdrOrgSeeder {
|
|||||||
throw new Error(`missing_role:${role.key}`);
|
throw new Error(`missing_role:${role.key}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return role.permissions.map((permission) => {
|
return role.permissionKeys.map((permissionKey) => {
|
||||||
const seededPermission = permissionByKey.get(permission.key);
|
const seededPermission = permissionByKey.get(permissionKey);
|
||||||
|
|
||||||
if (!seededPermission) {
|
if (!seededPermission) {
|
||||||
throw new Error(`missing_permission:${permission.key}`);
|
throw new Error(`missing_permission:${permissionKey}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import DemoUser2Page from "./pages/dashboard/demo/DemoUser2Page";
|
|||||||
import OverviewPage from "./pages/dashboard/OverviewPage";
|
import OverviewPage from "./pages/dashboard/OverviewPage";
|
||||||
import EmployeesPage from "./pages/dashboard/user-management/EmployeesPage";
|
import EmployeesPage from "./pages/dashboard/user-management/EmployeesPage";
|
||||||
import PermissionsPage from "./pages/dashboard/user-management/PermissionsPage";
|
import PermissionsPage from "./pages/dashboard/user-management/PermissionsPage";
|
||||||
|
import PositionTypesPage from "./pages/dashboard/user-management/PositionTypesPage";
|
||||||
import RolesPage from "./pages/dashboard/user-management/RolesPage";
|
import RolesPage from "./pages/dashboard/user-management/RolesPage";
|
||||||
import UserManagementPage from "./pages/dashboard/user-management/UserManagementPage";
|
import UserManagementPage from "./pages/dashboard/user-management/UserManagementPage";
|
||||||
import UsersPage from "./pages/dashboard/user-management/UsersPage";
|
import UsersPage from "./pages/dashboard/user-management/UsersPage";
|
||||||
@@ -74,6 +75,10 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
|||||||
label: "Employees",
|
label: "Employees",
|
||||||
href: "/dashboard/user-management/employees",
|
href: "/dashboard/user-management/employees",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Position Types",
|
||||||
|
href: "/dashboard/user-management/position-types",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Permissions",
|
label: "Permissions",
|
||||||
href: "/dashboard/user-management/permissions",
|
href: "/dashboard/user-management/permissions",
|
||||||
@@ -114,7 +119,6 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const hasPermission = (
|
const hasPermission = (
|
||||||
@@ -207,7 +211,8 @@ const App = () => {
|
|||||||
|
|
||||||
<Route path="user-management" element={<UserManagementPage />} />
|
<Route path="user-management" element={<UserManagementPage />} />
|
||||||
<Route path="user-management/users" element={<UsersPage />} />
|
<Route path="user-management/users" element={<UsersPage />} />
|
||||||
<Route path="user-management/employees" element={<EmployeesPage />} />
|
<Route path="user-management/position-types" element={<PositionTypesPage />} />
|
||||||
|
{/* <Route path="user-management/employees" element={<EmployeesPage />} /> */}
|
||||||
<Route path="user-management/permissions" element={<PermissionsPage />} />
|
<Route path="user-management/permissions" element={<PermissionsPage />} />
|
||||||
<Route path="user-management/roles" element={<RolesPage />} />
|
<Route path="user-management/roles" element={<RolesPage />} />
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ const EmployeesPage = () => {
|
|||||||
const responses = await Promise.all(
|
const responses = await Promise.all(
|
||||||
organizationIds.map((organizationId) =>
|
organizationIds.map((organizationId) =>
|
||||||
api.get<ListResponse<EmployeeRecord>>(
|
api.get<ListResponse<EmployeeRecord>>(
|
||||||
`/employees/${organizationId}/by-organization`,
|
`/backoffice/organizations/${organizationId}/employees`,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
skip: 0,
|
skip: 0,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -188,6 +188,46 @@ const getItems = <T,>(payload: ListResponse<T> | T[] | undefined | null) => {
|
|||||||
return payload.items ?? payload.data ?? [];
|
return payload.items ?? payload.data ?? [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mergeEmployeesByUser = (employees: EmployeeRecord[]) => {
|
||||||
|
const employeesByUserId = new Map<string, EmployeeRecord>();
|
||||||
|
|
||||||
|
for (const employee of employees) {
|
||||||
|
const userId = employee.user?.id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
employeesByUserId.set(employee.id, employee);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = employeesByUserId.get(userId);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
employeesByUserId.set(userId, employee);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingPositions = existing.employeePositions ?? [];
|
||||||
|
const nextPositions = employee.employeePositions ?? [];
|
||||||
|
const mergedPositions = Array.from(
|
||||||
|
new Map(
|
||||||
|
[...existingPositions, ...nextPositions].map((position) => [position.id, position]),
|
||||||
|
).values(),
|
||||||
|
);
|
||||||
|
|
||||||
|
employeesByUserId.set(userId, {
|
||||||
|
...existing,
|
||||||
|
...employee,
|
||||||
|
id: existing.id,
|
||||||
|
name: existing.name ?? employee.name,
|
||||||
|
status: existing.status ?? employee.status,
|
||||||
|
user: existing.user ?? employee.user,
|
||||||
|
employeePositions: mergedPositions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...employeesByUserId.values()];
|
||||||
|
};
|
||||||
|
|
||||||
const toInternalKey = (value: string) =>
|
const toInternalKey = (value: string) =>
|
||||||
value
|
value
|
||||||
.trim()
|
.trim()
|
||||||
@@ -611,9 +651,9 @@ const UserManagementPage = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.get<ListResponse<EmployeeRecord>>(
|
const response = await api.get<ListResponse<EmployeeRecord>>(
|
||||||
`/employees/${organizationId}/by-organization`,
|
`/backoffice/organizations/${organizationId}/employees`,
|
||||||
);
|
);
|
||||||
setOrgEmployees(getItems(response.data));
|
setOrgEmployees(mergeEmployeesByUser(getItems(response.data)));
|
||||||
} catch {
|
} catch {
|
||||||
setOrgEmployees([]);
|
setOrgEmployees([]);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -745,13 +785,19 @@ const UserManagementPage = () => {
|
|||||||
const refreshSelectedUnit = useCallback(async () => {
|
const refreshSelectedUnit = useCallback(async () => {
|
||||||
if (!selectedUnitId) {
|
if (!selectedUnitId) {
|
||||||
setPositions([]);
|
setPositions([]);
|
||||||
|
setPositionTypes([]);
|
||||||
|
setPositionTypesError(null);
|
||||||
setPositionMembers([]);
|
setPositionMembers([]);
|
||||||
setUnitAdminUserIds(new Set());
|
setUnitAdminUserIds(new Set());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all([loadPositions(selectedUnitId), loadUnitAdmins(selectedUnitId)]);
|
await Promise.all([
|
||||||
}, [loadPositions, loadUnitAdmins, selectedUnitId]);
|
loadPositions(selectedUnitId),
|
||||||
|
loadPositionTypes(selectedUnitId),
|
||||||
|
loadUnitAdmins(selectedUnitId),
|
||||||
|
]);
|
||||||
|
}, [loadPositionTypes, loadPositions, loadUnitAdmins, selectedUnitId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadOrganizations();
|
void loadOrganizations();
|
||||||
@@ -772,7 +818,7 @@ const UserManagementPage = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedOrgId(null);
|
setSelectedOrgId(visibleOrganizations[0]?.id ?? null);
|
||||||
setSelectedOrgConfiguration(null);
|
setSelectedOrgConfiguration(null);
|
||||||
setUnits([]);
|
setUnits([]);
|
||||||
setPositions([]);
|
setPositions([]);
|
||||||
@@ -780,6 +826,14 @@ const UserManagementPage = () => {
|
|||||||
setOrgEmployees([]);
|
setOrgEmployees([]);
|
||||||
}, [selectedOrgId, visibleOrganizations]);
|
}, [selectedOrgId, visibleOrganizations]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedOrgId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshSelectedOrg();
|
||||||
|
}, [refreshSelectedOrg, selectedOrgId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedOrgId) {
|
if (!selectedOrgId) {
|
||||||
setUnits([]);
|
setUnits([]);
|
||||||
@@ -802,9 +856,23 @@ const UserManagementPage = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!units.some((unit) => unit.id === selectedUnitId)) {
|
if (units.some((unit) => unit.id === selectedUnitId)) {
|
||||||
setSelectedUnitId(null);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSelectedUnitId(units[0]?.id ?? null);
|
||||||
|
}, [selectedOrgId, selectedUnitId, units]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedOrgId || !units.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedUnitId && units.some((unit) => unit.id === selectedUnitId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedUnitId(units[0]?.id ?? null);
|
||||||
}, [selectedOrgId, selectedUnitId, units]);
|
}, [selectedOrgId, selectedUnitId, units]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -820,6 +888,14 @@ const UserManagementPage = () => {
|
|||||||
}
|
}
|
||||||
}, [selectedUnitId]);
|
}, [selectedUnitId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedUnitId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshSelectedUnit();
|
||||||
|
}, [refreshSelectedUnit, selectedUnitId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedUnitId || !selectedPositionId) {
|
if (!selectedUnitId || !selectedPositionId) {
|
||||||
return;
|
return;
|
||||||
@@ -882,15 +958,11 @@ const UserManagementPage = () => {
|
|||||||
setSelectedPositionId(null);
|
setSelectedPositionId(null);
|
||||||
setExpandedDepartmentIds(new Set());
|
setExpandedDepartmentIds(new Set());
|
||||||
setPositions([]);
|
setPositions([]);
|
||||||
|
setPositionTypes([]);
|
||||||
|
setPositionTypesError(null);
|
||||||
setPositionMembers([]);
|
setPositionMembers([]);
|
||||||
setUnitAdminUserIds(new Set());
|
setUnitAdminUserIds(new Set());
|
||||||
resetMessages();
|
resetMessages();
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.all([loadPositions(unitId), loadUnitAdmins(unitId)]);
|
|
||||||
} catch {
|
|
||||||
// Individual loaders already handle their own error state.
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleDepartment = (departmentId: string) => {
|
const handleToggleDepartment = (departmentId: string) => {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ interface UserFormState {
|
|||||||
email: string;
|
email: string;
|
||||||
username: string;
|
username: string;
|
||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
|
assignOrganizationAdmin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ListResponse<T> {
|
interface ListResponse<T> {
|
||||||
@@ -75,6 +76,7 @@ const emptyUserForm: UserFormState = {
|
|||||||
email: "",
|
email: "",
|
||||||
username: "",
|
username: "",
|
||||||
phoneNumber: "",
|
phoneNumber: "",
|
||||||
|
assignOrganizationAdmin: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputClassName =
|
const inputClassName =
|
||||||
@@ -102,6 +104,46 @@ const getItems = <T,>(payload: ListResponse<T> | T[] | undefined | null) => {
|
|||||||
return payload.items ?? payload.data ?? [];
|
return payload.items ?? payload.data ?? [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mergeEmployeesByUser = (employees: EmployeeRecord[]) => {
|
||||||
|
const employeesByUserId = new Map<string, EmployeeRecord>();
|
||||||
|
|
||||||
|
for (const employee of employees) {
|
||||||
|
const userId = employee.user?.id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
employeesByUserId.set(employee.id, employee);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = employeesByUserId.get(userId);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
employeesByUserId.set(userId, employee);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingPositions = existing.employeePositions ?? [];
|
||||||
|
const nextPositions = employee.employeePositions ?? [];
|
||||||
|
const mergedPositions = Array.from(
|
||||||
|
new Map(
|
||||||
|
[...existingPositions, ...nextPositions].map((position) => [position.id, position]),
|
||||||
|
).values(),
|
||||||
|
);
|
||||||
|
|
||||||
|
employeesByUserId.set(userId, {
|
||||||
|
...existing,
|
||||||
|
...employee,
|
||||||
|
id: existing.id,
|
||||||
|
name: existing.name ?? employee.name,
|
||||||
|
status: existing.status ?? employee.status,
|
||||||
|
user: existing.user ?? employee.user,
|
||||||
|
employeePositions: mergedPositions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...employeesByUserId.values()];
|
||||||
|
};
|
||||||
|
|
||||||
const getErrorMessage = (error: unknown, fallback: string) => {
|
const getErrorMessage = (error: unknown, fallback: string) => {
|
||||||
if (isAxiosError(error)) {
|
if (isAxiosError(error)) {
|
||||||
const message = error.response?.data?.message;
|
const message = error.response?.data?.message;
|
||||||
@@ -229,9 +271,9 @@ const UsersPage = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.get<ListResponse<EmployeeRecord>>(
|
const response = await api.get<ListResponse<EmployeeRecord>>(
|
||||||
`/employees/${organizationId}/by-organization`,
|
`/backoffice/organizations/${organizationId}/employees`,
|
||||||
);
|
);
|
||||||
setOrgEmployees(getItems(response.data));
|
setOrgEmployees(mergeEmployeesByUser(getItems(response.data)));
|
||||||
} catch {
|
} catch {
|
||||||
setOrgEmployees([]);
|
setOrgEmployees([]);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -346,14 +388,23 @@ const UsersPage = () => {
|
|||||||
am: createUserForm.nameAm.trim(),
|
am: createUserForm.nameAm.trim(),
|
||||||
en: createUserForm.nameEn.trim(),
|
en: createUserForm.nameEn.trim(),
|
||||||
},
|
},
|
||||||
|
assignOrganizationAdmin: createUserForm.assignOrganizationAdmin,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const shouldAssignOrganizationAdmin = createUserForm.assignOrganizationAdmin;
|
||||||
setCreateUserForm(emptyUserForm);
|
setCreateUserForm(emptyUserForm);
|
||||||
setIsCreateUserOpen(false);
|
setIsCreateUserOpen(false);
|
||||||
setActionSuccess("User created. Default password: 12345678.");
|
setActionSuccess(
|
||||||
|
shouldAssignOrganizationAdmin
|
||||||
|
? "User created as organization admin. Default password: 12345678."
|
||||||
|
: "User created. Default password: 12345678.",
|
||||||
|
);
|
||||||
await loadOrgEmployees(selectedOrgId);
|
await loadOrgEmployees(selectedOrgId);
|
||||||
await openManageRolesDialog(response.data);
|
|
||||||
|
if (!shouldAssignOrganizationAdmin) {
|
||||||
|
await openManageRolesDialog(response.data);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setActionError(getErrorMessage(error, "Failed to create user."));
|
setActionError(getErrorMessage(error, "Failed to create user."));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -669,6 +720,24 @@ const UsersPage = () => {
|
|||||||
onChange={(event) => setCreateUserForm((current) => ({ ...current, phoneNumber: event.target.value }))}
|
onChange={(event) => setCreateUserForm((current) => ({ ...current, phoneNumber: event.target.value }))}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
<label className="flex items-start gap-3 rounded-2xl border border-border bg-background px-4 py-3 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={createUserForm.assignOrganizationAdmin}
|
||||||
|
onChange={(event) =>
|
||||||
|
setCreateUserForm((current) => ({
|
||||||
|
...current,
|
||||||
|
assignOrganizationAdmin: event.target.checked,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-foreground">Create as organization admin</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Also assigns built-in org admin access and the freight org manager role.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user