# Out-of-Plan Report Submit/Review Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add a "ส่งงานแผน" submit action to the out-of-plan report list (`agency-out-of-plan/out-of-plan`) and a new reviewer menu "ตรวจสอบรายงานผล(นอกแผน)" with 3 status tabs, "ส่งกลับแก้ไข", and "ตรวจสอบผ่าน" actions. **Architecture:** No new tables/endpoints. Everything hinges on the existing `agency_report.status_id` column (0/null=draft, 1=รอตรวจสอบ, 2=ส่งแก้ไข, 3=ตรวจสอบแล้ว) and the existing `out_of_plan_report_detail_uid` link column on `agency_report`. The reviewer menu mirrors the existing `check-project-report` (`typeUrl == 16`) 3-tab pattern exactly, scoped by filtering `out_of_plan_report_detail_uid` client-side (the generic backend query filter is equality-only — it cannot express "not null" — so both the new list and the pre-existing `check-project-report` list filter client-side with RxJS `map()`). The review form reuses the big shared `agency-report-form.component.ts` (already used by 6+ other typeUrl variants), gated behind new `typeUrl` values (29, 29161, 29162, 29163) so no existing flow is touched. Bulk submit reuses the existing `AgencyReportService.updateStatusAgency()` method (already wired to `POST update_status/1`). **Tech Stack:** Angular 17, Angular Material (`mat-table`, `mat-tab-group`, `mat-checkbox`), Angular CDK `SelectionModel`, RxJS, SweetAlert2 (via `SweetalertService`). ## Global Constraints - No backend (`rmutr-api`) changes in this plan — the two endpoints needed (`GET .../agency_report?status_id=N`, `POST .../agency_report/update_status/1`) already exist. - This repo has no meaningful unit-test culture for these Angular components (only CLI-boilerplate `.spec.ts` files exist project-wide). Per-task verification uses `ng build --configuration=production` (from `/Users/nut.looknut/Project/rmutr/rmutr-web`) to catch compile/template errors, plus a manual browser check via `npm start` for the final task. - Every new branch added to shared files (`request-budget-statistics.container.ts`, `request-budget-statistics-list.component.ts/html`, `agency-report-form.component.ts/html`) must be explicitly guarded by the new `typeUrl` values (29, 29161, 29162, 29163) or by `out_of_plan_report_detail_uid` truthiness — never change behavior for any other `typeUrl`. - Follow existing code conventions exactly: raw `Swal.fire(...)` / `this.swSV.confirmSave()` patterns already used in the touched files — don't introduce a new dialog library or pattern. - `AgencyReportService.updateStatusAgency(data)` (POST `{fullUrl}/update_status/1`) is the existing bulk-submit method — reuse it, don't add a new API method. - Reuse the existing shared search component (`request-budget-statistics.search.component.ts`, fields: `budget_year_name_th`, `project_name_th`, `budget_plan_name_th`, `budget_project_name_th`, `responsible_faculty_name_th`) for the new reviewer menu — do not build new search UI. --- ### Task 1: New routes and menu entry **Files:** - Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/app-routing.module.ts:3011-3016` - Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/core/data/navigator.ts:1111-1117` **Interfaces:** - Consumes: nothing new - Produces: route `type` values `29` (list), `29161`/`29162`/`29163` (review-edit sub-routes) and route paths `check-project-report-out-of-plan`, `agency-report-out-of-plan-edit-wait`, `agency-report-out-of-plan-edit-send`, `agency-report-out-of-plan-edit-pass` — every later task's `typeUrl` checks and `router.navigate()` calls target these exact values/paths. - [ ] **Step 1: Add the 4 new routes** Find, in `app-routing.module.ts` (the block right after the existing `agency-report-edit-pass` route): ```ts { path: 'agency-report-edit-pass', loadChildren: () => import('./feature/budget-request/request/request-budget-statistics/request-budget-statistics.module') .then(m => m.RequestBudgetStatisticsModule), data: { menuName: `การบริหารและรายงานผล${seperation}ตรวจสอบรายงานผลโครงการ`, type: 202163 } }, { path: 'original-project-proposal1', ``` Change to: ```ts { path: 'agency-report-edit-pass', loadChildren: () => import('./feature/budget-request/request/request-budget-statistics/request-budget-statistics.module') .then(m => m.RequestBudgetStatisticsModule), data: { menuName: `การบริหารและรายงานผล${seperation}ตรวจสอบรายงานผลโครงการ`, type: 202163 } }, { path: 'check-project-report-out-of-plan', loadChildren: () => import('./feature/budget-request/request/request-budget-statistics/request-budget-statistics.module') .then(m => m.RequestBudgetStatisticsModule), data: { menuName: `การบริหารและรายงานผล${seperation}ตรวจสอบรายงานผล(นอกแผน)`, type: 29 } }, { path: 'agency-report-out-of-plan-edit-wait', loadChildren: () => import('./feature/budget-request/request/request-budget-statistics/request-budget-statistics.module') .then(m => m.RequestBudgetStatisticsModule), data: { menuName: `การบริหารและรายงานผล${seperation}ตรวจสอบรายงานผล(นอกแผน)`, type: 29161 } }, { path: 'agency-report-out-of-plan-edit-send', loadChildren: () => import('./feature/budget-request/request/request-budget-statistics/request-budget-statistics.module') .then(m => m.RequestBudgetStatisticsModule), data: { menuName: `การบริหารและรายงานผล${seperation}ตรวจสอบรายงานผล(นอกแผน)`, type: 29162 } }, { path: 'agency-report-out-of-plan-edit-pass', loadChildren: () => import('./feature/budget-request/request/request-budget-statistics/request-budget-statistics.module') .then(m => m.RequestBudgetStatisticsModule), data: { menuName: `การบริหารและรายงานผล${seperation}ตรวจสอบรายงานผล(นอกแผน)`, type: 29163 } }, { path: 'original-project-proposal1', ``` - [ ] **Step 2: Add the menu entry** Find, in `navigator.ts`: ```ts { id: 'agency-report-out-of-plan', code: 'administer-011-007-2', title: 'หน่วยงานทำรายงานผล(นอกแผน)', type: 'basic', icon: 'fiber_manual_record', link: '/app/agency-out-of-plan/out-of-plan', }, { id: 'check-project-report', ``` Change to: ```ts { id: 'agency-report-out-of-plan', code: 'administer-011-007-2', title: 'หน่วยงานทำรายงานผล(นอกแผน)', type: 'basic', icon: 'fiber_manual_record', link: '/app/agency-out-of-plan/out-of-plan', }, { id: 'check-project-report-out-of-plan', code: 'administer-011-007-3', title: 'ตรวจสอบรายงานผล(นอกแผน)', type: 'basic', icon: 'fiber_manual_record', link: '/app/check-project-report-out-of-plan', }, { id: 'check-project-report', ``` - [ ] **Step 3: Build to confirm no errors** Run: `cd /Users/nut.looknut/Project/rmutr/rmutr-web && ng build --configuration=production` Expected: build succeeds with no new errors. - [ ] **Step 4: Commit** ```bash git add rmutr-web/src/app/app-routing.module.ts rmutr-web/src/app/core/data/navigator.ts git commit -m "feat: add routes and menu entry for out-of-plan report review" ``` --- ### Task 2: `list29-1`/`list29-2`/`list29-3` components (review tabs) **Files:** - Create: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-1/list29-1.component.ts` - Create: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-1/list29-1.component.html` - Create: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-1/list29-1.component.scss` - Create: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-2/list29-2.component.ts` - Create: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-2/list29-2.component.html` - Create: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-3/list29-3.component.ts` - Create: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-3/list29-3.component.html` - Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/request-budget-statistics.module.ts:46,190` **Interfaces:** - Consumes: `@Input() dataSource: any` — an array of `agency_report` rows (set by Task 3's container wiring). Each row has `agency_report_uid`, `budget_year_name_th`, `project_name_th`, `budget_project_name_th`(displayed via `text_6`... actually mirrors `list16-1` exactly, see below), `budget_topic_name_th`, `responsible_faculty_name_th`. - Produces: `app-list29-1`, `app-list29-2`, `app-list29-3` selectors — Task 3 wires these into `request-budget-statistics-list.component.html`. Each has `edit(val)` navigating to its dedicated review route from Task 1. These 3 components are a direct copy of the existing `list16-1`/`list16-2`/`list16-3` pattern (same table shape, same `BaseList`/`updateMatTable` usage), minus the unrelated P/A-send checkbox feature that lives only in `list16-3` (that's a different, unrelated feature bolted onto that specific tab — not part of this workflow). Each adds one static badge column ("นอกแผน") since every row here is out-of-plan by definition (per the approved design spec). - [ ] **Step 1: Create `list29-1.component.ts`** ```ts import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Input, Output, OnChanges, SimpleChanges } from '@angular/core'; import { PageEvent } from '@angular/material/paginator'; import { Router } from '@angular/router'; import { BaseList } from 'src/app/core/base/base-list'; @Component({ selector: 'app-list29-1', templateUrl: './list29-1.component.html', styleUrls: ['./list29-1.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class List29_1Component extends BaseList implements OnInit, OnChanges { @Input() dataSource: any = []; @Output() onchange = new EventEmitter(); @Output() onedit = new EventEmitter(); @Output() ondelete = new EventEmitter(); @Output() onexcel = new EventEmitter(); data constructor( public router: Router ) { super(); } ngOnChanges(changes: SimpleChanges): void { if ('dataSource' in changes && changes?.dataSource?.currentValue) { this.data = this.dataSource this.dataSource = this.updateMatTable(this.dataSource ? this.dataSource : []); } } ngOnInit(): void { } edit(val) { this.router.navigate(['app/agency-report-out-of-plan-edit-wait/edit-report', val.agency_report_uid]) } delete(val) { this.ondelete.emit(val) } change(event: PageEvent) { let page: number = event.pageIndex + 1 let table: any = { page: page, size: event.pageSize } this.onchange.emit(table) } excel(val) { this.onexcel.emit(val) } } ``` - [ ] **Step 2: Create `list29-1.component.html`** ```html
ประเภท นอกแผน ปีงบประมาณ {{x.budget_year_name_th}} ชื่อโครงการ {{x.project_name_th}} ผลผลิต {{x.text_6}} ด้าน {{x.text_7}} ชื่อหน่วยงาน {{x.responsible_faculty_name_th}} ดูรายงานผล create

