251213:1509 Docunment Number Businee Rule not correct
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled

This commit is contained in:
admin
2025-12-13 15:09:01 +07:00
parent d964546c8d
commit ec35521258
64 changed files with 11956 additions and 223 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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<typeof this.correspondenceService.create>[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<typeof this.correspondenceService.create>[1]
);
}
@Get(':id/references')
@ApiOperation({ summary: 'Get referenced documents' })
@ApiResponse({

View File

@@ -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,

View File

@@ -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();
});
});
});

View File

@@ -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<CorrespondenceStatus>,
@InjectRepository(CorrespondenceReference)
private referenceRepo: Repository<CorrespondenceReference>,
@InjectRepository(Organization)
private orgRepo: Repository<Organization>,
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<Correspondence> = {};
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<CorrespondenceRevision> = {};
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,
},
});
}
}

View File

@@ -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: '<p>...</p>',
})
@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' }[];
}

View File

@@ -0,0 +1,6 @@
import { PartialType } from '@nestjs/swagger';
import { CreateCorrespondenceDto } from './create-correspondence.dto';
export class UpdateCorrespondenceDto extends PartialType(
CreateCorrespondenceDto
) {}

View File

@@ -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;
}

View File

@@ -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',

View File

@@ -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[];
}

View File

@@ -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<string> {
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
*/

View File

@@ -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: '<p>...</p>' })
@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()

View File

@@ -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()

View File

@@ -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)

View File

@@ -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;

View File

@@ -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;

View File

