This commit is contained in:
@@ -101,6 +101,7 @@ import { MigrationModule } from './modules/migration/migration.module';
|
||||
username: configService.get<string>('DB_USERNAME'),
|
||||
password: configService.get<string>('DB_PASSWORD'),
|
||||
database: configService.get<string>('DB_DATABASE'),
|
||||
charset: 'utf8mb4',
|
||||
autoLoadEntities: true,
|
||||
synchronize: false, // Production Ready: false
|
||||
}),
|
||||
|
||||
@@ -45,7 +45,9 @@ export class FileStorageService {
|
||||
*/
|
||||
async upload(file: Express.Multer.File, userId: number): Promise<Attachment> {
|
||||
const tempId = uuidv4();
|
||||
const fileExt = path.extname(file.originalname);
|
||||
// Fix: แปลงชื่อไฟล์จาก Latin1 → UTF-8 (Multer/busboy decodes as Latin1 by default)
|
||||
const originalFilename = this.fixMulterFilename(file.originalname);
|
||||
const fileExt = path.extname(originalFilename);
|
||||
const storedFilename = `${uuidv4()}${fileExt}`;
|
||||
const tempPath = path.join(this.tempDir, storedFilename);
|
||||
|
||||
@@ -62,7 +64,7 @@ export class FileStorageService {
|
||||
|
||||
// 3. สร้าง Record ใน Database
|
||||
const attachment = this.attachmentRepository.create({
|
||||
originalFilename: file.originalname,
|
||||
originalFilename,
|
||||
storedFilename: storedFilename,
|
||||
filePath: tempPath, // เก็บ path ปัจจุบันไปก่อน
|
||||
mimeType: file.mimetype,
|
||||
@@ -197,6 +199,22 @@ export class FileStorageService {
|
||||
return { stream, attachment };
|
||||
}
|
||||
|
||||
/**
|
||||
* แก้ปัญหา Multer/busboy ถอดรหัสชื่อไฟล์เป็น Latin1 แทน UTF-8
|
||||
* ทำให้ภาษาไทยกลายเป็น mojibake (เช่น ผรม → 脿赂聹脿赂拢脿赂隆)
|
||||
* วิธีแก้: แปลง latin1 bytes กลับเป็น UTF-8
|
||||
*/
|
||||
private fixMulterFilename(originalname: string): string {
|
||||
try {
|
||||
const decoded = Buffer.from(originalname, 'latin1').toString('utf8');
|
||||
// ตรวจสอบว่า decoded string มี valid UTF-8 characters
|
||||
// ถ้า originalname เป็น ASCII อยู่แล้ว ผลลัพธ์จะเหมือนเดิม
|
||||
return decoded;
|
||||
} catch {
|
||||
return originalname;
|
||||
}
|
||||
}
|
||||
|
||||
private calculateChecksum(buffer: Buffer): string {
|
||||
return crypto.createHash('sha256').update(buffer).digest('hex');
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export const databaseConfig: TypeOrmModuleOptions = {
|
||||
username: process.env.DB_USERNAME || 'root',
|
||||
password: process.env.DB_PASSWORD || 'Center#2025',
|
||||
database: process.env.DB_DATABASE || 'lcbp3_dev',
|
||||
charset: 'utf8mb4',
|
||||
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
|
||||
migrations: [__dirname + '/../database/migrations/*{.ts,.js}'],
|
||||
synchronize: false,
|
||||
|
||||
@@ -35,13 +35,17 @@ import { RequirePermission } from '../../common/decorators/require-permission.de
|
||||
import { Audit } from '../../common/decorators/audit.decorator';
|
||||
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
import { ProjectService } from '../project/project.service';
|
||||
|
||||
@ApiTags('Drawings - AS Built')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
@Controller('drawings/asbuilt')
|
||||
export class AsBuiltDrawingController {
|
||||
constructor(private readonly asBuiltDrawingService: AsBuiltDrawingService) {}
|
||||
constructor(
|
||||
private readonly asBuiltDrawingService: AsBuiltDrawingService,
|
||||
private readonly projectService: ProjectService
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create new AS Built Drawing' })
|
||||
@@ -74,6 +78,10 @@ export class AsBuiltDrawingController {
|
||||
@ApiResponse({ status: 200, description: 'List of AS Built Drawings' })
|
||||
@RequirePermission('drawing.view')
|
||||
async findAll(@Query() searchDto: SearchAsBuiltDrawingDto) {
|
||||
const project = await this.projectService.findOneByUuid(
|
||||
searchDto.projectUuid
|
||||
);
|
||||
searchDto.projectId = project.id;
|
||||
return this.asBuiltDrawingService.findAll(searchDto);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { RequirePermission } from '../../common/decorators/require-permission.de
|
||||
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
import { ProjectService } from '../project/project.service';
|
||||
|
||||
@ApiTags('Contract Drawings')
|
||||
@ApiBearerAuth()
|
||||
@@ -29,7 +30,8 @@ import { User } from '../user/entities/user.entity';
|
||||
@Controller('drawings/contract')
|
||||
export class ContractDrawingController {
|
||||
constructor(
|
||||
private readonly contractDrawingService: ContractDrawingService
|
||||
private readonly contractDrawingService: ContractDrawingService,
|
||||
private readonly projectService: ProjectService
|
||||
) {}
|
||||
|
||||
// Force rebuild for DTO changes
|
||||
@@ -47,7 +49,11 @@ export class ContractDrawingController {
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Search Contract Drawings' })
|
||||
@RequirePermission('document.view') // สิทธิ์ ID 31: ดูเอกสารทั่วไป
|
||||
findAll(@Query() searchDto: SearchContractDrawingDto) {
|
||||
async findAll(@Query() searchDto: SearchContractDrawingDto) {
|
||||
const project = await this.projectService.findOneByUuid(
|
||||
searchDto.projectUuid
|
||||
);
|
||||
searchDto.projectId = project.id;
|
||||
return this.contractDrawingService.findAll(searchDto);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import { DrawingMasterDataController } from './drawing-master-data.controller';
|
||||
// Modules
|
||||
import { FileStorageModule } from '../../common/file-storage/file-storage.module';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { ProjectModule } from '../project/project.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -62,6 +63,7 @@ import { UserModule } from '../user/user.module';
|
||||
]),
|
||||
FileStorageModule,
|
||||
UserModule,
|
||||
ProjectModule,
|
||||
],
|
||||
providers: [
|
||||
ShopDrawingService,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsNumber, IsOptional, IsString, Min } from 'class-validator';
|
||||
import { IsNumber, IsOptional, IsString, IsUUID, Min } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
@@ -6,10 +6,15 @@ import { Type } from 'class-transformer';
|
||||
* DTO for searching/filtering AS Built Drawings
|
||||
*/
|
||||
export class SearchAsBuiltDrawingDto {
|
||||
@ApiProperty({ description: 'Project ID' })
|
||||
@ApiProperty({ description: 'Project UUID' })
|
||||
@IsUUID()
|
||||
projectUuid!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Project ID (resolved internally)' })
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
projectId!: number;
|
||||
@IsOptional()
|
||||
projectId?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Filter by Main Category ID' })
|
||||
@Type(() => Number)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { IsInt, IsOptional, IsString, IsNotEmpty } from 'class-validator';
|
||||
import { IsInt, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class SearchContractDrawingDto {
|
||||
@IsUUID()
|
||||
projectUuid!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
@IsNotEmpty()
|
||||
projectId!: number; // จำเป็น: ใส่ !
|
||||
projectId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { IsInt, IsOptional, IsString } from 'class-validator';
|
||||
import { IsInt, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class SearchShopDrawingDto {
|
||||
@IsUUID()
|
||||
projectUuid!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
projectId!: number; // จำเป็น: ใส่ !
|
||||
projectId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
|
||||
@@ -21,13 +21,17 @@ import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
import { Audit } from '../../common/decorators/audit.decorator'; // Import
|
||||
import { ProjectService } from '../project/project.service';
|
||||
|
||||
@ApiTags('Shop Drawings')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
@Controller('drawings/shop')
|
||||
export class ShopDrawingController {
|
||||
constructor(private readonly shopDrawingService: ShopDrawingService) {}
|
||||
constructor(
|
||||
private readonly shopDrawingService: ShopDrawingService,
|
||||
private readonly projectService: ProjectService
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create new Shop Drawing with initial revision' })
|
||||
@@ -40,7 +44,11 @@ export class ShopDrawingController {
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Search Shop Drawings' })
|
||||
@RequirePermission('drawing.view')
|
||||
findAll(@Query() searchDto: SearchShopDrawingDto) {
|
||||
async findAll(@Query() searchDto: SearchShopDrawingDto) {
|
||||
const project = await this.projectService.findOneByUuid(
|
||||
searchDto.projectUuid
|
||||
);
|
||||
searchDto.projectId = project.id;
|
||||
return this.shopDrawingService.findAll(searchDto);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user