diff --git a/.agent/rules/GEMINI.md b/.agent/rules/GEMINI.ิbak similarity index 100% rename from .agent/rules/GEMINI.md rename to .agent/rules/GEMINI.ิbak diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ca7428..3f3f3f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,36 @@ - Performance optimization and load testing - Production deployment preparation +## 1.6.0 (2025-12-13) + +### Summary +**Schema Refactoring Release** - Major restructuring of correspondence and RFA tables for improved data consistency. + +### Database Schema Changes 💾 + +#### Breaking Changes ⚠️ +- **`correspondence_recipients`**: FK changed from `correspondence_revisions(correspondence_id)` → `correspondences(id)` +- **`rfa_items`**: Column renamed `rfarev_correspondence_id` → `rfa_revision_id` + +#### Schema Refactoring +- **`correspondences`**: Reordered columns, `discipline_id` now inline (no ALTER TABLE) +- **`correspondence_revisions`**: + - Renamed: `title` → `subject` + - Added: `body TEXT`, `remarks TEXT`, `schema_version INT` + - Added Virtual Columns: `v_ref_project_id`, `v_doc_subtype` +- **`rfas`**: + - Changed to Shared PK pattern (no AUTO_INCREMENT) + - PK now FK to `correspondences(id)` +- **`rfa_revisions`**: + - Removed: `correspondence_id` (uses rfas.id instead) + - Renamed: `title` → `subject` + - Added: `body TEXT`, `remarks TEXT`, `due_date DATETIME`, `schema_version INT` + - Added Virtual Column: `v_ref_drawing_count` + +### Documentation 📚 +- Updated Data Dictionary to v1.6.0 +- Updated schema SQL files (`lcbp3-v1.6.0-schema.sql`, seed files) + ## 1.5.1 (2025-12-10) ### Summary diff --git a/README.md b/README.md index 9cc54b0..dd158f7 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,20 @@ > > ระบบบริหารจัดการเอกสารโครงการแบบครบวงจร สำหรับโครงการก่อสร้างท่าเรือแหลมฉบังระยะที่ 3 -[![Version](https://img.shields.io/badge/version-1.5.1-blue.svg)](./CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-1.6.0-blue.svg)](./CHANGELOG.md) [![License](https://img.shields.io/badge/license-Internal-red.svg)]() [![Status](https://img.shields.io/badge/status-Production%20Ready-brightgreen.svg)]() --- -## 📈 Current Status (As of 2025-12-10) +## 📈 Current Status (As of 2025-12-13) **Overall Progress: ~95% Feature Complete - Production Ready** - ✅ **Backend**: All 18 core modules implemented (~95%) - ✅ **Frontend**: All 15 UI tasks completed (100%) -- ✅ **Database**: Schema v1.5.1 active with complete seed data -- ✅ **Documentation**: Comprehensive specs/ at v1.5.1 +- ✅ **Database**: Schema v1.6.0 active with complete seed data +- ✅ **Documentation**: Comprehensive specs/ at v1.6.0 - ✅ **Admin Tools**: Workflow & Numbering configuration UIs complete - 🔄 **Testing**: E2E tests and UAT in progress - 📋 **Next**: Production deployment preparation @@ -304,16 +304,16 @@ lcbp3-dms/ | **Requirements** | ข้อกำหนดระบบและฟังก์ชันการทำงาน | `specs/01-requirements/` | | **Architecture** | สถาปัตยกรรมระบบ, ADRs | `specs/02-architecture/` | | **Implementation** | แนวทางการพัฒนา Backend/Frontend | `specs/03-implementation/` | -| **Database** | Schema v1.5.1 + Seed Data | `specs/07-database/` | +| **Database** | Schema v1.6.0 + Seed Data | `specs/07-database/` | ### Schema & Seed Data ```bash # Import schema -mysql -u root -p lcbp3_dev < specs/07-database/lcbp3-v1.5.1-schema.sql +mysql -u root -p lcbp3_dev < specs/07-database/lcbp3-v1.6.0-schema.sql # Import seed data -mysql -u root -p lcbp3_dev < specs/07-database/lcbp3-v1.5.1-seed.sql +mysql -u root -p lcbp3_dev < specs/07-database/lcbp3-v1.6.0-seed-basic.sql ``` ### Legacy Documentation @@ -558,11 +558,11 @@ This project is **Internal Use Only** - ลิขสิทธิ์เป็น - ✅ Responsive Layout (Desktop & Mobile) **Documentation** -- ✅ Complete specs/ v1.5.1 (21 requirements, 17 ADRs) -- ✅ Database Schema v1.5.1 with seed data +- ✅ Complete specs/ v1.6.0 (21 requirements, 17 ADRs) +- ✅ Database Schema v1.6.0 with seed data - ✅ Implementation & Operations Guides -### Version 1.6.0 (Planned - Q1 2026) +### Version 1.7.0 (Planned - Q1 2026) **Production Enhancements** - 📋 E2E Test Coverage (Playwright/Cypress) diff --git a/backend/src/modules/correspondence/correspondence.controller.ts b/backend/src/modules/correspondence/correspondence.controller.ts index 5fdafc4..b82f2e2 100644 --- a/backend/src/modules/correspondence/correspondence.controller.ts +++ b/backend/src/modules/correspondence/correspondence.controller.ts @@ -9,6 +9,7 @@ import { ParseIntPipe, Query, Delete, + Put, } from '@nestjs/common'; import { ApiTags, @@ -19,6 +20,7 @@ import { import { CorrespondenceService } from './correspondence.service'; import { CorrespondenceWorkflowService } from './correspondence-workflow.service'; import { CreateCorrespondenceDto } from './dto/create-correspondence.dto'; +import { UpdateCorrespondenceDto } from './dto/update-correspondence.dto'; import { SubmitCorrespondenceDto } from './dto/submit-correspondence.dto'; import { WorkflowActionDto } from './dto/workflow-action.dto'; import { AddReferenceDto } from './dto/add-reference.dto'; @@ -92,6 +94,23 @@ export class CorrespondenceController { ); } + @Post('preview-number') + @ApiOperation({ summary: 'Preview next document number' }) + @ApiResponse({ + status: 200, + description: 'Return preview number and status.', + }) + @RequirePermission('correspondence.create') + previewNumber( + @Body() createDto: CreateCorrespondenceDto, + @Request() req: Request & { user: unknown } + ) { + return this.correspondenceService.previewDocumentNumber( + createDto, + req.user as Parameters[1] + ); + } + @Get() @ApiOperation({ summary: 'Search correspondences' }) @ApiResponse({ status: 200, description: 'Return list of correspondences.' }) @@ -140,6 +159,26 @@ export class CorrespondenceController { return this.correspondenceService.findOne(id); } + @Put(':id') + @ApiOperation({ summary: 'Update correspondence (Draft only)' }) + @ApiResponse({ + status: 200, + description: 'Correspondence updated successfully.', + }) + @RequirePermission('correspondence.create') // Assuming create permission is enough for draft update, or add 'correspondence.edit' + @Audit('correspondence.update', 'correspondence') + update( + @Param('id', ParseIntPipe) id: number, + @Body() updateDto: UpdateCorrespondenceDto, + @Request() req: Request & { user: unknown } + ) { + return this.correspondenceService.update( + id, + updateDto, + req.user as Parameters[1] + ); + } + @Get(':id/references') @ApiOperation({ summary: 'Get referenced documents' }) @ApiResponse({ diff --git a/backend/src/modules/correspondence/correspondence.module.ts b/backend/src/modules/correspondence/correspondence.module.ts index 8033823..f100fda 100644 --- a/backend/src/modules/correspondence/correspondence.module.ts +++ b/backend/src/modules/correspondence/correspondence.module.ts @@ -10,6 +10,8 @@ import { CorrespondenceRevision } from './entities/correspondence-revision.entit import { CorrespondenceType } from './entities/correspondence-type.entity'; import { CorrespondenceStatus } from './entities/correspondence-status.entity'; import { CorrespondenceReference } from './entities/correspondence-reference.entity'; +import { CorrespondenceRecipient } from './entities/correspondence-recipient.entity'; +import { Organization } from '../organization/entities/organization.entity'; // Dependent Modules import { DocumentNumberingModule } from '../document-numbering/document-numbering.module'; @@ -32,6 +34,8 @@ import { SearchModule } from '../search/search.module'; CorrespondenceType, CorrespondenceStatus, CorrespondenceReference, + CorrespondenceRecipient, + Organization, ]), DocumentNumberingModule, JsonSchemaModule, diff --git a/backend/src/modules/correspondence/correspondence.service.spec.ts b/backend/src/modules/correspondence/correspondence.service.spec.ts index 83d6665..beded2e 100644 --- a/backend/src/modules/correspondence/correspondence.service.spec.ts +++ b/backend/src/modules/correspondence/correspondence.service.spec.ts @@ -118,8 +118,8 @@ describe('CorrespondenceService', () => { describe('findAll', () => { it('should return correspondences array', async () => { const result = await service.findAll({ projectId: 1 }); - expect(Array.isArray(result)).toBeTruthy(); - expect(result).toBeDefined(); + expect(Array.isArray(result.data)).toBeTruthy(); + expect(result.meta).toBeDefined(); }); }); }); diff --git a/backend/src/modules/correspondence/correspondence.service.ts b/backend/src/modules/correspondence/correspondence.service.ts index 41c298e..0100e5a 100644 --- a/backend/src/modules/correspondence/correspondence.service.ts +++ b/backend/src/modules/correspondence/correspondence.service.ts @@ -17,12 +17,16 @@ import { CorrespondenceRevision } from './entities/correspondence-revision.entit import { CorrespondenceType } from './entities/correspondence-type.entity'; import { CorrespondenceStatus } from './entities/correspondence-status.entity'; import { CorrespondenceReference } from './entities/correspondence-reference.entity'; +import { CorrespondenceRecipient } from './entities/correspondence-recipient.entity'; import { User } from '../user/entities/user.entity'; +import { Organization } from '../organization/entities/organization.entity'; // DTOs import { CreateCorrespondenceDto } from './dto/create-correspondence.dto'; +import { UpdateCorrespondenceDto } from './dto/update-correspondence.dto'; import { AddReferenceDto } from './dto/add-reference.dto'; import { SearchCorrespondenceDto } from './dto/search-correspondence.dto'; +import { DeepPartial } from 'typeorm'; // Services import { DocumentNumberingService } from '../document-numbering/document-numbering.service'; @@ -52,6 +56,8 @@ export class CorrespondenceService { private statusRepo: Repository, @InjectRepository(CorrespondenceReference) private referenceRepo: Repository, + @InjectRepository(Organization) + private orgRepo: Repository, private numberingService: DocumentNumberingService, private jsonSchemaService: JsonSchemaService, @@ -121,10 +127,17 @@ export class CorrespondenceService { try { const orgCode = 'ORG'; // TODO: Fetch real ORG Code from Organization Entity - // Extract recipient organization from details - const recipientOrganizationId = createDto.details?.to_organization_id as - | number - | undefined; + // [v1.5.1] Extract recipient organization from recipients array (Primary TO) + const toRecipient = createDto.recipients?.find((r) => r.type === 'TO'); + const recipientOrganizationId = toRecipient?.organizationId; + + let recipientCode = ''; + if (recipientOrganizationId) { + const recOrg = await this.orgRepo.findOne({ + where: { id: recipientOrganizationId }, + }); + if (recOrg) recipientCode = recOrg.organizationCode; + } const docNumber = await this.numberingService.generateNextNumber({ projectId: createDto.projectId, @@ -137,6 +150,8 @@ export class CorrespondenceService { customTokens: { TYPE_CODE: type.typeCode, ORG_CODE: orgCode, + RECIPIENT_CODE: recipientCode, + REC_CODE: recipientCode, }, }); @@ -157,13 +172,29 @@ export class CorrespondenceService { revisionLabel: 'A', isCurrent: true, statusId: statusDraft.id, - title: createDto.title, + subject: createDto.subject, + body: createDto.body, + remarks: createDto.remarks, + dueDate: createDto.dueDate ? new Date(createDto.dueDate) : undefined, description: createDto.description, details: createDto.details, createdBy: user.user_id, + schemaVersion: 1, }); await queryRunner.manager.save(revision); + // Save Recipients + if (createDto.recipients && createDto.recipients.length > 0) { + const recipients = createDto.recipients.map((r) => + queryRunner.manager.create(CorrespondenceRecipient, { + correspondenceId: savedCorr.id, + recipientOrganizationId: r.organizationId, + recipientType: r.type, + }) + ); + await queryRunner.manager.save(recipients); + } + await queryRunner.commitTransaction(); // Start Workflow Instance (non-blocking) @@ -190,7 +221,7 @@ export class CorrespondenceService { id: savedCorr.id, type: 'correspondence', docNumber: docNumber, - title: createDto.title, + title: createDto.subject, description: createDto.description, status: 'DRAFT', projectId: createDto.projectId, @@ -256,7 +287,7 @@ export class CorrespondenceService { if (search) { query.andWhere( - '(corr.correspondenceNumber LIKE :search OR rev.title LIKE :search)', + '(corr.correspondenceNumber LIKE :search OR rev.subject LIKE :search)', { search: `%${search}%` } ); } @@ -286,6 +317,8 @@ export class CorrespondenceService { 'type', 'project', 'originator', + 'recipients', + 'recipients.recipientOrganization', // [v1.5.1] Fixed relation name ], }); @@ -352,4 +385,181 @@ export class CorrespondenceService { return { outgoing, incoming }; } + + async update(id: number, updateDto: UpdateCorrespondenceDto, user: User) { + // 1. Find Current Revision + const revision = await this.revisionRepo.findOne({ + where: { + correspondenceId: id, + isCurrent: true, + }, + relations: ['correspondence'], + }); + + if (!revision) { + throw new NotFoundException( + `Current revision for correspondence ${id} not found` + ); + } + + // 2. Check Permission + if (revision.statusId) { + const status = await this.statusRepo.findOne({ + where: { id: revision.statusId }, + }); + if (status && status.statusCode !== 'DRAFT') { + throw new BadRequestException('Only DRAFT documents can be updated'); + } + } + + // 3. Update Correspondence Entity if needed + const correspondenceUpdate: DeepPartial = {}; + if (updateDto.disciplineId) + correspondenceUpdate.disciplineId = updateDto.disciplineId; + if (updateDto.projectId) + correspondenceUpdate.projectId = updateDto.projectId; + + if (Object.keys(correspondenceUpdate).length > 0) { + await this.correspondenceRepo.update(id, correspondenceUpdate); + } + + // 4. Update Revision Entity + const revisionUpdate: DeepPartial = {}; + if (updateDto.subject) revisionUpdate.subject = updateDto.subject; + if (updateDto.body) revisionUpdate.body = updateDto.body; + if (updateDto.remarks) revisionUpdate.remarks = updateDto.remarks; + // Format Date correctly if string + if (updateDto.dueDate) revisionUpdate.dueDate = new Date(updateDto.dueDate); + if (updateDto.description) + revisionUpdate.description = updateDto.description; + if (updateDto.details) revisionUpdate.details = updateDto.details; + + if (Object.keys(revisionUpdate).length > 0) { + await this.revisionRepo.update(revision.id, revisionUpdate); + } + + // 5. Update Recipients if provided + if (updateDto.recipients) { + const recipientRepo = this.dataSource.getRepository( + CorrespondenceRecipient + ); + await recipientRepo.delete({ correspondenceId: id }); + + const newRecipients = updateDto.recipients.map((r) => + recipientRepo.create({ + correspondenceId: id, + recipientOrganizationId: r.organizationId, + recipientType: r.type, + }) + ); + await recipientRepo.save(newRecipients); + } + + // 6. Regenerate Document Number if structural fields changed (Recipient, Discipline, Type, Project) + // AND it is a DRAFT. + const hasRecipientChange = !!updateDto.recipients?.find( + (r) => r.type === 'TO' + ); + const hasStructureChange = + updateDto.typeId || + updateDto.disciplineId || + updateDto.projectId || + hasRecipientChange; + + if (hasStructureChange) { + // Re-fetch fresh data for context + const freshCorr = await this.correspondenceRepo.findOne({ + where: { id }, + relations: ['type', 'recipients', 'recipients.recipientOrganization'], + }); + + if (freshCorr) { + const toRecipient = freshCorr.recipients?.find( + (r) => r.recipientType === 'TO' + ); + const recipientOrganizationId = toRecipient?.recipientOrganizationId; + const type = freshCorr.type; + + let recipientCode = ''; + if (toRecipient?.recipientOrganization) { + recipientCode = toRecipient.recipientOrganization.organizationCode; + } else if (recipientOrganizationId) { + // Fallback fetch if relation not loaded (though we added it) + const recOrg = await this.orgRepo.findOne({ + where: { id: recipientOrganizationId }, + }); + if (recOrg) recipientCode = recOrg.organizationCode; + } + + const orgCode = 'ORG'; // Placeholder + + const newDocNumber = await this.numberingService.generateNextNumber({ + projectId: freshCorr.projectId, + originatorId: freshCorr.originatorId!, + typeId: freshCorr.correspondenceTypeId, + disciplineId: freshCorr.disciplineId, + // Use undefined for subTypeId if not present implicitly + year: new Date().getFullYear(), + recipientOrganizationId: recipientOrganizationId ?? 0, + customTokens: { + TYPE_CODE: type?.typeCode || '', + ORG_CODE: orgCode, + RECIPIENT_CODE: recipientCode, + REC_CODE: recipientCode, + }, + }); + + await this.correspondenceRepo.update(id, { + correspondenceNumber: newDocNumber, + }); + } + } + + return this.findOne(id); + } + + async previewDocumentNumber(createDto: CreateCorrespondenceDto, user: User) { + const type = await this.typeRepo.findOne({ + where: { id: createDto.typeId }, + }); + if (!type) throw new NotFoundException('Document Type not found'); + + let userOrgId = user.primaryOrganizationId; + if (!userOrgId) { + const fullUser = await this.userService.findOne(user.user_id); + if (fullUser) userOrgId = fullUser.primaryOrganizationId; + } + + if (createDto.originatorId && createDto.originatorId !== userOrgId) { + // Allow impersonation for preview + userOrgId = createDto.originatorId; + } + + // Extract recipient from recipients array + const toRecipient = createDto.recipients?.find((r) => r.type === 'TO'); + const recipientOrganizationId = toRecipient?.organizationId; + + let recipientCode = ''; + if (recipientOrganizationId) { + const recOrg = await this.orgRepo.findOne({ + where: { id: recipientOrganizationId }, + }); + if (recOrg) recipientCode = recOrg.organizationCode; + } + + return this.numberingService.previewNextNumber({ + projectId: createDto.projectId, + originatorId: userOrgId!, + typeId: createDto.typeId, + disciplineId: createDto.disciplineId, + subTypeId: createDto.subTypeId, + recipientOrganizationId, + year: new Date().getFullYear(), + customTokens: { + TYPE_CODE: type.typeCode, + RECIPIENT_CODE: recipientCode, + REC_CODE: recipientCode, + }, + }); + } } diff --git a/backend/src/modules/correspondence/dto/create-correspondence.dto.ts b/backend/src/modules/correspondence/dto/create-correspondence.dto.ts index 8538a52..9bcf3c4 100644 --- a/backend/src/modules/correspondence/dto/create-correspondence.dto.ts +++ b/backend/src/modules/correspondence/dto/create-correspondence.dto.ts @@ -5,6 +5,8 @@ import { IsOptional, IsBoolean, IsObject, + IsDateString, + IsArray, } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; @@ -30,12 +32,36 @@ export class CreateCorrespondenceDto { subTypeId?: number; // [Req 6B] ประเภทย่อย (เช่น MAT, SHP สำหรับ Transmittal/RFA) @ApiProperty({ - description: 'Correspondence Title', + description: 'Correspondence Subject', example: 'Monthly Progress Report', }) @IsString() @IsNotEmpty() - title!: string; + subject!: string; + + @ApiPropertyOptional({ + description: 'Body/Content', + example: '