@@ -31,7 +31,7 @@ export class RfaWorkflowService {
private readonly statusRepo: Repository<RfaStatusCode>,
@InjectRepository(RfaApproveCode)
private readonly approveCodeRepo: Repository<RfaApproveCode>,
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<string, string> = {
@@ -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}`
);
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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) {
</p>
</div>
{currentRevision?.body && (
<div>
<h3 className="font-semibold mb-2">Content</h3>
<div className="text-gray-700 whitespace-pre-wrap p-3 bg-muted/10 rounded-md border">
{currentRevision.body}
</div>
</div>
)}
{currentRevision?.remarks && (
<div>
<h3 className="font-semibold mb-2">Remarks</h3>
<p className="text-gray-600 italic">
{currentRevision.remarks}
</p>
</div>
)}
<hr className="my-4 border-t" />
<div>
@@ -223,8 +241,8 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {
<div>
<p className="text-sm font-medium text-muted-foreground">Originator</p>
<p className="font-medium mt-1">{data.originator?.orgName || '-'}</p>
<p className="text-xs text-muted-foreground">{data.originator?.orgCode || '-'}</p>
<p className="font-medium mt-1">{data.originator?.organizationName || '-'}</p>
<p className="text-xs text-muted-foreground">{data.originator?.organizationCode || '-'}</p>
</div>
<div>

View File

@@ -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 (
<form onSubmit={handleSubmit(onSubmit)} className="max-w-3xl space-y-6">
{/* Preview Section */}
{preview && (
<div className="p-4 rounded-md bg-muted border border-border">
<p className="text-sm text-muted-foreground mb-1">Document Number Preview</p>
<div className="flex items-center gap-3">
<span className="text-xl font-bold font-mono text-primary tracking-wide">{preview.number}</span>
{preview.isDefaultTemplate && (
<span className="text-[10px] uppercase font-semibold px-2 py-0.5 rounded-full bg-yellow-100 text-yellow-800 border border-yellow-200">
Default Template
</span>
)}
</div>
</div>
)}
{/* Document Metadata Section */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Project Dropdown */}
@@ -191,14 +253,37 @@ export function CorrespondenceForm({ initialData, id }: { initialData?: any, id?
)}
</div>
{/* Body */}
<div className="space-y-2">
<Label htmlFor="body">Body (Content)</Label>
<Textarea
id="body"
{...register("body")}
rows={6}
placeholder="Enter letter content..."
/>
</div>
{/* Remarks & Due Date */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="remarks">Remarks</Label>
<Input id="remarks" {...register("remarks")} placeholder="Optional remarks" />
</div>
<div className="space-y-2">
<Label htmlFor="dueDate">Due Date</Label>
<Input id="dueDate" type="date" {...register("dueDate")} />
</div>
</div>
{/* Description */}
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Label htmlFor="description">Description (Internal Note)</Label>
<Textarea
id="description"
{...register("description")}
rows={4}
placeholder="Enter description details..."
rows={2}
placeholder="Enter description..."
/>
</div>

View File

@@ -30,11 +30,11 @@ export function CorrespondenceList({ data }: CorrespondenceListProps) {
),
},
{
accessorKey: "title",
accessorKey: "subject",
header: "Subject",
cell: ({ row }) => (
<div className="max-w-[300px] truncate" title={row.original.title}>
{row.original.title}
<div className="max-w-[300px] truncate" title={row.original.subject}>
{row.original.subject}
</div>
),
},

View File

@@ -5,6 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { Plus, Trash2, Loader2 } from "lucide-react";
@@ -19,6 +20,8 @@ import { useRouter } from "next/navigation";
import { useCreateRFA } from "@/hooks/use-rfa";
import { useDisciplines, useContracts } from "@/hooks/use-master-data";
import { CreateRFADto } from "@/types/rfa";
import { useState, useEffect } from "react";
import { correspondenceService } from "@/lib/services/correspondence.service";
const rfaItemSchema = z.object({
itemNo: z.string().min(1, "Item No is required"),
@@ -30,8 +33,10 @@ const rfaSchema = z.object({
contractId: z.number().min(1, "Contract is required"),
disciplineId: z.number().min(1, "Discipline is required"),
rfaTypeId: z.number().min(1, "Type is required"),
title: z.string().min(5, "Title must be at least 5 characters"),
subject: z.string().min(5, "Subject must be at least 5 characters"),
description: z.string().optional(),
body: z.string().optional(),
remarks: z.string().optional(),
toOrganizationId: z.number().min(1, "Please select To Organization"),
dueDate: z.string().optional(),
shopDrawingRevisionIds: z.array(z.number()).optional(),
@@ -61,8 +66,10 @@ export function RFAForm() {
contractId: 0,
disciplineId: 0,
rfaTypeId: 0,
title: "",
subject: "",
description: "",
body: "",
remarks: "",
toOrganizationId: 0,
dueDate: "",
shopDrawingRevisionIds: [],
@@ -73,6 +80,40 @@ export function RFAForm() {
const selectedContractId = watch("contractId");
const { data: disciplines, isLoading: isLoadingDisciplines } = useDisciplines(selectedContractId);
// Watch fields for preview
const rfaTypeId = watch("rfaTypeId");
const disciplineId = watch("disciplineId");
const toOrganizationId = watch("toOrganizationId");
// -- Preview Logic --
const [preview, setPreview] = useState<{ number: string; isDefaultTemplate: boolean } | null>(null);
useEffect(() => {
if (!rfaTypeId || !disciplineId || !toOrganizationId) {
setPreview(null);
return;
}
const fetchPreview = async () => {
try {
const res = await correspondenceService.previewNumber({
projectId: currentProjectId,
typeId: rfaTypeId, // RfaTypeId acts as TypeId
disciplineId,
// RFA uses 'TO' organization as recipient
recipients: [{ organizationId: toOrganizationId, type: 'TO' }],
dueDate: new Date().toISOString()
});
setPreview(res);
} catch (err) {
setPreview(null);
}
};
const timer = setTimeout(fetchPreview, 500);
return () => clearTimeout(timer);
}, [rfaTypeId, disciplineId, toOrganizationId, currentProjectId]);
const { fields, append, remove } = useFieldArray({
control,
name: "items",
@@ -92,24 +133,49 @@ export function RFAForm() {
return (
<form onSubmit={handleSubmit(onSubmit)} className="max-w-4xl space-y-6">
{/* Preview Section */}
{preview && (
<Card className="p-4 bg-muted border-l-4 border-l-primary">
<p className="text-sm text-muted-foreground mb-1">Document Number Preview</p>
<div className="flex items-center gap-3">
<span className="text-xl font-bold font-mono text-primary tracking-wide">{preview.number}</span>
{preview.isDefaultTemplate && (
<span className="text-[10px] uppercase font-semibold px-2 py-0.5 rounded-full bg-yellow-100 text-yellow-800 border border-yellow-200">
Default Template
</span>
)}
</div>
</Card>
)}
{/* Basic Info */}
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">RFA Information</h3>
<div className="space-y-4">
<div>
<Label htmlFor="title">Title *</Label>
<Input id="title" {...register("title")} placeholder="Enter title" />
{errors.title && (
<Label htmlFor="subject">Subject *</Label>
<Input id="subject" {...register("subject")} placeholder="Enter subject" />
{errors.subject && (
<p className="text-sm text-destructive mt-1">
{errors.title.message}
{errors.subject.message}
</p>
)}
</div>
<div>
<Label htmlFor="body">Body (Content)</Label>
<Textarea id="body" {...register("body")} rows={4} placeholder="Enter content..." />
</div>
<div>
<Label htmlFor="remarks">Remarks</Label>
<Input id="remarks" {...register("remarks")} placeholder="Optional remarks" />
</div>
<div>
<Label htmlFor="description">Description</Label>
<Input id="description" {...register("description")} placeholder="Enter description" />
<Input id="description" {...register("description")} placeholder="Enter key description" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">

View File

@@ -21,8 +21,7 @@ export function RFAList({ data }: RFAListProps) {
accessorKey: "rfa_number",
header: "RFA No.",
cell: ({ row }) => {
const rev = row.original.revisions?.[0];
return <span className="font-medium">{rev?.correspondence?.correspondenceNumber || '-'}</span>;
return <span className="font-medium">{row.original.correspondence?.correspondenceNumber || '-'}</span>;
},
},
{
@@ -31,8 +30,8 @@ export function RFAList({ data }: RFAListProps) {
cell: ({ row }) => {
const rev = row.original.revisions?.[0];
return (
<div className="max-w-[300px] truncate" title={rev?.title}>
{rev?.title || '-'}
<div className="max-w-[300px] truncate" title={rev?.subject}>
{rev?.subject || '-'}
</div>
);
},
@@ -41,8 +40,7 @@ export function RFAList({ data }: RFAListProps) {
accessorKey: "contract_name", // AccessorKey can be anything if we provide cell
header: "Contract",
cell: ({ row }) => {
const rev = row.original.revisions?.[0];
return <span>{rev?.correspondence?.project?.projectName || '-'}</span>;
return <span>{row.original.correspondence?.project?.projectName || '-'}</span>;
},
},
{
@@ -54,7 +52,10 @@ export function RFAList({ data }: RFAListProps) {
accessorKey: "createdAt",
header: "Created",
cell: ({ row }) => {
const date = row.original.revisions?.[0]?.correspondence?.createdAt;
const date = row.original.correspondence?.createdAt || row.original.revisions?.[0]?.createdAt; // Fallback or strict?
// In backend I set RFA -> Correspondence (createdAt is in Correspondence base)
// But RFA revision also has createdAt?
// Use correspondence.createdAt usually for document date.
return date ? format(new Date(date), "dd MMM yyyy") : '-';
},
},

View File

@@ -70,5 +70,12 @@ export const correspondenceService = {
data: data
});
return response.data;
},
/**
* Preview Document Number
*/
previewNumber: async (data: Partial<CreateCorrespondenceDto>) => {
const response = await apiClient.post("/correspondences/preview-number", data);
return response.data;
}
};

View File

@@ -18,7 +18,11 @@ export interface CorrespondenceRevision {
id: number;
revisionNumber: number;
revisionLabel?: string; // e.g. "A", "00"
title: string;
subject: string;
body?: string;
remarks?: string;
dueDate?: string;
schemaVersion?: number;
description?: string;
isCurrent: boolean;
status?: {
@@ -40,7 +44,7 @@ export interface CorrespondenceRevision {
originator?: Organization;
project?: { id: number; projectName: string; projectCode: string };
type?: { id: number; typeName: string; typeCode: string };
}
};
}
// Keep explicit Correspondence for Detail View if needed, or merge concepts
@@ -58,14 +62,27 @@ export interface Correspondence {
project?: { id: number; projectName: string; projectCode: string };
type?: { id: number; typeName: string; typeCode: string };
revisions?: CorrespondenceRevision[]; // Nested revisions
recipients?: {
correspondenceId: number;
recipientOrganizationId: number;
recipientType: 'TO' | 'CC';
recipientOrganization?: Organization;
}[];
}
export interface CreateCorrespondenceDto {
projectId: number;
typeId: number;
subTypeId?: number;
disciplineId?: number;
subject: string;
body?: string;
remarks?: string;
dueDate?: string;
description?: string;
documentTypeId: number;
fromOrganizationId: number;
toOrganizationId: number;
importance: "NORMAL" | "HIGH" | "URGENT";
details?: Record<string, any>;
isInternal?: boolean;
originatorId?: number;
recipients?: { organizationId: number; type: 'TO' | 'CC' }[];
attachments?: File[];
}

View File

@@ -5,20 +5,29 @@ export interface CreateCorrespondenceDto {
projectId: number;
/** ID ของประเภทเอกสาร (เช่น RFA, LETTER) */
typeId: number;
typeId: number;
/** [Req 6B] สาขางาน (เช่น GEN, STR) */
disciplineId?: number;
disciplineId?: number;
/** [Req 6B] ประเภทย่อย (เช่น MAT, SHP สำหรับ Transmittal/RFA) */
subTypeId?: number;
/** หัวข้อเอกสาร */
title: string;
subject: string;
/** รายละเอียดเพิ่มเติม (Optional) */
description?: string;
/** เนื้อหาเอกสาร (Rich Text) */
body?: string;
/** หมายเหตุ */
remarks?: string;
/** กำหนดวันตอบกลับ (ISO Date String) */
dueDate?: string;
/** ข้อมูล JSON เฉพาะประเภท (เช่น RFI question, RFA details) */
details?: Record<string, any>;
@@ -29,4 +38,7 @@ export interface CreateCorrespondenceDto {
* ใช้กรณี Admin สร้างเอกสารแทนผู้อื่น
*/
originatorId?: number;
}
/** รายชื่อผู้รับ */
recipients?: { organizationId: number; type: 'TO' | 'CC' }[];
}

View File

@@ -12,7 +12,13 @@ export interface CreateRfaDto {
disciplineId?: number;
/** หัวข้อเรื่อง */
title: string;
subject: string;
/** เนื้อหา (Rich Text) */
body?: string;
/** หมายเหตุ */
remarks?: string;
/** ส่งถึงใคร (สำหรับ Routing Step 1) */
toOrganizationId: number;

View File

@@ -8,13 +8,22 @@ export interface RFAItem {
}
export interface RFA {
id: number;
id: number; // Shared PK with Correspondence
rfaTypeId: number;
createdBy: number;
disciplineId?: number;
revisions: {
id: number;
revisionNumber: number;
subject: string;
isCurrent: boolean;
createdAt?: string;
statusCode?: { statusCode: string; statusName: string };
items?: {
shopDrawingRevision?: {
id: number;
revisionLabel: string;
shopDrawing?: { drawingType?: { hasNumber: boolean } }; // Mock structure
attachments?: { id: number; url: string; name: string }[]
}
}[];
@@ -24,25 +33,33 @@ export interface RFA {
name: string;
code: string;
};
// Deprecated/Mapped fields (keep optional if frontend uses them elsewhere)
rfaId?: number;
rfaNumber?: string;
subject?: string;
status?: string;
createdAt?: string;
contractName?: string;
disciplineName?: string;
// Shared Correspondence Relation
correspondence?: {
id: number;
correspondenceNumber: string;
projectId: number;
originatorId?: number;
createdAt?: string;
project?: {
projectName: string;
projectCode: string;
};
};
// Deprecated/Mapped fields
correspondenceNumber?: string; // Convenience accessor
}
export interface CreateRFADto {
projectId?: number;
projectId: number;
rfaTypeId: number;
title: string;
disciplineId?: number;
subject: string;
body?: string; // [New]
remarks?: string; // [New]
dueDate?: string; // [New]
description?: string;
contractId: number;
disciplineId: number;
toOrganizationId: number;
dueDate?: string;
documentDate?: string;
details?: Record<string, any>;
shopDrawingRevisionIds?: number[];
items: RFAItem[];
}

View File

@@ -455,7 +455,7 @@ CREATE TABLE correspondence_revisions (
received_date DATETIME COMMENT 'วันที่ลงรับเอกสาร',
due_date DATETIME COMMENT 'วันที่ครบกำหนด',
description TEXT COMMENT 'คำอธิบายการแก้ไขใน Revision นี้',
details JSON COMMENT 'ข้อมูลเฉพาะ (เช่น RFI details)',
details JSON COMMENT 'ข้อมูลเฉพาะ (เช่น LETTER details)',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้างเอกสาร',
created_by INT COMMENT 'ผู้สร้าง',
updated_by INT COMMENT 'ผู้แก้ไขล่าสุด',

View File

@@ -1,9 +1,9 @@
# LCBP3-DMS - Project Overview
**Project Name:** Laem Chabang Port Phase 3 - Document Management System (LCBP3-DMS)
**Version:** 1.5.1
**Status:** Active Development (~80% Complete)
**Last Updated:** 2025-12-09
**Version:** 1.6.0
**Status:** Active Development (~95% Complete)
**Last Updated:** 2025-12-13
---
@@ -188,7 +188,7 @@ lcbp3/
│ ├── 04-operations/ # Deployment & operations
│ ├── 05-decisions/ # Architecture Decision Records (17 ADRs)
│ ├── 06-tasks/ # Development tasks & progress
│ ├── 07-database/ # Database schema v1.5.1 & seed data
│ ├── 07-database/ # Database schema v1.6.0 & seed data
│ └── 09-history/ # Archived implementations
├── docker-compose.yml # Docker services configuration
@@ -384,9 +384,9 @@ lcbp3/
## 📝 Document Control
- **Version:** 1.5.1
- **Version:** 1.6.0
- **Status:** Active Development
- **Last Updated:** 2025-12-09
- **Last Updated:** 2025-12-13
- **Next Review:** 2026-01-01
- **Owner:** System Architect
- **Classification:** Internal Use Only
@@ -397,6 +397,7 @@ lcbp3/
| Version | Date | Description |
| ------- | ---------- | ------------------------------------------ |
| 1.6.0 | 2025-12-13 | Schema refactoring, documentation updated |
| 1.5.1 | 2025-12-09 | TASK-FE-011/012 completed, docs updated |
| 1.5.1 | 2025-12-02 | Reorganized documentation structure |
| 1.5.0 | 2025-12-01 | Complete specification with ADRs and tasks |

View File

@@ -1,7 +1,7 @@
# Glossary - คำศัพท์และคำย่อทางเทคนิค
**Project:** LCBP3-DMS
**Version:** 1.5.1
**Version:** 1.6.0
**Last Updated:** 2025-12-02
---
@@ -471,26 +471,26 @@ Logging library สำหรับ Node.js
## 📚 Acronyms Reference (อ้างอิงตัวย่อ)
| Acronym | Full Form | Thai |
| ------- | --------------------------------- | ------------------------------- |
| Acronym | Full Form | Thai |
| ------- | --------------------------------- | -------------------------- |
| ADR | Architecture Decision Record | บันทึกการตัดสินใจทางสถาปัตยกรรม |
| API | Application Programming Interface | ส่วนต่อประสานโปรแกรม |
| CRUD | Create, Read, Update, Delete | สร้าง อ่าน แก้ไข ลบ |
| DMS | Document Management System | ระบบจัดการเอกสาร |
| API | Application Programming Interface | ส่วนต่อประสานโปรแกรม |
| CRUD | Create, Read, Update, Delete | สร้าง อ่าน แก้ไข ลบ |
| DMS | Document Management System | ระบบจัดการเอกสาร |
| DTO | Data Transfer Object | วัตถุถ่ายโอนข้อมูล |
| JWT | JSON Web Token | โทเคนเว็บ JSON |
| JWT | JSON Web Token | โทเคนเว็บ JSON |
| LCBP3 | Laem Chabang Port Phase 3 | ท่าเรือแหลมฉบังระยะที่ 3 |
| MVP | Minimum Viable Product | ผลิตภัณฑ์ขั้นต่ำที่ใช้งานได้ |
| MVP | Minimum Viable Product | ผลิตภัณฑ์ขั้นต่ำที่ใช้งานได้ |
| ORM | Object-Relational Mapping | การแมปวัตถุกับฐานข้อมูล |
| RBAC | Role-Based Access Control | การควบคุมการเข้าถึงตามบทบาท |
| REST | Representational State Transfer | การถ่ายโอนสถานะแบบนำเสนอ |
| RFA | Request for Approval | เอกสารขออนุมัติ |
| RTO | Recovery Time Objective | เวลาเป้าหมายในการกู้คืน |
| RBAC | Role-Based Access Control | การควบคุมการเข้าถึงตามบทบาท |
| REST | Representational State Transfer | การถ่ายโอนสถานะแบบนำเสนอ |
| RFA | Request for Approval | เอกสารขออนุมัติ |
| RTO | Recovery Time Objective | เวลาเป้าหมายในการกู้คืน |
| RPO | Recovery Point Objective | จุดเป้าหมายในการกู้คืน |
| UAT | User Acceptance Testing | การทดสอบการยอมรับของผู้ใช้ |
| UAT | User Acceptance Testing | การทดสอบการยอมรับของผู้ใช้ |
---
**Version:** 1.5.1
**Version:** 1.6.0
**Last Updated:** 2025-12-02
**Next Review:** 2026-03-01

View File

@@ -1,7 +1,7 @@
# Quick Start Guide
**Project:** LCBP3-DMS
**Version:** 1.5.1
**Version:** 1.6.0
**Last Updated:** 2025-12-02
---
@@ -579,5 +579,5 @@ git push origin feature/my-feature
**Welcome aboard! 🎉**
**Version:** 1.5.1
**Version:** 1.6.0
**Last Updated:** 2025-12-02

View File

@@ -2,7 +2,7 @@
---
title: 'Functional Requirements: Document Numbering Management'
version: 1.5.1
version: 1.6.0
status: draft
owner: Nattanin Peancharoen
last_updated: 2025-12-02

View File

@@ -1,8 +1,8 @@
# 📋 Requirements Specification
**Version:** 1.5.1
**Version:** 1.6.0
**Status:** Active
**Last Updated:** 2025-12-02
**Last Updated:** 2025-12-13
---
@@ -66,6 +66,12 @@ This directory contains the functional and non-functional requirements for the L
## 🔄 Recent Changes
### v1.6.0 (2025-12-13)
-**Schema Refactoring** - Major restructuring of correspondence and RFA tables
- ✅ Updated data dictionary for schema v1.6.0
- ✅ Breaking changes documented in CHANGELOG.md
### v1.5.1 (2025-12-02)
-**Reorganized Document Numbering documentation**
@@ -172,8 +178,8 @@ All requirements documents must meet these criteria:
## 📝 Document Control
- **Version:** 1.5.1
- **Version:** 1.6.0
- **Owner:** System Architect (Nattanin Peancharoen)
- **Last Review:** 2025-12-10
- **Last Review:** 2025-12-13
- **Next Review:** 2026-01-01
- **Classification:** Internal Use Only

View File

@@ -10,9 +10,9 @@
| Attribute | Value |
| ------------------ | -------------------------------- |
| **Version** | 1.5.1 |
| **Version** | 1.6.0 |
| **Status** | Active |
| **Last Updated** | 2025-12-02 |
| **Last Updated** | 2025-12-13 |
| **Owner** | Nattanin Peancharoen |
| **Classification** | Internal Technical Documentation |
@@ -463,6 +463,7 @@ sequenceDiagram
| Version | Date | Author | Changes |
| ------- | ---------- | ----------- | ---------------------------------- |
| 1.6.0 | 2025-12-13 | Nattanin P. | Schema refactoring v1.6.0 |
| 1.5.0 | 2025-11-30 | Nattanin P. | Initial architecture specification |
| 1.4.5 | 2025-11-29 | - | Added security requirements |
| 1.4.4 | 2025-11-28 | - | Enhanced resilience patterns |
@@ -489,7 +490,7 @@ sequenceDiagram
<div align="center">
**LCBP3-DMS Architecture Specification v1.5.0**
**LCBP3-DMS Architecture Specification v1.6.0**
[System Architecture](./system-architecture.md) • [API Design](./api-design.md) • [Data Model](./data-model.md)

View File

@@ -3,7 +3,7 @@
---
**title:** 'API Design'
**version:** 1.5.1
**version:** 1.6.0
**status:** active
**owner:** Nattanin Peancharoen
**last_updated:** 2025-12-02
@@ -546,7 +546,7 @@ X-API-Deprecation-Info: https://docs.np-dms.work/migration/v2
**Document Control:**
- **Version:** 1.5.1
- **Version:** 1.6.0
- **Status:** Active
- **Last Updated:** 2025-12-02
- **Last Updated:** 2025-12-13
- **Owner:** Nattanin Peancharoen

View File

@@ -943,9 +943,9 @@ graph LR
**Document Control:**
- **Version:** 1.5.1
- **Version:** 1.6.0
- **Status:** Active
- **Last Updated:** 2025-12-02
- **Last Updated:** 2025-12-13
- **Owner:** Nattanin Peancharoen
```

View File

@@ -1,7 +1,7 @@
# Operations Documentation
**Project:** LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)
**Version:** 1.5.1
**Version:** 1.6.0
**Last Updated:** 2025-12-02
---
@@ -185,6 +185,6 @@ graph TB
---
**Version:** 1.5.1
**Version:** 1.6.0
**Status:** Active
**Classification:** Internal Use Only

