# Task: Drawing Module (Shop & Contract Drawings)

**Status:** In Progress
**Priority:** P2 (Medium - Supporting Module)
**Estimated Effort:** 6-8 days
**Dependencies:** TASK-BE-001, TASK-BE-002, TASK-BE-003, TASK-BE-004
**Owner:** Backend Team

---

## 📋 Overview

สร้าง Drawing Module สำหรับจัดการ Shop Drawings (แบบก่อสร้าง) และ Contract Drawings (แบบคู่สัญญา)

---

## 🎯 Objectives

- ✅ Contract Drawing Management
- ✅ Shop Drawing with Master-Revision Pattern
- ✅ Drawing Categories
- ✅ Drawing References/Links
- ✅ Version Control
- ✅ Search & Filter

---

## 📝 Acceptance Criteria

1. **Contract Drawings:**
   - ✅ Upload contract drawings
   - ✅ Categorize by discipline
   - ✅ Link to project/contract
   - ✅ Search by drawing number

2. **Shop Drawings:**
   - ✅ Create shop drawing with auto-number
   - ✅ Create revisions
   - ✅ Link to contract drawings
   - ✅ Track submission status

3. **Drawing Management:**
   - ✅ Version tracking
   - ✅ Drawing categories
   - ✅ Cross-references
   - ✅ Attachment management

---

## 🛠️ Implementation Steps

### 1. Entities

```typescript
// File: backend/src/modules/drawing/entities/contract-drawing.entity.ts
@Entity('contract_drawings')
export class ContractDrawing {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 100 })
  drawing_number: string;

  @Column({ length: 500 })
  drawing_title: string;

  @Column()
  contract_id: number;

  @Column({ nullable: true })
  discipline_id: number;

  @Column({ nullable: true })
  category_id: number;

  @Column({ type: 'date', nullable: true })
  issue_date: Date;

  @Column({ length: 50, nullable: true })
  revision: string;

  @Column({ nullable: true })
  attachment_id: number; // PDF file

  @CreateDateColumn()
  created_at: Date;

  @DeleteDateColumn()
  deleted_at: Date;

  @ManyToOne(() => Contract)
  @JoinColumn({ name: 'contract_id' })
  contract: Contract;

  @ManyToOne(() => Discipline)
  @JoinColumn({ name: 'discipline_id' })
  discipline: Discipline;

  @ManyToOne(() => Attachment)
  @JoinColumn({ name: 'attachment_id' })
  attachment: Attachment;

  @Index(['contract_id', 'drawing_number'], { unique: true })
  _contractDrawingIndex: void;
}
```

```typescript
// File: backend/src/modules/drawing/entities/shop-drawing.entity.ts
@Entity('shop_drawings')
export class ShopDrawing extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 100, unique: true })
  drawing_number: string;

  @Column({ length: 500 })
  drawing_title: string;

  @Column()
  project_id: number;

  @Column()
  contractor_organization_id: number;

  @Column({ nullable: true })
  discipline_id: number;

  @Column({ nullable: true })
  category_id: number;

  @Column({ default: 'draft' })
  status: string;

  @Column()
  created_by_user_id: number;

  @DeleteDateColumn()
  deleted_at: Date;

  @ManyToOne(() => Project)
  @JoinColumn({ name: 'project_id' })
  project: Project;

  @OneToMany(() => ShopDrawingRevision, (rev) => rev.shopDrawing)
  revisions: ShopDrawingRevision[];

  @ManyToMany(() => ContractDrawing)
  @JoinTable({
    name: 'shop_drawing_references',
    joinColumn: { name: 'shop_drawing_id' },
    inverseJoinColumn: { name: 'contract_drawing_id' },
  })
  contractDrawingReferences: ContractDrawing[];
}
```