...

', + }) + @IsString() + @IsOptional() + body?: string; + + @ApiPropertyOptional({ + description: 'Remarks', + example: 'Note...', + }) + @IsString() + @IsOptional() + remarks?: string; + + @ApiPropertyOptional({ + description: 'Due Date', + example: '2025-12-06T00:00:00Z', + }) + @IsDateString() + @IsOptional() + dueDate?: string; @ApiPropertyOptional({ description: 'Correspondence Description', @@ -66,4 +92,12 @@ export class CreateCorrespondenceDto { @IsInt() @IsOptional() originatorId?: number; + + @ApiPropertyOptional({ + description: 'Recipients', + example: [{ organizationId: 1, type: 'TO' }], + }) + @IsArray() + @IsOptional() + recipients?: { organizationId: number; type: 'TO' | 'CC' }[]; } diff --git a/backend/src/modules/correspondence/dto/update-correspondence.dto.ts b/backend/src/modules/correspondence/dto/update-correspondence.dto.ts new file mode 100644 index 0000000..855c801 --- /dev/null +++ b/backend/src/modules/correspondence/dto/update-correspondence.dto.ts @@ -0,0 +1,6 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateCorrespondenceDto } from './create-correspondence.dto'; + +export class UpdateCorrespondenceDto extends PartialType( + CreateCorrespondenceDto +) {} diff --git a/backend/src/modules/correspondence/entities/correspondence-recipient.entity.ts b/backend/src/modules/correspondence/entities/correspondence-recipient.entity.ts new file mode 100644 index 0000000..3f6f22f --- /dev/null +++ b/backend/src/modules/correspondence/entities/correspondence-recipient.entity.ts @@ -0,0 +1,26 @@ +import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { Correspondence } from './correspondence.entity'; +import { Organization } from '../../organization/entities/organization.entity'; + +@Entity('correspondence_recipients') +export class CorrespondenceRecipient { + @PrimaryColumn({ name: 'correspondence_id' }) + correspondenceId!: number; + + @PrimaryColumn({ name: 'recipient_organization_id' }) + recipientOrganizationId!: number; + + @PrimaryColumn({ name: 'recipient_type', type: 'enum', enum: ['TO', 'CC'] }) + recipientType!: 'TO' | 'CC'; + + // Relations + @ManyToOne(() => Correspondence, (corr) => corr.recipients, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'correspondence_id' }) + correspondence!: Correspondence; + + @ManyToOne(() => Organization) + @JoinColumn({ name: 'recipient_organization_id' }) + recipientOrganization!: Organization; +} diff --git a/backend/src/modules/correspondence/entities/correspondence-revision.entity.ts b/backend/src/modules/correspondence/entities/correspondence-revision.entity.ts index 4298221..452f3d3 100644 --- a/backend/src/modules/correspondence/entities/correspondence-revision.entity.ts +++ b/backend/src/modules/correspondence/entities/correspondence-revision.entity.ts @@ -35,15 +35,24 @@ export class CorrespondenceRevision { @Column({ name: 'correspondence_status_id' }) statusId!: number; - @Column({ length: 255 }) - title!: string; + @Column({ length: 500 }) + subject!: string; @Column({ name: 'description', type: 'text', nullable: true }) description?: string; + @Column({ type: 'text', nullable: true }) + body?: string; + + @Column({ type: 'text', nullable: true }) + remarks?: string; + @Column({ type: 'json', nullable: true }) details?: any; // เก็บข้อมูลแบบ Dynamic ตาม Type + @Column({ name: 'schema_version', default: 1 }) + schemaVersion!: number; + // ✅ [New] Virtual Column: ดึง Project ID จาก JSON details @Column({ name: 'v_ref_project_id', diff --git a/backend/src/modules/correspondence/entities/correspondence.entity.ts b/backend/src/modules/correspondence/entities/correspondence.entity.ts index 4047f3a..96c404b 100644 --- a/backend/src/modules/correspondence/entities/correspondence.entity.ts +++ b/backend/src/modules/correspondence/entities/correspondence.entity.ts @@ -12,7 +12,9 @@ import { Project } from '../../project/entities/project.entity'; import { Organization } from '../../organization/entities/organization.entity'; import { CorrespondenceType } from './correspondence-type.entity'; import { User } from '../../user/entities/user.entity'; -import { CorrespondenceRevision } from './correspondence-revision.entity'; // เดี๋ยวสร้าง +import { CorrespondenceRecipient } from './correspondence-recipient.entity'; +import { CorrespondenceRevision } from './correspondence-revision.entity'; +import { Discipline } from '../../master/entities/discipline.entity'; @Entity('correspondences') export class Correspondence { @@ -68,9 +70,9 @@ export class Correspondence { creator?: User; // [New V1.5.1] - @ManyToOne('Discipline') + @ManyToOne(() => Discipline) @JoinColumn({ name: 'discipline_id' }) - discipline?: any; // Use 'any' or import Discipline entity if available to avoid circular dependency issues if not careful, but better to import. + discipline?: Discipline; // One Correspondence has Many Revisions @OneToMany( @@ -78,4 +80,11 @@ export class Correspondence { (revision) => revision.correspondence ) revisions?: CorrespondenceRevision[]; + + @OneToMany( + () => CorrespondenceRecipient, + (recipient) => recipient.correspondence, + { cascade: true } + ) + recipients?: CorrespondenceRecipient[]; } diff --git a/backend/src/modules/document-numbering/document-numbering.service.ts b/backend/src/modules/document-numbering/document-numbering.service.ts index a10b35b..31582d2 100644 --- a/backend/src/modules/document-numbering/document-numbering.service.ts +++ b/backend/src/modules/document-numbering/document-numbering.service.ts @@ -222,6 +222,53 @@ export class DocumentNumberingService implements OnModuleInit, OnModuleDestroy { } } + /** + * Preview the next document number without incrementing the counter. + * Returns the number and whether a custom template was found. + */ + async previewNextNumber( + ctx: GenerateNumberContext + ): Promise<{ number: string; isDefaultTemplate: boolean }> { + const year = ctx.year || new Date().getFullYear(); + const disciplineId = ctx.disciplineId || 0; + + // 1. Resolve Tokens + const tokens = await this.resolveTokens(ctx, year); + + // 2. Get Format Template + const { template, isDefault } = await this.getFormatTemplateWithMeta( + ctx.projectId, + ctx.typeId + ); + + // 3. Get Current Counter (No Lock needed for preview) + const recipientId = ctx.recipientOrganizationId ?? -1; + const subTypeId = ctx.subTypeId ?? 0; + const rfaTypeId = ctx.rfaTypeId ?? 0; + + const counter = await this.counterRepo.findOne({ + where: { + projectId: ctx.projectId, + originatorId: ctx.originatorId, + recipientOrganizationId: recipientId, + typeId: ctx.typeId, + subTypeId: subTypeId, + rfaTypeId: rfaTypeId, + disciplineId: disciplineId, + year: year, + }, + }); + + const nextSeq = (counter?.lastNumber || 0) + 1; + + const generatedNumber = this.replaceTokens(template, tokens, nextSeq); + + return { + number: generatedNumber, + isDefaultTemplate: isDefault, + }; + } + /** * Helper: ดึงข้อมูล Code ต่างๆ จาก ID เพื่อนำมาแทนที่ใน Template */ @@ -239,17 +286,20 @@ export class DocumentNumberingService implements OnModuleInit, OnModuleDestroy { throw new NotFoundException('Project, Organization, or Type not found'); } - let disciplineCode = '000'; - if (ctx.disciplineId) { + // [v1.5.1] Support Custom Tokens Override + const custom = ctx.customTokens || {}; + + let disciplineCode = custom.DISCIPLINE_CODE || '000'; + if (!custom.DISCIPLINE_CODE && ctx.disciplineId) { const discipline = await this.disciplineRepo.findOne({ where: { id: ctx.disciplineId }, }); if (discipline) disciplineCode = discipline.disciplineCode; } - let subTypeCode = '00'; - let subTypeNumber = '00'; - if (ctx.subTypeId) { + let subTypeCode = custom.SUB_TYPE_CODE || '00'; + let subTypeNumber = custom.SUB_TYPE_NUMBER || '00'; + if (!custom.SUB_TYPE_CODE && ctx.subTypeId) { const subType = await this.subTypeRepo.findOne({ where: { id: ctx.subTypeId }, }); @@ -264,8 +314,12 @@ export class DocumentNumberingService implements OnModuleInit, OnModuleDestroy { const yearTh = (year + 543).toString(); // [v1.5.1] Resolve recipient organization - let recipientCode = ''; - if (ctx.recipientOrganizationId && ctx.recipientOrganizationId > 0) { + let recipientCode = custom.RECIPIENT_CODE || custom.REC_CODE || ''; + if ( + !recipientCode && + ctx.recipientOrganizationId && + ctx.recipientOrganizationId > 0 + ) { const recipient = await this.orgRepo.findOne({ where: { id: ctx.recipientOrganizationId }, }); @@ -288,17 +342,36 @@ export class DocumentNumberingService implements OnModuleInit, OnModuleDestroy { } /** - * Helper: หา Template จาก DB หรือใช้ Default + * Helper: Find Template from DB or use Default (with metadata) + */ + private async getFormatTemplateWithMeta( + projectId: number, + typeId: number + ): Promise<{ template: string; isDefault: boolean }> { + const format = await this.formatRepo.findOne({ + where: { projectId, correspondenceTypeId: typeId }, + }); + + if (format) { + return { template: format.formatTemplate, isDefault: false }; + } + + // Default Fallback Format + return { template: '{ORG}-{RECIPIENT}-{SEQ:4}-{YEAR}', isDefault: true }; + } + + /** + * Legacy wrapper for backward compatibility */ private async getFormatTemplate( projectId: number, typeId: number ): Promise { - const format = await this.formatRepo.findOne({ - where: { projectId, correspondenceTypeId: typeId }, - }); - // Default Fallback Format (ตาม Req 2.1) - return format ? format.formatTemplate : '{ORG}-{RECIPIENT}-{SEQ:4}-{YEAR}'; + const { template } = await this.getFormatTemplateWithMeta( + projectId, + typeId + ); + return template; } /** @@ -338,10 +411,6 @@ export class DocumentNumberingService implements OnModuleInit, OnModuleDestroy { return result; } - /** - * [P0-4] Log successful number generation to audit table - */ - /** * [P0-4] Log successful number generation to audit table */ diff --git a/backend/src/modules/rfa/dto/create-rfa-revision.dto.ts b/backend/src/modules/rfa/dto/create-rfa-revision.dto.ts index 04e75a7..8b2301b 100644 --- a/backend/src/modules/rfa/dto/create-rfa-revision.dto.ts +++ b/backend/src/modules/rfa/dto/create-rfa-revision.dto.ts @@ -10,10 +10,25 @@ import { import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class CreateRfaRevisionDto { - @ApiProperty({ description: 'RFA Title', example: 'RFA for Building A' }) + @ApiProperty({ description: 'RFA Subject', example: 'RFA for Building A' }) @IsString() @IsNotEmpty() - title!: string; + subject!: string; + + @ApiPropertyOptional({ description: 'Body', example: '