View File

@@ -1,7 +1,7 @@
# Backup & Recovery Procedures
**Project:** LCBP3-DMS
**Version:** 1.5.1
**Version:** 1.6.0
**Last Updated:** 2025-12-02
---
@@ -369,6 +369,6 @@ WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 YEAR);
---
**Version:** 1.5.1
**Version:** 1.6.0
**Last Review:** 2025-12-01
**Next Review:** 2026-03-01

View File

@@ -3,7 +3,7 @@
---
**Project:** LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)
**Version:** 1.5.1
**Version:** 1.6.0
**Last Updated:** 2025-12-02
**Owner:** Operations Team
**Status:** Active
@@ -932,6 +932,6 @@ docker exec lcbp3-mariadb mysql -u root -p -e "
---
**Version:** 1.5.1
**Version:** 1.6.0
**Last Updated:** 2025-12-02
**Next Review:** 2026-06-01

View File

@@ -2,7 +2,7 @@
---
title: 'Operations Guide: Document Numbering System'
version: 1.5.1
version: 1.6.0
status: draft
owner: Operations Team
last_updated: 2025-12-02

View File

@@ -1,7 +1,7 @@
# Environment Setup & Configuration
**Project:** LCBP3-DMS
**Version:** 1.5.1
**Version:** 1.6.0
**Last Updated:** 2025-12-02
---
@@ -458,6 +458,6 @@ docker exec lcbp3-backend env | grep NODE_ENV
---
**Version:** 1.5.1
**Version:** 1.6.0
**Last Review:** 2025-12-01
**Next Review:** 2026-03-01