```typescript
// File: backend/src/modules/drawing/entities/shop-drawing-revision.entity.ts
@Entity('shop_drawing_revisions')
export class ShopDrawingRevision {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  shop_drawing_id: number;

  @Column({ default: 1 })
  revision_number: number;

  @Column({ type: 'text', nullable: true })
  description: string;

  @Column({ type: 'json', nullable: true })
  details: any;

  @Column({ type: 'date', nullable: true })
  submission_date: Date;

  @Column()
  created_by_user_id: number;

  @CreateDateColumn()
  created_at: Date;

  @ManyToOne(() => ShopDrawing, (sd) => sd.revisions)
  @JoinColumn({ name: 'shop_drawing_id' })
  shopDrawing: ShopDrawing;

  @ManyToMany(() => Attachment)
  @JoinTable({
    name: 'shop_drawing_attachments',
    joinColumn: { name: 'shop_drawing_revision_id' },
    inverseJoinColumn: { name: 'attachment_id' },
  })
  attachments: Attachment[];
}
```

### 2. Service

```typescript
// File: backend/src/modules/drawing/drawing.service.ts
@Injectable()
export class DrawingService {
  constructor(
    @InjectRepository(ContractDrawing)
    private contractDrawingRepo: Repository<ContractDrawing>,
    @InjectRepository(ShopDrawing)
    private shopDrawingRepo: Repository<ShopDrawing>,
    @InjectRepository(ShopDrawingRevision)
    private shopRevisionRepo: Repository<ShopDrawingRevision>,
    private fileStorage: FileStorageService,
    private docNumbering: DocumentNumberingService,
    private dataSource: DataSource
  ) {}

  // Contract Drawing Methods
  async createContractDrawing(dto: CreateContractDrawingDto, userId: number): Promise<ContractDrawing> {
    return this.dataSource.transaction(async (manager) => {
      // Commit drawing file
      const attachments = await this.fileStorage.commitFiles([dto.temp_file_id], null, 'contract_drawing', manager);

      const contractDrawing = manager.create(ContractDrawing, {
        drawing_number: dto.drawing_number,
        drawing_title: dto.drawing_title,
        contract_id: dto.contract_id,
        discipline_id: dto.discipline_id,
        category_id: dto.category_id,
        issue_date: dto.issue_date,
        revision: dto.revision || 'A',
        attachment_id: attachments[0].id,
      });

      return manager.save(contractDrawing);
    });
  }

  async findAllContractDrawings(query: SearchDrawingDto): Promise<PaginatedResult<ContractDrawing>> {
    const queryBuilder = this.contractDrawingRepo
      .createQueryBuilder('cd')
      .leftJoinAndSelect('cd.contract', 'contract')
      .leftJoinAndSelect('cd.discipline', 'discipline')
      .leftJoinAndSelect('cd.attachment', 'attachment')
      .where('cd.deleted_at IS NULL');

    if (query.contract_id) {
      queryBuilder.andWhere('cd.contract_id = :contractId', {
        contractId: query.contract_id,
      });
    }

    if (query.discipline_id) {
      queryBuilder.andWhere('cd.discipline_id = :disciplineId', {
        disciplineId: query.discipline_id,
      });
    }

    if (query.search) {
      queryBuilder.andWhere('(cd.drawing_number LIKE :search OR cd.drawing_title LIKE :search)', {
        search: `%${query.search}%`,
      });
    }

    const page = query.page || 1;
    const limit = query.limit || 20;
    const skip = (page - 1) * limit;

    const [items, total] = await queryBuilder
      .orderBy('cd.drawing_number', 'ASC')
      .skip(skip)
      .take(limit)
      .getManyAndCount();

    return { items, total, page, limit, totalPages: Math.ceil(total / limit) };
  }

  // Shop Drawing Methods
  async createShopDrawing(dto: CreateShopDrawingDto, userId: number): Promise<ShopDrawing> {
    return this.dataSource.transaction(async (manager) => {
      // Generate drawing number
      const drawingNumber = await this.docNumbering.generateNextNumber({
        projectId: dto.project_id,
        organizationId: dto.contractor_organization_id,
        typeId: 999, // Shop Drawing type
        disciplineId: dto.discipline_id,
      });

      // Create shop drawing master
      const shopDrawing = manager.create(ShopDrawing, {
        drawing_number: drawingNumber,
        drawing_title: dto.drawing_title,
        project_id: dto.project_id,
        contractor_organization_id: dto.contractor_organization_id,
        discipline_id: dto.discipline_id,
        category_id: dto.category_id,
        status: 'draft',
        created_by_user_id: userId,
      });
      await manager.save(shopDrawing);

      // Create initial revision
      const revision = manager.create(ShopDrawingRevision, {
        shop_drawing_id: shopDrawing.id,
        revision_number: 1,
        description: dto.description,
        details: dto.details,
        submission_date: dto.submission_date,
        created_by_user_id: userId,
      });
      await manager.save(revision);

      // Commit files
      if (dto.temp_file_ids?.length > 0) {
        const attachments = await this.fileStorage.commitFiles(
          dto.temp_file_ids,
          shopDrawing.id,
          'shop_drawing',
          manager
        );
        revision.attachments = attachments;
        await manager.save(revision);
      }

      // Link contract drawing references
      if (dto.contract_drawing_ids?.length > 0) {
        const contractDrawings = await manager.findByIds(ContractDrawing, dto.contract_drawing_ids);
        shopDrawing.contractDrawingReferences = contractDrawings;
        await manager.save(shopDrawing);
      }

      return shopDrawing;
    });
  }

  async createShopDrawingRevision(
    shopDrawingId: number,
    dto: CreateShopDrawingRevisionDto,
    userId: number
  ): Promise<ShopDrawingRevision> {
    return this.dataSource.transaction(async (manager) => {
      const latestRevision = await manager.findOne(ShopDrawingRevision, {
        where: { shop_drawing_id: shopDrawingId },
        order: { revision_number: 'DESC' },
      });

      const nextRevisionNumber = (latestRevision?.revision_number || 0) + 1;

      const revision = manager.create(ShopDrawingRevision, {
        shop_drawing_id: shopDrawingId,
        revision_number: nextRevisionNumber,
        description: dto.description,
        details: dto.details,
        submission_date: dto.submission_date,
        created_by_user_id: userId,
      });

      await manager.save(revision);

      if (dto.temp_file_ids?.length > 0) {
        const attachments = await this.fileStorage.commitFiles(
          dto.temp_file_ids,
          shopDrawingId,
          'shop_drawing',
          manager
        );
        revision.attachments = attachments;
        await manager.save(revision);
      }

      return revision;
    });
  }

  async findAllShopDrawings(query: SearchDrawingDto): Promise<PaginatedResult<ShopDrawing>> {
    const queryBuilder = this.shopDrawingRepo
      .createQueryBuilder('sd')
      .leftJoinAndSelect('sd.project', 'project')
      .leftJoinAndSelect('sd.revisions', 'revisions')
      .leftJoinAndSelect('sd.contractDrawingReferences', 'refs')
      .where('sd.deleted_at IS NULL');

    if (query.project_id) {
      queryBuilder.andWhere('sd.project_id = :projectId', {
        projectId: query.project_id,
      });
    }

    if (query.search) {
      queryBuilder.andWhere('(sd.drawing_number LIKE :search OR sd.drawing_title LIKE :search)', {
        search: `%${query.search}%`,
      });
    }

    const page = query.page || 1;
    const limit = query.limit || 20;
    const skip = (page - 1) * limit;

    const [items, total] = await queryBuilder.orderBy('sd.created_at', 'DESC').skip(skip).take(limit).getManyAndCount();

    return { items, total, page, limit, totalPages: Math.ceil(total / limit) };
  }

  async findOneShopDrawing(id: number): Promise<ShopDrawing> {
    const shopDrawing = await this.shopDrawingRepo.findOne({
      where: { id, deleted_at: IsNull() },
      relations: ['revisions', 'revisions.attachments', 'contractDrawingReferences', 'project'],
      order: { revisions: { revision_number: 'DESC' } },
    });

    if (!shopDrawing) {
      throw new NotFoundException(`Shop Drawing #${id} not found`);
    }

    return shopDrawing;
  }
}
```

### 3. Controller

```typescript
// File: backend/src/modules/drawing/drawing.controller.ts
@Controller('drawings')
@UseGuards(JwtAuthGuard, PermissionGuard)
@ApiTags('Drawings')
export class DrawingController {
  constructor(private service: DrawingService) {}

