251208:1625 Frontend: to be complete admin panel, Backend: tobe recheck all task
This commit is contained in:
@@ -25,23 +25,39 @@
|
||||
```
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── common/ # Shared utilities, decorators, guards
|
||||
│ │ ├── auth/ # Authentication module
|
||||
│ │ ├── config/ # Configuration management
|
||||
│ ├── common/ # Shared utilities
|
||||
│ │ ├── decorators/ # Custom decorators
|
||||
│ │ ├── dtos/ # Common DTOs
|
||||
│ │ ├── entities/ # Base entities
|
||||
│ │ ├── filters/ # Exception filters
|
||||
│ │ ├── guards/ # Auth guards, RBAC
|
||||
│ │ ├── interceptors/ # Logging, transform, idempotency
|
||||
│ │ └── file-storage/ # Two-phase file storage
|
||||
│ │ ├── interfaces/ # Common interfaces
|
||||
│ │ └── utils/ # Helper functions
|
||||
│ ├── config/ # Configuration management
|
||||
│ ├── database/
|
||||
│ │ ├── migrations/
|
||||
│ │ └── seeds/
|
||||
│ ├── modules/ # Business modules (domain-driven)
|
||||
│ │ ├── user/
|
||||
│ │ ├── project/
|
||||
│ │ ├── auth/
|
||||
│ │ ├── circulation/
|
||||
│ │ ├── correspondence/
|
||||
│ │ ├── dashboard/
|
||||
│ │ ├── document-numbering/
|
||||
│ │ ├── drawing/
|
||||
│ │ ├── json-schema/
|
||||
│ │ ├── master/
|
||||
│ │ ├── monitoring/
|
||||
│ │ ├── notification/
|
||||
│ │ ├── organizations/
|
||||
│ │ ├── project/
|
||||
│ │ ├── rfa/
|
||||
│ │ ├── workflow-engine/
|
||||
│ │ └── ...
|
||||
│ └── database/
|
||||
│ ├── migrations/
|
||||
│ └── seeds/
|
||||
│ │ ├── search/
|
||||
│ │ ├── transmittal/
|
||||
│ │ ├── user/
|
||||
│ │ └── workflow-engine/
|
||||
│ ├── app.module.ts
|
||||
│ └── main.ts
|
||||
├── test/ # E2E tests
|
||||
└── scripts/ # Utility scripts
|
||||
```
|
||||
|
||||
@@ -35,13 +35,17 @@ frontend/
|
||||
│ ├── forms/ # Form components
|
||||
│ ├── layout/ # Layout components (Navbar, Sidebar)
|
||||
│ └── tables/ # Data table components
|
||||
├── hooks/ # Custom React hooks (Root level)
|
||||
├── lib/
|
||||
│ ├── api/ # API client (Axios)
|
||||
│ ├── hooks/ # Custom React hooks
|
||||
│ ├── services/ # API service functions
|
||||
│ └── stores/ # Zustand stores
|
||||
│ ├── stores/ # Zustand stores
|
||||
│ └── utils.ts # Cn utility
|
||||
├── providers/ # Context providers
|
||||
├── public/ # Static assets
|
||||
├── styles/ # Global styles
|
||||
├── types/ # TypeScript types & DTOs
|
||||
└── providers/ # Context providers
|
||||
└── middleware.ts # Next.js Middleware
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
1. **Database Schema:**
|
||||
|
||||
- ✅ ทุกตารางถูกสร้างตาม Data Dictionary v1.4.5
|
||||
- ✅ ทุกตารางถูกสร้างตาม Data Dictionary v1.5.1
|
||||
- ✅ Foreign Keys ถูกต้องครบถ้วน
|
||||
- ✅ Indexes ครบตาม Specification
|
||||
- ✅ Virtual Columns สำหรับ JSON fields
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Task: Document Numbering Service
|
||||
|
||||
**Status:** Not Started
|
||||
**Status:** Ready for Implementation
|
||||
**Priority:** P1 (High - Critical for Documents)
|
||||
**Estimated Effort:** 7-8 days
|
||||
**Dependencies:** TASK-BE-001 (Database), TASK-BE-002 (Auth), TASK-BE-003 (Redis Setup)
|
||||
@@ -50,14 +50,18 @@
|
||||
- ✅ Fallback to DB pessimistic lock when Redis unavailable
|
||||
- ✅ Retry with exponential backoff (5 retries max for lock, 2 for version conflict, 3 for DB errors)
|
||||
|
||||
### 3. Document Types Support
|
||||
### 3. Document Types Support & Scoping
|
||||
|
||||
- ✅ LETTER / RFI / MEMO / EMAIL / MOM / INSTRUCTION / NOTICE / OTHER
|
||||
- ✅ **General Correspondence** (LETTER / MEMO / etc.) → **Project Level Scope**
|
||||
- Counter Key: `(project_id, originator_org_id, recipient_org_id, corr_type_id, 0, 0, 0, year)`
|
||||
- ✅ TRANSMITTAL
|
||||
- *Note*: Templates separate per Project. `{PROJECT}` token is optional in format but counter is partitioned by Project.
|
||||
|
||||
- ✅ **Transmittal** → **Project Level Scope** with Sub-Type lookup
|
||||
- Counter Key: `(project_id, originator_org_id, recipient_org_id, corr_type_id, sub_type_id, 0, 0, year)`
|
||||
- ✅ RFA
|
||||
|
||||
- ✅ **RFA** → **Contract Level Scope** (Implicit)
|
||||
- Counter Key: `(project_id, originator_org_id, NULL, corr_type_id, 0, rfa_type_id, discipline_id, year)`
|
||||
- *Mechanism*: `rfa_type_id` and `discipline_id` are linked to specific Contracts in the DB. Different contracts have different Type IDs, ensuring separate counters.
|
||||
|
||||
### 4. Error Handling
|
||||
|
||||
@@ -85,41 +89,33 @@
|
||||
|
||||
### Step 1: Database Entities
|
||||
|
||||
#### 1.1 Document Number Config Entity
|
||||
|
||||
```typescript
|
||||
// File: backend/src/modules/document-numbering/entities/document-number-config.entity.ts
|
||||
// File: backend/src/modules/document-numbering/entities/document-number-format.entity.ts
|
||||
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Project } from '../../project/entities/project.entity';
|
||||
import { DocumentType } from '../../document-type/entities/document-type.entity';
|
||||
import { CorrespondenceType } from '../../correspondence-type/entities/correspondence-type.entity';
|
||||
|
||||
@Entity('document_number_configs')
|
||||
export class DocumentNumberConfig {
|
||||
@Entity('document_number_formats')
|
||||
export class DocumentNumberFormat {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
project_id: number;
|
||||
|
||||
@Column()
|
||||
doc_type_id: number;
|
||||
@Column({ name: 'correspondence_type_id' })
|
||||
correspondenceTypeId: number;
|
||||
|
||||
@Column({ default: 0, comment: 'ประเภทย่อย (nullable, use 0 for fallback)' })
|
||||
sub_type_id: number;
|
||||
// Note: Schema currently only has project_id + correspondence_type_id.
|
||||
// If we need sub_type/discipline specific templates, we might need schema update or use JSON config.
|
||||
// For now, aligning with lcbp3-v1.5.1-schema.sql which has format_template column.
|
||||
|
||||
@Column({ default: 0, comment: 'สาขาวิชา (nullable, use 0 for fallback)' })
|
||||
discipline_id: number;
|
||||
|
||||
@Column({ length: 255, comment: 'e.g. {PROJECT}-{ORG}-{TYPE}-{DISCIPLINE}-{SEQ:4}-{REV}' })
|
||||
template: string;
|
||||
@Column({ name: 'format_template', length: 255, comment: 'e.g. {PROJECT}-{ORIGINATOR}-{CORR_TYPE}-{SEQ:4}' })
|
||||
formatTemplate: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ default: 0, comment: 'For template versioning' })
|
||||
version: number;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@@ -130,11 +126,10 @@ export class DocumentNumberConfig {
|
||||
@JoinColumn({ name: 'project_id' })
|
||||
project: Project;
|
||||
|
||||
@ManyToOne(() => DocumentType)
|
||||
@JoinColumn({ name: 'doc_type_id' })
|
||||
documentType: DocumentType;
|
||||
@ManyToOne(() => CorrespondenceType)
|
||||
@JoinColumn({ name: 'correspondence_type_id' })
|
||||
correspondenceType: CorrespondenceType;
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 Document Number Counter Entity
|
||||
|
||||
@@ -158,8 +153,8 @@ export class DocumentNumberCounter {
|
||||
@PrimaryColumn({ name: 'originator_organization_id' })
|
||||
originatorOrganizationId: number;
|
||||
|
||||
@PrimaryColumn({ name: 'recipient_organization_id', nullable: true })
|
||||
recipientOrganizationId: number | null; // NULL for RFA
|
||||
@PrimaryColumn({ name: 'recipient_organization_id', default: -1 })
|
||||
recipientOrganizationId: number; // -1 if NULL (standardized for composite key)
|
||||
|
||||
@PrimaryColumn({ name: 'correspondence_type_id' })
|
||||
correspondenceTypeId: number;
|
||||
@@ -189,7 +184,7 @@ export class DocumentNumberCounter {
|
||||
|
||||
> **⚠️ หมายเหตุ Schema:**
|
||||
>
|
||||
> - Primary Key ใช้ `COALESCE(recipient_organization_id, 0)` ในการสร้าง constraint (ดู migration file)
|
||||
> - Primary Key ใช้ `recipient_organization_id = -1` แทน NULL (ตาม Schema v1.5.1)
|
||||
> - `sub_type_id`, `rfa_type_id`, `discipline_id` ใช้ `0` แทน NULL
|
||||
> - Counter reset อัตโนมัติทุกปี (แยก counter ตาม `current_year`)
|
||||
|
||||
@@ -309,7 +304,7 @@ import { Repository, DataSource } from 'typeorm';
|
||||
import Redlock from 'redlock';
|
||||
import Redis from 'ioredis';
|
||||
import { DocumentNumberCounter } from './entities/document-number-counter.entity';
|
||||
import { DocumentNumberConfig } from './entities/document-number-config.entity';
|
||||
import { DocumentNumberFormat } from './entities/document-number-format.entity';
|
||||
import { DocumentNumberAudit } from './entities/document-number-audit.entity';
|
||||
import { GenerateNumberDto } from './dto/generate-number.dto';
|
||||
import { MetricsService } from '../metrics/metrics.service';
|
||||
@@ -321,8 +316,8 @@ export class DocumentNumberingService {
|
||||
constructor(
|
||||
@InjectRepository(DocumentNumberCounter)
|
||||
private counterRepo: Repository<DocumentNumberCounter>,
|
||||
@InjectRepository(DocumentNumberConfig)
|
||||
private configRepo: Repository<DocumentNumberConfig>,
|
||||
@InjectRepository(DocumentNumberFormat)
|
||||
private formatRepo: Repository<DocumentNumberFormat>,
|
||||
@InjectRepository(DocumentNumberAudit)
|
||||
private auditRepo: Repository<DocumentNumberAudit>,
|
||||
private dataSource: DataSource,
|
||||
@@ -470,8 +465,8 @@ export class DocumentNumberingService {
|
||||
}
|
||||
|
||||
// Step 4: Get config and format number
|
||||
const config = await this.getConfig(dto.projectId, dto.docTypeId, subTypeId, disciplineId);
|
||||
const formattedNumber = await this.formatNumber(config.template, {
|
||||
const config = await this.getConfig(dto.projectId, dto.docTypeId);
|
||||
const formattedNumber = await this.formatNumber(config.formatTemplate, {
|
||||
projectId: dto.projectId,
|
||||
docTypeId: dto.docTypeId,
|
||||
subTypeId,
|
||||
@@ -561,8 +556,8 @@ export class DocumentNumberingService {
|
||||
}
|
||||
|
||||
// Format number
|
||||
const config = await this.getConfig(dto.projectId, dto.docTypeId, subTypeId, disciplineId);
|
||||
const formattedNumber = await this.formatNumber(config.template, {
|
||||
const config = await this.getConfig(dto.projectId, dto.docTypeId);
|
||||
const formattedNumber = await this.formatNumber(config.formatTemplate, {
|
||||
projectId: dto.projectId,
|
||||
docTypeId: dto.docTypeId,
|
||||
subTypeId,
|
||||
@@ -576,7 +571,7 @@ export class DocumentNumberingService {
|
||||
await manager.save(DocumentNumberAudit, {
|
||||
generated_number: formattedNumber,
|
||||
counter_key: `db_lock:${dto.projectId}:${dto.docTypeId}`,
|
||||
template_used: config.template,
|
||||
template_used: config.formatTemplate,
|
||||
sequence_number: nextNumber,
|
||||
user_id: dto.userId,
|
||||
ip_address: dto.ipAddress,
|
||||
@@ -596,8 +591,9 @@ export class DocumentNumberingService {
|
||||
private async formatNumber(template: string, data: any): Promise<string> {
|
||||
const tokens = {
|
||||
'{PROJECT}': await this.getProjectCode(data.projectId),
|
||||
'{ORG}': await this.getOrgCode(data.organizationId),
|
||||
'{TYPE}': await this.getTypeCode(data.docTypeId),
|
||||
'{ORIGINATOR}': await this.getOriginatorOrgCode(data.originatorOrganizationId),
|
||||
'{RECIPIENT}': await this.getRecipientOrgCode(data.recipientOrganizationId),
|
||||
'{CORR_TYPE}': await this.getTypeCode(data.docTypeId),
|
||||
'{SUB_TYPE}': await this.getSubTypeCode(data.subTypeId),
|
||||
'{DISCIPLINE}': await this.getDisciplineCode(data.disciplineId),
|
||||
'{CATEGORY}': await this.getCategoryCode(data.categoryId),
|
||||
@@ -661,39 +657,26 @@ export class DocumentNumberingService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration template
|
||||
* Get configuration template (Format)
|
||||
*/
|
||||
private async getConfig(
|
||||
projectId: number,
|
||||
docTypeId: number,
|
||||
subTypeId: number,
|
||||
disciplineId: number,
|
||||
): Promise<DocumentNumberConfig> {
|
||||
// Try exact match first
|
||||
let config = await this.configRepo.findOne({
|
||||
correspondenceTypeId: number,
|
||||
): Promise<DocumentNumberFormat> {
|
||||
// Note: Schema currently only separates by project_id and correspondence_type_id
|
||||
// If we need sub-type specific templates, we should check if they are supported in the future schema.
|
||||
// Converting old logic slightly to match v1.5.1 schema columns.
|
||||
|
||||
const config = await this.formatRepo.findOne({
|
||||
where: {
|
||||
project_id: projectId,
|
||||
doc_type_id: docTypeId,
|
||||
sub_type_id: subTypeId,
|
||||
discipline_id: disciplineId,
|
||||
correspondenceTypeId: correspondenceTypeId,
|
||||
},
|
||||
});
|
||||
|
||||
// Fallback to default (subTypeId=0, disciplineId=0)
|
||||
if (!config) {
|
||||
config = await this.configRepo.findOne({
|
||||
where: {
|
||||
project_id: projectId,
|
||||
doc_type_id: docTypeId,
|
||||
sub_type_id: 0,
|
||||
discipline_id: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
throw new NotFoundException(
|
||||
`Document number config not found for project=${projectId}, docType=${docTypeId}`
|
||||
`Document number format not found for project=${projectId}, type=${correspondenceTypeId}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1241,14 +1224,14 @@ ensure:
|
||||
|
||||
## 🚨 Risks & Mitigation
|
||||
|
||||
| Risk | Impact | Probability | Mitigation |
|
||||
|------|--------|-------------|------------|
|
||||
| Redis lock failure | High | Low | Automatic fallback to DB lock |
|
||||
| Version conflicts under high load | Medium | Medium | Exponential backoff retry (2x) |
|
||||
| Lock timeout | Medium | Low | Retry 5x with exponential backoff |
|
||||
| Performance degradation | High | Medium | Redis caching, connection pooling, monitoring |
|
||||
| DB connection pool exhaustion | High | Low | Retry 3x, increase pool size, monitoring |
|
||||
| Rate limit bypass | Medium | Low | Multi-layer limiting (user + IP + global) |
|
||||
| Risk | Impact | Probability | Mitigation |
|
||||
| --------------------------------- | ------ | ----------- | --------------------------------------------- |
|
||||
| Redis lock failure | High | Low | Automatic fallback to DB lock |
|
||||
| Version conflicts under high load | Medium | Medium | Exponential backoff retry (2x) |
|
||||
| Lock timeout | Medium | Low | Retry 5x with exponential backoff |
|
||||
| Performance degradation | High | Medium | Redis caching, connection pooling, monitoring |
|
||||
| DB connection pool exhaustion | High | Low | Retry 3x, increase pool size, monitoring |
|
||||
| Rate limit bypass | Medium | Low | Multi-layer limiting (user + IP + global) |
|
||||
|
||||
---
|
||||
|
||||
@@ -1292,7 +1275,7 @@ Stored in database (`document_number_configs` table), configurable per:
|
||||
|
||||
## 🔄 Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 1.0 | 2025-11-30 | Initial task definition |
|
||||
| 2.0 | 2025-12-02 | Comprehensive update with all 9 tokens, 4 document types, 4 error scenarios, audit logging, monitoring, rate limiting, and complete implementation details |
|
||||
| Version | Date | Changes |
|
||||
| ------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1.0 | 2025-11-30 | Initial task definition |
|
||||
| 2.0 | 2025-12-02 | Comprehensive update with all 9 tokens, 4 document types, 4 error scenarios, audit logging, monitoring, rate limiting, and complete implementation details |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Task: Workflow Engine Module
|
||||
|
||||
**Status:** Not Started
|
||||
**Status:** Completed
|
||||
**Priority:** P0 (Critical - Core Infrastructure)
|
||||
**Estimated Effort:** 10-14 days
|
||||
**Dependencies:** TASK-BE-001 (Database), TASK-BE-002 (Auth)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Task: RFA Module
|
||||
|
||||
**Status:** Not Started
|
||||
**Status:** In Progress
|
||||
**Priority:** P1 (High - Core Business Module)
|
||||
**Estimated Effort:** 8-12 days
|
||||
**Dependencies:** TASK-BE-001, TASK-BE-002, TASK-BE-003, TASK-BE-004, TASK-BE-006
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Task: Drawing Module (Shop & Contract Drawings)
|
||||
|
||||
**Status:** Not Started
|
||||
**Status:** In Progress
|
||||
**Priority:** P2 (Medium - Supporting Module)
|
||||
**Estimated Effort:** 6-8 days
|
||||
**Dependencies:** TASK-BE-001, TASK-BE-002, TASK-BE-003, TASK-BE-004
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Task: Circulation & Transmittal Modules
|
||||
|
||||
**Status:** Not Started
|
||||
**Status:** In Progress
|
||||
**Priority:** P2 (Medium)
|
||||
**Estimated Effort:** 5-7 days
|
||||
**Dependencies:** TASK-BE-001, TASK-BE-002, TASK-BE-003, TASK-BE-006
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Task: Search & Elasticsearch Integration
|
||||
|
||||
**Status:** Not Started
|
||||
**Status:** 🚧 In Progress
|
||||
**Priority:** P2 (Medium - Performance Enhancement)
|
||||
**Estimated Effort:** 4-6 days
|
||||
**Dependencies:** TASK-BE-001, TASK-BE-005, TASK-BE-007
|
||||
@@ -16,11 +16,11 @@
|
||||
|
||||
## 🎯 Objectives
|
||||
|
||||
- ✅ Elasticsearch Integration
|
||||
- ✅ Full-text Search (Correspondences, RFAs, Drawings)
|
||||
- ✅ Advanced Filters
|
||||
- ✅ Search Result Aggregations
|
||||
- ✅ Auto-indexing
|
||||
- [x] Elasticsearch Integration
|
||||
- [x] Full-text Search (Correspondences, RFAs, Drawings)
|
||||
- [x] Advanced Filters
|
||||
- [ ] Search Result Aggregations (Pending verification)
|
||||
- [x] Auto-indexing (Implemented via Direct Call, not Queue yet)
|
||||
|
||||
---
|
||||
|
||||
@@ -28,21 +28,21 @@
|
||||
|
||||
1. **Search Capabilities:**
|
||||
|
||||
- ✅ Search across multiple document types
|
||||
- ✅ Full-text search in title, description
|
||||
- ✅ Filter by project, status, date range
|
||||
- ✅ Sort results by relevance/date
|
||||
- [x] Search across multiple document types
|
||||
- [x] Full-text search in title, description
|
||||
- [x] Filter by project, status, date range
|
||||
- [x] Sort results by relevance/date
|
||||
|
||||
2. **Indexing:**
|
||||
|
||||
- ✅ Auto-index on document create/update
|
||||
- ✅ Async indexing (via queue)
|
||||
- ✅ Bulk re-indexing command
|
||||
- [x] Auto-index on document create/update (Direct Call implemented)
|
||||
- [ ] Async indexing (via queue) - **Pending**
|
||||
- [ ] Bulk re-indexing command - **Pending**
|
||||
|
||||
3. **Performance:**
|
||||
- ✅ Search results < 500ms
|
||||
- ✅ Pagination support
|
||||
- ✅ Highlight search terms
|
||||
- [x] Search results < 500ms
|
||||
- [x] Pagination support
|
||||
- [x] Highlight search terms
|
||||
|
||||
---
|
||||
|
||||
@@ -462,12 +462,12 @@ describe('SearchService', () => {
|
||||
|
||||
## 📦 Deliverables
|
||||
|
||||
- [ ] SearchService with Elasticsearch
|
||||
- [ ] Search Indexer (Queue Worker)
|
||||
- [ ] Index Mappings
|
||||
- [ ] Queue Integration
|
||||
- [ ] Search Controller
|
||||
- [ ] Bulk Re-indexing Command
|
||||
- [x] SearchService with Elasticsearch
|
||||
- [ ] Search Indexer (Queue Worker) - **Pending**
|
||||
- [x] Index Mappings (Implemented in Service)
|
||||
- [ ] Queue Integration - **Pending**
|
||||
- [x] Search Controller
|
||||
- [ ] Bulk Re-indexing Command - **Pending**
|
||||
- [ ] Unit Tests (75% coverage)
|
||||
- [ ] API Documentation
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Task: Notification & Audit Log Services
|
||||
|
||||
**Status:** Not Started
|
||||
**Status:** Completed
|
||||
**Priority:** P3 (Low - Supporting Services)
|
||||
**Estimated Effort:** 3-5 days
|
||||
**Dependencies:** TASK-BE-001, TASK-BE-002
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Task: Master Data Management Module
|
||||
|
||||
**Status:** Not Started
|
||||
**Status:** Completed
|
||||
**Priority:** P1 (High - Required for System Setup)
|
||||
**Estimated Effort:** 6-8 days
|
||||
**Dependencies:** TASK-BE-001 (Database), TASK-BE-002 (Auth)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Task: User Management Module
|
||||
|
||||
**Status:** Not Started
|
||||
**Status:** Completed
|
||||
**Priority:** P1 (High - Core User Features)
|
||||
**Estimated Effort:** 5-7 days
|
||||
**Dependencies:** TASK-BE-001 (Database), TASK-BE-002 (Auth & RBAC)
|
||||
|
||||
@@ -29,11 +29,11 @@ Build UI for configuring and managing workflows using the DSL-based workflow eng
|
||||
|
||||
## ✅ Acceptance Criteria
|
||||
|
||||
- [ ] List all workflows with status
|
||||
- [ ] Create/edit workflows with DSL editor
|
||||
- [ ] Visual workflow builder functional
|
||||
- [ ] DSL validation shows errors
|
||||
- [ ] Test workflow with sample data
|
||||
- [x] List all workflows with status
|
||||
- [x] Create/edit workflows with DSL editor
|
||||
- [x] Visual workflow builder functional
|
||||
- [x] DSL validation shows errors
|
||||
- [x] Test workflow with sample data
|
||||
- [ ] Workflow templates available
|
||||
- [ ] Version history viewable
|
||||
|
||||
|
||||
@@ -29,13 +29,13 @@ Build UI for configuring and managing document numbering templates including tem
|
||||
|
||||
## ✅ Acceptance Criteria
|
||||
|
||||
- [ ] List all numbering templates by document type
|
||||
- [ ] Create/edit templates with format preview
|
||||
- [ ] Template variables easily selectable
|
||||
- [ ] Preview shows example numbers
|
||||
- [ ] View current number sequences
|
||||
- [ ] Annual reset configurable
|
||||
- [ ] Validation prevents conflicts
|
||||
- [x] List all numbering templates by document type
|
||||
- [x] Create/edit templates with format preview
|
||||
- [x] Template variables easily selectable
|
||||
- [x] Preview shows example numbers
|
||||
- [x] View current number sequences
|
||||
- [x] Annual reset configurable
|
||||
- [x] Validation prevents conflicts
|
||||
|
||||
---
|
||||
|
||||
@@ -67,10 +67,21 @@ export default function NumberingPage() {
|
||||
Manage document numbering templates and sequences
|
||||
</p>
|
||||
</div>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Template
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Select defaultValue="1">
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="Select Project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">LCBP3</SelectItem>
|
||||
<SelectItem value="2">LCBP3-Maintenance</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Template
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
@@ -161,14 +172,16 @@ import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
const VARIABLES = [
|
||||
{ key: '{ORG}', name: 'Organization Code', example: 'กทท' },
|
||||
{ key: '{DOCTYPE}', name: 'Document Type', example: 'CORR' },
|
||||
{ key: '{DISC}', name: 'Discipline', example: 'STR' },
|
||||
{ key: '{YYYY}', name: 'Year (4-digit)', example: '2025' },
|
||||
{ key: '{YY}', name: 'Year (2-digit)', example: '25' },
|
||||
{ key: '{MM}', name: 'Month', example: '12' },
|
||||
{ key: '{SEQ}', name: 'Sequence Number', example: '0001' },
|
||||
{ key: '{CONTRACT}', name: 'Contract Code', example: 'C01' },
|
||||
{ key: '{ORIGINATOR}', name: 'Originator Code', example: 'PAT' },
|
||||
{ key: '{RECIPIENT}', name: 'Recipient Code', example: 'CN' },
|
||||
{ key: '{CORR_TYPE}', name: 'Correspondence Type', example: 'RFA' },
|
||||
{ key: '{SUB_TYPE}', name: 'Sub Type', example: '21' },
|
||||
{ key: '{DISCIPLINE}', name: 'Discipline', example: 'STR' },
|
||||
{ key: '{RFA_TYPE}', name: 'RFA Type', example: 'SDW' },
|
||||
{ key: '{YEAR:B.E.}', name: 'Year (B.E.)', example: '2568' },
|
||||
{ key: '{YEAR:A.D.}', name: 'Year (A.D.)', example: '2025' },
|
||||
{ key: '{SEQ:4}', name: 'Sequence (4-digit)', example: '0001' },
|
||||
{ key: '{REV}', name: 'Revision', example: 'A' },
|
||||
];
|
||||
|
||||
export function TemplateEditor({ template, onSave }: any) {
|
||||
@@ -201,9 +214,16 @@ export function TemplateEditor({ template, onSave }: any) {
|
||||
<SelectValue placeholder="Select document type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="correspondence">Correspondence</SelectItem>
|
||||
<SelectItem value="rfa">RFA</SelectItem>
|
||||
<SelectItem value="drawing">Drawing</SelectItem>
|
||||
<SelectItem value="RFA">RFA</SelectItem>
|
||||
<SelectItem value="RFI">RFI</SelectItem>
|
||||
<SelectItem value="TRANSMITTAL">Transmittal</SelectItem>
|
||||
<SelectItem value="LETTER">Letter</SelectItem>
|
||||
<SelectItem value="MEMO">Memorandum</SelectItem>
|
||||
<SelectItem value="EMAIL">Email</SelectItem>
|
||||
<SelectItem value="MOM">Minutes of Meeting</SelectItem>
|
||||
<SelectItem value="INSTRUCTION">Instruction</SelectItem>
|
||||
<SelectItem value="NOTICE">Notice</SelectItem>
|
||||
<SelectItem value="OTHER">Other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -5,21 +5,21 @@
|
||||
|
||||
## 📊 Overview
|
||||
|
||||
| Task ID | Title | Status | Completion % | Notes |
|
||||
| --------------- | ------------------------- | ----------------- | ------------ | ------------------------------------------------------ |
|
||||
| **TASK-BE-001** | Database Migrations | ✅ **Done** | 100% | Schema v1.5.1 active. TypeORM configured. |
|
||||
| **TASK-BE-002** | Auth & RBAC | ✅ **Done** | 100% | JWT, Refresh Token, RBAC Guard, Permissions complete. |
|
||||
| **TASK-BE-003** | File Storage | ✅ **Done** | 100% | MinIO/S3 strategies implemented (in `common`). |
|
||||
| **TASK-BE-004** | Document Numbering | ✅ **Done** | 100% | **High Quality**: Redlock + Optimistic Locking logic. |
|
||||
| **TASK-BE-005** | Correspondence Module | ✅ **Done** | 95% | CRUD, Workflow Submit, References, Audit Log complete. |
|
||||
| **TASK-BE-006** | Workflow Engine | ✅ **Done** | 100% | DSL Evaluator, Versioning, Event Dispatching complete. |
|
||||
| **TASK-BE-007** | RFA Module | ✅ **Done** | 95% | Full Swagger, Revision handling, Workflow integration. |
|
||||
| **TASK-BE-008** | Drawing Module | ✅ **Done** | 95% | Split into `ShopDrawing` & `ContractDrawing`. |
|
||||
| **TASK-BE-009** | Circulation & Transmittal | ✅ **Done** | 90% | Modules exist and registered in `app.module.ts`. |
|
||||
| **TASK-BE-010** | Search (Elasticsearch) | 🚧 **In Progress** | 80% | Module registered, needs deep verification of mapping. |
|
||||
| **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 ID | Title | Status | Completion % | Notes |
|
||||
| --------------- | ------------------------- | ----------------- | ------------ | ----------------------------------------------------------------------- |
|
||||
| **TASK-BE-001** | Database Migrations | ✅ **Done** | 100% | Schema v1.5.1 active. TypeORM configured. |
|
||||
| **TASK-BE-002** | Auth & RBAC | ✅ **Done** | 100% | JWT, Refresh Token, RBAC Guard, Permissions complete. |
|
||||
| **TASK-BE-003** | File Storage | ✅ **Done** | 100% | MinIO/S3 strategies implemented (in `common`). |
|
||||
| **TASK-BE-004** | Document Numbering | ✅ **Done** | 100% | **High Quality**: Redlock + Optimistic Locking logic. |
|
||||
| **TASK-BE-005** | Correspondence Module | ✅ **Done** | 95% | CRUD, Workflow Submit, References, Audit Log complete. |
|
||||
| **TASK-BE-006** | Workflow Engine | ✅ **Done** | 100% | DSL Evaluator, Versioning, Event Dispatching complete. |
|
||||
| **TASK-BE-007** | RFA Module | ✅ **Done** | 95% | Full Swagger, Revision handling, Workflow integration. |
|
||||
| **TASK-BE-008** | Drawing Module | ✅ **Done** | 95% | Split into `ShopDrawing` & `ContractDrawing`. |
|
||||
| **TASK-BE-009** | Circulation & Transmittal | ✅ **Done** | 90% | Modules exist and registered in `app.module.ts`. |
|
||||
| **TASK-BE-010** | Search (Elasticsearch) | 🚧 **In Progress** | 70% | Basic search working (Direct Indexing). Missing: Queue & Bulk Re-index. |
|
||||
| **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. |
|
||||
|
||||
## 🛠 Detailed Findings by Component
|
||||
|
||||
|
||||
@@ -16,12 +16,12 @@ FOREIGN KEYS (FK),
|
||||
| id | INT | PRIMARY KEY,
|
||||
AUTO_INCREMENT | UNIQUE identifier FOR organization role | | role_name | VARCHAR(20) | NOT NULL,
|
||||
UNIQUE | Role name (
|
||||
OWNER,
|
||||
DESIGNER,
|
||||
CONSULTANT,
|
||||
CONTRACTOR,
|
||||
THIRD PARTY
|
||||
) | ** INDEXES **: - PRIMARY KEY (id) - UNIQUE (role_name) ** Business Rules **: - Predefined system roles FOR organization TYPES - Cannot be deleted IF referenced by organizations ---
|
||||
) |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp |
|
||||
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp |
|
||||
| deleted_at | DATETIME | NULL | Soft delete timestamp | ** INDEXES **: - PRIMARY KEY (id) - UNIQUE (role_name) ** Business Rules **: - Predefined system roles FOR organization TYPES - Cannot be deleted IF referenced by organizations ---
|
||||
|
||||
### 1.2 organizations
|
||||
|
||||
@@ -29,7 +29,8 @@ UNIQUE | Role name (
|
||||
| id | INT | PRIMARY KEY,
|
||||
AUTO_INCREMENT | UNIQUE identifier FOR organization | | organization_code | VARCHAR(20) | NOT NULL,
|
||||
UNIQUE | Organization code (e.g., 'กทท.', 'TEAM') | | organization_name | VARCHAR(255) | NOT NULL | FULL organization name | | is_active | BOOLEAN | DEFAULT TRUE | Active STATUS (1 = active, 0 = inactive) | | created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | | updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last
|
||||
UPDATE timestamp | ** INDEXES **: - PRIMARY KEY (id) - UNIQUE (organization_code) - INDEX (is_active) ** Relationships **: - Referenced by: users,
|
||||
UPDATE timestamp |
|
||||
| deleted_at | DATETIME | NULL | Soft delete timestamp | ** INDEXES **: - PRIMARY KEY (id) - UNIQUE (organization_code) - INDEX (is_active) ** Relationships **: - Referenced by: users,
|
||||
project_organizations,
|
||||
contract_organizations,
|
||||
correspondences,
|
||||
@@ -40,7 +41,11 @@ UPDATE timestamp | ** INDEXES **: - PRIMARY KEY (id) - UNIQUE (organization_code
|
||||
* * Purpose **: MASTER TABLE FOR ALL projects IN the system | COLUMN Name | Data TYPE | Constraints | Description | | ------------ | ------------ | --------------------------- | ----------------------------- |
|
||||
| id | INT | PRIMARY KEY,
|
||||
AUTO_INCREMENT | UNIQUE identifier FOR project | | project_code | VARCHAR(50) | NOT NULL,
|
||||
UNIQUE | Project code (e.g., 'LCBP3') | | project_name | VARCHAR(255) | NOT NULL | FULL project name | | is_active | TINYINT(1) | DEFAULT 1 | Active STATUS | ** INDEXES **: - PRIMARY KEY (id) - UNIQUE (project_code) - INDEX (is_active) ** Relationships **: - Referenced by: contracts,
|
||||
UNIQUE | Project code (e.g., 'LCBP3') | | project_name | VARCHAR(255) | NOT NULL | FULL project name | | is_active | TINYINT(1) | DEFAULT 1 | Active STATUS |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp |
|
||||
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp |
|
||||
| deleted_at | DATETIME | NULL | Soft delete timestamp |
|
||||
** INDEXES **: - PRIMARY KEY (id) - UNIQUE (project_code) - INDEX (is_active) ** Relationships **: - Referenced by: contracts,
|
||||
correspondences,
|
||||
document_number_formats,
|
||||
drawings ---
|
||||
@@ -53,7 +58,8 @@ UPDATE timestamp | ** INDEXES **: - PRIMARY KEY (id) - UNIQUE (organization_code
|
||||
FK | Reference TO projects TABLE | | contract_code | VARCHAR(50) | NOT NULL,
|
||||
UNIQUE | Contract code | | contract_name | VARCHAR(255) | NOT NULL | FULL contract name | | description | TEXT | NULL | Contract description | | start_date | DATE | NULL | Contract START date | | end_date | DATE | NULL | Contract
|
||||
END date | | is_active | BOOLEAN | DEFAULT TRUE | Active STATUS | | created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | | updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last
|
||||
UPDATE timestamp | ** INDEXES **: - PRIMARY KEY (id) - UNIQUE (contract_code) - FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE - INDEX (project_id, is_active) ** Relationships **: - Parent: projects - Referenced by: contract_organizations,
|
||||
UPDATE timestamp |
|
||||
| deleted_at | DATETIME | NULL | Soft delete timestamp | ** INDEXES **: - PRIMARY KEY (id) - UNIQUE (contract_code) - FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE - INDEX (project_id, is_active) ** Relationships **: - Parent: projects - Referenced by: contract_organizations,
|
||||
user_assignments ---
|
||||
|
||||
### 1.5 disciplines (NEW v1.5.1)
|
||||
@@ -87,7 +93,11 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
||||
AUTO_INCREMENT | UNIQUE identifier FOR role | | role_name | VARCHAR(100) | NOT NULL | Role name (e.g., 'Superadmin', 'Document Control') | | scope | ENUM | NOT NULL | Scope LEVEL: GLOBAL,
|
||||
Organization,
|
||||
Project,
|
||||
Contract | | description | TEXT | NULL | Role description | | is_system | BOOLEAN | DEFAULT FALSE | System role flag (cannot be deleted) | ** INDEXES **: - PRIMARY KEY (role_id) - INDEX (scope) ** Relationships **: - Referenced by: role_permissions,
|
||||
Contract | | description | TEXT | NULL | Role description | | is_system | BOOLEAN | DEFAULT FALSE | System role flag (cannot be deleted) |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp |
|
||||
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp |
|
||||
| deleted_at | DATETIME | NULL | Soft delete timestamp |
|
||||
** INDEXES **: - PRIMARY KEY (role_id) - INDEX (scope) ** Relationships **: - Referenced by: role_permissions,
|
||||
user_assignments ---
|
||||
|
||||
### 2.3 permissions
|
||||
@@ -97,7 +107,11 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
||||
AUTO_INCREMENT | UNIQUE identifier FOR permission | | permission_name | VARCHAR(100) | NOT NULL,
|
||||
UNIQUE | Permission code (e.g., 'rfas.create', 'document.view') | | description | TEXT | NULL | Permission description | | module | VARCHAR(50) | NULL | Related module name | | scope_level | ENUM | NULL | Scope: GLOBAL,
|
||||
ORG,
|
||||
PROJECT | | is_active | TINYINT(1) | DEFAULT 1 | Active STATUS | ** INDEXES **: - PRIMARY KEY (permission_id) - UNIQUE (permission_name) - INDEX (module) - INDEX (scope_level) - INDEX (is_active) ** Relationships **: - Referenced by: role_permissions ---
|
||||
PROJECT | | is_active | TINYINT(1) | DEFAULT 1 | Active STATUS |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp |
|
||||
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp |
|
||||
| deleted_at | DATETIME | NULL | Soft delete timestamp |
|
||||
** INDEXES **: - PRIMARY KEY (permission_id) - UNIQUE (permission_name) - INDEX (module) - INDEX (scope_level) - INDEX (is_active) ** Relationships **: - Referenced by: role_permissions ---
|
||||
|
||||
### 2.4 role_permissions
|
||||
|
||||
@@ -205,6 +219,9 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
||||
| type_name | VARCHAR(255) | NOT NULL | Full type name |
|
||||
| sort_order | INT | DEFAULT 0 | Display order |
|
||||
| is_active | TINYINT(1) | DEFAULT 1 | Active status |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp |
|
||||
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Last update timestamp |
|
||||
| deleted_at | DATETIME | NULL | Soft delete timestamp |
|
||||
|
||||
**Indexes**:
|
||||
|
||||
|
||||
@@ -204,7 +204,10 @@ DROP TABLE IF EXISTS organizations;
|
||||
-- ตาราง Master เก็บประเภทบทบาทขององค์กร
|
||||
CREATE TABLE organization_roles (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
||||
role_name VARCHAR(20) NOT NULL UNIQUE COMMENT 'ชื่อบทบาท (OWNER, DESIGNER, CONSULTANT, CONTRACTOR, THIRD PARTY)'
|
||||
role_name VARCHAR(20) NOT NULL UNIQUE COMMENT 'ชื่อบทบาท (OWNER, DESIGNER, CONSULTANT, CONTRACTOR, THIRD PARTY)',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
|
||||
deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)'
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บประเภทบทบาทขององค์กร';
|
||||
|
||||
-- ตาราง Master เก็บข้อมูลองค์กรทั้งหมดที่เกี่ยวข้องในระบบ
|
||||
@@ -216,6 +219,7 @@ CREATE TABLE organizations (
|
||||
is_active BOOLEAN DEFAULT TRUE COMMENT 'สถานะการใช้งาน',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
|
||||
deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)',
|
||||
FOREIGN KEY (role_id) REFERENCES organization_roles (id) ON DELETE
|
||||
SET NULL
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลองค์กรทั้งหมดที่เกี่ยวข้องในระบบ';
|
||||
@@ -227,8 +231,12 @@ CREATE TABLE projects (
|
||||
project_name VARCHAR(255) NOT NULL COMMENT 'ชื่อโครงการ',
|
||||
-- parent_project_id INT COMMENT 'รหัสโครงการหลัก (ถ้ามี)',
|
||||
-- contractor_organization_id INT COMMENT 'รหัสองค์กรผู้รับเหมา (ถ้ามี)',
|
||||
is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน' -- FOREIGN KEY (parent_project_id) REFERENCES projects(id) ON DELETE SET NULL,
|
||||
is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน',
|
||||
-- FOREIGN KEY (parent_project_id) REFERENCES projects(id) ON DELETE SET NULL,
|
||||
-- FOREIGN KEY (contractor_organization_id) REFERENCES organizations(id) ON DELETE SET NULL
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
|
||||
deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)'
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลโครงการ';
|
||||
|
||||
-- ตาราง Master เก็บข้อมูลสัญญา
|
||||
@@ -243,6 +251,7 @@ CREATE TABLE contracts (
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
|
||||
deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)',
|
||||
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลสัญญา';
|
||||
|
||||
@@ -295,7 +304,10 @@ CREATE TABLE roles (
|
||||
) NOT NULL,
|
||||
-- ขอบเขตของบทบาท (จากข้อ 4.3)
|
||||
description TEXT COMMENT 'คำอธิบายบทบาท',
|
||||
is_system BOOLEAN DEFAULT FALSE COMMENT '(1 = บทบาทของระบบ ลบไม่ได้)'
|
||||
is_system BOOLEAN DEFAULT FALSE COMMENT '(1 = บทบาทของระบบ ลบไม่ได้)',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
|
||||
deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)'
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บ "บทบาท" ของผู้ใช้ในระบบ';
|
||||
|
||||
-- ตาราง Master เก็บ "สิทธิ์" (Permission) หรือ "การกระทำ" ทั้งหมดในระบบ
|
||||
@@ -305,7 +317,10 @@ CREATE TABLE permissions (
|
||||
description TEXT COMMENT 'คำอธิบายสิทธิ์',
|
||||
module VARCHAR(50) COMMENT 'โมดูลที่เกี่ยวข้อง',
|
||||
scope_level ENUM('GLOBAL', 'ORG', 'PROJECT') COMMENT 'ระดับขอบเขตของสิทธิ์',
|
||||
is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน'
|
||||
is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
|
||||
deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)'
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บ "สิทธิ์" (Permission) หรือ "การกระทำ" ทั้งหมดในระบบ';
|
||||
|
||||
-- ตารางเชื่อมระหว่าง roles และ permissions (M:N)
|
||||
@@ -388,7 +403,10 @@ CREATE TABLE correspondence_types (
|
||||
type_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสประเภท (เช่น RFA, RFI)',
|
||||
type_name VARCHAR(255) NOT NULL COMMENT 'ชื่อประเภท',
|
||||
sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล',
|
||||
is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน '
|
||||
is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
|
||||
deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)'
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บประเภทเอกสารโต้ตอบ';
|
||||
|
||||
-- ตาราง Master เก็บสถานะของเอกสาร
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1067
specs/07-database/permissions-seed-data.sql
Normal file
1067
specs/07-database/permissions-seed-data.sql
Normal file
File diff suppressed because it is too large
Load Diff
276
specs/07-database/permissions-verification.sql
Normal file
276
specs/07-database/permissions-verification.sql
Normal file
@@ -0,0 +1,276 @@
|
||||
-- ==========================================================
|
||||
-- Permission System Verification Queries
|
||||
-- File: specs/07-database/permissions-verification.sql
|
||||
-- Purpose: Verify permissions setup after seed data deployment
|
||||
-- ==========================================================
|
||||
-- ==========================================================
|
||||
-- 1. COUNT PERMISSIONS PER CATEGORY
|
||||
-- ==========================================================
|
||||
SELECT CASE
|
||||
WHEN permission_id BETWEEN 1 AND 10 THEN '001-010: System & Global'
|
||||
WHEN permission_id BETWEEN 11 AND 20 THEN '011-020: Organization Management'
|
||||
WHEN permission_id BETWEEN 21 AND 40 THEN '021-040: User & Role Management'
|
||||
WHEN permission_id BETWEEN 41 AND 50 THEN '041-050: Master Data Management'
|
||||
WHEN permission_id BETWEEN 51 AND 70 THEN '051-070: Document Management (Generic)'
|
||||
WHEN permission_id BETWEEN 71 AND 80 THEN '071-080: Correspondence Module'
|
||||
WHEN permission_id BETWEEN 81 AND 90 THEN '081-090: RFA Module'
|
||||
WHEN permission_id BETWEEN 91 AND 100 THEN '091-100: Drawing Module'
|
||||
WHEN permission_id BETWEEN 101 AND 110 THEN '101-110: Circulation Module'
|
||||
WHEN permission_id BETWEEN 111 AND 120 THEN '111-120: Transmittal Module'
|
||||
WHEN permission_id BETWEEN 121 AND 130 THEN '121-130: Workflow Engine'
|
||||
WHEN permission_id BETWEEN 131 AND 140 THEN '131-140: Document Numbering'
|
||||
WHEN permission_id BETWEEN 141 AND 150 THEN '141-150: Search & Reporting'
|
||||
WHEN permission_id BETWEEN 151 AND 160 THEN '151-160: Notification & Dashboard'
|
||||
WHEN permission_id BETWEEN 161 AND 170 THEN '161-170: JSON Schema Management'
|
||||
WHEN permission_id BETWEEN 171 AND 180 THEN '171-180: Monitoring & Admin Tools'
|
||||
WHEN permission_id BETWEEN 201 AND 220 THEN '201-220: Project & Contract Management'
|
||||
ELSE 'Unknown Range'
|
||||
END AS category_range,
|
||||
COUNT(*) AS permission_count
|
||||
FROM permissions
|
||||
GROUP BY CASE
|
||||
WHEN permission_id BETWEEN 1 AND 10 THEN '001-010: System & Global'
|
||||
WHEN permission_id BETWEEN 11 AND 20 THEN '011-020: Organization Management'
|
||||
WHEN permission_id BETWEEN 21 AND 40 THEN '021-040: User & Role Management'
|
||||
WHEN permission_id BETWEEN 41 AND 50 THEN '041-050: Master Data Management'
|
||||
WHEN permission_id BETWEEN 51 AND 70 THEN '051-070: Document Management (Generic)'
|
||||
WHEN permission_id BETWEEN 71 AND 80 THEN '071-080: Correspondence Module'
|
||||
WHEN permission_id BETWEEN 81 AND 90 THEN '081-090: RFA Module'
|
||||
WHEN permission_id BETWEEN 91 AND 100 THEN '091-100: Drawing Module'
|
||||
WHEN permission_id BETWEEN 101 AND 110 THEN '101-110: Circulation Module'
|
||||
WHEN permission_id BETWEEN 111 AND 120 THEN '111-120: Transmittal Module'
|
||||
WHEN permission_id BETWEEN 121 AND 130 THEN '121-130: Workflow Engine'
|
||||
WHEN permission_id BETWEEN 131 AND 140 THEN '131-140: Document Numbering'
|
||||
WHEN permission_id BETWEEN 141 AND 150 THEN '141-150: Search & Reporting'
|
||||
WHEN permission_id BETWEEN 151 AND 160 THEN '151-160: Notification & Dashboard'
|
||||
WHEN permission_id BETWEEN 161 AND 170 THEN '161-170: JSON Schema Management'
|
||||
WHEN permission_id BETWEEN 171 AND 180 THEN '171-180: Monitoring & Admin Tools'
|
||||
WHEN permission_id BETWEEN 201 AND 220 THEN '201-220: Project & Contract Management'
|
||||
ELSE 'Unknown Range'
|
||||
END
|
||||
ORDER BY MIN(permission_id);
|
||||
|
||||
-- ==========================================================
|
||||
-- 2. COUNT PERMISSIONS PER ROLE
|
||||
-- ==========================================================
|
||||
SELECT r.role_id,
|
||||
r.role_name,
|
||||
r.scope,
|
||||
COUNT(rp.permission_id) AS permission_count
|
||||
FROM roles r
|
||||
LEFT JOIN role_permissions rp ON r.role_id = rp.role_id
|
||||
GROUP BY r.role_id,
|
||||
r.role_name,
|
||||
r.scope
|
||||
ORDER BY r.role_id;
|
||||
|
||||
-- ==========================================================
|
||||
-- 3. CHECK TOTAL PERMISSION COUNT
|
||||
-- ==========================================================
|
||||
SELECT 'Total Permissions' AS metric,
|
||||
COUNT(*) AS COUNT
|
||||
FROM permissions
|
||||
UNION ALL
|
||||
SELECT 'Active Permissions',
|
||||
COUNT(*)
|
||||
FROM permissions
|
||||
WHERE is_active = 1;
|
||||
|
||||
-- ==========================================================
|
||||
-- 4. CHECK FOR MISSING PERMISSIONS (Used in Code but Not in DB)
|
||||
-- ==========================================================
|
||||
-- List of permissions actually used in controllers
|
||||
WITH code_permissions AS (
|
||||
SELECT 'system.manage_all' AS permission_name
|
||||
UNION
|
||||
SELECT 'system.impersonate'
|
||||
UNION
|
||||
SELECT 'organization.view'
|
||||
UNION
|
||||
SELECT 'organization.create'
|
||||
UNION
|
||||
SELECT 'user.create'
|
||||
UNION
|
||||
SELECT 'user.view'
|
||||
UNION
|
||||
SELECT 'user.edit'
|
||||
UNION
|
||||
SELECT 'user.delete'
|
||||
UNION
|
||||
SELECT 'user.manage_assignments'
|
||||
UNION
|
||||
SELECT 'role.assign_permissions'
|
||||
UNION
|
||||
SELECT 'project.create'
|
||||
UNION
|
||||
SELECT 'project.view'
|
||||
UNION
|
||||
SELECT 'project.edit'
|
||||
UNION
|
||||
SELECT 'project.delete'
|
||||
UNION
|
||||
SELECT 'contract.create'
|
||||
UNION
|
||||
SELECT 'contract.view'
|
||||
UNION
|
||||
SELECT 'contract.edit'
|
||||
UNION
|
||||
SELECT 'contract.delete'
|
||||
UNION
|
||||
SELECT 'master_data.view'
|
||||
UNION
|
||||
SELECT 'master_data.manage'
|
||||
UNION
|
||||
SELECT 'master_data.drawing_category.manage'
|
||||
UNION
|
||||
SELECT 'master_data.tag.manage'
|
||||
UNION
|
||||
SELECT 'document.view'
|
||||
UNION
|
||||
SELECT 'document.create'
|
||||
UNION
|
||||
SELECT 'document.edit'
|
||||
UNION
|
||||
SELECT 'document.delete'
|
||||
UNION
|
||||
SELECT 'correspondence.create'
|
||||
UNION
|
||||
SELECT 'rfa.create'
|
||||
UNION
|
||||
SELECT 'drawing.create'
|
||||
UNION
|
||||
SELECT 'drawing.view'
|
||||
UNION
|
||||
SELECT 'circulation.create'
|
||||
UNION
|
||||
SELECT 'circulation.respond'
|
||||
UNION
|
||||
SELECT 'workflow.action_review'
|
||||
UNION
|
||||
SELECT 'workflow.manage_definitions'
|
||||
UNION
|
||||
SELECT 'search.advanced'
|
||||
UNION
|
||||
SELECT 'json_schema.view'
|
||||
UNION
|
||||
SELECT 'json_schema.manage'
|
||||
UNION
|
||||
SELECT 'monitoring.manage_maintenance'
|
||||
)
|
||||
SELECT cp.permission_name,
|
||||
CASE
|
||||
WHEN p.permission_id IS NULL THEN '❌ MISSING'
|
||||
ELSE '✅ EXISTS'
|
||||
END AS STATUS,
|
||||
p.permission_id
|
||||
FROM code_permissions cp
|
||||
LEFT JOIN permissions p ON cp.permission_name = p.permission_name
|
||||
ORDER BY STATUS DESC,
|
||||
cp.permission_name;
|
||||
|
||||
-- ==========================================================
|
||||
-- 5. LIST PERMISSIONS FOR EACH ROLE
|
||||
-- ==========================================================
|
||||
SELECT r.role_name,
|
||||
r.scope,
|
||||
GROUP_CONCAT(
|
||||
p.permission_name
|
||||
ORDER BY p.permission_id SEPARATOR ', '
|
||||
) AS permissions
|
||||
FROM roles r
|
||||
LEFT JOIN role_permissions rp ON r.role_id = rp.role_id
|
||||
LEFT JOIN permissions p ON rp.permission_id = p.permission_id
|
||||
GROUP BY r.role_id,
|
||||
r.role_name,
|
||||
r.scope
|
||||
ORDER BY r.role_id;
|
||||
|
||||
-- ==========================================================
|
||||
-- 6. CHECK SUPERADMIN HAS ALL PERMISSIONS
|
||||
-- ==========================================================
|
||||
SELECT 'Superadmin Permission Coverage' AS metric,
|
||||
CONCAT(
|
||||
COUNT(DISTINCT rp.permission_id),
|
||||
' / ',
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM permissions
|
||||
WHERE is_active = 1
|
||||
),
|
||||
' (',
|
||||
ROUND(
|
||||
COUNT(DISTINCT rp.permission_id) * 100.0 / (
|
||||
SELECT COUNT(*)
|
||||
FROM permissions
|
||||
WHERE is_active = 1
|
||||
),
|
||||
1
|
||||
),
|
||||
'%)'
|
||||
) AS coverage
|
||||
FROM role_permissions rp
|
||||
WHERE rp.role_id = 1;
|
||||
|
||||
-- Superadmin
|
||||
-- ==========================================================
|
||||
-- 7. CHECK FOR DUPLICATE PERMISSIONS
|
||||
-- ==========================================================
|
||||
SELECT permission_name,
|
||||
COUNT(*) AS duplicate_count
|
||||
FROM permissions
|
||||
GROUP BY permission_name
|
||||
HAVING COUNT(*) > 1;
|
||||
|
||||
-- ==========================================================
|
||||
-- 8. CHECK PERMISSIONS WITHOUT ROLE ASSIGNMENTS
|
||||
-- ==========================================================
|
||||
SELECT p.permission_id,
|
||||
p.permission_name,
|
||||
p.description
|
||||
FROM permissions p
|
||||
LEFT JOIN role_permissions rp ON p.permission_id = rp.permission_id
|
||||
WHERE rp.permission_id IS NULL
|
||||
AND p.is_active = 1
|
||||
ORDER BY p.permission_id;
|
||||
|
||||
-- ==========================================================
|
||||
-- 9. CHECK USER PERMISSION VIEW (v_user_all_permissions)
|
||||
-- ==========================================================
|
||||
-- Test with user_id = 1 (Superadmin)
|
||||
SELECT 'User 1 (Superadmin) Permissions' AS metric,
|
||||
COUNT(*) AS permission_count
|
||||
FROM v_user_all_permissions
|
||||
WHERE user_id = 1;
|
||||
|
||||
-- List first 10 permissions for user 1
|
||||
SELECT user_id,
|
||||
permission_name
|
||||
FROM v_user_all_permissions
|
||||
WHERE user_id = 1
|
||||
ORDER BY permission_name
|
||||
LIMIT 10;
|
||||
|
||||
-- ==========================================================
|
||||
-- 10. CHECK SPECIFIC CRITICAL PERMISSIONS
|
||||
-- ==========================================================
|
||||
SELECT permission_name,
|
||||
permission_id,
|
||||
CASE
|
||||
WHEN permission_id IS NOT NULL THEN '✅ Exists'
|
||||
ELSE '❌ Missing'
|
||||
END AS STATUS
|
||||
FROM (
|
||||
SELECT 'system.manage_all' AS permission_name
|
||||
UNION
|
||||
SELECT 'document.view'
|
||||
UNION
|
||||
SELECT 'user.create'
|
||||
UNION
|
||||
SELECT 'master_data.manage'
|
||||
UNION
|
||||
SELECT 'drawing.view'
|
||||
UNION
|
||||
SELECT 'workflow.action_review'
|
||||
) required_perms
|
||||
LEFT JOIN permissions p USING (permission_name)
|
||||
ORDER BY permission_name;
|
||||
1281
specs/09-history/20251208-TASK-BE-004-document-numbering.md
Normal file
1281
specs/09-history/20251208-TASK-BE-004-document-numbering.md
Normal file
File diff suppressed because it is too large
Load Diff
537
specs/09-history/20251208-TASK-FE-012-numbering-config-ui.md
Normal file
537
specs/09-history/20251208-TASK-FE-012-numbering-config-ui.md
Normal file
@@ -0,0 +1,537 @@
|
||||
# TASK-FE-012: Document Numbering Configuration UI
|
||||
|
||||
**ID:** TASK-FE-012
|
||||
**Title:** Document Numbering Template Management UI
|
||||
**Category:** Administration
|
||||
**Priority:** P2 (Medium)
|
||||
**Effort:** 3-4 days
|
||||
**Dependencies:** TASK-FE-010, TASK-BE-004
|
||||
**Assigned To:** Frontend Developer
|
||||
|
||||
---
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
Build UI for configuring and managing document numbering templates including template builder, preview generator, and number sequence management.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Objectives
|
||||
|
||||
1. Create numbering template list and management
|
||||
2. Build template editor with format preview
|
||||
3. Implement template variable selector
|
||||
4. Add numbering sequence viewer
|
||||
5. Create template testing interface
|
||||
6. Implement annual reset configuration
|
||||
|
||||
---
|
||||
|
||||
## ✅ Acceptance Criteria
|
||||
|
||||
- [x] List all numbering templates by document type
|
||||
- [x] Create/edit templates with format preview
|
||||
- [x] Template variables easily selectable
|
||||
- [x] Preview shows example numbers
|
||||
- [x] View current number sequences
|
||||
- [x] Annual reset configurable
|
||||
- [x] Validation prevents conflicts
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Implementation Steps
|
||||
|
||||
### Step 1: Template List Page
|
||||
|
||||
```typescript
|
||||
// File: src/app/(admin)/admin/numbering/page.tsx
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Plus, Edit, Eye } from 'lucide-react';
|
||||
|
||||
export default function NumberingPage() {
|
||||
const [templates, setTemplates] = useState([]);
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">
|
||||
Document Numbering Configuration
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-1">
|
||||
Manage document numbering templates and sequences
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Select defaultValue="1">
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="Select Project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">LCBP3</SelectItem>
|
||||
<SelectItem value="2">LCBP3-Maintenance</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Template
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{templates.map((template: any) => (
|
||||
<Card key={template.template_id} className="p-6">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{template.document_type_name}
|
||||
</h3>
|
||||
<Badge>{template.discipline_code || 'All'}</Badge>
|
||||
<Badge variant={template.is_active ? 'success' : 'secondary'}>
|
||||
{template.is_active ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-100 rounded px-3 py-2 mb-3 font-mono text-sm">
|
||||
{template.template_format}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-600">Example: </span>
|
||||
<span className="font-medium">
|
||||
{template.example_number}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">Current Sequence: </span>
|
||||
<span className="font-medium">
|
||||
{template.current_number}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">Annual Reset: </span>
|
||||
<span className="font-medium">
|
||||
{template.reset_annually ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">Padding: </span>
|
||||
<span className="font-medium">
|
||||
{template.padding_length} digits
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
View Sequences
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Template Editor Component
|
||||
|
||||
```typescript
|
||||
// File: src/components/numbering/template-editor.tsx
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
const VARIABLES = [
|
||||
{ key: '{ORIGINATOR}', name: 'Originator Code', example: 'PAT' },
|
||||
{ key: '{RECIPIENT}', name: 'Recipient Code', example: 'CN' },
|
||||
{ key: '{CORR_TYPE}', name: 'Correspondence Type', example: 'RFA' },
|
||||
{ key: '{SUB_TYPE}', name: 'Sub Type', example: '21' },
|
||||
{ key: '{DISCIPLINE}', name: 'Discipline', example: 'STR' },
|
||||
{ key: '{RFA_TYPE}', name: 'RFA Type', example: 'SDW' },
|
||||
{ key: '{YEAR:B.E.}', name: 'Year (B.E.)', example: '2568' },
|
||||
{ key: '{YEAR:A.D.}', name: 'Year (A.D.)', example: '2025' },
|
||||
{ key: '{SEQ:4}', name: 'Sequence (4-digit)', example: '0001' },
|
||||
{ key: '{REV}', name: 'Revision', example: 'A' },
|
||||
];
|
||||
|
||||
export function TemplateEditor({ template, onSave }: any) {
|
||||
const [format, setFormat] = useState(template?.template_format || '');
|
||||
const [preview, setPreview] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// Generate preview
|
||||
let previewText = format;
|
||||
VARIABLES.forEach((v) => {
|
||||
previewText = previewText.replace(new RegExp(v.key, 'g'), v.example);
|
||||
});
|
||||
setPreview(previewText);
|
||||
}, [format]);
|
||||
|
||||
const insertVariable = (variable: string) => {
|
||||
setFormat((prev) => prev + variable);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6 space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Template Configuration</h3>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div>
|
||||
<Label>Document Type *</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select document type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="RFA">RFA</SelectItem>
|
||||
<SelectItem value="RFI">RFI</SelectItem>
|
||||
<SelectItem value="TRANSMITTAL">Transmittal</SelectItem>
|
||||
<SelectItem value="LETTER">Letter</SelectItem>
|
||||
<SelectItem value="MEMO">Memorandum</SelectItem>
|
||||
<SelectItem value="EMAIL">Email</SelectItem>
|
||||
<SelectItem value="MOM">Minutes of Meeting</SelectItem>
|
||||
<SelectItem value="INSTRUCTION">Instruction</SelectItem>
|
||||
<SelectItem value="NOTICE">Notice</SelectItem>
|
||||
<SelectItem value="OTHER">Other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Discipline (Optional)</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="All disciplines" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">All</SelectItem>
|
||||
<SelectItem value="STR">STR - Structure</SelectItem>
|
||||
<SelectItem value="ARC">ARC - Architecture</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Template Format *</Label>
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
value={format}
|
||||
onChange={(e) => setFormat(e.target.value)}
|
||||
placeholder="e.g., {ORG}-{DOCTYPE}-{YYYY}-{SEQ}"
|
||||
className="font-mono"
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{VARIABLES.map((v) => (
|
||||
<Button
|
||||
key={v.key}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => insertVariable(v.key)}
|
||||
type="button"
|
||||
>
|
||||
{v.key}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Preview</Label>
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<p className="text-sm text-gray-600 mb-1">Example number:</p>
|
||||
<p className="text-2xl font-mono font-bold text-green-700">
|
||||
{preview || 'Enter format above'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Sequence Padding Length</Label>
|
||||
<Input type="number" defaultValue={4} min={1} max={10} />
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Number of digits (e.g., 4 = 0001, 0002)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Starting Number</Label>
|
||||
<Input type="number" defaultValue={1} min={1} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2">
|
||||
<Checkbox defaultChecked />
|
||||
<span className="text-sm">Reset annually (on January 1st)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Variable Reference */}
|
||||
<div>
|
||||
<h4 className="font-semibold mb-3">Available Variables</h4>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{VARIABLES.map((v) => (
|
||||
<div
|
||||
key={v.key}
|
||||
className="flex items-center justify-between p-2 bg-gray-50 rounded"
|
||||
>
|
||||
<div>
|
||||
<Badge variant="outline" className="font-mono">
|
||||
{v.key}
|
||||
</Badge>
|
||||
<p className="text-xs text-gray-600 mt-1">{v.name}</p>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">{v.example}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button onClick={onSave}>Save Template</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Number Sequence Viewer
|
||||
|
||||
```typescript
|
||||
// File: src/components/numbering/sequence-viewer.tsx
|
||||
'use client';
|
||||
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
|
||||
export function SequenceViewer({ templateId }: { templateId: number }) {
|
||||
const [sequences, setSequences] = useState([]);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold">Number Sequences</h3>
|
||||
<Button variant="outline" size="sm">
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
placeholder="Search by year, organization..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{sequences.map((seq: any) => (
|
||||
<div
|
||||
key={seq.sequence_id}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 rounded"
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-medium">{seq.year}</span>
|
||||
{seq.organization_code && (
|
||||
<Badge>{seq.organization_code}</Badge>
|
||||
)}
|
||||
{seq.discipline_code && (
|
||||
<Badge variant="outline">{seq.discipline_code}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
Current: {seq.current_number} | Last Generated:{' '}
|
||||
{seq.last_generated_number}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
Updated {new Date(seq.updated_at).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Template Testing Dialog
|
||||
|
||||
```typescript
|
||||
// File: src/components/numbering/template-tester.tsx
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card } from '@/components/ui/card';
|
||||
|
||||
export function TemplateTester({ open, onOpenChange, template }: any) {
|
||||
const [testData, setTestData] = useState({
|
||||
organization_id: 1,
|
||||
discipline_id: null,
|
||||
year: new Date().getFullYear(),
|
||||
});
|
||||
const [generatedNumber, setGeneratedNumber] = useState('');
|
||||
|
||||
const handleTest = async () => {
|
||||
// Call API to generate test number
|
||||
const response = await fetch('/api/numbering/test', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ template_id: template.template_id, ...testData }),
|
||||
});
|
||||
const result = await response.json();
|
||||
setGeneratedNumber(result.number);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Test Number Generation</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Organization</Label>
|
||||
<Select value={testData.organization_id.toString()}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">กทท.</SelectItem>
|
||||
<SelectItem value="2">สค©.</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Discipline (Optional)</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select discipline" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">STR</SelectItem>
|
||||
<SelectItem value="2">ARC</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleTest} className="w-full">
|
||||
Generate Test Number
|
||||
</Button>
|
||||
|
||||
{generatedNumber && (
|
||||
<Card className="p-4 bg-green-50 border-green-200">
|
||||
<p className="text-sm text-gray-600 mb-1">Generated Number:</p>
|
||||
<p className="text-2xl font-mono font-bold text-green-700">
|
||||
{generatedNumber}
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Deliverables
|
||||
|
||||
- [ ] Template list page
|
||||
- [ ] Template editor with variable selector
|
||||
- [ ] Live preview generator
|
||||
- [ ] Number sequence viewer
|
||||
- [ ] Template testing interface
|
||||
- [ ] Annual reset configuration
|
||||
- [ ] Validation rules
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
1. **Template Creation**
|
||||
|
||||
- Create template → Preview updates
|
||||
- Insert variables → Format correct
|
||||
- Save template → Persists
|
||||
|
||||
2. **Number Generation**
|
||||
|
||||
- Test template → Generates number
|
||||
- Variables replaced correctly
|
||||
- Sequence increments
|
||||
|
||||
3. **Sequence Management**
|
||||
- View sequences → Shows all active sequences
|
||||
- Search sequences → Filters correctly
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Related Documents
|
||||
|
||||
- [TASK-BE-004: Document Numbering](./TASK-BE-004-document-numbering.md)
|
||||
- [ADR-002: Document Numbering Strategy](../../05-decisions/ADR-002-document-numbering-strategy.md)
|
||||
|
||||
---
|
||||
|
||||
**Created:** 2025-12-01
|
||||
**Status:** Ready
|
||||
Reference in New Issue
Block a user