View File

@@ -1,7 +1,7 @@
# Incident Response Procedures
**Project:** LCBP3-DMS
**Version:** 1.5.1
**Version:** 1.6.0
**Last Updated:** 2025-12-02
---
@@ -478,6 +478,6 @@ Database connection pool was exhausted due to slow queries not releasing connect
---
**Version:** 1.5.1
**Version:** 1.6.0
**Last Review:** 2025-12-01
**Next Review:** 2026-03-01

View File

@@ -1,7 +1,7 @@
# Maintenance Procedures
**Project:** LCBP3-DMS
**Version:** 1.5.1
**Version:** 1.6.0
**Last Updated:** 2025-12-02
---
@@ -496,6 +496,6 @@ echo "Security maintenance completed: $(date)"
---
**Version:** 1.5.1
**Version:** 1.6.0
**Last Review:** 2025-12-01
**Next Review:** 2026-03-01

View File

@@ -1,7 +1,7 @@
# Monitoring & Alerting
**Project:** LCBP3-DMS
**Version:** 1.5.1
**Version:** 1.6.0
**Last Updated:** 2025-12-02
---
@@ -438,6 +438,6 @@ ab -n 1000 -c 10 \
---
**Version:** 1.5.1
**Version:** 1.6.0
**Last Review:** 2025-12-01
**Next Review:** 2026-03-01