``` - [ ] **Step 3: Create `list29-1.component.scss`** ```scss ``` (empty — no extra styles needed, matches `list16-2`/`list16-3`'s empty/minimal scss) - [ ] **Step 4: Create `list29-2.component.ts`** (identical to `list29-1` except `edit()` target) ```ts import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Input, Output, OnChanges, SimpleChanges } from '@angular/core'; import { PageEvent } from '@angular/material/paginator'; import { Router } from '@angular/router'; import { BaseList } from 'src/app/core/base/base-list'; @Component({ selector: 'app-list29-2', templateUrl: './list29-2.component.html', styleUrls: ['./list29-2.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class List29_2Component extends BaseList implements OnInit, OnChanges { @Input() dataSource: any = []; @Output() onchange = new EventEmitter(); @Output() onedit = new EventEmitter(); @Output() ondelete = new EventEmitter(); @Output() onexcel = new EventEmitter(); data constructor( public router: Router ) { super(); } ngOnChanges(changes: SimpleChanges): void { if ('dataSource' in changes && changes?.dataSource?.currentValue) { this.data = this.dataSource this.dataSource = this.updateMatTable(this.dataSource ? this.dataSource : []); } } ngOnInit(): void { } edit(val) { this.router.navigate(['app/agency-report-out-of-plan-edit-send/edit-report', val.agency_report_uid]) } delete(val) { this.ondelete.emit(val) } change(event: PageEvent) { let page: number = event.pageIndex + 1 let table: any = { page: page, size: event.pageSize } this.onchange.emit(table) } excel(val) { this.onexcel.emit(val) } } ``` - [ ] **Step 5: Create `list29-2.component.scss`** ```scss ``` (empty file) - [ ] **Step 6: Create `list29-2.component.html`** (identical to `list29-1.component.html`) ```html
ประเภท นอกแผน ปีงบประมาณ {{x.budget_year_name_th}} ชื่อโครงการ {{x.project_name_th}} ผลผลิต {{x.text_6}} ด้าน {{x.text_7}} ชื่อหน่วยงาน {{x.responsible_faculty_name_th}} ดูรายงานผล create

``` - [ ] **Step 7: Create `list29-3.component.ts`** (identical, `edit()` targets the "pass" route) ```ts import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Input, Output, OnChanges, SimpleChanges } from '@angular/core'; import { PageEvent } from '@angular/material/paginator'; import { Router } from '@angular/router'; import { BaseList } from 'src/app/core/base/base-list'; @Component({ selector: 'app-list29-3', templateUrl: './list29-3.component.html', styleUrls: ['./list29-3.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class List29_3Component extends BaseList implements OnInit, OnChanges { @Input() dataSource: any = []; @Output() onchange = new EventEmitter(); @Output() onedit = new EventEmitter(); @Output() ondelete = new EventEmitter(); @Output() onexcel = new EventEmitter(); data constructor( public router: Router ) { super(); } ngOnChanges(changes: SimpleChanges): void { if ('dataSource' in changes && changes?.dataSource?.currentValue) { this.data = this.dataSource this.dataSource = this.updateMatTable(this.dataSource ? this.dataSource : []); } } ngOnInit(): void { } edit(val) { this.router.navigate(['app/agency-report-out-of-plan-edit-pass/edit-report', val.agency_report_uid]) } delete(val) { this.ondelete.emit(val) } change(event: PageEvent) { let page: number = event.pageIndex + 1 let table: any = { page: page, size: event.pageSize } this.onchange.emit(table) } excel(val) { this.onexcel.emit(val) } } ``` - [ ] **Step 8: Create `list29-3.component.scss`** ```scss ``` (empty file) - [ ] **Step 9: Create `list29-3.component.html`** ```html
ประเภท นอกแผน ปีงบประมาณ {{x.budget_year_name_th}} ชื่อโครงการ {{x.project_name_th}} ผลผลิต {{x.text_6}} ด้าน {{x.text_7}} ชื่อหน่วยงาน {{x.responsible_faculty_name_th}} ดูรายงานผล create

``` - [ ] **Step 10: Register the 3 components in the module** Find, in `request-budget-statistics.module.ts`: ```ts import { List16_1Component } from './presenter/list/request-budget-statistics-list/list16-1/list16-1.component'; import { List16_2Component } from './presenter/list/request-budget-statistics-list/list16-2/list16-2.component'; import { List16_3Component } from './presenter/list/request-budget-statistics-list/list16-3/list16-3.component'; import { List20Component } from './presenter/list/request-budget-statistics-list/list20/list20.component'; ``` Change to: ```ts import { List16_1Component } from './presenter/list/request-budget-statistics-list/list16-1/list16-1.component'; import { List16_2Component } from './presenter/list/request-budget-statistics-list/list16-2/list16-2.component'; import { List16_3Component } from './presenter/list/request-budget-statistics-list/list16-3/list16-3.component'; import { List29_1Component } from './presenter/list/request-budget-statistics-list/list29-1/list29-1.component'; import { List29_2Component } from './presenter/list/request-budget-statistics-list/list29-2/list29-2.component'; import { List29_3Component } from './presenter/list/request-budget-statistics-list/list29-3/list29-3.component'; import { List20Component } from './presenter/list/request-budget-statistics-list/list20/list20.component'; ``` Find: ```ts List16_1Component, List16_2Component, List16_3Component, List20Component, ``` Change to: ```ts List16_1Component, List16_2Component, List16_3Component, List29_1Component, List29_2Component, List29_3Component, List20Component, ``` - [ ] **Step 11: Build to confirm no errors** Run: `cd /Users/nut.looknut/Project/rmutr/rmutr-web && ng build --configuration=production` Expected: build succeeds with no new errors. - [ ] **Step 12: Commit** ```bash git add rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-1/ rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-2/ rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-3/ rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/request-budget-statistics.module.ts git commit -m "feat: add list29-1/2/3 review tab components for out-of-plan reports" ``` --- ### Task 3: Wire the new reviewer list end-to-end + fix `check-project-report` leak **Files:** - Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/container/request-other-expenses/request-budget-statistics.container.ts` - Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/container/request-other-expenses/request-budget-statistics.container.html` - Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/request-budget-statistics-list.component.ts` - Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/request-budget-statistics-list.component.html` **Interfaces:** - Consumes: `app-list29-1/2/3` selectors (Task 2), route path `check-project-report-out-of-plan` (Task 1, `type: 29`) - Produces: `dataSource29_1$/29_2$/29_3$` observables on the container — final task in this chain, nothing downstream depends on these further. - [ ] **Step 1: Add the 3 new dataSource properties** Find, in `request-budget-statistics.container.ts`: ```ts dataSource16_1$ = new Observable() dataSource16_2$ = new Observable() dataSource16_3$ = new Observable() dataSourceBetweenYear$ = new Observable() ``` Change to: ```ts dataSource16_1$ = new Observable() dataSource16_2$ = new Observable() dataSource16_3$ = new Observable() dataSource29_1$ = new Observable() dataSource29_2$ = new Observable() dataSource29_3$ = new Observable() dataSourceBetweenYear$ = new Observable() ``` - [ ] **Step 2: Fix `getAll()`'s initial (unfiltered) load — exclude out-of-plan from 16, include only out-of-plan in 29** Find, in `getAll()`: ```ts this.dataSource16_1$ = this.AgencyReportSV.queryString(`?status_id=1`) this.dataSource16_2$ = this.AgencyReportSV.queryString(`?status_id=2`) this.dataSource16_3$ = this.AgencyReportSV.queryString(`?status_id=3`) this.dataSourceBetweenYear$ = this.requestBudgetSV.queryString(`?is_between_year=true`) ``` Change to: ```ts this.dataSource16_1$ = this.AgencyReportSV.queryString(`?status_id=1`).pipe(map((data: any[]) => (data || []).filter(d => !d.out_of_plan_report_detail_uid))) this.dataSource16_2$ = this.AgencyReportSV.queryString(`?status_id=2`).pipe(map((data: any[]) => (data || []).filter(d => !d.out_of_plan_report_detail_uid))) this.dataSource16_3$ = this.AgencyReportSV.queryString(`?status_id=3`).pipe(map((data: any[]) => (data || []).filter(d => !d.out_of_plan_report_detail_uid))) this.dataSource29_1$ = this.AgencyReportSV.queryString(`?status_id=1`).pipe(map((data: any[]) => (data || []).filter(d => !!d.out_of_plan_report_detail_uid))) this.dataSource29_2$ = this.AgencyReportSV.queryString(`?status_id=2`).pipe(map((data: any[]) => (data || []).filter(d => !!d.out_of_plan_report_detail_uid))) this.dataSource29_3$ = this.AgencyReportSV.queryString(`?status_id=3`).pipe(map((data: any[]) => (data || []).filter(d => !!d.out_of_plan_report_detail_uid))) this.dataSourceBetweenYear$ = this.requestBudgetSV.queryString(`?is_between_year=true`) ``` (`map` is already imported at the top of this file — used by many other branches — no new import needed.) - [ ] **Step 3: Fix the `onsearch()` query branch for `typeUrl == 16` and add the `typeUrl == 29` branch** Find, in `onsearch()`: ```ts else if(this.typeUrl == 16){ this.dataSource16_1$ = this.AgencyReportSV.queryString(`?status_id=1&${queryStr}`) this.dataSource16_2$ = this.AgencyReportSV.queryString(`?status_id=2&${queryStr}`) this.dataSource16_3$ = this.AgencyReportSV.queryString(`?status_id=3&${queryStr}`) } ``` Change to: ```ts else if(this.typeUrl == 16){ const excludeOutOfPlan = (data: any[]) => (data || []).filter(d => !d.out_of_plan_report_detail_uid) this.dataSource16_1$ = this.AgencyReportSV.queryString(`?status_id=1&${queryStr}`).pipe(map(excludeOutOfPlan)) this.dataSource16_2$ = this.AgencyReportSV.queryString(`?status_id=2&${queryStr}`).pipe(map(excludeOutOfPlan)) this.dataSource16_3$ = this.AgencyReportSV.queryString(`?status_id=3&${queryStr}`).pipe(map(excludeOutOfPlan)) } else if(this.typeUrl == 29){ const onlyOutOfPlan = (data: any[]) => (data || []).filter(d => !!d.out_of_plan_report_detail_uid) this.dataSource29_1$ = this.AgencyReportSV.queryString(`?status_id=1&${queryStr}`).pipe(map(onlyOutOfPlan)) this.dataSource29_2$ = this.AgencyReportSV.queryString(`?status_id=2&${queryStr}`).pipe(map(onlyOutOfPlan)) this.dataSource29_3$ = this.AgencyReportSV.queryString(`?status_id=3&${queryStr}`).pipe(map(onlyOutOfPlan)) } ``` - [ ] **Step 4: Bind the 3 new observables in the container template** Find, in `request-budget-statistics.container.html`: ```html [dataSource16_1]="dataSource16_1$ | async" [dataSource16_2]="dataSource16_2$ | async" [dataSource16_3]="dataSource16_3$ | async" ``` Change to: ```html [dataSource16_1]="dataSource16_1$ | async" [dataSource16_2]="dataSource16_2$ | async" [dataSource16_3]="dataSource16_3$ | async" [dataSource29_1]="dataSource29_1$ | async" [dataSource29_2]="dataSource29_2$ | async" [dataSource29_3]="dataSource29_3$ | async" ``` - [ ] **Step 5: Add the 3 new `@Input()`s to the presenter list component** Find, in `request-budget-statistics-list.component.ts`: ```ts @Input() dataSource16_1: any = []; @Input() dataSource16_2: any = []; @Input() dataSource16_3: any = []; ``` Change to: ```ts @Input() dataSource16_1: any = []; @Input() dataSource16_2: any = []; @Input() dataSource16_3: any = []; @Input() dataSource29_1: any = []; @Input() dataSource29_2: any = []; @Input() dataSource29_3: any = []; ``` - [ ] **Step 6: Add the `typeUrl == 29` tab block to the presenter list template** Find, in `request-budget-statistics-list.component.html`: ```html ``` Change to: ```html ``` - [ ] **Step 7: Build to confirm no errors** Run: `cd /Users/nut.looknut/Project/rmutr/rmutr-web && ng build --configuration=production` Expected: build succeeds with no new errors. - [ ] **Step 8: Commit** ```bash git add rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/container/request-other-expenses/request-budget-statistics.container.ts rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/container/request-other-expenses/request-budget-statistics.container.html rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/request-budget-statistics-list.component.ts rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/request-budget-statistics-list.component.html git commit -m "feat: wire out-of-plan reviewer list (typeUrl 29) and stop out-of-plan reports leaking into check-project-report" ``` --- ### Task 4: Report form — lock, badge, review buttons, `close()` routing **Files:** - Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/form/agency-report-form/agency-report-form.component.ts` - Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/form/agency-report-form/agency-report-form.component.html` **Interfaces:** - Consumes: `typeUrl` values `29161`/`29162`/`29163` (Task 1), `agency_report.out_of_plan_report_detail_uid`/`status_id` (existing fields, no new backend needed) - Produces: `isLocked: boolean` class field — the template reads it to hide the save button and show a locked banner. `close()` now routes `29161/29162/29163` back to `check-project-report-out-of-plan`. - [ ] **Step 1: Add the `isLocked` field** Find: ```ts urlPath typeUrl tabIndex ``` Change to: ```ts urlPath typeUrl tabIndex isLocked: boolean = false ``` - [ ] **Step 2: Route the 3 new review typeUrls through the existing `typeUrl == 282` load branch, and lock the form when the loaded report is already submitted or done** Find the full `else if (this.typeUrl == 282)` block: ```ts // type 282: เปิดฟอร์มจากรายงานที่มีอยู่แล้ว → โหลด agency_report ตรงๆ แล้ว state='edit' else if (this.typeUrl == 282) { this.AgencyReportSV.get(this.uniquekey).pipe( tap((x: any) => { this.form.patchValue(x) this.change_year(x.budget_year_name_th) if (x.agency_report_items) { const formArray = this.form.get('agency_report_items') as FormArray x.agency_report_items.forEach((agency_report_item) => { formArray.push(this.agency_report_item_form(agency_report_item)) }) } if (x.agency_report_details) { x.agency_report_details.forEach((agency_report_detail) => { let form_agency_report_details = this.form.get('agency_report_details') as FormArray form_agency_report_details.push(this.agency_report_details_form(agency_report_detail)) }) const firstIssue = x.agency_report_details.find(d => d.type == 1 && d.text_1) if (firstIssue) this.change_budget_strategy_faculty_strategic_edit(firstIssue.text_1) } if (x.agency_report_popularities) { x.agency_report_popularities.forEach((p) => { const arr = this.form.get('agency_report_popularities') as FormArray arr.push(this.fb.group(p)) }) this.cdRef.detectChanges() } Swal.close() }), catchError(err => { Swal.close() return of(null) }) ).subscribe() } ``` Change to: ```ts // type 282/29161/29162/29163: เปิดฟอร์มจากรายงานที่มีอยู่แล้ว → โหลด agency_report ตรงๆ แล้ว state='edit' // (29161/29162/29163 = งานแผนเปิดดูจากเมนู "ตรวจสอบรายงานผล(นอกแผน)" — ใช้ loader เดียวกับ 282) else if (this.typeUrl == 282 || this.typeUrl == 29161 || this.typeUrl == 29162 || this.typeUrl == 29163) { this.AgencyReportSV.get(this.uniquekey).pipe( tap((x: any) => { this.form.patchValue(x) this.change_year(x.budget_year_name_th) if (x.agency_report_items) { const formArray = this.form.get('agency_report_items') as FormArray x.agency_report_items.forEach((agency_report_item) => { formArray.push(this.agency_report_item_form(agency_report_item)) }) } if (x.agency_report_details) { x.agency_report_details.forEach((agency_report_detail) => { let form_agency_report_details = this.form.get('agency_report_details') as FormArray form_agency_report_details.push(this.agency_report_details_form(agency_report_detail)) }) const firstIssue = x.agency_report_details.find(d => d.type == 1 && d.text_1) if (firstIssue) this.change_budget_strategy_faculty_strategic_edit(firstIssue.text_1) } if (x.agency_report_popularities) { x.agency_report_popularities.forEach((p) => { const arr = this.form.get('agency_report_popularities') as FormArray arr.push(this.fb.group(p)) }) this.cdRef.detectChanges() } if (x.out_of_plan_report_detail_uid && (x.status_id === 1 || x.status_id === 3)) { this.isLocked = true this.form.disable() } Swal.close() }), catchError(err => { Swal.close() return of(null) }) ).subscribe() } ``` - [ ] **Step 3: Route `close()` for the 3 new review typeUrls back to the new menu** Find: ```ts close() { if(this.typeUrl == 202161 || this.typeUrl == 202163 ){ this.router.navigate(['app/check-project-report']) } else if(this.typeUrl == 202162){ this.router.navigate(['app/send-edit-agency-report']) } else if(this.typeUrl == 281 || this.typeUrl == 282){ this.router.navigate(['app/agency-out-of-plan/out-of-plan']) } else{ this.router.navigate(['app/agency-report']) } } ``` Change to: ```ts close() { if(this.typeUrl == 202161 || this.typeUrl == 202163 ){ this.router.navigate(['app/check-project-report']) } else if(this.typeUrl == 202162){ this.router.navigate(['app/send-edit-agency-report']) } else if(this.typeUrl == 29161 || this.typeUrl == 29162 || this.typeUrl == 29163){ this.router.navigate(['app/check-project-report-out-of-plan']) } else if(this.typeUrl == 281 || this.typeUrl == 282){ this.router.navigate(['app/agency-out-of-plan/out-of-plan']) } else{ this.router.navigate(['app/agency-report']) } } ``` - [ ] **Step 4: Add the "โครงการ"/"นอกแผน" badge at the top of the form** Find, at the very top of `agency-report-form.component.html`: ```html
``` Change to: ```html
นอกแผน โครงการ
``` - [ ] **Step 5: Gate the bottom action buttons for the 3 new review typeUrls + show the locked banner** Find: ```html
``` Change to: ```html รายงานนี้ถูกล็อก ไม่สามารถแก้ไขได้
``` `save_pass()` sets `status_id = 3` and PUTs; `save_send()` sets `status_id = 2` and PUTs — both already exist unchanged (`agency-report-form.component.ts:4116-4179`) and both call `getCleanedFormValue()` → `form.getRawValue()`, which includes disabled-control values, so they work correctly even when `isLocked` has called `form.disable()`. - [ ] **Step 6: Build to confirm no errors** Run: `cd /Users/nut.looknut/Project/rmutr/rmutr-web && ng build --configuration=production` Expected: build succeeds with no new errors. - [ ] **Step 7: Commit** ```bash git add rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/form/agency-report-form/agency-report-form.component.ts rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/form/agency-report-form/agency-report-form.component.html git commit -m "feat: lock out-of-plan report form once submitted, add project/out-of-plan badge, wire review actions for new typeUrls" ``` --- ### Task 5: Out-of-plan list — checkbox, status badge, "ส่งงานแผน" button **Files:** - Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/form/agency-out-of-plan-form/agency-out-of-plan-form.component.ts` - Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/form/agency-out-of-plan-form/agency-out-of-plan-form.component.html` **Interfaces:** - Consumes: `AgencyReportService.updateStatusAgency(data)` (existing, `agency-report.service.ts`, POSTs to `update_status/1`) - Produces: nothing further downstream — this is the last functional task; sent rows become visible in the Task 3 reviewer list purely because they now match `status_id=1 && out_of_plan_report_detail_uid != null`. - [ ] **Step 1: Track `_status_id` alongside `_agency_report_uid`, add `selection`, and the send handler** Find: ```ts import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { Router } from '@angular/router'; import { Observable, of, forkJoin } from 'rxjs'; import { catchError, concatMap, filter, map, switchMap, tap } from 'rxjs/operators'; import * as XLSX from 'xlsx-js-style'; import { SweetalertService } from 'src/app/core/service/sweetalert/sweetalert'; import { OutOfPlanReportDetailService } from 'src/app/core/service/request-budget/out-of-plan-report-detail.service'; import { OutOfPlanReportPDetailService } from 'src/app/core/service/request-budget/out-of-plan-report-p-detail.service'; import { OutOfPlanReportADetailService } from 'src/app/core/service/request-budget/out-of-plan-report-a-detail.service'; import { AgencyReportService } from 'src/app/core/service/request-budget/agency-report.service'; ``` Change to: ```ts import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { Router } from '@angular/router'; import { SelectionModel } from '@angular/cdk/collections'; import { Observable, of, forkJoin, throwError } from 'rxjs'; import { catchError, concatMap, filter, map, switchMap, tap } from 'rxjs/operators'; import * as XLSX from 'xlsx-js-style'; import { SweetalertService } from 'src/app/core/service/sweetalert/sweetalert'; import { OutOfPlanReportDetailService } from 'src/app/core/service/request-budget/out-of-plan-report-detail.service'; import { OutOfPlanReportPDetailService } from 'src/app/core/service/request-budget/out-of-plan-report-p-detail.service'; import { OutOfPlanReportADetailService } from 'src/app/core/service/request-budget/out-of-plan-report-a-detail.service'; import { AgencyReportService } from 'src/app/core/service/request-budget/agency-report.service'; ``` Find: ```ts isExporting = false details: any[] = [] private detailsToDelete: string[] = [] private pDetailsToDelete: string[] = [] private aDetailsToDelete: string[] = [] loading = true ``` Change to: ```ts isExporting = false details: any[] = [] private detailsToDelete: string[] = [] private pDetailsToDelete: string[] = [] private aDetailsToDelete: string[] = [] loading = true selection = new SelectionModel(true, []) ``` Find, inside `load()`: ```ts // สร้าง Map: out_of_plan_report_detail_uid → agency_report_uid const agencyByDetail = new Map() for (const ar of agencyReports || []) { const k = ar?.out_of_plan_report_detail_uid if (!k) continue agencyByDetail.set(k, ar.agency_report_uid) } ``` Change to: ```ts // สร้าง Map: out_of_plan_report_detail_uid → { agency_report_uid, status_id } const agencyByDetail = new Map() for (const ar of agencyReports || []) { const k = ar?.out_of_plan_report_detail_uid if (!k) continue agencyByDetail.set(k, { agency_report_uid: ar.agency_report_uid, status_id: ar.status_id }) } ``` Find: ```ts this.allDetails = (details || []).map(d => { const uid = d.out_of_plan_report_detail_uid const pList = (pByDetail.get(uid) || []).sort((x, y) => (x.sequence_no ?? 0) - (y.sequence_no ?? 0)) const aList = (aByDetail.get(uid) || []).sort((x, y) => (x.sequence_no ?? 0) - (y.sequence_no ?? 0)) return { ...d, _agency_report_uid: agencyByDetail.get(uid) || null, out_of_plan_report_p_details: pList.length > 0 ? pList : [{ sequence_no: 1 }], out_of_plan_report_a_details: aList.length > 0 ? aList : [{ sequence_no: 1 }], } }) this.details = [...this.allDetails] this.loading = false this.cdRef.detectChanges() }), catchError(err => { this.swSV.errText(err?.error?.description || 'โหลดข้อมูลไม่สำเร็จ') this.loading = false this.cdRef.detectChanges() return of(null) }) ).subscribe() } ``` Change to: ```ts this.allDetails = (details || []).map(d => { const uid = d.out_of_plan_report_detail_uid const pList = (pByDetail.get(uid) || []).sort((x, y) => (x.sequence_no ?? 0) - (y.sequence_no ?? 0)) const aList = (aByDetail.get(uid) || []).sort((x, y) => (x.sequence_no ?? 0) - (y.sequence_no ?? 0)) const agency = agencyByDetail.get(uid) return { ...d, _agency_report_uid: agency?.agency_report_uid || null, _status_id: agency ? agency.status_id : null, out_of_plan_report_p_details: pList.length > 0 ? pList : [{ sequence_no: 1 }], out_of_plan_report_a_details: aList.length > 0 ? aList : [{ sequence_no: 1 }], } }) this.details = [...this.allDetails] this.selection.clear() this.loading = false this.cdRef.detectChanges() }), catchError(err => { this.swSV.errText(err?.error?.description || 'โหลดข้อมูลไม่สำเร็จ') this.loading = false this.cdRef.detectChanges() return of(null) }) ).subscribe() } private sendableDetails(): any[] { return (this.details || []).filter(d => this.isSendable(d)) } isSendable(detail: any): boolean { return !!detail?._agency_report_uid && detail?._status_id !== 1 && detail?._status_id !== 3 } isAllSelected(): boolean { const sendable = this.sendableDetails() return sendable.length > 0 && this.selection.selected.length === sendable.length } masterToggle(): void { if (this.isAllSelected()) { this.selection.clear() return } this.selection.select(...this.sendableDetails()) } sendToPlan(): void { if (this.selection.selected.length === 0) return this.swSV.confirmSave('ต้องการส่งรายการที่เลือกไปงานแผนหรือไม่?').pipe( filter(x => x.isConfirmed), concatMap(() => this.agencyReportSV.updateStatusAgency( this.selection.selected.map(d => ({ agency_report_uid: d._agency_report_uid })) ).pipe( catchError(err => { this.swSV.errText(err?.error?.description || 'ส่งไม่สำเร็จ') return throwError(err) }) )), tap(() => this.swSV.updateSuccess()), tap(() => this.load()), ).subscribe() } ``` - [ ] **Step 2: Add the checkbox column, status badge column, and "ส่งงานแผน" toolbar button** Find, in `agency-out-of-plan-form.component.html`, the header row (``): ```html ลำดับ ``` Change to: ```html สถานะ ลำดับ ``` Find the first-P-row block that renders the rowspan-ed shared cells (the block starting right after `` and ``): ```html {{i + 1}} ``` Change to: ```html ยังไม่ทำรายงาน ร่าง รอตรวจสอบ ส่งแก้ไข ตรวจสอบแล้ว {{i + 1}} ``` Find, in the bottom actions bar: ```html
``` Change to: ```html
``` - [ ] **Step 3: Build to confirm no errors** Run: `cd /Users/nut.looknut/Project/rmutr/rmutr-web && ng build --configuration=production` Expected: build succeeds with no new errors. - [ ] **Step 4: Commit** ```bash git add rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/form/agency-out-of-plan-form/agency-out-of-plan-form.component.ts rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/form/agency-out-of-plan-form/agency-out-of-plan-form.component.html git commit -m "feat: add checkbox, status badge, and send-to-planning button to out-of-plan list" ``` --- ### Task 6: End-to-end manual verification **Files:** none (verification only) **Interfaces:** none - [ ] **Step 1: Start the dev server** Run: `cd /Users/nut.looknut/Project/rmutr/rmutr-web && npm start` - [ ] **Step 2: Draft → fill report → check status badge** Open "การบริหารและรายงานผล > หน่วยงานทำรายงานผล(นอกแผน)" → open (or add) a project row → click the report icon to fill in the report → save. Expected: back on the list, the row shows status badge "ร่าง", checkbox enabled. - [ ] **Step 3: Send to planning** Tick the checkbox on 1-2 rows with badge "ร่าง" → click "ส่งงานแผน" → confirm the dialog. Expected: success toast, badge changes to "รอตรวจสอบ", checkbox now disabled/unchecked. - [ ] **Step 4: Confirm the form is locked** Click the report icon on a now-"รอตรวจสอบ" row. Expected: form loads with all fields disabled, red text "รายงานนี้ถูกล็อก ไม่สามารถแก้ไขได้" visible near the bottom, no "บันทึก" button, badge at top reads "นอกแผน". - [ ] **Step 5: Reviewer sees it in the new menu** Open "การบริหารและรายงานผล > ตรวจสอบรายงานผล(นอกแผน)" → tab "รอตรวจสอบ". Expected: the sent report(s) appear, each row showing badge "นอกแผน". Search by ปีงบประมาณ and by ชื่อโครงการ (top search panel) and confirm filtering works. - [ ] **Step 6: Send back for revision** Click the edit icon on a row in "รอตรวจสอบ" → click "ส่งแก้ไข". Expected: navigates back to "ตรวจสอบรายงานผล(นอกแผน)"; the item now appears in tab "ส่งแก้ไข". - [ ] **Step 7: Agency edits and resends** Back in "หน่วยงานทำรายงานผล(นอกแผน)", confirm the same row now shows badge "ส่งแก้ไข" and checkbox is enabled again. Open the report — confirm fields are editable (not locked) — make a change, save. Tick the checkbox, click "ส่งงานแผน" again. Expected: badge returns to "รอตรวจสอบ". - [ ] **Step 8: Approve** In "ตรวจสอบรายงานผล(นอกแผน)" tab "รอตรวจสอบ", open the report → click "ตรวจสอบผ่าน". Expected: navigates back to the menu; item now appears in tab "ตรวจสอบแล้ว". Reopening it from that tab shows the form locked (read-only). - [ ] **Step 9: Regression check — regular (in-plan) report flow untouched** Open "การบริหารและรายงานผล > ตรวจสอบรายงานผลโครงการ" (the original, pre-existing menu) → confirm out-of-plan reports sent in earlier steps do **not** appear anywhere in its 3 tabs, and any pre-existing regular (non-out-of-plan) reports still appear as before. Open a regular report from this menu → confirm the header badge reads "โครงการ" and "ตรวจสอบผ่าน"/"ส่งแก้ไข" buttons still work exactly as before (unaffected by the `isLocked`/badge additions, since those are gated on `out_of_plan_report_detail_uid`/new typeUrl values only).