  // Contract Drawings
  @Post('contract')
  @RequirePermission('drawing.create')
  async createContractDrawing(@Body() dto: CreateContractDrawingDto, @CurrentUser() user: User) {
    return this.service.createContractDrawing(dto, user.user_id);
  }

  @Get('contract')
  @RequirePermission('drawing.view')
  async findAllContractDrawings(@Query() query: SearchDrawingDto) {
    return this.service.findAllContractDrawings(query);
  }

  // Shop Drawings
  @Post('shop')
  @RequirePermission('drawing.create')
  @UseInterceptors(IdempotencyInterceptor)
  async createShopDrawing(@Body() dto: CreateShopDrawingDto, @CurrentUser() user: User) {
    return this.service.createShopDrawing(dto, user.user_id);
  }

  @Post('shop/:id/revisions')
  @RequirePermission('drawing.edit')
  async createShopDrawingRevision(
    @Param('id', ParseIntPipe) id: number,
    @Body() dto: CreateShopDrawingRevisionDto,
    @CurrentUser() user: User
  ) {
    return this.service.createShopDrawingRevision(id, dto, user.user_id);
  }

  @Get('shop')
  @RequirePermission('drawing.view')
  async findAllShopDrawings(@Query() query: SearchDrawingDto) {
    return this.service.findAllShopDrawings(query);
  }