View File

@@ -1,7 +1,7 @@
# Security Operations
**Project:** LCBP3-DMS
**Version:** 1.5.1
**Version:** 1.6.0
**Last Updated:** 2025-12-02
---
@@ -439,6 +439,6 @@ echo "Account compromise response completed for User ID: $USER_ID"
---
**Version:** 1.5.1
**Version:** 1.6.0
**Last Review:** 2025-12-01
**Next Review:** 2026-03-01

View File

@@ -1,6 +1,6 @@
# Architecture Decision Records (ADRs)
**Version:** 1.5.1
**Version:** 1.6.0
**Last Updated:** 2025-12-02
**Project:** LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)
@@ -28,45 +28,45 @@ Architecture Decision Records (ADRs) เป็นเอกสารที่บ
### Core Architecture Decisions
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | ------------------------------- | ----------- | ---------- | ---------------------------------------------------------------------------- |
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | ------------------------------- | ---------- | ---------- | ------------------------------------------------------------------------- |
| [ADR-001](./ADR-001-unified-workflow-engine.md) | Unified Workflow Engine | ✅ Accepted | 2025-11-30 | ใช้ DSL-based Workflow Engine สำหรับ Correspondences, RFAs, และ Circulations |
| [ADR-002](./ADR-002-document-numbering-strategy.md) | Document Numbering Strategy | ✅ Accepted | 2025-11-30 | Double-lock mechanism (Redis + DB Optimistic Lock) สำหรับเลขที่เอกสาร |
| [ADR-003](./ADR-003-file-storage-approach.md) | Two-Phase File Storage Approach | ✅ Accepted | 2025-11-30 | Upload → Temp → Commit to Permanent เพื่อป้องกัน Orphan Files |
| [ADR-002](./ADR-002-document-numbering-strategy.md) | Document Numbering Strategy | ✅ Accepted | 2025-11-30 | Double-lock mechanism (Redis + DB Optimistic Lock) สำหรับเลขที่เอกสาร |
| [ADR-003](./ADR-003-file-storage-approach.md) | Two-Phase File Storage Approach | ✅ Accepted | 2025-11-30 | Upload → Temp → Commit to Permanent เพื่อป้องกัน Orphan Files |
### Security & Access Control
| ADR | Title | Status | Date | Summary |
| ------------------------------------------- | ----------------------------- | ----------- | ---------- | ------------------------------------------------------------- |
| ADR | Title | Status | Date | Summary |
| ------------------------------------------- | ----------------------------- | ---------- | ---------- | ------------------------------------------------------------- |
| [ADR-004](./ADR-004-rbac-implementation.md) | RBAC Implementation (4-Level) | ✅ Accepted | 2025-11-30 | Hierarchical RBAC: Global → Organization → Project → Contract |
### Technology & Infrastructure
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | ------------------------------------ | ----------- | ---------- | -------------------------------------------------------------- |
| [ADR-005](./ADR-005-technology-stack.md) | Technology Stack Selection | ✅ Accepted | 2025-11-30 | Full Stack TypeScript: NestJS + Next.js + MariaDB + Redis |
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | ------------------------------------ | ---------- | ---------- | ------------------------------------------------------------ |
| [ADR-005](./ADR-005-technology-stack.md) | Technology Stack Selection | ✅ Accepted | 2025-11-30 | Full Stack TypeScript: NestJS + Next.js + MariaDB + Redis |
| [ADR-006](./ADR-006-redis-caching-strategy.md) | Redis Usage & Caching Strategy | ✅ Accepted | 2025-11-30 | Redis สำหรับ Distributed Lock, Cache, Queue, และ Rate Limiting |
| [ADR-009](./ADR-009-database-migration-strategy.md) | Database Migration & Deployment | ✅ Accepted | 2025-12-01 | TypeORM Migrations พร้อม Blue-Green Deployment |
| [ADR-015](./ADR-015-deployment-infrastructure.md) | Deployment & Infrastructure Strategy | ✅ Accepted | 2025-12-01 | Docker Compose with Blue-Green Deployment on QNAP |
| [ADR-016](./ADR-016-security-authentication.md) | Security & Authentication Strategy | ✅ Accepted | 2025-12-01 | JWT + bcrypt + OWASP Security Best Practices |
| [ADR-009](./ADR-009-database-migration-strategy.md) | Database Migration & Deployment | ✅ Accepted | 2025-12-01 | TypeORM Migrations พร้อม Blue-Green Deployment |
| [ADR-015](./ADR-015-deployment-infrastructure.md) | Deployment & Infrastructure Strategy | ✅ Accepted | 2025-12-01 | Docker Compose with Blue-Green Deployment on QNAP |
| [ADR-016](./ADR-016-security-authentication.md) | Security & Authentication Strategy | ✅ Accepted | 2025-12-01 | JWT + bcrypt + OWASP Security Best Practices |
### API & Integration
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | ----------------------------- | ----------- | ---------- | ----------------------------------------------------------------------------- |
| [ADR-007](./ADR-007-api-design-error-handling.md) | API Design & Error Handling | ✅ Accepted | 2025-12-01 | Standard REST API with Custom Error Format + NestJS Exception Filters |
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | ----------------------------- | ---------- | ---------- | --------------------------------------------------------------------------- |
| [ADR-007](./ADR-007-api-design-error-handling.md) | API Design & Error Handling | ✅ Accepted | 2025-12-01 | Standard REST API with Custom Error Format + NestJS Exception Filters |
| [ADR-008](./ADR-008-email-notification-strategy.md) | Email & Notification Strategy | ✅ Accepted | 2025-12-01 | BullMQ + Redis Queue สำหรับ Multi-channel Notifications (Email, LINE, In-app) |
### Observability
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | ----------------------------- | ----------- | ---------- | ------------------------------------------------------------- |
| ADR | Title | Status | Date | Summary |
| --------------------------------------------------- | ----------------------------- | ---------- | ---------- | ------------------------------------------------------------ |
| [ADR-010](./ADR-010-logging-monitoring-strategy.md) | Logging & Monitoring Strategy | ✅ Accepted | 2025-12-01 | Winston Structured Logging พร้อม Future ELK Stack Integration |
### Frontend Architecture
| ADR | Title | Status | Date | Summary |
| ------------------------------------------------ | -------------------------------- | ----------- | ---------- | ----------------------------------------------------- |
| ADR | Title | Status | Date | Summary |
| ------------------------------------------------ | -------------------------------- | ---------- | ---------- | ----------------------------------------------------- |
| [ADR-011](./ADR-011-nextjs-app-router.md) | Next.js App Router & Routing | ✅ Accepted | 2025-12-01 | App Router with Server Components and Nested Layouts |
| [ADR-012](./ADR-012-ui-component-library.md) | UI Component Library (Shadcn/UI) | ✅ Accepted | 2025-12-01 | Shadcn/UI + Tailwind CSS for Full Component Ownership |
| [ADR-013](./ADR-013-form-handling-validation.md) | Form Handling & Validation | ✅ Accepted | 2025-12-01 | React Hook Form + Zod for Type-Safe Forms |
@@ -356,5 +356,5 @@ graph TB
---
**Version:** 1.5.1
**Version:** 1.6.0
**Last Review:** 2025-12-02

