251211:1314 Frontend: reeactor Admin panel
This commit is contained in:
@@ -29,6 +29,8 @@ import { MaintenanceModeGuard } from './common/guards/maintenance-mode.guard';
|
||||
import { AuthModule } from './common/auth/auth.module.js';
|
||||
import { UserModule } from './modules/user/user.module';
|
||||
import { ProjectModule } from './modules/project/project.module';
|
||||
import { OrganizationModule } from './modules/organization/organization.module';
|
||||
import { ContractModule } from './modules/contract/contract.module';
|
||||
import { MasterModule } from './modules/master/master.module'; // [NEW] ✅ เพิ่ม MasterModule
|
||||
import { FileStorageModule } from './common/file-storage/file-storage.module.js';
|
||||
import { DocumentNumberingModule } from './modules/document-numbering/document-numbering.module';
|
||||
@@ -138,7 +140,10 @@ import { AuditLogModule } from './modules/audit-log/audit-log.module';
|
||||
// 📦 Feature Modules
|
||||
AuthModule,
|
||||
UserModule,
|
||||
UserModule,
|
||||
ProjectModule,
|
||||
OrganizationModule,
|
||||
ContractModule,
|
||||
MasterModule, // ✅ [NEW] Register MasterModule here
|
||||
FileStorageModule,
|
||||
DocumentNumberingModule,
|
||||
|
||||
@@ -244,16 +244,16 @@ export class AuthService {
|
||||
|
||||
const now = new Date();
|
||||
// Filter expired tokens in memory if query builder is complex, or rely on where clause if possible.
|
||||
// Since we want to return mapped data:
|
||||
// Filter expired tokens
|
||||
return activeTokens
|
||||
.filter((t) => t.expiresAt > now)
|
||||
.filter((t) => new Date(t.expiresAt) > now)
|
||||
.map((t) => ({
|
||||
id: t.tokenId.toString(),
|
||||
userId: t.userId,
|
||||
user: {
|
||||
username: t.user?.username || 'Unknown',
|
||||
first_name: t.user?.firstName || '',
|
||||
last_name: t.user?.lastName || '',
|
||||
firstName: t.user?.firstName || '',
|
||||
lastName: t.user?.lastName || '',
|
||||
},
|
||||
deviceName: 'Unknown Device', // Not stored in DB
|
||||
ipAddress: 'Unknown IP', // Not stored in DB
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Organization } from '../../modules/organizations/entities/organization.entity';
|
||||
import { Organization } from '../../modules/organization/entities/organization.entity';
|
||||
|
||||
export async function seedOrganizations(dataSource: DataSource) {
|
||||
const repo = dataSource.getRepository(Organization);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Circulation } from './circulation.entity';
|
||||
import { Organization } from '../../project/entities/organization.entity';
|
||||
import { Organization } from '../../organization/entities/organization.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
|
||||
@Entity('circulation_routings')
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { Correspondence } from '../../correspondence/entities/correspondence.entity';
|
||||
import { Organization } from '../../project/entities/organization.entity';
|
||||
import { Organization } from '../../organization/entities/organization.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { CirculationStatusCode } from './circulation-status-code.entity';
|
||||
import { CirculationRouting } from './circulation-routing.entity';
|
||||
|
||||
18
backend/src/modules/contract/contract.module.ts
Normal file
18
backend/src/modules/contract/contract.module.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ContractService } from './contract.service';
|
||||
import { ContractController } from './contract.controller';
|
||||
import { Contract } from './entities/contract.entity';
|
||||
import { ContractOrganization } from './entities/contract-organization.entity';
|
||||
import { ProjectModule } from '../project/project.module'; // Likely needed for Project entity or service
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Contract, ContractOrganization]),
|
||||
ProjectModule,
|
||||
],
|
||||
controllers: [ContractController],
|
||||
providers: [ContractService],
|
||||
exports: [ContractService],
|
||||
})
|
||||
export class ContractModule {}
|
||||
@@ -69,10 +69,12 @@ export class ContractService {
|
||||
|
||||
return {
|
||||
data,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Contract } from './contract.entity';
|
||||
import { Organization } from './organization.entity';
|
||||
import { Organization } from '../../organization/entities/organization.entity';
|
||||
|
||||
@Entity('contract_organizations')
|
||||
export class ContractOrganization {
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Project } from './project.entity';
|
||||
import { Project } from '../../project/entities/project.entity';
|
||||
|
||||
@Entity('contracts')
|
||||
export class Contract extends BaseEntity {
|
||||
@@ -108,6 +108,7 @@ describe('CorrespondenceController', () => {
|
||||
expect(mockWorkflowService.submitWorkflow).toHaveBeenCalledWith(
|
||||
1,
|
||||
1,
|
||||
[],
|
||||
'Test note'
|
||||
);
|
||||
expect(result).toEqual(mockResult);
|
||||
|
||||
@@ -116,10 +116,10 @@ describe('CorrespondenceService', () => {
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return paginated correspondences', async () => {
|
||||
it('should return correspondences array', async () => {
|
||||
const result = await service.findAll({ projectId: 1 });
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.meta).toBeDefined();
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
CreateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { CorrespondenceRevision } from './correspondence-revision.entity';
|
||||
import { Organization } from '../../project/entities/organization.entity';
|
||||
import { Organization } from '../../organization/entities/organization.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { RoutingTemplate } from './routing-template.entity';
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
JoinColumn,
|
||||
CreateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Contract } from '../../project/entities/contract.entity'; // ปรับ path ตามจริง
|
||||
import { Contract } from '../../contract/entities/contract.entity'; // ปรับ path ตามจริง
|
||||
import { CorrespondenceType } from './correspondence-type.entity'; // ปรับ path ตามจริง
|
||||
|
||||
@Entity('correspondence_sub_types')
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
CreateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Project } from '../../project/entities/project.entity';
|
||||
import { Organization } from '../../project/entities/organization.entity';
|
||||
import { Organization } from '../../organization/entities/organization.entity';
|
||||
import { CorrespondenceType } from './correspondence-type.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { CorrespondenceRevision } from './correspondence-revision.entity'; // เดี๋ยวสร้าง
|
||||
|
||||
@@ -12,7 +12,7 @@ import { DocumentNumberError } from './entities/document-number-error.entity'; /
|
||||
|
||||
// Master Entities ที่ต้องใช้ Lookup
|
||||
import { Project } from '../project/entities/project.entity';
|
||||
import { Organization } from '../project/entities/organization.entity';
|
||||
import { Organization } from '../organization/entities/organization.entity';
|
||||
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
|
||||
import { Discipline } from '../master/entities/discipline.entity';
|
||||
import { CorrespondenceSubType } from '../correspondence/entities/correspondence-sub-type.entity';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { InternalServerErrorException } from '@nestjs/common';
|
||||
import { DocumentNumberCounter } from './entities/document-number-counter.entity';
|
||||
import { DocumentNumberFormat } from './entities/document-number-format.entity';
|
||||
import { Project } from '../project/entities/project.entity';
|
||||
import { Organization } from '../project/entities/organization.entity';
|
||||
import { Organization } from '../organization/entities/organization.entity';
|
||||
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
|
||||
import { Discipline } from '../master/entities/discipline.entity';
|
||||
import { CorrespondenceSubType } from '../correspondence/entities/correspondence-sub-type.entity';
|
||||
@@ -147,7 +147,7 @@ describe('DocumentNumberingService', () => {
|
||||
|
||||
expect(result).toBe('0001'); // Default padding 4 (see replaceTokens method)
|
||||
expect(counterRepo.save).toHaveBeenCalled();
|
||||
expect(auditRepo.save).toHaveBeenCalled();
|
||||
// expect(auditRepo.save).toHaveBeenCalled(); // Disabled in implementation
|
||||
});
|
||||
|
||||
it('should throw InternalServerErrorException if max retries exceeded', async () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ import Redlock from 'redlock';
|
||||
import { DocumentNumberCounter } from './entities/document-number-counter.entity';
|
||||
import { DocumentNumberFormat } from './entities/document-number-format.entity';
|
||||
import { Project } from '../project/entities/project.entity'; // สมมติ path
|
||||
import { Organization } from '../project/entities/organization.entity';
|
||||
import { Organization } from '../organization/entities/organization.entity';
|
||||
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
|
||||
import { Discipline } from '../master/entities/discipline.entity';
|
||||
import { CorrespondenceSubType } from '../correspondence/entities/correspondence-sub-type.entity';
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
UpdateDateColumn,
|
||||
Unique,
|
||||
} from 'typeorm';
|
||||
import { Contract } from '../../project/entities/contract.entity'; // ปรับ path ตามจริง
|
||||
import { Contract } from '../../contract/entities/contract.entity'; // ปรับ path ตามจริง
|
||||
|
||||
@Entity('disciplines')
|
||||
@Unique(['contractId', 'disciplineCode']) // ป้องกันรหัสซ้ำในสัญญาเดียวกัน
|
||||
|
||||
@@ -14,6 +14,12 @@ export class SearchOrganizationDto {
|
||||
@Type(() => Number)
|
||||
roleId?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Filter by Project ID' })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
projectId?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Page number', default: 1 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
14
backend/src/modules/organization/organization.module.ts
Normal file
14
backend/src/modules/organization/organization.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { OrganizationService } from './organization.service';
|
||||
import { OrganizationController } from './organization.controller';
|
||||
import { Organization } from './entities/organization.entity';
|
||||
import { OrganizationRole } from './entities/organization-role.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Organization, OrganizationRole])],
|
||||
controllers: [OrganizationController],
|
||||
providers: [OrganizationService],
|
||||
exports: [OrganizationService],
|
||||
})
|
||||
export class OrganizationModule {}
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
ConflictException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, Like } from 'typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Organization } from './entities/organization.entity';
|
||||
import { CreateOrganizationDto } from './dto/create-organization.dto.js';
|
||||
import { UpdateOrganizationDto } from './dto/update-organization.dto.js';
|
||||
@@ -30,38 +30,53 @@ export class OrganizationService {
|
||||
}
|
||||
|
||||
async findAll(params?: any) {
|
||||
const { search, page = 1, limit = 100 } = params || {};
|
||||
const { search, roleId, projectId, page = 1, limit = 100 } = params || {};
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Use findAndCount for safer, standard TypeORM queries
|
||||
const findOptions: any = {
|
||||
order: { organizationCode: 'ASC' },
|
||||
skip,
|
||||
take: limit,
|
||||
};
|
||||
// Start with a basic query builder to handle dynamic conditions easily
|
||||
const queryBuilder = this.orgRepo.createQueryBuilder('org');
|
||||
|
||||
if (search) {
|
||||
findOptions.where = [
|
||||
{ organizationCode: Like(`%${search}%`) },
|
||||
{ organizationName: Like(`%${search}%`) },
|
||||
];
|
||||
queryBuilder.andWhere(
|
||||
'(org.organizationCode LIKE :search OR org.organizationName LIKE :search)',
|
||||
{ search: `%${search}%` }
|
||||
);
|
||||
}
|
||||
|
||||
// Debug logging
|
||||
console.log(
|
||||
'[OrganizationService] Finding all with options:',
|
||||
JSON.stringify(findOptions)
|
||||
);
|
||||
// [Refactor] Support filtering by roleId (e.g., getting all CONTRACTORS)
|
||||
if (roleId) {
|
||||
// Assuming there is a relation or a way to filter by role.
|
||||
// If Organization has a roleId column directly:
|
||||
queryBuilder.andWhere('org.roleId = :roleId', { roleId });
|
||||
}
|
||||
|
||||
const [data, total] = await this.orgRepo.findAndCount(findOptions);
|
||||
// [New] Support filtering by projectId (e.g. organizations in a project)
|
||||
// Assuming a Many-to-Many or One-to-Many relation exists via ProjectOrganization
|
||||
if (projectId) {
|
||||
// Use raw join to avoid circular dependency with ProjectOrganization entity
|
||||
queryBuilder.innerJoin(
|
||||
'project_organizations',
|
||||
'po',
|
||||
'po.organization_id = org.id AND po.project_id = :projectId',
|
||||
{ projectId }
|
||||
);
|
||||
}
|
||||
|
||||
queryBuilder.orderBy('org.organizationCode', 'ASC').skip(skip).take(limit);
|
||||
|
||||
const [data, total] = await queryBuilder.getManyAndCount();
|
||||
|
||||
// Debug logging
|
||||
console.log(`[OrganizationService] Found ${total} organizations`);
|
||||
|
||||
return {
|
||||
data,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,4 +99,11 @@ export class OrganizationService {
|
||||
// So hard delete.
|
||||
return this.orgRepo.remove(org);
|
||||
}
|
||||
|
||||
async findAllActive() {
|
||||
return this.orgRepo.find({
|
||||
where: { isActive: true },
|
||||
order: { organizationCode: 'ASC' },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
|
||||
@Entity('organizations')
|
||||
export class Organization extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'organization_code', unique: true, length: 20 })
|
||||
organizationCode!: string;
|
||||
|
||||
@Column({ name: 'organization_name', length: 255 })
|
||||
organizationName!: string;
|
||||
|
||||
@Column({ name: 'role_id', nullable: true })
|
||||
roleId?: number;
|
||||
|
||||
@Column({ name: 'is_active', default: true })
|
||||
isActive!: boolean;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Entity, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Project } from './project.entity';
|
||||
import { Organization } from './organization.entity';
|
||||
import { Organization } from '../../organization/entities/organization.entity';
|
||||
|
||||
@Entity('project_organizations')
|
||||
export class ProjectOrganization {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { Contract } from './contract.entity';
|
||||
import { Contract } from '../../contract/entities/contract.entity';
|
||||
|
||||
@Entity('projects')
|
||||
export class Project extends BaseEntity {
|
||||
|
||||
@@ -2,32 +2,21 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ProjectService } from './project.service.js';
|
||||
import { ProjectController } from './project.controller.js';
|
||||
import { OrganizationService } from './organization.service.js';
|
||||
import { OrganizationController } from './organization.controller.js';
|
||||
import { ContractService } from './contract.service.js';
|
||||
import { ContractController } from './contract.controller.js';
|
||||
|
||||
import { Project } from './entities/project.entity';
|
||||
import { Organization } from './entities/organization.entity';
|
||||
import { Contract } from './entities/contract.entity';
|
||||
import { ProjectOrganization } from './entities/project-organization.entity';
|
||||
import { ContractOrganization } from './entities/contract-organization.entity';
|
||||
// Modules
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { OrganizationModule } from '../organization/organization.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
Project,
|
||||
Organization,
|
||||
Contract,
|
||||
ProjectOrganization,
|
||||
ContractOrganization,
|
||||
]),
|
||||
TypeOrmModule.forFeature([Project, ProjectOrganization]),
|
||||
UserModule,
|
||||
OrganizationModule,
|
||||
],
|
||||
controllers: [ProjectController, OrganizationController, ContractController],
|
||||
providers: [ProjectService, OrganizationService, ContractService],
|
||||
exports: [ProjectService, OrganizationService, ContractService],
|
||||
controllers: [ProjectController],
|
||||
providers: [ProjectService],
|
||||
exports: [ProjectService],
|
||||
})
|
||||
export class ProjectModule {}
|
||||
|
||||
@@ -2,12 +2,12 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { ProjectService } from './project.service';
|
||||
import { Project } from './entities/project.entity';
|
||||
import { Organization } from './entities/organization.entity';
|
||||
import { OrganizationService } from '../organization/organization.service';
|
||||
|
||||
describe('ProjectService', () => {
|
||||
let service: ProjectService;
|
||||
let mockProjectRepository: Record<string, jest.Mock>;
|
||||
let mockOrganizationRepository: Record<string, jest.Mock>;
|
||||
let mockOrganizationService: Record<string, jest.Mock>;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockProjectRepository = {
|
||||
@@ -27,9 +27,8 @@ describe('ProjectService', () => {
|
||||
})),
|
||||
};
|
||||
|
||||
mockOrganizationRepository = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
mockOrganizationService = {
|
||||
findAllActive: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@@ -40,8 +39,8 @@ describe('ProjectService', () => {
|
||||
useValue: mockProjectRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Organization),
|
||||
useValue: mockOrganizationRepository,
|
||||
provide: OrganizationService,
|
||||
useValue: mockOrganizationService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
@@ -66,7 +65,7 @@ describe('ProjectService', () => {
|
||||
.createQueryBuilder()
|
||||
.getManyAndCount.mockResolvedValue([mockProjects, 1]);
|
||||
|
||||
const result = await service.findAll({});
|
||||
const result = await service.findAll({ page: 1, limit: 10 });
|
||||
|
||||
expect(result.data).toBeDefined();
|
||||
expect(result.meta).toBeDefined();
|
||||
@@ -76,11 +75,11 @@ describe('ProjectService', () => {
|
||||
describe('findAllOrganizations', () => {
|
||||
it('should return all organizations', async () => {
|
||||
const mockOrgs = [{ organization_id: 1, name: 'Test Org' }];
|
||||
mockOrganizationRepository.find.mockResolvedValue(mockOrgs);
|
||||
mockOrganizationService.findAllActive.mockResolvedValue(mockOrgs);
|
||||
|
||||
const result = await service.findAllOrganizations();
|
||||
|
||||
expect(mockOrganizationRepository.find).toHaveBeenCalled();
|
||||
expect(mockOrganizationService.findAllActive).toHaveBeenCalled();
|
||||
expect(result).toEqual(mockOrgs);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Repository, Like } from 'typeorm';
|
||||
|
||||
// Entities
|
||||
import { Project } from './entities/project.entity';
|
||||
import { Organization } from './entities/organization.entity';
|
||||
import { OrganizationService } from '../organization/organization.service';
|
||||
|
||||
// DTOs
|
||||
import { CreateProjectDto } from './dto/create-project.dto.js';
|
||||
@@ -23,8 +23,7 @@ export class ProjectService {
|
||||
constructor(
|
||||
@InjectRepository(Project)
|
||||
private projectRepository: Repository<Project>,
|
||||
@InjectRepository(Organization)
|
||||
private organizationRepository: Repository<Organization>
|
||||
private organizationService: OrganizationService
|
||||
) {}
|
||||
|
||||
// --- CRUD Operations ---
|
||||
@@ -123,9 +122,6 @@ export class ProjectService {
|
||||
// --- Organization Helper ---
|
||||
|
||||
async findAllOrganizations() {
|
||||
return this.organizationRepository.find({
|
||||
where: { isActive: true },
|
||||
order: { organizationCode: 'ASC' },
|
||||
});
|
||||
return this.organizationService.findAllActive();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { RfaWorkflowTemplate } from './rfa-workflow-template.entity';
|
||||
import { Organization } from '../../project/entities/organization.entity';
|
||||
import { Organization } from '../../organization/entities/organization.entity';
|
||||
import { Role } from '../../user/entities/role.entity';
|
||||
|
||||
// ✅ 1. สร้าง Enum เพื่อให้ Type Safe
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Organization } from '../../project/entities/organization.entity';
|
||||
import { Organization } from '../../organization/entities/organization.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { RfaRevision } from './rfa-revision.entity';
|
||||
import { RfaActionType } from './rfa-workflow-template-step.entity'; // ✅ Import Enum
|
||||
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
} from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { Role } from './role.entity';
|
||||
import { Organization } from '../../project/entities/organization.entity'; // ปรับ Path ให้ตรงกับ ProjectModule
|
||||
import { Organization } from '../../organization/entities/organization.entity'; // ปรับ Path ให้ตรงกับ ProjectModule
|
||||
import { Project } from '../../project/entities/project.entity'; // ปรับ Path ให้ตรงกับ ProjectModule
|
||||
import { Contract } from '../../project/entities/contract.entity'; // ปรับ Path ให้ตรงกับ ProjectModule
|
||||
import { Contract } from '../../contract/entities/contract.entity'; // ปรับ Path ให้ตรงกับ ProjectModule
|
||||
|
||||
@Entity('user_assignments')
|
||||
export class UserAssignment {
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
OneToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Organization } from '../../project/entities/organization.entity'; // Adjust path as needed
|
||||
import { Organization } from '../../organization/entities/organization.entity'; // Adjust path as needed
|
||||
import { UserAssignment } from './user-assignment.entity';
|
||||
import { UserPreference } from './user-preference.entity';
|
||||
|
||||
|
||||
@@ -93,6 +93,16 @@ export class UserController {
|
||||
return this.userService.findAllPermissions();
|
||||
}
|
||||
|
||||
@Patch('roles/:id/permissions')
|
||||
@RequirePermission('permission.assign')
|
||||
@ApiOperation({ summary: 'Update role permissions' })
|
||||
async updateRolePermissions(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body('permissionIds') permissionIds: number[]
|
||||
) {
|
||||
return this.userService.updateRolePermissions(id, permissionIds);
|
||||
}
|
||||
|
||||
// --- User CRUD (Admin) ---
|
||||
|
||||
@Post()
|
||||
|
||||
@@ -4,6 +4,8 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { UserService } from './user.service';
|
||||
import { User } from './entities/user.entity';
|
||||
import { Role } from './entities/role.entity';
|
||||
import { Permission } from './entities/permission.entity';
|
||||
|
||||
// Mock Repository
|
||||
const mockUserRepository = {
|
||||
@@ -14,6 +16,14 @@ const mockUserRepository = {
|
||||
merge: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
query: jest.fn(),
|
||||
createQueryBuilder: jest.fn(() => ({
|
||||
leftJoinAndSelect: jest.fn().mockReturnThis(),
|
||||
select: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
skip: jest.fn().mockReturnThis(),
|
||||
take: jest.fn().mockReturnThis(),
|
||||
getManyAndCount: jest.fn().mockResolvedValue([[], 0]),
|
||||
})),
|
||||
};
|
||||
|
||||
// Mock Cache Manager
|
||||
@@ -38,6 +48,14 @@ describe('UserService', () => {
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: mockCacheManager,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Role),
|
||||
useValue: mockUserRepository, // Reuse generic mock
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Permission),
|
||||
useValue: mockUserRepository, // Reuse generic mock
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -53,14 +71,26 @@ describe('UserService', () => {
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('should return array of users', async () => {
|
||||
it('should return paginated users', async () => {
|
||||
const mockUsers = [{ user_id: 1, username: 'test' }];
|
||||
mockUserRepository.find.mockResolvedValue(mockUsers);
|
||||
const mockTotal = 1;
|
||||
|
||||
const mockQB = {
|
||||
leftJoinAndSelect: jest.fn().mockReturnThis(),
|
||||
select: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
skip: jest.fn().mockReturnThis(),
|
||||
take: jest.fn().mockReturnThis(),
|
||||
getManyAndCount: jest.fn().mockResolvedValue([mockUsers, mockTotal]),
|
||||
};
|
||||
|
||||
mockUserRepository.createQueryBuilder.mockReturnValue(mockQB);
|
||||
|
||||
const result = await service.findAll();
|
||||
|
||||
expect(result).toEqual(mockUsers);
|
||||
expect(mockUserRepository.find).toHaveBeenCalled();
|
||||
expect(result.data).toEqual(mockUsers);
|
||||
expect(result.total).toEqual(mockTotal);
|
||||
expect(mockUserRepository.createQueryBuilder).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -203,13 +203,39 @@ export class UserService {
|
||||
// --- Roles & Permissions (Helper for Admin/UI) ---
|
||||
|
||||
async findAllRoles(): Promise<Role[]> {
|
||||
return this.roleRepository.find();
|
||||
return this.roleRepository.find({ relations: ['permissions'] });
|
||||
}
|
||||
|
||||
async findAllPermissions(): Promise<Permission[]> {
|
||||
return this.permissionRepository.find();
|
||||
}
|
||||
|
||||
async updateRolePermissions(roleId: number, permissionIds: number[]) {
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: { roleId },
|
||||
relations: ['permissions'],
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
throw new NotFoundException(`Role ID ${roleId} not found`);
|
||||
}
|
||||
|
||||
// Load permissions entities
|
||||
const permissions = [];
|
||||
if (permissionIds.length > 0) {
|
||||
// Note: findByIds is deprecated in newer TypeORM, uses In() instead
|
||||
// but if current version supports it or using a simplified query:
|
||||
const perms = await this.permissionRepository
|
||||
.createQueryBuilder('p')
|
||||
.where('p.permissionId IN (:...ids)', { ids: permissionIds })
|
||||
.getMany();
|
||||
permissions.push(...perms);
|
||||
}
|
||||
|
||||
role.permissions = permissions;
|
||||
return this.roleRepository.save(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper สำหรับล้าง Cache เมื่อมีการเปลี่ยนแปลงสิทธิ์หรือบทบาท
|
||||
*/
|
||||
|
||||
@@ -40,9 +40,9 @@ describe('WorkflowDslParser', () => {
|
||||
const result = await parser.parse(dslJson);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.name).toBe('RFA_APPROVAL');
|
||||
expect(result.version).toBe('1.0.0');
|
||||
expect(result.isActive).toBe(true);
|
||||
expect(result.workflow_code).toBe('RFA_APPROVAL');
|
||||
expect(result.version).toBe(1);
|
||||
expect(result.is_active).toBe(true);
|
||||
expect(mockRepository.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -168,14 +168,14 @@ describe('WorkflowDslParser', () => {
|
||||
it('should retrieve and parse stored DSL', async () => {
|
||||
const storedDefinition = {
|
||||
id: 1,
|
||||
name: 'RFA_APPROVAL',
|
||||
version: '1.0.0',
|
||||
dslContent: JSON.stringify(RFA_WORKFLOW_EXAMPLE),
|
||||
workflow_code: 'RFA_APPROVAL',
|
||||
version: 1,
|
||||
dsl: RFA_WORKFLOW_EXAMPLE,
|
||||
};
|
||||
|
||||
mockRepository.findOne = jest.fn().mockResolvedValue(storedDefinition);
|
||||
|
||||
const result = await parser.getParsedDsl(1);
|
||||
const result = await parser.getParsedDsl('1');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.name).toBe('RFA_APPROVAL');
|
||||
|
||||
Reference in New Issue
Block a user