  @Get('shop/:id')
  @RequirePermission('drawing.view')
  async findOneShopDrawing(@Param('id', ParseIntPipe) id: number) {
    return this.service.findOneShopDrawing(id);
  }
}
```

---

## ✅ Testing & Verification

### 1. Unit Tests

```typescript
describe('DrawingService', () => {
  it('should create contract drawing with PDF', async () => {
    const dto = {
      drawing_number: 'A-001',
      drawing_title: 'Floor Plan',
      contract_id: 1,
      temp_file_id: 'temp-pdf-id',
    };

    const result = await service.createContractDrawing(dto, 1);
    expect(result.attachment_id).toBeDefined();
  });

  it('should create shop drawing with auto number', async () => {
    const dto = {
      drawing_title: 'Shop Drawing Test',
      project_id: 1,
      contractor_organization_id: 3,
      contract_drawing_ids: [1, 2],
    };

    const result = await service.createShopDrawing(dto, 1);
    expect(result.drawing_number).toMatch(/^TEAM-SD-\d{4}-\d{4}$/);
    expect(result.contractDrawingReferences).toHaveLength(2);
  });
});
```

---

## 📚 Related Documents

- [Data Model - Drawings](../02-architecture/data-model.md#drawings)
- [Functional Requirements - Drawings](../01-requirements/03.4-contract-drawing.md)

---

## 📦 Deliverables

- [ ] ContractDrawing Entity
- [ ] ShopDrawing & ShopDrawingRevision Entities
- [ ] DrawingService (Both types)
- [ ] DrawingController
- [ ] DTOs
- [ ] Unit Tests (80% coverage)
- [ ] API Documentation

---

## 🚨 Risks & Mitigation

| Risk                       | Impact | Mitigation                        |
| -------------------------- | ------ | --------------------------------- |
| Large drawing files        | Medium | File size validation, compression |
| Drawing reference tracking | Medium | Junction table management         |
| Version confusion          | Low    | Clear revision numbering          |

---

## 📌 Notes

- Contract drawings: PDF uploads only
- Shop drawings: Auto-numbered with revisions
- Cross-references tracked in junction table
- Categories and disciplines from master data
