333 lines
9.8 KiB
TypeScript
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);
|
|
}
|
|
}
|