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
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.tsfiles exist project-wide). Per-task verification usesng build --configuration=production(from/Users/nut.looknut/Project/rmutr/rmutr-web) to catch compile/template errors, plus a manual browser check vianpm startfor 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 newtypeUrlvalues (29, 29161, 29162, 29163) or byout_of_plan_report_detail_uidtruthiness — never change behavior for any othertypeUrl. - 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
typevalues29(list),29161/29162/29163(review-edit sub-routes) and route pathscheck-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'stypeUrlchecks androuter.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 ofagency_reportrows (set by Task 3's container wiring). Each row hasagency_report_uid,budget_year_name_th,project_name_th,budget_project_name_th(displayed viatext_6... actually mirrorslist16-1exactly, see below),budget_topic_name_th,responsible_faculty_name_th. - Produces:
app-list29-1,app-list29-2,app-list29-3selectors — Task 3 wires these intorequest-budget-statistics-list.component.html. Each hasedit(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 tolist29-1exceptedit()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 tolist29-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/3selectors (Task 2), route pathcheck-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 fortypeUrl == 16and add thetypeUrl == 29branch
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 == 29tab 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:
typeUrlvalues29161/29162/29163(Task 1),agency_report.out_of_plan_report_detail_uid/status_id(existing fields, no new backend needed) -
Produces:
isLocked: booleanclass field — the template reads it to hide the save button and show a locked banner.close()now routes29161/29162/29163back tocheck-project-report-out-of-plan. -
Step 1: Add the
isLockedfield
Find:
urlPath
typeUrl
tabIndex
Change to:
urlPath
typeUrl
tabIndex
isLocked: boolean = false
- Step 2: Route the 3 new review typeUrls through the existing
typeUrl == 282load 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 toupdate_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_idalongside_agency_report_uid, addselection, 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).