251208:1625 Frontend: to be complete admin panel, Backend: tobe recheck all task
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled

This commit is contained in:
admin
2025-12-08 16:25:56 +07:00
parent dcd126d704
commit 863a727756
64 changed files with 5956 additions and 1256 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

View 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