View File

@@ -0,0 +1,251 @@
# Task: Backend Schema v1.6.0 Migration
**Status:** ✅ Completed
**Priority:** P1 (High - Breaking Changes)
**Estimated Effort:** 3-5 days
**Dependencies:** Schema v1.6.0 already created
**Owner:** Backend Team
---
## 📋 Overview
อัพเดท Backend Entities และ DTOs ให้ตรงกับ Schema v1.6.0 ที่มีการ Refactor โครงสร้างตาราง
---
## 🎯 Objectives
- [x] Update Correspondence Entities
- [x] Update RFA Entities (Shared PK Pattern)
- [x] Update DTOs for new field names
- [x] Update Services for new relationships
- [x] Add/Update Unit Tests
---
## 📝 Schema Changes Summary
### Breaking Changes ⚠️
| Table | Change | Impact |
| --------------------------- | ---------------------------------------------- | --------------- |
| `correspondence_recipients` | FK → `correspondences(id)` | Update relation |
| `rfa_items` | `rfarev_correspondence_id``rfa_revision_id` | Rename column |
### Column Changes
| Table | Old | New | Notes |
| -------------------------- | ------------------- | ----------------------------- | ----------- |
| `correspondence_revisions` | `title` | `subject` | Rename |
| `correspondence_revisions` | - | `body`, `remarks` | Add columns |
| `rfa_revisions` | `title` | `subject` | Rename |
| `rfa_revisions` | `correspondence_id` | - | Remove |
| `rfa_revisions` | - | `body`, `remarks`, `due_date` | Add columns |
### Architecture Changes
| Table | Change |
| ------ | ---------------------------------------------------- |
| `rfas` | Shared PK with `correspondences` (no AUTO_INCREMENT) |
| `rfas` | `id` references `correspondences(id)` |
---
## 🛠️ Implementation Steps
### 1. Update CorrespondenceRevision Entity
```typescript
// File: backend/src/modules/correspondence/entities/correspondence-revision.entity.ts
// BEFORE
@Column()
title: string;
// AFTER
@Column()
subject: string;
@Column({ type: 'text', nullable: true })
body: string;
@Column({ type: 'text', nullable: true })
remarks: string;
@Column({ name: 'schema_version', default: 1 })
schemaVersion: number;
```
### 2. Update CorrespondenceRecipient Entity
```typescript
// File: backend/src/modules/correspondence/entities/correspondence-recipient.entity.ts
// BEFORE
@ManyToOne(() => CorrespondenceRevision, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'correspondence_id', referencedColumnName: 'correspondenceId' })
revision: CorrespondenceRevision;
// AFTER
@ManyToOne(() => Correspondence, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'correspondence_id' })
correspondence: Correspondence;
```
### 3. Update RFA Entity (Shared PK Pattern)
```typescript
// File: backend/src/modules/rfa/entities/rfa.entity.ts
// BEFORE
@PrimaryGeneratedColumn()
id: number;
// AFTER
@PrimaryColumn()
id: number;
@OneToOne(() => Correspondence, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id' })
correspondence: Correspondence;
```
### 4. Update RfaRevision Entity
```typescript
// File: backend/src/modules/rfa/entities/rfa-revision.entity.ts
// REMOVE
@Column({ name: 'correspondence_id' })
correspondenceId: number;
// RENAME
@Column()
subject: string; // was: title
// ADD
@Column({ type: 'text', nullable: true })
body: string;
@Column({ type: 'text', nullable: true })
remarks: string;
@Column({ name: 'due_date', type: 'datetime', nullable: true })
dueDate: Date;
```
### 5. Update RfaItem Entity
```typescript
// File: backend/src/modules/rfa/entities/rfa-item.entity.ts
// BEFORE
@Column({ name: 'rfarev_correspondence_id' })
rfaRevCorrespondenceId: number;
// AFTER
@Column({ name: 'rfa_revision_id' })
rfaRevisionId: number;
@ManyToOne(() => RfaRevision, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'rfa_revision_id' })
rfaRevision: RfaRevision;
```
### 6. Update DTOs
```typescript
// correspondence/dto/create-correspondence-revision.dto.ts
export class CreateCorrespondenceRevisionDto {
subject: string; // was: title
body?: string;
remarks?: string;
}
// rfa/dto/create-rfa-revision.dto.ts
export class CreateRfaRevisionDto {
subject: string; // was: title
body?: string;
remarks?: string;
dueDate?: Date;
}
```
---
## 🗂️ Files to Modify
### Entities
| File | Status | Changes |
| ------------------------------------ | ------ | ----------------------------------------- |
| `correspondence.entity.ts` | ✅ | Minor: add recipients relation |
| `correspondence-revision.entity.ts` | ✅ | Rename title→subject, add body/remarks |
| `correspondence-recipient.entity.ts` | ✅ | FK change to correspondence |
| `rfa.entity.ts` | ✅ | Shared PK pattern |
| `rfa-revision.entity.ts` | ✅ | Remove correspondenceId, add body/remarks |
| `rfa-item.entity.ts` | ✅ | Rename column |
### DTOs
| File | Status | Changes |
| --------------------------------------- | ------ | ------------------------------- |
| `create-correspondence-revision.dto.ts` | ✅ | title→subject, add body/remarks |
| `update-correspondence-revision.dto.ts` | ✅ | Same |
| `create-rfa-revision.dto.ts` | ✅ | title→subject, add fields |
| `update-rfa-revision.dto.ts` | ✅ | Same |
| `create-rfa-item.dto.ts` | ✅ | Column rename |
### Services
| File | Status | Changes |
| --------------------------- | ------ | -------------------------------- |
| `correspondence.service.ts` | ✅ | Update queries for new relations |
| `rfa.service.ts` | ✅ | Handle Shared PK creation |
---
## ✅ Verification
### Unit Tests
```bash
# Run existing tests to verify compatibility
pnpm test:watch correspondence
pnpm test:watch rfa
```
### Integration Tests
1. Create new Correspondence → verify subject field saved
2. Create new RFA → verify Shared PK pattern works
3. Verify recipients linked to correspondence (not revision)
4. Verify RFA items linked via rfa_revision_id
---
## 📚 Related Documents
- [Schema v1.6.0](../07-database/lcbp3-v1.6.0-schema.sql)
- [Data Dictionary v1.6.0](../07-database/data-dictionary-v1.6.0.md)
- [CHANGELOG v1.6.0](../../CHANGELOG.md)
---
## 🚨 Risks & Mitigation
| Risk | Impact | Mitigation |
| ----------------- | ------ | ------------------------------------ |
| Breaking frontend | High | Update frontend types simultaneously |
| Data migration | Medium | Schema already handles FK changes |
| Test failures | Low | Update tests with new field names |
---
## 📌 Notes
- Schema v1.6.0 SQL files already exist in `specs/07-database/`
- This task focuses on **backend code changes only**
- Frontend will need separate task for DTO/type updates
- Consider feature flag for gradual rollout