...

' }) + @IsString() + @IsOptional() + body?: string; + + @ApiPropertyOptional({ description: 'Remarks', example: 'Note' }) + @IsString() + @IsOptional() + remarks?: string; + + @ApiPropertyOptional({ description: 'Due Date', example: '2025-12-06' }) + @IsDateString() + @IsOptional() + dueDate?: string; @ApiProperty({ description: 'RFA Status Code ID', example: 1 }) @IsInt() diff --git a/backend/src/modules/rfa/dto/create-rfa.dto.ts b/backend/src/modules/rfa/dto/create-rfa.dto.ts index fad4f86..33acba3 100644 --- a/backend/src/modules/rfa/dto/create-rfa.dto.ts +++ b/backend/src/modules/rfa/dto/create-rfa.dto.ts @@ -35,7 +35,22 @@ export class CreateRfaDto { }) @IsString() @IsNotEmpty() - title!: string; + subject!: string; + + @ApiProperty({ description: 'Body', required: false }) + @IsString() + @IsOptional() + body?: string; + + @ApiProperty({ description: 'Remarks', required: false }) + @IsString() + @IsOptional() + remarks?: string; + + @ApiProperty({ description: 'Due Date', required: false }) + @IsDateString() + @IsOptional() + dueDate?: string; @ApiProperty({ description: 'รายละเอียดเพิ่มเติม', required: false }) @IsString() diff --git a/backend/src/modules/rfa/entities/rfa-item.entity.ts b/backend/src/modules/rfa/entities/rfa-item.entity.ts index cd8e8e1..238a1a1 100644 --- a/backend/src/modules/rfa/entities/rfa-item.entity.ts +++ b/backend/src/modules/rfa/entities/rfa-item.entity.ts @@ -4,7 +4,7 @@ import { ShopDrawingRevision } from '../../drawing/entities/shop-drawing-revisio @Entity('rfa_items') export class RfaItem { - @PrimaryColumn({ name: 'rfarev_correspondence_id' }) + @PrimaryColumn({ name: 'rfa_revision_id' }) rfaRevisionId!: number; @PrimaryColumn({ name: 'shop_drawing_revision_id' }) @@ -14,11 +14,7 @@ export class RfaItem { @ManyToOne(() => RfaRevision, (rfaRev) => rfaRev.items, { onDelete: 'CASCADE', }) - @JoinColumn({ name: 'rfarev_correspondence_id' }) // Link to correspondence_id of the revision (as per SQL schema) OR id - // Note: ตาม SQL Schema "rfarev_correspondence_id" FK ไปที่ correspondence_revisions(correspondence_id) - // แต่เพื่อให้ TypeORM ใช้ง่าย ปกติเราจะ Link ไปที่ PK ของ RfaRevision - // **แต่** ตาม SQL: FOREIGN KEY (rfarev_correspondence_id) REFERENCES correspondences(id) - // ดังนั้นต้องระวังจุดนี้ ใน Service เราจะใช้ correspondenceId เป็น Key + @JoinColumn({ name: 'rfa_revision_id' }) rfaRevision!: RfaRevision; @ManyToOne(() => ShopDrawingRevision) diff --git a/backend/src/modules/rfa/entities/rfa-revision.entity.ts b/backend/src/modules/rfa/entities/rfa-revision.entity.ts index c979f56..77f0134 100644 --- a/backend/src/modules/rfa/entities/rfa-revision.entity.ts +++ b/backend/src/modules/rfa/entities/rfa-revision.entity.ts @@ -9,7 +9,6 @@ import { PrimaryGeneratedColumn, Unique, } from 'typeorm'; -import { Correspondence } from '../../correspondence/entities/correspondence.entity'; import { User } from '../../user/entities/user.entity'; import { RfaApproveCode } from './rfa-approve-code.entity'; import { RfaItem } from './rfa-item.entity'; @@ -24,9 +23,6 @@ export class RfaRevision { @PrimaryGeneratedColumn() id!: number; - @Column({ name: 'correspondence_id' }) - correspondenceId!: number; - @Column({ name: 'rfa_id' }) rfaId!: number; @@ -45,8 +41,8 @@ export class RfaRevision { @Column({ name: 'rfa_approve_code_id', nullable: true }) rfaApproveCodeId?: number; - @Column({ length: 255 }) - title!: string; + @Column({ length: 500 }) + subject!: string; @Column({ name: 'document_date', type: 'date', nullable: true }) documentDate?: Date; @@ -57,12 +53,21 @@ export class RfaRevision { @Column({ name: 'received_date', type: 'datetime', nullable: true }) receivedDate?: Date; + @Column({ name: 'due_date', type: 'datetime', nullable: true }) + dueDate?: Date; + @Column({ name: 'approved_date', type: 'date', nullable: true }) approvedDate?: Date; @Column({ type: 'text', nullable: true }) description?: string; + @Column({ type: 'text', nullable: true }) + body?: string; + + @Column({ type: 'text', nullable: true }) + remarks?: string; + // --- JSON & Schema Section --- @Column({ type: 'json', nullable: true }) @@ -95,10 +100,6 @@ export class RfaRevision { // --- Relations --- - @ManyToOne(() => Correspondence) - @JoinColumn({ name: 'correspondence_id' }) - correspondence!: Correspondence; - @ManyToOne(() => Rfa) @JoinColumn({ name: 'rfa_id' }) rfa!: Rfa; diff --git a/backend/src/modules/rfa/entities/rfa.entity.ts b/backend/src/modules/rfa/entities/rfa.entity.ts index b2bb14d..5abefd5 100644 --- a/backend/src/modules/rfa/entities/rfa.entity.ts +++ b/backend/src/modules/rfa/entities/rfa.entity.ts @@ -6,18 +6,24 @@ import { JoinColumn, ManyToOne, OneToMany, - PrimaryGeneratedColumn, + PrimaryColumn, + OneToOne, } from 'typeorm'; -import { Discipline } from '../../master/entities/discipline.entity'; // Import ใหม่ + import { User } from '../../user/entities/user.entity'; +import { Correspondence } from '../../correspondence/entities/correspondence.entity'; // Import import { RfaRevision } from './rfa-revision.entity'; import { RfaType } from './rfa-type.entity'; @Entity('rfas') export class Rfa { - @PrimaryGeneratedColumn() + @PrimaryColumn() id!: number; + @OneToOne(() => Correspondence, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'id' }) + correspondence!: Correspondence; + @Column({ name: 'rfa_type_id' }) rfaTypeId!: number; @@ -35,11 +41,6 @@ export class Rfa { @JoinColumn({ name: 'rfa_type_id' }) rfaType!: RfaType; - // ✅ [NEW] Relation - @ManyToOne(() => Discipline) - @JoinColumn({ name: 'discipline_id' }) - discipline?: Discipline; - @ManyToOne(() => User) @JoinColumn({ name: 'created_by' }) creator?: User; diff --git a/backend/src/modules/rfa/rfa-workflow.service.ts b/backend/src/modules/rfa/rfa-workflow.service.ts index da6d5e0..2331aa0 100644 --- a/backend/src/modules/rfa/rfa-workflow.service.ts +++ b/backend/src/modules/rfa/rfa-workflow.service.ts @@ -31,7 +31,7 @@ export class RfaWorkflowService { private readonly statusRepo: Repository, @InjectRepository(RfaApproveCode) private readonly approveCodeRepo: Repository, - private readonly dataSource: DataSource, + private readonly dataSource: DataSource ) {} /** @@ -46,19 +46,23 @@ export class RfaWorkflowService { // 1. ดึงข้อมูล Revision ปัจจุบัน const revision = await this.revisionRepo.findOne({ where: { id: rfaId, isCurrent: true }, - relations: ['rfa'], + relations: [ + 'rfa', + 'rfa.correspondence', + 'rfa.correspondence.discipline', + ], }); if (!revision) { throw new NotFoundException( - `Current Revision for RFA ID ${rfaId} not found`, + `Current Revision for RFA ID ${rfaId} not found` ); } // 2. สร้าง Context (ข้อมูลประกอบการตัดสินใจ) const context = { rfaType: revision.rfa.rfaTypeId, - discipline: revision.rfa.discipline, + discipline: revision.rfa.correspondence?.discipline, ownerId: userId, // อาจเพิ่มเงื่อนไขอื่นๆ เช่น จำนวนวัน, ความเร่งด่วน }; @@ -69,7 +73,7 @@ export class RfaWorkflowService { this.WORKFLOW_CODE, 'rfa_revision', revision.id.toString(), - context, + context ); // 4. Auto Transition: SUBMIT @@ -78,7 +82,7 @@ export class RfaWorkflowService { 'SUBMIT', userId, note || 'RFA Submitted', - {}, + {} ); // 5. Sync สถานะกลับตาราง RFA Revision @@ -86,13 +90,13 @@ export class RfaWorkflowService { revision, transitionResult.nextState, undefined, - queryRunner, + queryRunner ); await queryRunner.commitTransaction(); this.logger.log( - `Started workflow for RFA #${rfaId} (Instance: ${instance.id})`, + `Started workflow for RFA #${rfaId} (Instance: ${instance.id})` ); return { @@ -114,7 +118,7 @@ export class RfaWorkflowService { async processAction( instanceId: string, userId: number, - dto: WorkflowTransitionDto, + dto: WorkflowTransitionDto ) { // 1. ส่งคำสั่งให้ Engine ประมวลผล const result = await this.workflowEngine.processTransition( @@ -122,7 +126,7 @@ export class RfaWorkflowService { dto.action, userId, dto.comment, - dto.payload, + dto.payload ); // 2. Sync สถานะกลับตารางเดิม @@ -148,7 +152,7 @@ export class RfaWorkflowService { revision: RfaRevision, workflowState: string, approveCodeStr?: string, // เช่น '1A', '1C' - queryRunner?: any, + queryRunner?: any ) { // 1. Map Workflow State -> RFA Status Code (DFT, FAP, FCO...) const statusMap: Record = { @@ -187,7 +191,7 @@ export class RfaWorkflowService { await manager.save(revision); this.logger.log( - `Synced RFA Status Revision ${revision.id}: State=${workflowState} -> Status=${targetStatusCode}, AppCode=${approveCodeStr}`, + `Synced RFA Status Revision ${revision.id}: State=${workflowState} -> Status=${targetStatusCode}, AppCode=${approveCodeStr}` ); } } diff --git a/backend/src/modules/rfa/rfa.module.ts b/backend/src/modules/rfa/rfa.module.ts index 29e36ff..dabce1b 100644 --- a/backend/src/modules/rfa/rfa.module.ts +++ b/backend/src/modules/rfa/rfa.module.ts @@ -5,6 +5,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; // Entities import { CorrespondenceRouting } from '../correspondence/entities/correspondence-routing.entity'; import { Correspondence } from '../correspondence/entities/correspondence.entity'; +import { CorrespondenceRecipient } from '../correspondence/entities/correspondence-recipient.entity'; import { RoutingTemplate } from '../correspondence/entities/routing-template.entity'; import { RoutingTemplateStep } from '../correspondence/entities/routing-template-step.entity'; import { ShopDrawingRevision } from '../drawing/entities/shop-drawing-revision.entity'; @@ -47,6 +48,7 @@ import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module' CorrespondenceRouting, RoutingTemplate, RoutingTemplateStep, + CorrespondenceRecipient, ]), DocumentNumberingModule, UserModule, diff --git a/backend/src/modules/rfa/rfa.service.ts b/backend/src/modules/rfa/rfa.service.ts index 62fc42f..999e355 100644 --- a/backend/src/modules/rfa/rfa.service.ts +++ b/backend/src/modules/rfa/rfa.service.ts @@ -148,11 +148,14 @@ export class RfaService { revisionLabel: '0', isCurrent: true, rfaStatusCodeId: statusDraft.id, - title: createDto.title, + subject: createDto.subject, + body: createDto.body, + remarks: createDto.remarks, description: createDto.description, documentDate: createDto.documentDate ? new Date(createDto.documentDate) : new Date(), + dueDate: createDto.dueDate ? new Date(createDto.dueDate) : undefined, createdBy: user.user_id, details: createDto.details, schemaVersion: 1, @@ -209,7 +212,7 @@ export class RfaService { id: savedCorr.id, type: 'rfa', docNumber: docNumber, - title: createDto.title, + title: createDto.subject, description: createDto.description, status: 'DRAFT', projectId: createDto.projectId, @@ -242,10 +245,10 @@ export class RfaService { // [Force Rebuild] const queryBuilder = this.rfaRepo .createQueryBuilder('rfa') + .leftJoinAndSelect('rfa.correspondence', 'corr') .leftJoinAndSelect('rfa.revisions', 'rev') - .leftJoinAndSelect('rev.correspondence', 'corr') .leftJoinAndSelect('corr.project', 'project') - .leftJoinAndSelect('rfa.discipline', 'discipline') + .leftJoinAndSelect('corr.discipline', 'discipline') .leftJoinAndSelect('rev.statusCode', 'status') .leftJoinAndSelect('rev.items', 'items') .leftJoinAndSelect('items.shopDrawingRevision', 'sdRev') @@ -271,7 +274,7 @@ export class RfaService { if (search) { queryBuilder.andWhere( - '(corr.correspondenceNumber LIKE :search OR rev.title LIKE :search)', + '(corr.correspondenceNumber LIKE :search OR rev.subject LIKE :search)', { search: `%${search}%` } ); } @@ -301,11 +304,11 @@ export class RfaService { const rfa = await this.rfaRepo.findOne({ where: { id }, relations: [ + 'correspondence', // ✅ Add relation to master correspondence 'rfaType', 'revisions', 'revisions.statusCode', 'revisions.approveCode', - 'revisions.correspondence', 'revisions.items', 'revisions.items.shopDrawingRevision', 'revisions.items.shopDrawingRevision.shopDrawing', @@ -370,7 +373,7 @@ export class RfaService { // Create First Routing Step const firstStep = steps[0]; const routing = queryRunner.manager.create(CorrespondenceRouting, { - correspondenceId: currentRevision.correspondenceId, + correspondenceId: rfa.correspondence.id, // ✅ Use master correspondence id templateId: template.id, sequence: 1, fromOrganizationId: user.primaryOrganizationId, @@ -392,8 +395,8 @@ export class RfaService { if (recipientUserId) { await this.notificationService.send({ userId: recipientUserId, - title: `RFA Submitted: ${currentRevision.title}`, - message: `RFA ${currentRevision.correspondence.correspondenceNumber} submitted for approval.`, + title: `RFA Submitted: ${currentRevision.subject}`, + message: `RFA ${rfa.correspondence.correspondenceNumber} submitted for approval.`, type: 'SYSTEM', entityType: 'rfa', entityId: rfa.id, @@ -421,7 +424,7 @@ export class RfaService { const currentRouting = await this.routingRepo.findOne({ where: { - correspondenceId: currentRevision.correspondenceId, + correspondenceId: rfa.correspondence.id, // ✅ Use master correspondence id status: 'SENT', }, order: { sequence: 'DESC' }, @@ -482,7 +485,7 @@ export class RfaService { const nextRouting = queryRunner.manager.create( CorrespondenceRouting, { - correspondenceId: currentRevision.correspondenceId, + correspondenceId: rfa.correspondence.id, // ✅ Use master correspondence id templateId: template.id, sequence: result.nextStepSequence, fromOrganizationId: user.primaryOrganizationId, diff --git a/frontend/components/correspondences/detail.tsx b/frontend/components/correspondences/detail.tsx index a45d24e..84f7403 100644 --- a/frontend/components/correspondences/detail.tsx +++ b/frontend/components/correspondences/detail.tsx @@ -28,7 +28,7 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) { // Derive Current Revision Data const currentRevision = data.revisions?.find(r => r.isCurrent) || data.revisions?.[0]; - const subject = currentRevision?.title || "-"; + const subject = currentRevision?.subject || "-"; const description = currentRevision?.description || "-"; const status = currentRevision?.status?.statusCode || "UNKNOWN"; // e.g. DRAFT const attachments = currentRevision?.attachments || []; @@ -169,6 +169,24 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {

+ {currentRevision?.body && ( +
+

Content

+
+ {currentRevision.body} +
+
+ )} + + {currentRevision?.remarks && ( +
+

Remarks

+

+ {currentRevision.remarks} +

+
+ )} +
@@ -223,8 +241,8 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {

Originator

-

{data.originator?.orgName || '-'}

-

{data.originator?.orgCode || '-'}

+

{data.originator?.organizationName || '-'}

+

{data.originator?.organizationCode || '-'}

diff --git a/frontend/components/correspondences/form.tsx b/frontend/components/correspondences/form.tsx index 7e19a60..ee280c9 100644 --- a/frontend/components/correspondences/form.tsx +++ b/frontend/components/correspondences/form.tsx @@ -21,6 +21,8 @@ import { useCreateCorrespondence, useUpdateCorrespondence } from "@/hooks/use-co import { Organization } from "@/types/organization"; import { useOrganizations, useProjects, useCorrespondenceTypes, useDisciplines } from "@/hooks/use-master-data"; import { CreateCorrespondenceDto } from "@/types/dto/correspondence/create-correspondence.dto"; +import { useState, useEffect } from "react"; +import { correspondenceService } from "@/lib/services/correspondence.service"; // Updated Zod Schema with all required fields const correspondenceSchema = z.object({ @@ -29,6 +31,9 @@ const correspondenceSchema = z.object({ disciplineId: z.number().optional(), subject: z.string().min(5, "Subject must be at least 5 characters"), description: z.string().optional(), + body: z.string().optional(), + remarks: z.string().optional(), + dueDate: z.string().optional(), // ISO Date string fromOrganizationId: z.number().min(1, "Please select From Organization"), toOrganizationId: z.number().min(1, "Please select To Organization"), importance: z.enum(["NORMAL", "HIGH", "URGENT"]), @@ -54,10 +59,14 @@ export function CorrespondenceForm({ initialData, id }: { initialData?: any, id? projectId: initialData?.projectId || undefined, documentTypeId: initialData?.correspondenceTypeId || undefined, disciplineId: initialData?.disciplineId || undefined, - subject: currentRev?.title || "", + subject: currentRev?.subject || currentRev?.title || "", description: currentRev?.description || "", + body: currentRev?.body || "", + remarks: currentRev?.remarks || "", + dueDate: currentRev?.dueDate ? new Date(currentRev.dueDate).toISOString().split('T')[0] : undefined, fromOrganizationId: initialData?.originatorId || undefined, - toOrganizationId: currentRev?.details?.to_organization_id || undefined, + // Map initial recipient (TO) - Simplified for now + toOrganizationId: initialData?.recipients?.find((r: any) => r.recipientType === 'TO')?.recipientOrganizationId || undefined, importance: currentRev?.details?.importance || "NORMAL", }; @@ -84,11 +93,16 @@ export function CorrespondenceForm({ initialData, id }: { initialData?: any, id? projectId: data.projectId, typeId: data.documentTypeId, disciplineId: data.disciplineId, - title: data.subject, + subject: data.subject, description: data.description, + body: data.body, + remarks: data.remarks, + dueDate: data.dueDate ? new Date(data.dueDate).toISOString() : undefined, originatorId: data.fromOrganizationId, + recipients: [ + { organizationId: data.toOrganizationId, type: 'TO' } + ], details: { - to_organization_id: data.toOrganizationId, importance: data.importance }, }; @@ -108,8 +122,56 @@ export function CorrespondenceForm({ initialData, id }: { initialData?: any, id? const isPending = createMutation.isPending || updateMutation.isPending; + // -- Preview Logic -- + const [preview, setPreview] = useState<{ number: string; isDefaultTemplate: boolean } | null>(null); + + useEffect(() => { + if (!projectId || !documentTypeId || !fromOrgId || !toOrgId) { + setPreview(null); + return; + } + + const fetchPreview = async () => { + try { + const res = await correspondenceService.previewNumber({ + projectId, + typeId: documentTypeId, + disciplineId, + originatorId: fromOrgId, + // Map recipients structure matching backend expectation + recipients: [{ organizationId: toOrgId, type: 'TO' }], + // Add date just to be safe, though service uses 'now' + dueDate: new Date().toISOString() + }); + setPreview(res); + } catch (err) { + setPreview(null); + } + }; + + const timer = setTimeout(fetchPreview, 500); + return () => clearTimeout(timer); + }, [projectId, documentTypeId, disciplineId, fromOrgId, toOrgId]); + + + return (
+ {/* Preview Section */} + {preview && ( +
+

Document Number Preview

+
+ {preview.number} + {preview.isDefaultTemplate && ( + + Default Template + + )} +
+
+ )} + {/* Document Metadata Section */}
{/* Project Dropdown */} @@ -191,14 +253,37 @@ export function CorrespondenceForm({ initialData, id }: { initialData?: any, id? )}
+ {/* Body */} +
+ +