Files
lcbp3/backend/src/modules/drawing/shop-drawing.service.ts
T
admin 1d3479770b
Build and Deploy / deploy (push) Has been cancelled
260320:1131 Refactor Overrall #01
2026-03-20 11:31:27 +07:00

333 lines
9.8 KiB
TypeScript

import {
Injectable,
NotFoundException,
ConflictException,
Logger,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, In } from 'typeorm';
// Entities
import { ShopDrawing } from './entities/shop-drawing.entity';
import { ShopDrawingRevision } from './entities/shop-drawing-revision.entity';
import { ContractDrawing } from './entities/contract-drawing.entity';
import { Attachment } from '../../common/file-storage/entities/attachment.entity';
import { User } from '../user/entities/user.entity';
// DTOs
import { CreateShopDrawingDto } from './dto/create-shop-drawing.dto';
import { CreateShopDrawingRevisionDto } from './dto/create-shop-drawing-revision.dto';
import { SearchShopDrawingDto } from './dto/search-shop-drawing.dto';
// Services
import { FileStorageService } from '../../common/file-storage/file-storage.service';
import { UuidResolverService } from '../../common/services/uuid-resolver.service';
@Injectable()
export class ShopDrawingService {
private readonly logger = new Logger(ShopDrawingService.name);
constructor(
@InjectRepository(ShopDrawing)
private shopDrawingRepo: Repository<ShopDrawing>,
@InjectRepository(ShopDrawingRevision)
private revisionRepo: Repository<ShopDrawingRevision>,
@InjectRepository(ContractDrawing)
private contractDrawingRepo: Repository<ContractDrawing>,
@InjectRepository(Attachment)
private attachmentRepo: Repository<Attachment>,
private fileStorageService: FileStorageService,
private dataSource: DataSource,
private uuidResolver: UuidResolverService
) {}
/**
* สร้าง Shop Drawing ใหม่ พร้อม Revision แรก (Rev 0)
*/
async create(createDto: CreateShopDrawingDto, user: User) {
// 1. Check Duplicate
const exists = await this.shopDrawingRepo.findOne({
where: { drawingNumber: createDto.drawingNumber },
});
if (exists) {
throw new ConflictException(
`Drawing number "${createDto.drawingNumber}" already exists.`
);
}
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 2. Prepare Relations
let contractDrawings: ContractDrawing[] = [];
if (createDto.contractDrawingIds?.length) {
contractDrawings = await this.contractDrawingRepo.findBy({
id: In(createDto.contractDrawingIds),
});
}
let attachments: Attachment[] = [];
if (createDto.attachmentIds?.length) {
attachments = await this.attachmentRepo.findBy({
id: In(createDto.attachmentIds),
});
}
// ADR-019: Resolve UUID→INT
const internalProjectId = await this.uuidResolver.resolveProjectId(
createDto.projectId
);
// 3. Create Master Shop Drawing
const shopDrawing = queryRunner.manager.create(ShopDrawing, {
projectId: internalProjectId,
drawingNumber: createDto.drawingNumber,
mainCategoryId: createDto.mainCategoryId,
subCategoryId: createDto.subCategoryId,
updatedBy: user.user_id,
});
const savedShopDrawing = await queryRunner.manager.save(shopDrawing);
// 4. Create First Revision (Rev 0)
const revision = queryRunner.manager.create(ShopDrawingRevision, {
shopDrawingId: savedShopDrawing.id,
revisionNumber: 0,
revisionLabel: createDto.revisionLabel || '0',
title: createDto.title, // Add title to revision
revisionDate: createDto.revisionDate
? new Date(createDto.revisionDate)
: new Date(),
description: createDto.description,
contractDrawings: contractDrawings,
attachments: attachments,
});
await queryRunner.manager.save(revision);
// 5. Commit Files
if (createDto.attachmentIds?.length) {
await this.fileStorageService.commit(
createDto.attachmentIds.map(String),
{ issueDate: revision.revisionDate, documentType: 'ShopDrawing' }
);
}
await queryRunner.commitTransaction();
// ✅ FIX: Return ข้อมูลของ ShopDrawing และ Revision (ไม่ใช่ savedCorr หรือ docNumber)
return {
...savedShopDrawing,
currentRevision: revision,
};
} catch (err) {
await queryRunner.rollbackTransaction();
this.logger.error(
`Failed to create shop drawing: ${(err as Error).message}`
);
throw err;
} finally {
await queryRunner.release();
}
}
/**
* เพิ่ม Revision ใหม่ (Add Revision)
*/
async createRevision(
shopDrawingId: number,
createDto: CreateShopDrawingRevisionDto
) {
const shopDrawing = await this.shopDrawingRepo.findOneBy({
id: shopDrawingId,
});
if (!shopDrawing) {
throw new NotFoundException('Shop Drawing not found');
}
const exists = await this.revisionRepo.findOne({
where: { shopDrawingId, revisionLabel: createDto.revisionLabel },
});
if (exists) {
throw new ConflictException(
`Revision label "${createDto.revisionLabel}" already exists for this drawing.`
);
}
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
let contractDrawings: ContractDrawing[] = [];
if (createDto.contractDrawingIds?.length) {
contractDrawings = await this.contractDrawingRepo.findBy({
id: In(createDto.contractDrawingIds),
});
}
let attachments: Attachment[] = [];
if (createDto.attachmentIds?.length) {
attachments = await this.attachmentRepo.findBy({
id: In(createDto.attachmentIds),
});
}
const latestRev = await this.revisionRepo.findOne({
where: { shopDrawingId },
order: { revisionNumber: 'DESC' },
});
const nextRevNum = (latestRev?.revisionNumber ?? -1) + 1;
const revision = queryRunner.manager.create(ShopDrawingRevision, {
shopDrawingId,
revisionNumber: nextRevNum,
revisionLabel: createDto.revisionLabel,
title: createDto.title, // Add title from DTO
legacyDrawingNumber: createDto.legacyDrawingNumber, // Add legacy number
revisionDate: createDto.revisionDate
? new Date(createDto.revisionDate)
: new Date(),
description: createDto.description,
contractDrawings: contractDrawings,
attachments: attachments,
});
await queryRunner.manager.save(revision);
if (createDto.attachmentIds?.length) {
await this.fileStorageService.commit(
createDto.attachmentIds.map(String),
{ issueDate: revision.revisionDate, documentType: 'ShopDrawing' }
);
}
await queryRunner.commitTransaction();
return revision;
} catch (err) {
await queryRunner.rollbackTransaction();
this.logger.error(`Failed to create revision: ${(err as Error).message}`);
throw err;
} finally {
await queryRunner.release();
}
}
/**
* ค้นหา Shop Drawing
*/
async findAll(searchDto: SearchShopDrawingDto) {
const {
projectId,
mainCategoryId,
// subCategoryId, // Unused
search,
page = 1,
limit = 20,
} = searchDto;
const query = this.shopDrawingRepo
.createQueryBuilder('sd')
.leftJoinAndSelect('sd.mainCategory', 'mainCat')
.leftJoinAndSelect('sd.subCategory', 'subCat')
.leftJoinAndSelect('sd.revisions', 'rev')
.where('sd.projectId = :projectId', { projectId });
if (mainCategoryId) {
query.andWhere('sd.mainCategoryId = :mainCategoryId', { mainCategoryId });
}
if (search) {
query.andWhere('sd.drawingNumber LIKE :search', {
search: `%${search}%`,
});
}
query.orderBy('sd.updatedAt', 'DESC');
const skip = (page - 1) * limit;
query.skip(skip).take(limit);
const [items, total] = await query.getManyAndCount();
// Transform Data
const transformedItems = items.map((item) => {
item.revisions.sort((a, b) => b.revisionNumber - a.revisionNumber);
const currentRevision = item.revisions[0];
return {
...item,
currentRevision,
revisions: undefined,
};
});
return {
data: transformedItems,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
/**
* ดูรายละเอียด Shop Drawing
*/
async findOne(id: number) {
const shopDrawing = await this.shopDrawingRepo.findOne({
where: { id },
relations: [
'mainCategory',
'subCategory',
'revisions',
'revisions.attachments',
'revisions.contractDrawings',
],
order: {
revisions: { revisionNumber: 'DESC' },
},
});
if (!shopDrawing) {
throw new NotFoundException(`Shop Drawing ID ${id} not found`);
}
return shopDrawing;
}
async findOneByUuid(uuid: string) {
const shopDrawing = await this.shopDrawingRepo.findOne({
where: { uuid },
relations: [
'mainCategory',
'subCategory',
'revisions',
'revisions.attachments',
'revisions.contractDrawings',
],
order: {
revisions: { revisionNumber: 'DESC' },
},
});
if (!shopDrawing) {
throw new NotFoundException(`Shop Drawing UUID ${uuid} not found`);
}
return shopDrawing;
}
/**
* ลบ Shop Drawing
*/
async remove(id: number, user: User) {
const shopDrawing = await this.findOne(id);
shopDrawing.updatedBy = user.user_id;
await this.shopDrawingRepo.save(shopDrawing);
return this.shopDrawingRepo.softRemove(shopDrawing);
}
}