251213:1509 Docunment Number Businee Rule not correct
This commit is contained in:
30
CHANGELOG.md
30
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
|
||||
|
||||
20
README.md
20
README.md
@@ -4,20 +4,20 @@
|
||||
>
|
||||
> ระบบบริหารจัดการเอกสารโครงการแบบครบวงจร สำหรับโครงการก่อสร้างท่าเรือแหลมฉบังระยะที่ 3
|
||||
|
||||
[](./CHANGELOG.md)
|
||||
[](./CHANGELOG.md)
|
||||
[]()
|
||||
[]()
|
||||
|
||||
---
|
||||
|
||||
## 📈 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)
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' }[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateCorrespondenceDto } from './create-correspondence.dto';
|
||||
|
||||
export class UpdateCorrespondenceDto extends PartialType(
|
||||
CreateCorrespondenceDto
|
||||
) {}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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") : '-';
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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' }[];
|
||||
}
|
||||
|
||||
@@ -12,7 +12,13 @@ export interface CreateRfaDto {
|
||||
disciplineId?: number;
|
||||
|
||||
/** หัวข้อเรื่อง */
|
||||
title: string;
|
||||
subject: string;
|
||||
|
||||
/** เนื้อหา (Rich Text) */
|
||||
body?: string;
|
||||
|
||||
/** หมายเหตุ */
|
||||
remarks?: string;
|
||||
|
||||
/** ส่งถึงใคร (สำหรับ Routing Step 1) */
|
||||
toOrganizationId: number;
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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 'ผู้แก้ไขล่าสุด',
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
251
specs/06-tasks/TASK-BE-015-schema-v160-migration.md
Normal file
251
specs/06-tasks/TASK-BE-015-schema-v160-migration.md
Normal 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
|
||||
263
specs/06-tasks/TASK-FE-016-schema-v160-adaptation.md
Normal file
263
specs/06-tasks/TASK-FE-016-schema-v160-adaptation.md
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
1729
specs/07-database/data-dictionary-v1.6.0.md
Normal file
1729
specs/07-database/data-dictionary-v1.6.0.md
Normal file
File diff suppressed because it is too large
Load Diff
1857
specs/07-database/lcbp3-v1.6.0-schema.sql
Normal file
1857
specs/07-database/lcbp3-v1.6.0-schema.sql
Normal file
File diff suppressed because it is too large
Load Diff
3223
specs/07-database/lcbp3-v1.6.0-seed-basic.sql
Normal file
3223
specs/07-database/lcbp3-v1.6.0-seed-basic.sql
Normal file
File diff suppressed because it is too large
Load Diff
2569
specs/07-database/lcbp3-v1.6.0-seed-contractdrawing.sql
Normal file
2569
specs/07-database/lcbp3-v1.6.0-seed-contractdrawing.sql
Normal file
File diff suppressed because it is too large
Load Diff
1067
specs/07-database/lcbp3-v1.6.0-seed-permissions.sql
Normal file
1067
specs/07-database/lcbp3-v1.6.0-seed-permissions.sql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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`).
|
||||
Reference in New Issue
Block a user