251208:1625 Frontend: to be complete admin panel, Backend: tobe recheck all task
This commit is contained in:
@@ -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 ต้องมีครบทุกสิทธิ์)
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -29,5 +29,9 @@ export class SearchContractDrawingDto {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
pageSize: number = 20; // มีค่า Default ไม่ต้องใส่ ! หรือ ?
|
||||
limit: number = 20;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
type?: string;
|
||||
}
|
||||
|
||||
@@ -28,5 +28,5 @@ export class SearchShopDrawingDto {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
pageSize: number = 20; // มีค่า Default
|
||||
limit: number = 20; // มีค่า Default
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,5 +29,5 @@ export class SearchRfaDto {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
pageSize: number = 20;
|
||||
limit: number = 20;
|
||||
}
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -30,5 +30,5 @@ export class SearchTransmittalDto {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
pageSize: number = 20;
|
||||
limit: number = 20;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user