251208:1625 Frontend: to be complete admin panel, Backend: tobe recheck all task
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled

This commit is contained in:
admin
2025-12-08 16:25:56 +07:00
parent dcd126d704
commit 863a727756
64 changed files with 5956 additions and 1256 deletions

View File

@@ -34,9 +34,8 @@ export class RbacGuard implements CanActivate {
}
// 3. (สำคัญ) ดึงสิทธิ์ทั้งหมดของ User คนนี้จาก Database
// เราต้องเขียนฟังก์ชัน getUserPermissions ใน UserService เพิ่ม (เดี๋ยวพาทำ)
const userPermissions = await this.userService.getUserPermissions(
user.userId
user.user_id // ✅ FIX: ใช้ user_id ตาม Entity field name
);
// 4. ตรวจสอบว่ามีสิทธิ์ที่ต้องการไหม? (User ต้องมีครบทุกสิทธิ์)

View File

@@ -11,6 +11,7 @@ export interface Response<T> {
statusCode: number;
message: string;
data: T;
meta?: any;
}
@Injectable()
@@ -19,14 +20,29 @@ export class TransformInterceptor<T>
{
intercept(
context: ExecutionContext,
next: CallHandler,
next: CallHandler
): Observable<Response<T>> {
return next.handle().pipe(
map((data) => ({
statusCode: context.switchToHttp().getResponse().statusCode,
message: data?.message || 'Success', // ถ้า data มี message ให้ใช้ ถ้าไม่มีใช้ 'Success'
data: data?.result || data, // รองรับกรณีส่ง object ที่มี key result มา
})),
map((data: any) => {
const response = context.switchToHttp().getResponse();
// Handle Pagination Response (Standardize)
// ถ้า data มี structure { data: [], meta: {} } ให้ unzip ออกมา
if (data && data.data && data.meta) {
return {
statusCode: response.statusCode,
message: data.message || 'Success',
data: data.data,
meta: data.meta,
};
}
return {
statusCode: response.statusCode,
message: data?.message || 'Success',
data: data?.result || data,
};
})
);
}
}

View File