View File

@@ -0,0 +1,263 @@
# Task: Frontend Schema v1.6.0 Adaptation
**Status:** ✅ Completed
**Priority:** P1 (High - Breaking Changes)
**Estimated Effort:** 2-3 days
**Dependencies:** TASK-BE-015 (Backend Migration)
**Owner:** Frontend Team
---
## 📋 Overview
อัพเดท Frontend Types, Services และ Forms ให้รองรับ Schema v1.6.0
---
## 🎯 Objectives
- [x] Update TypeScript Interfaces/Types
- [x] Update Form Components (field names)
- [x] Update API Service Calls
- [x] Update List/Table Columns
- [x] Verify E2E functionality
---
## 📊 Business Rule Changes Analysis
### 1. Correspondence Revisions ⚠️ UI IMPACT
| Change | Old Field | New Field | Business Rule |
| ---------- | --------- | --------- | --------------------------------------- |
| **Rename** | `title` | `subject` | Form label เปลี่ยนจาก "หัวเรื่อง" เป็น "เรื่อง" |
| **Add** | - | `body` | เพิ่ม Rich Text Editor สำหรับเนื้อความ |
| **Add** | - | `remarks` | เพิ่ม Textarea สำหรับหมายเหตุ |
| **Add** | - | `dueDate` | เพิ่ม Date Picker สำหรับกำหนดส่ง |
**UI Impact:**
- Correspondence Form: เพิ่ม 3 fields ใหม่
- Correspondence List: เปลี่ยน column header
- Correspondence Detail: แสดง body และ remarks
### 2. Correspondence Recipients ⚠️ RELATION CHANGE
| Before | After | Business Rule |
| ------------------------ | ---------------------- | -------------------------- |
| Recipients ผูกกับ Revision | Recipients ผูกกับ Master | ผู้รับคงที่ตลอด Revisions ทั้งหมด |
**UI Impact:**
- ย้าย Recipients Selection ออกจาก Revision Form
- ไปอยู่ใน Master Correspondence Form แทน
- Recipients จะไม่เปลี่ยนเมื่อสร้าง New Revision
### 3. RFA System 🔄 ARCHITECTURE CHANGE
| Change | Description | Business Rule |
| ---------------- | -------------------------- | -------------------------------------------- |
| **Shared ID** | RFA.id = Correspondence.id | สร้าง Correspondence ก่อน แล้ว RFA ใช้ ID เดียวกัน |
| **Subject** | `title``subject` | เหมือนกับ Correspondence |
| **Body/Remarks** | เพิ่ม fields ใหม่ | เหมือนกับ Correspondence |
| **Due Date** | เพิ่ม field | กำหนดวันที่ต้องตอบกลับ |
**UI Impact:**
- RFA Form: เพิ่ม body, remarks, dueDate
- RFA Creation Flow: อาจต้อง adjust การ submit
### 4. RFA Items ⚠️ API CHANGE
| Before | After | Impact |
| ------------------------ | --------------- | ------------------------------------------- |
| `rfaRevCorrespondenceId` | `rfaRevisionId` | เปลี่ยน property name ใน API request/response |
---
## 🛠️ Implementation Steps
### 1. Update TypeScript Types
```typescript
// lib/types/correspondence.ts
// BEFORE
interface CorrespondenceRevision {
title: string;
// ...
}
// AFTER
interface CorrespondenceRevision {
subject: string; // renamed from title
body?: string; // NEW
remarks?: string; // NEW
dueDate?: string; // NEW
schemaVersion?: number; // NEW
// ...
}
// Move recipients to master level
interface Correspondence {
// ...existing fields
recipients: CorrespondenceRecipient[]; // MOVED from revision
}
```
```typescript
// lib/types/rfa.ts
// BEFORE
interface RfaRevision {
correspondenceId: number;
title: string;
// ...
}
// AFTER
interface RfaRevision {
// correspondenceId: REMOVED
subject: string; // renamed from title
body?: string; // NEW
remarks?: string; // NEW
dueDate?: string; // NEW
// ...
}
// BEFORE
interface RfaItem {
rfaRevCorrespondenceId: number;
// ...
}
// AFTER
interface RfaItem {
rfaRevisionId: number; // renamed
// ...
}
```
### 2. Update Form Components
```typescript
// app/(dashboard)/correspondences/new/page.tsx
// app/(dashboard)/correspondences/[id]/edit/page.tsx
// CHANGES:
// 1. Rename form field: title → subject
// 2. Add new fields: body, remarks, dueDate
// 3. Move recipients to master section (not revision)
<FormField name="subject" label="เรื่อง" required /> {/* was: title */}
<FormField name="body" label="เนื้อความ" type="richtext" /> {/* NEW */}
<FormField name="remarks" label="หมายเหตุ" type="textarea" /> {/* NEW */}
<FormField name="dueDate" label="กำหนดส่ง" type="date" /> {/* NEW */}
```
### 3. Update List Columns
```typescript
// components/correspondence/correspondence-list.tsx
const columns = [
// BEFORE: { header: 'หัวเรื่อง', accessorKey: 'title' }
// AFTER:
{ header: 'เรื่อง', accessorKey: 'subject' },
{ header: 'กำหนดส่ง', accessorKey: 'dueDate' }, // NEW column
];
```
### 4. Update API Services
```typescript
// lib/services/correspondence.service.ts
// lib/services/rfa.service.ts
// Update DTO property names in API calls
// Ensure field mapping matches backend changes
```
---
## 🗂️ Files to Modify
### Types
| File | Status | Changes |
| ----------------------------- | ------ | --------------------------------------- |
| `lib/types/correspondence.ts` | ✅ | title→subject, add body/remarks/dueDate |
| `lib/types/rfa.ts` | ✅ | Same + remove correspondenceId |
### Forms
| File | Status | Changes |
| ---------------------------------------------------- | ------ | ---------------------- |
| `app/(dashboard)/correspondences/new/page.tsx` | ✅ | Add new fields, rename |
| `app/(dashboard)/correspondences/[id]/edit/page.tsx` | ✅ | Same |
| `app/(dashboard)/rfas/new/page.tsx` | ✅ | Add new fields, rename |
| `app/(dashboard)/rfas/[id]/edit/page.tsx` | ✅ | Same |
### Lists/Tables
| File | Status | Changes |
| --------------------------------------------------- | ------ | ------------- |
| `components/correspondence/correspondence-list.tsx` | ✅ | Column rename |
| `components/rfa/rfa-list.tsx` | ✅ | Column rename |
### Services
| File | Status | Changes |
| ---------------------------------------- | ------ | ----------- |
| `lib/services/correspondence.service.ts` | ✅ | DTO mapping |
| `lib/services/rfa.service.ts` | ✅ | DTO mapping |
---
## ✅ Verification
### Manual Testing
1. **Correspondence Flow:**
- [ ] Create new correspondence → verify subject, body, remarks saved
- [ ] Edit existing → verify field display correctly
- [ ] List view shows "เรื่อง" column
2. **RFA Flow:**
- [ ] Create new RFA → verify new fields
- [ ] Add RFA Items → verify API works with new field names
- [ ] Due date displays and functions correctly
3. **Recipients:**
- [ ] Recipients assigned at master level
- [ ] Creating new revision doesn't reset recipients
### E2E Tests
```bash
pnpm test:e2e -- --grep "correspondence"
pnpm test:e2e -- --grep "rfa"
```
---
## 📚 Related Documents
- [TASK-BE-015](./TASK-BE-015-schema-v160-migration.md) - Backend Migration
- [Schema v1.6.0](../07-database/lcbp3-v1.6.0-schema.sql)
- [CHANGELOG v1.6.0](../../CHANGELOG.md)
---
## 🚨 Risks & Mitigation
| Risk | Impact | Mitigation |
| ---------------------- | ------ | ---------------------------- |
| Field name mismatch | High | Coordinate with backend team |
| Form validation errors | Medium | Test all forms thoroughly |
| List display issues | Low | Update column configs |
---
## 📌 Notes
- ต้องรอ Backend deploy ก่อน จึงจะ test ได้
- Recipients logic change อาจส่งผลต่อ business flow
- Consider feature flag for gradual rollout

