Files
rmutr/docs/superpowers/plans/2026-07-21-out-of-plan-report-review.md
T
Nut.ไปเรื่อย 8e7a0cba4c docs: add implementation plan for out-of-plan report submit/review
Task-by-task plan (routes/menu, review-tab components, container wiring,
report-form lock/badge/buttons, submit list UI, e2e verification) for the
design in docs/superpowers/specs/2026-07-21-out-of-plan-report-review-design.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01613NaY9LAvy2wFXEeyE5eR
2026-07-21 23:20:36 +07:00

62 KiB

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

      {
        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:

      {
        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:

      {
        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:

      {
        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
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
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
<table mat-table [dataSource]="dataSource" style="width: 100%;">
    <tr mat-header-row
        *matHeaderRowDef="['type_badge','budget_year_name_th','project_name_th','budget_project_name_th','budget_topic_name_th','responsible_faculty_name_th','action']">
    </tr>
    <tr mat-row
        *matRowDef="let myRowData; columns: ['type_badge','budget_year_name_th','project_name_th','budget_project_name_th','budget_topic_name_th','responsible_faculty_name_th','action']">
    </tr>
    <ng-container matColumnDef="type_badge">
        <th mat-header-cell *matHeaderCellDef> ประเภท </th>
        <td mat-cell *matCellDef="let x">
            <span style="display:inline-block;background:#fef3c7;color:#92400e;padding:3px 10px;border-radius:10px;font-size:11px;font-weight:600;">นอกแผน</span>
        </td>
    </ng-container>
    <ng-container matColumnDef="budget_year_name_th">
        <th mat-header-cell *matHeaderCellDef> ปีงบประมาณ </th>
        <td mat-cell *matCellDef="let x;">
            {{x.budget_year_name_th}}
        </td>
    </ng-container>
    <ng-container matColumnDef="project_name_th">
        <th mat-header-cell *matHeaderCellDef> ชื่อโครงการ </th>
        <td mat-cell *matCellDef="let x">
            {{x.project_name_th}}
        </td>
    </ng-container>
    <ng-container matColumnDef="budget_project_name_th">
        <th mat-header-cell *matHeaderCellDef> ผลผลิต </th>
        <td mat-cell *matCellDef="let x">
            {{x.text_6}}
        </td>
    </ng-container>
    <ng-container matColumnDef="budget_topic_name_th">
        <th mat-header-cell *matHeaderCellDef> ด้าน </th>
        <td mat-cell *matCellDef="let x">
            {{x.text_7}}
        </td>
    </ng-container>
    <ng-container matColumnDef="responsible_faculty_name_th">
        <th mat-header-cell *matHeaderCellDef> ชื่อหน่วยงาน </th>
        <td mat-cell *matCellDef="let x">
            {{x.responsible_faculty_name_th}}
        </td>
    </ng-container>
    <ng-container matColumnDef="action">
        <th mat-header-cell *matHeaderCellDef> ดูรายงานผล </th>
        <td mat-cell *matCellDef="let x;let i = index" style="width: 70px;">
            <span class="material-icons" style="cursor: pointer;color: #F8A300;"
                (click)="edit(x)">create</span>
        </td>
    </ng-container>
</table>
<br>
<mat-paginator [pageSizeOptions]="[5, 10, 20]" showFirstLastButtons (page)="change($event)"></mat-paginator>
  • Step 3: Create list29-1.component.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)
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

(empty file)

  • Step 6: Create list29-2.component.html (identical to list29-1.component.html)
<table mat-table [dataSource]="dataSource" style="width: 100%;">
    <tr mat-header-row
        *matHeaderRowDef="['type_badge','budget_year_name_th','project_name_th','budget_project_name_th','budget_topic_name_th','responsible_faculty_name_th','action']">
    </tr>
    <tr mat-row
        *matRowDef="let myRowData; columns: ['type_badge','budget_year_name_th','project_name_th','budget_project_name_th','budget_topic_name_th','responsible_faculty_name_th','action']">
    </tr>
    <ng-container matColumnDef="type_badge">
        <th mat-header-cell *matHeaderCellDef> ประเภท </th>
        <td mat-cell *matCellDef="let x">
            <span style="display:inline-block;background:#fef3c7;color:#92400e;padding:3px 10px;border-radius:10px;font-size:11px;font-weight:600;">นอกแผน</span>
        </td>
    </ng-container>
    <ng-container matColumnDef="budget_year_name_th">
        <th mat-header-cell *matHeaderCellDef> ปีงบประมาณ </th>
        <td mat-cell *matCellDef="let x;">
            {{x.budget_year_name_th}}
        </td>
    </ng-container>
    <ng-container matColumnDef="project_name_th">
        <th mat-header-cell *matHeaderCellDef> ชื่อโครงการ </th>
        <td mat-cell *matCellDef="let x">
            {{x.project_name_th}}
        </td>
    </ng-container>
    <ng-container matColumnDef="budget_project_name_th">
        <th mat-header-cell *matHeaderCellDef> ผลผลิต </th>
        <td mat-cell *matCellDef="let x">
            {{x.text_6}}
        </td>
    </ng-container>
    <ng-container matColumnDef="budget_topic_name_th">
        <th mat-header-cell *matHeaderCellDef> ด้าน </th>
        <td mat-cell *matCellDef="let x">
            {{x.text_7}}
        </td>
    </ng-container>
    <ng-container matColumnDef="responsible_faculty_name_th">
        <th mat-header-cell *matHeaderCellDef> ชื่อหน่วยงาน </th>
        <td mat-cell *matCellDef="let x">
            {{x.responsible_faculty_name_th}}
        </td>
    </ng-container>
    <ng-container matColumnDef="action">
        <th mat-header-cell *matHeaderCellDef> ดูรายงานผล </th>
        <td mat-cell *matCellDef="let x;let i = index" style="width: 70px;">
            <span class="material-icons" style="cursor: pointer;color: #F8A300;"
                (click)="edit(x)">create</span>
        </td>
    </ng-container>
</table>
<br>
<mat-paginator [pageSizeOptions]="[5, 10, 20]" showFirstLastButtons (page)="change($event)"></mat-paginator>
  • Step 7: Create list29-3.component.ts (identical, edit() targets the "pass" route)
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

(empty file)

  • Step 9: Create list29-3.component.html
<table mat-table [dataSource]="dataSource" style="width: 100%;">
    <tr mat-header-row
        *matHeaderRowDef="['type_badge','budget_year_name_th','project_name_th','budget_project_name_th','budget_topic_name_th','responsible_faculty_name_th','action']">
    </tr>
    <tr mat-row
        *matRowDef="let myRowData; columns: ['type_badge','budget_year_name_th','project_name_th','budget_project_name_th','budget_topic_name_th','responsible_faculty_name_th','action']">
    </tr>
    <ng-container matColumnDef="type_badge">
        <th mat-header-cell *matHeaderCellDef> ประเภท </th>
        <td mat-cell *matCellDef="let x">
            <span style="display:inline-block;background:#fef3c7;color:#92400e;padding:3px 10px;border-radius:10px;font-size:11px;font-weight:600;">นอกแผน</span>
        </td>
    </ng-container>
    <ng-container matColumnDef="budget_year_name_th">
        <th mat-header-cell *matHeaderCellDef> ปีงบประมาณ </th>
        <td mat-cell *matCellDef="let x;">
            {{x.budget_year_name_th}}
        </td>
    </ng-container>
    <ng-container matColumnDef="project_name_th">
        <th mat-header-cell *matHeaderCellDef> ชื่อโครงการ </th>
        <td mat-cell *matCellDef="let x">
            {{x.project_name_th}}
        </td>
    </ng-container>
    <ng-container matColumnDef="budget_project_name_th">
        <th mat-header-cell *matHeaderCellDef> ผลผลิต </th>
        <td mat-cell *matCellDef="let x">
            {{x.text_6}}
        </td>
    </ng-container>
    <ng-container matColumnDef="budget_topic_name_th">
        <th mat-header-cell *matHeaderCellDef> ด้าน </th>
        <td mat-cell *matCellDef="let x">
            {{x.text_7}}
        </td>
    </ng-container>
    <ng-container matColumnDef="responsible_faculty_name_th">
        <th mat-header-cell *matHeaderCellDef> ชื่อหน่วยงาน </th>
        <td mat-cell *matCellDef="let x">
            {{x.responsible_faculty_name_th}}
        </td>
    </ng-container>
    <ng-container matColumnDef="action">
        <th mat-header-cell *matHeaderCellDef> ดูรายงานผล </th>
        <td mat-cell *matCellDef="let x;let i = index" style="width: 70px;">
            <span class="material-icons" style="cursor: pointer;color: #F8A300;"
                (click)="edit(x)">create</span>
        </td>
    </ng-container>
</table>
<br>
<mat-paginator [pageSizeOptions]="[5, 10, 20]" showFirstLastButtons (page)="change($event)"></mat-paginator>
  • Step 10: Register the 3 components in the module

Find, in request-budget-statistics.module.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:

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:

    List16_1Component,
    List16_2Component,
    List16_3Component,
    List20Component,

Change to:

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

  dataSource16_1$ = new Observable<any>()
  dataSource16_2$ = new Observable<any>()
  dataSource16_3$ = new Observable<any>()
  dataSourceBetweenYear$ = new Observable<any>()

Change to:

  dataSource16_1$ = new Observable<any>()
  dataSource16_2$ = new Observable<any>()
  dataSource16_3$ = new Observable<any>()
  dataSource29_1$ = new Observable<any>()
  dataSource29_2$ = new Observable<any>()
  dataSource29_3$ = new Observable<any>()
  dataSourceBetweenYear$ = new Observable<any>()
  • Step 2: Fix getAll()'s initial (unfiltered) load — exclude out-of-plan from 16, include only out-of-plan in 29

Find, in getAll():

    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:

    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():

     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:

     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:

[dataSource16_1]="dataSource16_1$ | async"
[dataSource16_2]="dataSource16_2$ | async"
[dataSource16_3]="dataSource16_3$ | async"

Change to:

[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:

  @Input() dataSource16_1: any = [];
  @Input() dataSource16_2: any = [];
  @Input() dataSource16_3: any = [];

Change to:

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

<ng-container *ngIf="typeUrl == 16">
    <mat-tab-group >
        <mat-tab label="รอตรวจสอบ">
            <app-list16-1
                [dataSource]="dataSource16_1"
                (onedit)="edit($event)"
                (ondelete)="onDelete($event)"
                (onchange)="change($event)"
                (onexcel)="excel($event)"
            ></app-list16-1>
        </mat-tab>

        <mat-tab label="ส่งแก้ไข">
            <app-list16-2
                [dataSource]="dataSource16_2"
                (onedit)="edit($event)"
                (ondelete)="onDelete($event)"
                (onchange)="change($event)"
                (onexcel)="excel($event)"
            ></app-list16-2>
        </mat-tab>

        <mat-tab label="ตรวจสอบแล้ว">
            <app-list16-3
                [dataSource]="dataSource16_3"
                (onedit)="edit($event)"
                (ondelete)="onDelete($event)"
                (onchange)="change($event)"
                (onSelect)="select16($event)"
                (onexcel)="excel($event)"
            ></app-list16-3>
        </mat-tab>
    </mat-tab-group>
</ng-container>

<ng-container *ngIf="typeUrl == 18">

Change to:

<ng-container *ngIf="typeUrl == 16">
    <mat-tab-group >
        <mat-tab label="รอตรวจสอบ">
            <app-list16-1
                [dataSource]="dataSource16_1"
                (onedit)="edit($event)"
                (ondelete)="onDelete($event)"
                (onchange)="change($event)"
                (onexcel)="excel($event)"
            ></app-list16-1>
        </mat-tab>

        <mat-tab label="ส่งแก้ไข">
            <app-list16-2
                [dataSource]="dataSource16_2"
                (onedit)="edit($event)"
                (ondelete)="onDelete($event)"
                (onchange)="change($event)"
                (onexcel)="excel($event)"
            ></app-list16-2>
        </mat-tab>

        <mat-tab label="ตรวจสอบแล้ว">
            <app-list16-3
                [dataSource]="dataSource16_3"
                (onedit)="edit($event)"
                (ondelete)="onDelete($event)"
                (onchange)="change($event)"
                (onSelect)="select16($event)"
                (onexcel)="excel($event)"
            ></app-list16-3>
        </mat-tab>
    </mat-tab-group>
</ng-container>

<ng-container *ngIf="typeUrl == 29">
    <mat-tab-group >
        <mat-tab label="รอตรวจสอบ">
            <app-list29-1
                [dataSource]="dataSource29_1"
                (ondelete)="onDelete($event)"
                (onchange)="change($event)"
                (onexcel)="excel($event)"
            ></app-list29-1>
        </mat-tab>

        <mat-tab label="ส่งแก้ไข">
            <app-list29-2
                [dataSource]="dataSource29_2"
                (ondelete)="onDelete($event)"
                (onchange)="change($event)"
                (onexcel)="excel($event)"
            ></app-list29-2>
        </mat-tab>

        <mat-tab label="ตรวจสอบแล้ว">
            <app-list29-3
                [dataSource]="dataSource29_3"
                (ondelete)="onDelete($event)"
                (onchange)="change($event)"
                (onexcel)="excel($event)"
            ></app-list29-3>
        </mat-tab>
    </mat-tab-group>
</ng-container>

<ng-container *ngIf="typeUrl == 18">
  • 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
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:

  urlPath
  typeUrl
  tabIndex

Change to:

  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:

        // 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:

        // 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:

  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:

  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:

<form [formGroup]="form">
  <div class="rmutr_card rmutr_container">

Change to:

<form [formGroup]="form">
  <div style="margin-bottom:8px;">
    <span *ngIf="form.value.out_of_plan_report_detail_uid" style="display:inline-block;background:#fef3c7;color:#92400e;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;">นอกแผน</span>
    <span *ngIf="!form.value.out_of_plan_report_detail_uid" style="display:inline-block;background:#dbeafe;color:#1e40af;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;">โครงการ</span>
  </div>
  <div class="rmutr_card rmutr_container">
  • Step 5: Gate the bottom action buttons for the 3 new review typeUrls + show the locked banner

Find:

    <button mat-raised-button cdkFocusInitial (click)="close()">ยกเลิก</button>
    <button *ngIf="typeUrl != 202161 " mat-raised-button class="bg-bpi-primary-color ml-2" (click)="save()">
      <span *ngIf="typeUrl == 202162">บันทึกและส่งงานแผน</span>
      <span *ngIf="typeUrl != 202162">บันทึก</span>
    </button>
    <ng-container >
      <button mat-raised-button class="buttonColorApprove ml-2"
          *ngIf="typeUrl == 202162"
          (click)="save_as62()"
          >บันทึกร่าง</button>
      <button mat-raised-button class="buttonColorApprove ml-2"
          *ngIf="typeUrl == 202161 "
          (click)="save_pass()"
          >ตรวจสอบผ่าน</button>


      <button mat-raised-button class="buttonColorReject ml-2"
        *ngIf="typeUrl == 202161 "
          (click)="save_send()" color="warn" >ส่งแก้ไข</button>
  </ng-container>
  </div>

Change to:

    <span *ngIf="isLocked" style="color:#ef4444;font-weight:600;margin-right:12px;">รายงานนี้ถูกล็อก ไม่สามารถแก้ไขได้</span>
    <button mat-raised-button cdkFocusInitial (click)="close()">ยกเลิก</button>
    <button *ngIf="typeUrl != 202161 && typeUrl != 29161 && typeUrl != 29162 && typeUrl != 29163 && !isLocked" mat-raised-button class="bg-bpi-primary-color ml-2" (click)="save()">
      <span *ngIf="typeUrl == 202162">บันทึกและส่งงานแผน</span>
      <span *ngIf="typeUrl != 202162">บันทึก</span>
    </button>
    <ng-container >
      <button mat-raised-button class="buttonColorApprove ml-2"
          *ngIf="typeUrl == 202162"
          (click)="save_as62()"
          >บันทึกร่าง</button>
      <button mat-raised-button class="buttonColorApprove ml-2"
          *ngIf="typeUrl == 202161 || typeUrl == 29161"
          (click)="save_pass()"
          >ตรวจสอบผ่าน</button>


      <button mat-raised-button class="buttonColorReject ml-2"
        *ngIf="typeUrl == 202161 || typeUrl == 29161"
          (click)="save_send()" color="warn" >ส่งแก้ไข</button>
  </ng-container>
  </div>

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

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:

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:

  isExporting = false
  details: any[] = []
  private detailsToDelete: string[] = []
  private pDetailsToDelete: string[] = []
  private aDetailsToDelete: string[] = []
  loading = true

Change to:

  isExporting = false
  details: any[] = []
  private detailsToDelete: string[] = []
  private pDetailsToDelete: string[] = []
  private aDetailsToDelete: string[] = []
  loading = true
  selection = new SelectionModel<any>(true, [])

Find, inside load():

        // สร้าง Map: out_of_plan_report_detail_uid → agency_report_uid
        const agencyByDetail = new Map<string, string>()
        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:

        // สร้าง Map: out_of_plan_report_detail_uid → { agency_report_uid, status_id }
        const agencyByDetail = new Map<string, { agency_report_uid: string, status_id: number }>()
        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:

        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:

        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 (<thead>):

        <thead>
          <tr class="tr_class">
            <th class="th_class" rowspan="5">ลำดับ</th>

Change to:

        <thead>
          <tr class="tr_class">
            <th class="th_class" rowspan="5">
              <mat-checkbox
                (click)="$event.stopPropagation()"
                (change)="$event ? masterToggle() : null"
                [checked]="selection.hasValue() && isAllSelected()"
                [indeterminate]="selection.hasValue() && !isAllSelected()">
              </mat-checkbox>
            </th>
            <th class="th_class" rowspan="5">สถานะ</th>
            <th class="th_class" rowspan="5">ลำดับ</th>

Find the first-P-row block that renders the rowspan-ed shared cells (the block starting right after <tr class="tr_p"> and <ng-container *ngIf="pIdx === 0">):

                <ng-container *ngIf="pIdx === 0">
                  <td class="td_class text-center"
                    [attr.rowspan]="(detail.out_of_plan_report_p_details?.length || 1) + (detail.out_of_plan_report_a_details?.length || 1)">
                    {{i + 1}}
                  </td>

Change to:

                <ng-container *ngIf="pIdx === 0">
                  <td class="td_class text-center"
                    [attr.rowspan]="(detail.out_of_plan_report_p_details?.length || 1) + (detail.out_of_plan_report_a_details?.length || 1)">
                    <mat-checkbox
                      (click)="$event.stopPropagation()"
                      (change)="$event ? selection.toggle(detail) : null"
                      [checked]="selection.isSelected(detail)"
                      [disabled]="!isSendable(detail)">
                    </mat-checkbox>
                  </td>
                  <td class="td_class text-center"
                    [attr.rowspan]="(detail.out_of_plan_report_p_details?.length || 1) + (detail.out_of_plan_report_a_details?.length || 1)">
                    <span *ngIf="!detail._agency_report_uid" style="color:#9ca3af;font-size:11px;">ยังไม่ทำรายงาน</span>
                    <span *ngIf="detail._agency_report_uid && (detail._status_id == null || detail._status_id === 0)" style="display:inline-block;background:#e5e7eb;color:#374151;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;">ร่าง</span>
                    <span *ngIf="detail._status_id === 1" style="display:inline-block;background:#fef9c3;color:#854d0e;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;">รอตรวจสอบ</span>
                    <span *ngIf="detail._status_id === 2" style="display:inline-block;background:#fee2e2;color:#991b1b;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;">ส่งแก้ไข</span>
                    <span *ngIf="detail._status_id === 3" style="display:inline-block;background:#dcfce7;color:#166534;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;">ตรวจสอบแล้ว</span>
                  </td>
                  <td class="td_class text-center"
                    [attr.rowspan]="(detail.out_of_plan_report_p_details?.length || 1) + (detail.out_of_plan_report_a_details?.length || 1)">
                    {{i + 1}}
                  </td>

Find, in the bottom actions bar:

  <div class="flex justify-between mt-4 mb-4">
    <button mat-raised-button color="primary" (click)="addDetail()">
      <mat-icon>add</mat-icon> เพิ่มโครงการใหม่
    </button>
    <div class="flex gap-2">

Change to:

  <div class="flex justify-between mt-4 mb-4">
    <div class="flex gap-2">
      <button mat-raised-button color="primary" (click)="addDetail()">
        <mat-icon>add</mat-icon> เพิ่มโครงการใหม่
      </button>
      <button mat-raised-button color="accent" [disabled]="selection.selected.length === 0" (click)="sendToPlan()">
        <mat-icon>send</mat-icon> ส่งงานแผน
      </button>
    </div>
    <div class="flex gap-2">
  • 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
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).