348 lines
18 KiB
Markdown
348 lines
18 KiB
Markdown
# Tasks: Transmittals + Circulation Complete Integration (v1.8.8 with Revision Refactor)
|
|
|
|
**Branch**: `001-transmittals-circulation` | **Total Tasks**: 36 (18 v1.8.7 + 18 v1.8.8 Phase 4) | **Phase**: Phase 4 Ready — Revision Refactor
|
|
|
|
---
|
|
|
|
## Phase 1 — Backend Foundation (Critical — blocks all frontend work)
|
|
|
|
### B1 — WorkflowEngineService: Add `getInstanceByEntity()`
|
|
- **File**: `backend/src/modules/workflow-engine/workflow-engine.service.ts`
|
|
- **Action**: Add method that queries `WorkflowInstance` by `entityType + entityId`; returns `{ id, currentState, availableActions? } | null`
|
|
- **Dependencies**: none
|
|
- **Status**: [x]
|
|
|
|
### B2 — TransmittalService: Expose `workflowInstanceId` in `findOneByUuid()`
|
|
- **File**: `backend/src/modules/transmittal/transmittal.service.ts`
|
|
- **Action**: Call `workflowEngine.getInstanceByEntity('transmittal', correspondenceId.toString())` and merge `workflowInstanceId`, `workflowState` into response
|
|
- **Dependencies**: B1
|
|
- **Status**: [x]
|
|
|
|
### B3 — TransmittalService: Add `purpose` filter to `findAll()`
|
|
- **File**: `backend/src/modules/transmittal/transmittal.service.ts`
|
|
- **Action**: Add `purpose?: string` to `SearchTransmittalDto` and apply `andWhere` in `findAll()`
|
|
- **Dependencies**: none (parallel with B1)
|
|
- **Status**: [x]
|
|
|
|
### B4 — TransmittalService: Add `submit()` with EC-RFA-004 validation
|
|
- **File**: `backend/src/modules/transmittal/transmittal.service.ts`
|
|
- **Action**: New `submit(uuid, user)` method; fetches all `transmittal_items`, checks each item's correspondence current revision status — throws `422 ValidationException` if any is `DRAFT`; then calls `workflowEngine.createInstance('TRANSMITTAL_FLOW_V1', 'transmittal', ...)` and transitions with `SUBMIT`
|
|
- **Dependencies**: B1
|
|
- **Status**: [x]
|
|
|
|
### B5 — TransmittalController: Add `POST /:uuid/submit` endpoint
|
|
- **File**: `backend/src/modules/transmittal/transmittal.controller.ts`
|
|
- **Action**: Add endpoint with `@RequirePermission('document.manage')`, `@Audit('transmittal.submit', 'transmittal')`
|
|
- **Dependencies**: B4
|
|
- **Status**: [x]
|
|
|
|
### B6 — CirculationService: Expose `workflowInstanceId` in `findOneByUuid()`
|
|
- **File**: `backend/src/modules/circulation/circulation.service.ts`
|
|
- **Action**: Call `workflowEngine.getInstanceByEntity('circulation', circulation.id.toString())`, merge into response; also compute `isOverdue` per routing based on `deadline_date`
|
|
- **Dependencies**: B1
|
|
- **Status**: [x]
|
|
|
|
### B7 — CirculationService: Add `reassignRouting()` (EC-CIRC-001)
|
|
- **File**: `backend/src/modules/circulation/circulation.service.ts`
|
|
- **Action**: Fetch routing, verify user has Document Control permission, resolve `newAssigneeUuid` → INT via `uuidResolver.resolveUserId()`, update `routing.assignedTo`, write audit log
|
|
- **Dependencies**: none
|
|
- **Status**: [x]
|
|
|
|
### B8 — CirculationService: Add `forceClose()` (EC-CIRC-002)
|
|
- **File**: `backend/src/modules/circulation/circulation.service.ts`
|
|
- **Action**: Require `reason` (non-empty), update all PENDING routings to `CANCELLED`, set `circulation.statusCode = 'CANCELLED'`, write audit log entry; use `queryRunner` for atomicity
|
|
- **Dependencies**: none
|
|
- **Status**: [x]
|
|
|
|
### B9 — CirculationController: Add reassign + force-close endpoints
|
|
- **File**: `backend/src/modules/circulation/circulation.controller.ts`
|
|
- **Action**:
|
|
- `PATCH /:uuid/routing/:routingId/reassign` — `@RequirePermission('circulation.manage')`
|
|
- `POST /:uuid/force-close` — `@RequirePermission('circulation.manage')`
|
|
- **Dependencies**: B7, B8
|
|
- **Status**: [x]
|
|
|
|
---
|
|
|
|
## Phase 2 — Frontend Types & Hooks (Important — depends on Phase 1)
|
|
|
|
### F1 — Update `types/transmittal.ts`
|
|
- **File**: `frontend/types/transmittal.ts`
|
|
- **Action**: Add `workflowInstanceId?: string`, `workflowState?: string`, `availableActions?: string[]` to `Transmittal` interface; add `purpose?: string` to `SearchTransmittalDto`; no `any` types (ADR-019)
|
|
- **Dependencies**: none (parallel with Phase 1)
|
|
- **Status**: [x]
|
|
|
|
### F2 — Update `types/circulation.ts`
|
|
- **File**: `frontend/types/circulation.ts`
|
|
- **Action**: Add `workflowInstanceId?: string`, `workflowState?: string`, `availableActions?: string[]` to `Circulation`; add `deadline?: string`, `assigneeType?: 'MAIN' | 'ACTION' | 'INFORMATION'` to `CirculationRouting`
|
|
- **Dependencies**: none
|
|
- **Status**: [x]
|
|
|
|
### F3 — Create `hooks/use-transmittal.ts`
|
|
- **File**: `frontend/hooks/use-transmittal.ts`
|
|
- **Action**: Create `useTransmittal(uuid: string | undefined)` with `queryKey: ['transmittal', uuid]`, `staleTime: 60_000`; export `transmittalKeys` query key factory
|
|
- **Dependencies**: F1
|
|
- **Status**: [x]
|
|
|
|
### F4 — Update `hooks/use-circulation.ts`
|
|
- **File**: `frontend/hooks/use-circulation.ts`
|
|
- **Action**: Add `useCirculation(uuid: string | undefined)` hook with `queryKey: ['circulation', uuid]`, `staleTime: 60_000`
|
|
- **Dependencies**: F2
|
|
- **Status**: [x]
|
|
|
|
---
|
|
|
|
## Phase 3 — Frontend Detail Pages (Important — depends on Phase 2 + Phase 1 deployed)
|
|
|
|
### F5 — Wire Transmittal detail page
|
|
- **File**: `frontend/app/(dashboard)/transmittals/[uuid]/page.tsx`
|
|
- **Action**:
|
|
- Replace inline `useQuery` with `useTransmittal(uuid)`
|
|
- Add `useWorkflowHistory(transmittal?.workflowInstanceId)`
|
|
- Add `const [pendingAttachmentIds, setPendingAttachmentIds] = useState<string[]>([])`
|
|
- Pass `instanceId`, `workflowState`, `availableActions`, `pendingAttachmentIds` to `IntegratedBanner`
|
|
- Pass `history`, `currentState`, `isLoading`, `error`, `onAttachmentsChange` to `WorkflowLifecycle` in Workflow tab
|
|
- **Dependencies**: F3, F1
|
|
- **Status**: [x]
|
|
|
|
### F6 — Wire Circulation detail page
|
|
- **File**: `frontend/app/(dashboard)/circulation/[uuid]/page.tsx`
|
|
- **Action**:
|
|
- Replace inline `useQuery` with `useCirculation(uuid)`
|
|
- Add `useWorkflowHistory(circulation?.workflowInstanceId)`
|
|
- Add `isOverdue(deadline?)` helper function
|
|
- Wire `IntegratedBanner` with `instanceId`, `workflowState`, `availableActions`
|
|
- Wire `WorkflowLifecycle` with history in Workflow tab
|
|
- Add Overdue badge to routing rows where `isOverdue(routing.deadline)` is true
|
|
- Replace hardcoded "Complete" button with proper workflow action
|
|
- **Dependencies**: F4, F2
|
|
- **Status**: [x]
|
|
|
|
---
|
|
|
|
## Phase 4 — List Page & i18n (Guidelines)
|
|
|
|
### F7 — Transmittal list page: add purpose filter
|
|
- **File**: `frontend/app/(dashboard)/transmittals/page.tsx`
|
|
- **Action**: Add `purpose` select filter (FOR_APPROVAL / FOR_INFORMATION / FOR_REVIEW / OTHER) passing to `transmittalService.getAll()`. Read current page to assess if pagination works.
|
|
- **Dependencies**: F1
|
|
- **Status**: [x]
|
|
|
|
### I1 — i18n keys for Transmittal/Circulation workflow
|
|
- **Files**: `public/locales/th/*.json`, `public/locales/en/*.json`
|
|
- **Action**: Check `use-translations.ts` for key lookup pattern; add missing keys: `transmittal.purpose.*`, `circulation.status.*`, `circulation.overdue`, `circulation.forceClose.*`, `circulation.reassign.*`
|
|
- **Dependencies**: F5, F6
|
|
- **Status**: [ ] *(low priority — pending)*
|
|
|
|
---
|
|
|
|
## Phase 5 — Tests (Tier 2 — required before merge)
|
|
|
|
### T1 — Transmittal service EC-RFA-004 unit test
|
|
- **File**: `backend/src/modules/transmittal/transmittal.service.spec.ts` (create if needed)
|
|
- **Action**: Test `submit()` throws `ValidationException` when item correspondence is DRAFT; test passes when all items are SUBMITTED
|
|
- **Dependencies**: B4
|
|
- **Status**: [x]
|
|
|
|
### T2 — Circulation service edge-case unit tests
|
|
- **File**: `backend/src/modules/circulation/circulation.service.spec.ts` (create if needed)
|
|
- **Action**: Test `reassignRouting()` — permission check, assignment update; test `forceClose()` — all pending routings cancelled, reason logged; test `isOverdue` helper (EC-CIRC-003)
|
|
- **Dependencies**: B7, B8
|
|
- **Status**: [x]
|
|
|
|
---
|
|
|
|
## Execution Order
|
|
|
|
```
|
|
B1 (parallel: B3, F1, F2)
|
|
→ B2, B4 (parallel), B6 (parallel)
|
|
→ B5 → T1
|
|
→ B7, B8 (parallel)
|
|
→ B9 → T2
|
|
→ F3, F4 (parallel after F1, F2)
|
|
→ F5, F6 (parallel after F3, F4)
|
|
→ F7, I1 (polish)
|
|
```
|
|
|
|
---
|
|
|
|
## Commit Message Convention
|
|
|
|
```
|
|
feat(transmittal): expose workflowInstanceId in findOneByUuid response
|
|
feat(circulation): expose workflowInstanceId + overdue in findOneByUuid
|
|
feat(circulation): add reassignRouting EC-CIRC-001 handler
|
|
feat(circulation): add forceClose EC-CIRC-002 handler
|
|
feat(transmittal): add submit endpoint with EC-RFA-004 validation
|
|
feat(frontend): wire WorkflowLifecycle in transmittal detail page
|
|
feat(frontend): wire WorkflowLifecycle + overdue badge in circulation detail
|
|
test(transmittal): EC-RFA-004 submit validation unit tests
|
|
test(circulation): EC-CIRC-001/002/003 edge case unit tests
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 4 — Transmittal Revision Refactor (v1.8.8)
|
|
|
|
**Based on**: Clarifications Session 2026-04-29
|
|
**Goal**: Restructure Transmittal to follow Master-Revision Pattern
|
|
|
|
### R1 — Schema: Add `revision_id` to `transmittal_items`
|
|
- **File**: `specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql`
|
|
- **Action**: Add `revision_id INT NULL` column with FK to `correspondence_revisions(id)`, create index per ADR-009
|
|
- **Dependencies**: none
|
|
- **Status**: [ ]
|
|
|
|
### R2 — Backend Entity: Update `TransmittalItem` with `revisionId`
|
|
- **File**: `backend/src/modules/transmittal/entities/transmittal-item.entity.ts`
|
|
- **Action**: Add `revisionId` column (nullable), add `@ManyToOne` relation to `CorrespondenceRevision`
|
|
- **Dependencies**: R1
|
|
- **Status**: [ ]
|
|
|
|
### R3 — Backend Service: Update `findOneByUuid` to read from revision
|
|
- **File**: `backend/src/modules/transmittal/transmittal.service.ts`
|
|
- **Action**: Join `correspondence_revisions`, read `purpose`/`remarks` from `details` JSON field; include `revisionId`, `revisionNumber`, `revisionLabel` in response
|
|
- **Dependencies**: R2
|
|
- **Status**: [ ]
|
|
|
|
### R4 — Backend Service: Add `createRevision()` method
|
|
- **File**: `backend/src/modules/transmittal/transmittal.service.ts`
|
|
- **Action**: Create new `correspondence_revisions` record, copy all items from current revision to new revision via `copyItemsToRevision()` helper
|
|
- **Dependencies**: R3
|
|
- **Status**: [ ]
|
|
|
|
### R5 — Backend Service: Add `copyItemsToRevision()` helper
|
|
- **File**: `backend/src/modules/transmittal/transmittal.service.ts`
|
|
- **Action**: Clone all `transmittal_items` where `revision_id = oldRevisionId`, insert new records with `revision_id = newRevisionId`. **Success Criteria**: (1) Item count in new revision equals old revision, (2) All `quantity` values preserved, (3) `item_correspondence_id` FK constraints pass, (4) Atomic transaction (rollback on failure).
|
|
- **Dependencies**: R2
|
|
- **Status**: [ ]
|
|
|
|
### R6 — Backend Controller: Add `POST /:uuid/revisions` endpoint
|
|
- **File**: `backend/src/modules/transmittal/transmittal.controller.ts`
|
|
- **Action**: New endpoint with `@RequirePermission('document.manage')` (ADR-016), `@Audit('transmittal.create-revision', 'transmittal')`, calls `createRevision()`, returns `{ revisionId, revisionNumber, revisionLabel }`
|
|
- **Dependencies**: R4
|
|
- **Status**: [ ]
|
|
- **Security**: CASL Guard required — Document Control role or above
|
|
|
|
### R7 — Backend Service: Update `submit()` for revision-scoped items
|
|
- **File**: `backend/src/modules/transmittal/transmittal.service.ts`
|
|
- **Action**: EC-RFA-004 validation checks items for current revision only; workflow instance binds to `correspondence_revisions.id`
|
|
- **Dependencies**: R3, R4
|
|
- **Status**: [ ]
|
|
|
|
### R8 — Frontend Types: Add revision fields to `Transmittal`
|
|
- **File**: `frontend/types/transmittal.ts`
|
|
- **Action**: Add `revisionId?: string`, `revisionNumber?: number`, `revisionLabel?: string` to `Transmittal` interface
|
|
- **Dependencies**: none (parallel)
|
|
- **Status**: [ ]
|
|
|
|
### R9 — Frontend Types: Add `revisionId` to `TransmittalItem`
|
|
- **File**: `frontend/types/transmittal.ts`
|
|
- **Action**: Add `revisionId?: string` to `TransmittalItem` interface
|
|
- **Dependencies**: R8
|
|
- **Status**: [ ]
|
|
|
|
### R10 — Frontend Page: Add revision selector to detail page
|
|
- **File**: `frontend/app/(dashboard)/transmittals/[uuid]/page.tsx`
|
|
- **Action**: Show revision dropdown when multiple revisions exist (like RFA pattern), display `revisionLabel` (A, B, C) in banner
|
|
- **Dependencies**: R8, R9
|
|
- **Status**: [ ]
|
|
|
|
### R11 — Frontend Hook: Update `useTransmittal` for revision context
|
|
- **File**: `frontend/hooks/use-transmittal.ts`
|
|
- **Action**: Add optional `revisionId` parameter to fetch specific revision; default to current revision
|
|
- **Dependencies**: R8
|
|
- **Status**: [ ]
|
|
|
|
### R12 — Workflow Engine: Update `getInstanceByEntity` for revision binding (ADR-019)
|
|
- **File**: `backend/src/modules/workflow-engine/workflow-engine.service.ts`
|
|
- **Action**: Support `entity_type='transmittal'` with `entity_id=revision.publicId` (UUID string, NOT revision.id INT). Ensure workflow instance stores and retrieves using UUIDv7 string per ADR-019.
|
|
- **Dependencies**: R3
|
|
- **Status**: [ ]
|
|
- **ADR-019 Check**: Use `revision.publicId` (string) — never `revision.id` (INT) for entity binding
|
|
|
|
### R14 — Backend Service: Update `create()` to write `purpose`/`remarks` to `details` JSON
|
|
- **File**: `backend/src/modules/transmittal/transmittal.service.ts`
|
|
- **Action**: In `create()`, stop writing `purpose`/`remarks` to `Transmittal` entity; instead store them in `CorrespondenceRevision.details = { purpose, remarks }` JSON field. Remove `purpose`/`remarks` from `queryRunner.manager.create(Transmittal, {...})` call.
|
|
- **Dependencies**: R3 (findOneByUuid reads from details)
|
|
- **Status**: [ ]
|
|
- **Note**: Must deploy BEFORE step 3 SQL (DROP COLUMN) in schema-02-tables.sql
|
|
|
|
### R15 — Schema: Drop `purpose` and `remarks` from `transmittals` table
|
|
- **File**: `specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql`
|
|
- **Action**: `ALTER TABLE transmittals DROP COLUMN purpose, DROP COLUMN remarks;` per ADR-009. Also remove corresponding TypeORM columns from `transmittal.entity.ts`.
|
|
- **Dependencies**: R14 (must be fully deployed first)
|
|
- **Status**: [ ]
|
|
- **ADR-009**: Direct SQL only — no TypeORM migration file
|
|
|
|
### R16 — DTO: Fix `TransmittalItemDto.itemId` to UUID (ADR-019)
|
|
- **File**: `backend/src/modules/transmittal/dto/create-transmittal.dto.ts` + `backend/src/modules/transmittal/transmittal.service.ts`
|
|
- **Action**: Change `itemId: number` + `@IsInt()` → `itemId: string` + `@IsUUID()`. In `create()`, replace direct assignment with `uuidResolver.resolveCorrespondenceId(item.itemId)` before saving `itemCorrespondenceId`.
|
|
- **Dependencies**: R1 (schema must be stable)
|
|
- **Status**: [ ]
|
|
- **ADR-019**: CRITICAL — Frontend must send `publicId` (UUID string), not INT id
|
|
|
|
### R17 — Schema + Entity: Add `itemType` column to `transmittal_items` (H1)
|
|
- **Files**: `specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql` + `backend/src/modules/transmittal/entities/transmittal-item.entity.ts` + `backend/src/modules/transmittal/transmittal.service.ts`
|
|
- **Action**: (1) SQL: `ALTER TABLE transmittal_items ADD COLUMN item_type VARCHAR(50) NULL COMMENT 'ประเภทเอกสาร เช่น DRAWING, RFA, CORRESPONDENCE' AFTER item_correspondence_id;` (ADR-009). (2) Entity: add `@Column({ name: 'item_type', nullable: true }) itemType?: string;`. (3) Service `create()`: save `itemType: item.itemType` from DTO (field already exists in `TransmittalItemDto`).
|
|
- **Dependencies**: R1
|
|
- **Status**: [ ]
|
|
- **Note**: Fixes H1 — DTO had `itemType` but it was never persisted to DB
|
|
|
|
### R18 — Service: Fix `ORG_CODE` hardcode in `create()` (M1)
|
|
- **Files**: `backend/src/modules/transmittal/transmittal.service.ts`
|
|
- **Action**: Before `generateNextNumber()`, fetch originator org: `const originatorOrg = await this.dataSource.manager.findOne(Organization, { where: { id: userOrgId } }); const orgCode = originatorOrg?.organizationCode ?? 'UNK';` — then replace `ORG_CODE: 'ORG'` with `ORG_CODE: orgCode`. Pattern matches `correspondence.service.ts` line 263-269.
|
|
- **Dependencies**: none (parallel)
|
|
- **Status**: [ ]
|
|
- **Note**: Fixes M1 — `Organization.organizationCode` field confirmed at `organization.entity.ts:24`
|
|
|
|
### R13 — Validation: ADR-019 UUID Compliance Check
|
|
- **File**: `backend/src/modules/transmittal/` + `frontend/types/transmittal.ts`
|
|
- **Action**: Verify all revision-related fields use `publicId` (string UUID) not `id` (INT): `revisionId`, `workflowInstanceId`, `transmittalId` in responses. Run `grep -n "parseInt\|Number(\|\.id[^a-zA-Z]"` to catch violations.
|
|
- **Dependencies**: R2, R3, R12
|
|
- **Status**: [ ]
|
|
- **ADR-019**: CRITICAL — Zero tolerance for INT ID exposure in API responses
|
|
|
|
---
|
|
|
|
## Phase 4 Execution Order
|
|
|
|
```
|
|
R1 (schema)
|
|
→ R2 (entity)
|
|
→ R3 (service findOneByUuid) ─┬→ R4 (createRevision) → R6 (controller endpoint)
|
|
│ → R5 (copyItems helper)
|
|
├→ R7 (submit update)
|
|
├→ R12 (workflow binding update)
|
|
└→ R13 (ADR-019 validation)
|
|
R8 (frontend types) ─┬→ R9 (item types)
|
|
→ R11 (hook update) │
|
|
→ R10 (page update) ─┘
|
|
R3 → R14 (create() writes to details JSON)
|
|
→ R15 (DROP COLUMN purpose/remarks) ← deploy R14 first
|
|
R1 → R17 (add item_type column + entity + save in create())
|
|
R18 (fix ORG_CODE — no dependencies, parallel safe)
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 4 Commit Message Convention
|
|
|
|
```
|
|
feat(schema): add revision_id to transmittal_items table (ADR-009)
|
|
feat(transmittal): add revisionId column to TransmittalItem entity
|
|
feat(transmittal): update findOneByUuid to read from correspondence_revisions
|
|
feat(transmittal): add createRevision with automatic item copying
|
|
feat(transmittal): add copyItemsToRevision helper method
|
|
feat(transmittal): add POST /:uuid/revisions endpoint
|
|
feat(transmittal): update submit for revision-scoped items (EC-RFA-004)
|
|
feat(frontend): add revision fields to Transmittal types
|
|
feat(frontend): add revision selector to transmittal detail page
|
|
feat(workflow-engine): update getInstanceByEntity for revision binding
|
|
chore(validation): ADR-019 UUID compliance check for revision refactor
|
|
fix(transmittal): change TransmittalItemDto.itemId from INT to UUID string (ADR-019)
|
|
feat(transmittal): add item_type column to transmittal_items and persist from DTO (H1)
|
|
fix(transmittal): replace hardcoded ORG_CODE with real organizationCode lookup (M1)
|
|
```
|