@@ -86,7 +86,7 @@ async function bootstrap() {
// 🚀 7. Start Server
const port = configService.get<number>('PORT') || 3001;
await app.listen(port);
await app.listen(port, '0.0.0.0');
logger.log(`Application is running on: ${await app.getUrl()}/api`);
logger.log(`Swagger UI is available at: ${await app.getUrl()}/docs`);

View File

@@ -5,28 +5,37 @@ import {
ManyToMany,
JoinTable,
} from 'typeorm';
import { BaseEntity } from '../../../common/entities/base.entity';
@Entity('permissions')
export class Permission {
@PrimaryGeneratedColumn()
export class Permission extends BaseEntity {
@PrimaryGeneratedColumn({ name: 'permission_id' })
id!: number;
@Column({ name: 'permission_code', length: 50, unique: true })
permissionCode!: string;
@Column({ name: 'permission_name', length: 100, unique: true })
permissionName!: string;
@Column({ name: 'description', type: 'text', nullable: true })
description!: string;
@Column({ name: 'resource', length: 50 })
resource!: string;
@Column({ name: 'module', length: 50, nullable: true })
module?: string;
@Column({ name: 'action', length: 50 })
action!: string;
@Column({
name: 'scope_level',
type: 'enum',
enum: ['GLOBAL', 'ORG', 'PROJECT'],
nullable: true,
})
scopeLevel?: 'GLOBAL' | 'ORG' | 'PROJECT';
@Column({ name: 'is_active', default: true, type: 'tinyint' })
isActive!: boolean;
}
@Entity('roles')
export class Role {
@PrimaryGeneratedColumn()
export class Role extends BaseEntity {
@PrimaryGeneratedColumn({ name: 'role_id' })
id!: number;
@Column({ name: 'role_name', length: 50, unique: true })
@@ -35,6 +44,16 @@ export class Role {
@Column({ name: 'description', type: 'text', nullable: true })
description!: string;
@Column({
type: 'enum',
enum: ['Global', 'Organization', 'Project', 'Contract'],
default: 'Global',
})
scope!: 'Global' | 'Organization' | 'Project' | 'Contract';
@Column({ name: 'is_system', default: false })
isSystem!: boolean;
@ManyToMany(() => Permission)
@JoinTable({
name: 'role_permissions',

View File

@@ -21,4 +21,15 @@ export class SearchCorrespondenceDto {
@Type(() => Number)
@IsInt()
statusId?: number;
// Pagination
@IsOptional()
@Type(() => Number)
@IsInt()
page?: number;
@IsOptional()
@Type(() => Number)
@IsInt()
limit?: number;
}

View File

@@ -29,15 +29,17 @@ import { User } from '../user/entities/user.entity';
@Controller('drawings/contract')
export class ContractDrawingController {
constructor(
private readonly contractDrawingService: ContractDrawingService,
private readonly contractDrawingService: ContractDrawingService
) {}
// Force rebuild for DTO changes
@Post()
@ApiOperation({ summary: 'Create new Contract Drawing' })
@RequirePermission('drawing.create') // สิทธิ์ ID 39: สร้าง/แก้ไขข้อมูลแบบ
create(
@Body() createDto: CreateContractDrawingDto,
@CurrentUser() user: User,
@CurrentUser() user: User
) {
return this.contractDrawingService.create(createDto, user);
}
@@ -62,7 +64,7 @@ export class ContractDrawingController {
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateDto: UpdateContractDrawingDto,
@CurrentUser() user: User,
@CurrentUser() user: User
) {
return this.contractDrawingService.update(id, updateDto, user);
}

View File

@@ -31,7 +31,7 @@ export class ContractDrawingService {
@InjectRepository(Attachment)
private attachmentRepo: Repository<Attachment>,
private fileStorageService: FileStorageService,
private dataSource: DataSource,
private dataSource: DataSource
) {}
/**
@@ -51,7 +51,7 @@ export class ContractDrawingService {
if (exists) {
throw new ConflictException(
`Contract Drawing No. "${createDto.contractDrawingNo}" already exists in this project.`,
`Contract Drawing No. "${createDto.contractDrawingNo}" already exists in this project.`
);
}
@@ -85,7 +85,7 @@ export class ContractDrawingService {
if (createDto.attachmentIds?.length) {
// ✅ FIX TS2345: แปลง number[] เป็น string[] ก่อนส่ง
await this.fileStorageService.commit(
createDto.attachmentIds.map(String),
createDto.attachmentIds.map(String)
);
}
@@ -95,7 +95,7 @@ export class ContractDrawingService {
await queryRunner.rollbackTransaction();
// ✅ FIX TS18046: Cast err เป็น Error
this.logger.error(
`Failed to create contract drawing: ${(err as Error).message}`,
`Failed to create contract drawing: ${(err as Error).message}`
);
throw err;
} finally {
@@ -114,7 +114,7 @@ export class ContractDrawingService {
subCategoryId,
search,
page = 1,
pageSize = 20,
limit = 20,
} = searchDto;
const query = this.drawingRepo
@@ -143,14 +143,14 @@ export class ContractDrawingService {
qb.where('drawing.contractDrawingNo LIKE :search', {
search: `%${search}%`,
}).orWhere('drawing.title LIKE :search', { search: `%${search}%` });
}),
})
);
}
query.orderBy('drawing.contractDrawingNo', 'ASC');
const skip = (page - 1) * pageSize;
query.skip(skip).take(pageSize);
const skip = (page - 1) * limit;
query.skip(skip).take(limit);
const [items, total] = await query.getManyAndCount();
@@ -159,8 +159,8 @@ export class ContractDrawingService {
meta: {
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
limit,
totalPages: Math.ceil(total / limit),
},
};
}
@@ -213,7 +213,7 @@ export class ContractDrawingService {
// Commit new files
// ✅ FIX TS2345: แปลง number[] เป็น string[] ก่อนส่ง
await this.fileStorageService.commit(
updateDto.attachmentIds.map(String),
updateDto.attachmentIds.map(String)
);
}
@@ -225,7 +225,7 @@ export class ContractDrawingService {
await queryRunner.rollbackTransaction();
// ✅ FIX TS18046: Cast err เป็น Error (Optional: Added logger here too for consistency)
this.logger.error(
`Failed to update contract drawing: ${(err as Error).message}`,
`Failed to update contract drawing: ${(err as Error).message}`
);
throw err;
} finally {

View File

@@ -29,5 +29,9 @@ export class SearchContractDrawingDto {
@IsOptional()
@IsInt()
@Type(() => Number)
pageSize: number = 20; // มีค่า Default ไม่ต้องใส่ ! หรือ ?
limit: number = 20;
@IsOptional()
@IsString()
type?: string;
}

View File

@@ -28,5 +28,5 @@ export class SearchShopDrawingDto {
@IsOptional()
@IsInt()
@Type(() => Number)
pageSize: number = 20; // มีค่า Default
limit: number = 20; // มีค่า Default
}

View File

@@ -208,10 +208,10 @@ export class ShopDrawingService {
const {
projectId,
mainCategoryId,
subCategoryId,
// subCategoryId, // Unused
search,
page = 1,
pageSize = 20,
limit = 20,
} = searchDto;
const query = this.shopDrawingRepo
@@ -225,10 +225,6 @@ export class ShopDrawingService {
query.andWhere('sd.mainCategoryId = :mainCategoryId', { mainCategoryId });
}
if (subCategoryId) {
query.andWhere('sd.subCategoryId = :subCategoryId', { subCategoryId });
}
if (search) {
query.andWhere(
new Brackets((qb) => {
@@ -241,8 +237,8 @@ export class ShopDrawingService {
query.orderBy('sd.updatedAt', 'DESC');
const skip = (page - 1) * pageSize;
query.skip(skip).take(pageSize);
const skip = (page - 1) * limit;
query.skip(skip).take(limit);
const [items, total] = await query.getManyAndCount();
@@ -262,8 +258,8 @@ export class ShopDrawingService {
meta: {
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
limit,
totalPages: Math.ceil(total / limit),
},
};
}

View File

@@ -29,5 +29,5 @@ export class SearchRfaDto {
@IsOptional()
@IsInt()
@Type(() => Number)
pageSize: number = 20;
limit: number = 20;
}

View File

@@ -6,6 +6,7 @@ import {
Param,
ParseIntPipe,
Post,
Query,
UseGuards,
} from '@nestjs/common';
import {
@@ -79,6 +80,14 @@ export class RfaController {
return this.rfaService.processAction(id, actionDto, user);
}
@Get()
@ApiOperation({ summary: 'List all RFAs with pagination' })
@ApiResponse({ status: 200, description: 'List of RFAs' })
@RequirePermission('document.view')
findAll(@Query() query: any) {
return this.rfaService.findAll(query);
}
@Get(':id')
@ApiOperation({ summary: 'Get RFA details with revisions and items' })
@ApiParam({ name: 'id', description: 'RFA ID' })

View File

@@ -230,6 +230,52 @@ export class RfaService {
// ... (ส่วน findOne, submit, processAction คงเดิมจากไฟล์ที่แนบมา แค่ปรับปรุงเล็กน้อยตาม Context) ...
async findAll(query: any) {
const { page = 1, limit = 20, projectId, status, search } = query;
const skip = (page - 1) * limit;
// Fix: Start query from Rfa entity instead of Correspondence,
// because Correspondence has no 'rfas' relation.
// [Force Rebuild]
const queryBuilder = this.rfaRepo
.createQueryBuilder('rfa')
.leftJoinAndSelect('rfa.revisions', 'rev')
.leftJoinAndSelect('rev.correspondence', 'corr')
.leftJoinAndSelect('rev.statusCode', 'status')
.where('rev.isCurrent = :isCurrent', { isCurrent: true });
if (projectId) {
queryBuilder.andWhere('corr.projectId = :projectId', { projectId });
}
if (status) {
queryBuilder.andWhere('status.statusCode = :status', { status });
}
if (search) {
queryBuilder.andWhere(
'(corr.correspondenceNumber LIKE :search OR rev.title LIKE :search)',
{ search: `%${search}%` }
);
}
const [items, total] = await queryBuilder
.orderBy('corr.createdAt', 'DESC')
.skip(skip)
.take(limit)
.getManyAndCount();
return {
data: items,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
async findOne(id: number) {
const rfa = await this.rfaRepo.findOne({
where: { id },

View File

@@ -30,5 +30,5 @@ export class SearchTransmittalDto {
@IsOptional()
@IsInt()
@Type(() => Number)
pageSize: number = 20;
limit: number = 20;
}