View File

@@ -20,6 +20,7 @@
| **TASK-BE-011** | Notification & Audit | ✅ **Done** | 100% | Global Audit Interceptor & Notification Module active. |
| **TASK-BE-012** | Master Data Management | ✅ **Done** | 100% | Disciplines, SubTypes, Tags, Config APIs complete. |
| **TASK-BE-013** | User Management | ✅ **Done** | 100% | CRUD, Assignments, Preferences, Soft Delete complete. |
| **TASK-BE-015** | Schema v1.6.0 Migration | ✅ **Done** | 100% | Correspondence/RFA Shared PK, New Fields (2025-12-13). |
## 🛠 Detailed Findings by Component

View File

@@ -22,6 +22,7 @@
| **TASK-FE-013** | Circulation & Transmittal | ✅ **Done** | 100% | Circulation and Transmittal modules implemented with DataTable. |
| **TASK-FE-014** | Reference Data UI | ✅ **Done** | 100% | Generic CRUD Table refactored (Skeleton/Dialogs). All pages linked. |
| **TASK-FE-015** | Security Admin UI | ✅ **Done** | 100% | RBAC Matrix, Roles, Active Sessions, System Logs implemented. |
| **TASK-FE-016** | Schema v1.6.0 Adaptation | ✅ **Done** | 100% | Update Forms/Types/Lists for v1.6.0 changes (2025-12-13). |
## 🛠 Detailed Status by Component

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
# Schema v1.6.0 Migration & Document Number Fixes (2025-12-13)
## Task Summary
This session focused on completing the migration to Schema v1.6.0 (Correspondence/RFA shared PK) and resolving critical bugs in the Document Numbering system.
### Status
- **Schema Migration**: Completed (Backend & Frontend)
- **Document Numbering**:
- Preview Fixed (Recipient Code resolution)
- Creation Fixed (Source data mapping)
- Update Logic Fixed (Auto-regeneration on Draft edit)
## Walkthrough & Changes
### 1. Correspondence Module
- **New Entity**: `CorrespondenceRecipient` to handle multiple recipients (TO/CC).
- **Entity Update**: `Correspondence` now has a `recipients` relation.
- **Entity Update**: `CorrespondenceRevision` renamed `title` to `subject`, added `body`, `remarks`, `dueDate`, `schemaVersion`.
- **Service Update**: `create` method now saves recipients and maps new revision fields.
- **DTO Update**: `CreateCorrespondenceDto` updated to support proper fields.
### 2. RFA Module
- **Shared Primary Key**: `Rfa` entity now shares PK with `Correspondence`.
- **Revision Update**: `RfaRevision` removed `correspondenceId` (access via `rfa.correspondence.id`), renamed `title` to `subject`, added new fields.
- **Item Update**: `RfaItem` FK column renamed to `rfa_revision_id`.
- **Service Update**: Only `RfaService` logic updated to handle shared PK and new field mappings. `findAll` query updated to join via `rfa.correspondence`.
### 3. Frontend Adaptation
- **Type Definitions**: Updated `CorrespondenceRevision` and `RFA` types to match schema v1.6.0.
- **Form Components**:
- `CorrespondenceForm`: Renamed `title` to `subject`, added `body`, `remarks`, `dueDate`.
- `RFAForm`: Renamed `title` to `subject`, added `body`, `remarks`.
- **List & Detail Views**: Updated accessor keys (`title` -> `subject`) and added display sections for new fields (Body, Remarks) in Detail views.
- **DTOs**: Updated `CreateCorrespondenceDto` and `CreateRFADto` to include new fields.
## Bug Fixes & Refinements (Session 2)
### Document Number Preview
- **Issue**: Preview showed `--` for recipient code.
- **Fix**:
- Implemented `customTokens` support in `DocumentNumberingService`.
- updated `CorrespondenceService.previewNextNumber` to manually resolve recipient code from `OrganizationRepository`.
### Correspondence Creation
- **Issue**: Generated document number used incorrect placeholder.
- **Fix**: Updated `create` method to extract recipient from `recipients` array instead of legacy `details` field.
### Edit Page Loading
- **Issue**: "Failed to load" error on Edit page.
- **Fix**: Corrected TypeORM relation path in `CorrespondenceService.findOne` from `recipients.organization` to properties `recipients.recipientOrganization`.
### Document Number Auto-Update
- **Feature**: Automatically regenerate document number when editing a Draft.
- **Implementation**: logic added to `update` method to re-calculate number if `type`, `discipline`, `project`, or `recipient` changes.
## Verification Results
- **Backend Tests**: `correspondence.service.spec.ts` passed.
- **Frontend Tests**: All 9 suites (113 tests) passed.
- **Manual Verification**: Verified Preview, Creation, and Edit flows.
## Future Tasks
- [ ] **Data Cleanup**: Migration script to fix existing document numbers with missing recipient codes (e.g., `คคง.--0001-2568` -> `คคง.-XYZ-0001-2568`).