From 8e7a0cba4c2a9c95c6298eca12d1c3d408b68dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nut=2E=E0=B9=84=E0=B8=9B=E0=B9=80=E0=B8=A3=E0=B8=B7?= =?UTF-8?q?=E0=B9=88=E0=B8=AD=E0=B8=A2?= Date: Tue, 21 Jul 2026 23:20:36 +0700 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01613NaY9LAvy2wFXEeyE5eR --- .../2026-07-21-out-of-plan-report-review.md | 1396 +++++++++++++++++ 1 file changed, 1396 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-21-out-of-plan-report-review.md diff --git a/docs/superpowers/plans/2026-07-21-out-of-plan-report-review.md b/docs/superpowers/plans/2026-07-21-out-of-plan-report-review.md new file mode 100644 index 0000000..45e4d87 --- /dev/null +++ b/docs/superpowers/plans/2026-07-21-out-of-plan-report-review.md @@ -0,0 +1,1396 @@ +# Out-of-Plan Report Submit/Review Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a "ส่งงานแผน" submit action to the out-of-plan report list (`agency-out-of-plan/out-of-plan`) and a new reviewer menu "ตรวจสอบรายงานผล(นอกแผน)" with 3 status tabs, "ส่งกลับแก้ไข", and "ตรวจสอบผ่าน" actions. + +**Architecture:** No new tables/endpoints. Everything hinges on the existing `agency_report.status_id` column (0/null=draft, 1=รอตรวจสอบ, 2=ส่งแก้ไข, 3=ตรวจสอบแล้ว) and the existing `out_of_plan_report_detail_uid` link column on `agency_report`. The reviewer menu mirrors the existing `check-project-report` (`typeUrl == 16`) 3-tab pattern exactly, scoped by filtering `out_of_plan_report_detail_uid` client-side (the generic backend query filter is equality-only — it cannot express "not null" — so both the new list and the pre-existing `check-project-report` list filter client-side with RxJS `map()`). The review form reuses the big shared `agency-report-form.component.ts` (already used by 6+ other typeUrl variants), gated behind new `typeUrl` values (29, 29161, 29162, 29163) so no existing flow is touched. Bulk submit reuses the existing `AgencyReportService.updateStatusAgency()` method (already wired to `POST update_status/1`). + +**Tech Stack:** Angular 17, Angular Material (`mat-table`, `mat-tab-group`, `mat-checkbox`), Angular CDK `SelectionModel`, RxJS, SweetAlert2 (via `SweetalertService`). + +## Global Constraints + +- No backend (`rmutr-api`) changes in this plan — the two endpoints needed (`GET .../agency_report?status_id=N`, `POST .../agency_report/update_status/1`) already exist. +- This repo has no meaningful unit-test culture for these Angular components (only CLI-boilerplate `.spec.ts` files exist project-wide). Per-task verification uses `ng build --configuration=production` (from `/Users/nut.looknut/Project/rmutr/rmutr-web`) to catch compile/template errors, plus a manual browser check via `npm start` for the final task. +- Every new branch added to shared files (`request-budget-statistics.container.ts`, `request-budget-statistics-list.component.ts/html`, `agency-report-form.component.ts/html`) must be explicitly guarded by the new `typeUrl` values (29, 29161, 29162, 29163) or by `out_of_plan_report_detail_uid` truthiness — never change behavior for any other `typeUrl`. +- Follow existing code conventions exactly: raw `Swal.fire(...)` / `this.swSV.confirmSave()` patterns already used in the touched files — don't introduce a new dialog library or pattern. +- `AgencyReportService.updateStatusAgency(data)` (POST `{fullUrl}/update_status/1`) is the existing bulk-submit method — reuse it, don't add a new API method. +- Reuse the existing shared search component (`request-budget-statistics.search.component.ts`, fields: `budget_year_name_th`, `project_name_th`, `budget_plan_name_th`, `budget_project_name_th`, `responsible_faculty_name_th`) for the new reviewer menu — do not build new search UI. + +--- + +### Task 1: New routes and menu entry + +**Files:** +- Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/app-routing.module.ts:3011-3016` +- Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/core/data/navigator.ts:1111-1117` + +**Interfaces:** +- Consumes: nothing new +- Produces: route `type` values `29` (list), `29161`/`29162`/`29163` (review-edit sub-routes) and route paths `check-project-report-out-of-plan`, `agency-report-out-of-plan-edit-wait`, `agency-report-out-of-plan-edit-send`, `agency-report-out-of-plan-edit-pass` — every later task's `typeUrl` checks and `router.navigate()` calls target these exact values/paths. + +- [ ] **Step 1: Add the 4 new routes** + +Find, in `app-routing.module.ts` (the block right after the existing `agency-report-edit-pass` route): +```ts + { + path: 'agency-report-edit-pass', + loadChildren: () => import('./feature/budget-request/request/request-budget-statistics/request-budget-statistics.module') + .then(m => m.RequestBudgetStatisticsModule), + data: { + menuName: `การบริหารและรายงานผล${seperation}ตรวจสอบรายงานผลโครงการ`, + type: 202163 + } + }, + { + path: 'original-project-proposal1', +``` +Change to: +```ts + { + path: 'agency-report-edit-pass', + loadChildren: () => import('./feature/budget-request/request/request-budget-statistics/request-budget-statistics.module') + .then(m => m.RequestBudgetStatisticsModule), + data: { + menuName: `การบริหารและรายงานผล${seperation}ตรวจสอบรายงานผลโครงการ`, + type: 202163 + } + }, + { + path: 'check-project-report-out-of-plan', + loadChildren: () => import('./feature/budget-request/request/request-budget-statistics/request-budget-statistics.module') + .then(m => m.RequestBudgetStatisticsModule), + data: { + menuName: `การบริหารและรายงานผล${seperation}ตรวจสอบรายงานผล(นอกแผน)`, + type: 29 + } + }, + { + path: 'agency-report-out-of-plan-edit-wait', + loadChildren: () => import('./feature/budget-request/request/request-budget-statistics/request-budget-statistics.module') + .then(m => m.RequestBudgetStatisticsModule), + data: { + menuName: `การบริหารและรายงานผล${seperation}ตรวจสอบรายงานผล(นอกแผน)`, + type: 29161 + } + }, + { + path: 'agency-report-out-of-plan-edit-send', + loadChildren: () => import('./feature/budget-request/request/request-budget-statistics/request-budget-statistics.module') + .then(m => m.RequestBudgetStatisticsModule), + data: { + menuName: `การบริหารและรายงานผล${seperation}ตรวจสอบรายงานผล(นอกแผน)`, + type: 29162 + } + }, + { + path: 'agency-report-out-of-plan-edit-pass', + loadChildren: () => import('./feature/budget-request/request/request-budget-statistics/request-budget-statistics.module') + .then(m => m.RequestBudgetStatisticsModule), + data: { + menuName: `การบริหารและรายงานผล${seperation}ตรวจสอบรายงานผล(นอกแผน)`, + type: 29163 + } + }, + { + path: 'original-project-proposal1', +``` + +- [ ] **Step 2: Add the menu entry** + +Find, in `navigator.ts`: +```ts + { + id: 'agency-report-out-of-plan', + code: 'administer-011-007-2', + title: 'หน่วยงานทำรายงานผล(นอกแผน)', + type: 'basic', + icon: 'fiber_manual_record', + link: '/app/agency-out-of-plan/out-of-plan', + }, + { + id: 'check-project-report', +``` +Change to: +```ts + { + id: 'agency-report-out-of-plan', + code: 'administer-011-007-2', + title: 'หน่วยงานทำรายงานผล(นอกแผน)', + type: 'basic', + icon: 'fiber_manual_record', + link: '/app/agency-out-of-plan/out-of-plan', + }, + { + id: 'check-project-report-out-of-plan', + code: 'administer-011-007-3', + title: 'ตรวจสอบรายงานผล(นอกแผน)', + type: 'basic', + icon: 'fiber_manual_record', + link: '/app/check-project-report-out-of-plan', + }, + { + id: 'check-project-report', +``` + +- [ ] **Step 3: Build to confirm no errors** + +Run: `cd /Users/nut.looknut/Project/rmutr/rmutr-web && ng build --configuration=production` +Expected: build succeeds with no new errors. + +- [ ] **Step 4: Commit** + +```bash +git add rmutr-web/src/app/app-routing.module.ts rmutr-web/src/app/core/data/navigator.ts +git commit -m "feat: add routes and menu entry for out-of-plan report review" +``` + +--- + +### Task 2: `list29-1`/`list29-2`/`list29-3` components (review tabs) + +**Files:** +- Create: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-1/list29-1.component.ts` +- Create: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-1/list29-1.component.html` +- Create: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-1/list29-1.component.scss` +- Create: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-2/list29-2.component.ts` +- Create: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-2/list29-2.component.html` +- Create: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-3/list29-3.component.ts` +- Create: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-3/list29-3.component.html` +- Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/request-budget-statistics.module.ts:46,190` + +**Interfaces:** +- Consumes: `@Input() dataSource: any` — an array of `agency_report` rows (set by Task 3's container wiring). Each row has `agency_report_uid`, `budget_year_name_th`, `project_name_th`, `budget_project_name_th`(displayed via `text_6`... actually mirrors `list16-1` exactly, see below), `budget_topic_name_th`, `responsible_faculty_name_th`. +- Produces: `app-list29-1`, `app-list29-2`, `app-list29-3` selectors — Task 3 wires these into `request-budget-statistics-list.component.html`. Each has `edit(val)` navigating to its dedicated review route from Task 1. + +These 3 components are a direct copy of the existing `list16-1`/`list16-2`/`list16-3` pattern (same table shape, same `BaseList`/`updateMatTable` usage), minus the unrelated P/A-send checkbox feature that lives only in `list16-3` (that's a different, unrelated feature bolted onto that specific tab — not part of this workflow). Each adds one static badge column ("นอกแผน") since every row here is out-of-plan by definition (per the approved design spec). + +- [ ] **Step 1: Create `list29-1.component.ts`** + +```ts +import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Input, Output, OnChanges, SimpleChanges } from '@angular/core'; +import { PageEvent } from '@angular/material/paginator'; +import { Router } from '@angular/router'; +import { BaseList } from 'src/app/core/base/base-list'; + +@Component({ + selector: 'app-list29-1', + templateUrl: './list29-1.component.html', + styleUrls: ['./list29-1.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class List29_1Component extends BaseList implements OnInit, OnChanges { + + @Input() dataSource: any = []; + @Output() onchange = new EventEmitter(); + @Output() onedit = new EventEmitter(); + @Output() ondelete = new EventEmitter(); + @Output() onexcel = new EventEmitter(); + data + + constructor( + public router: Router + ) { + super(); + } + + ngOnChanges(changes: SimpleChanges): void { + if ('dataSource' in changes && changes?.dataSource?.currentValue) { + this.data = this.dataSource + this.dataSource = this.updateMatTable(this.dataSource ? this.dataSource : []); + } + } + + ngOnInit(): void { + } + + edit(val) { + this.router.navigate(['app/agency-report-out-of-plan-edit-wait/edit-report', val.agency_report_uid]) + } + + delete(val) { + this.ondelete.emit(val) + } + + change(event: PageEvent) { + let page: number = event.pageIndex + 1 + let table: any = { + page: page, + size: event.pageSize + } + this.onchange.emit(table) + } + + excel(val) { + this.onexcel.emit(val) + } + +} +``` + +- [ ] **Step 2: Create `list29-1.component.html`** + +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ประเภท + นอกแผน + ปีงบประมาณ + {{x.budget_year_name_th}} + ชื่อโครงการ + {{x.project_name_th}} + ผลผลิต + {{x.text_6}} + ด้าน + {{x.text_7}} + ชื่อหน่วยงาน + {{x.responsible_faculty_name_th}} + ดูรายงานผล + create +
+
+ +``` + +- [ ] **Step 3: Create `list29-1.component.scss`** + +```scss +``` +(empty — no extra styles needed, matches `list16-2`/`list16-3`'s empty/minimal scss) + +- [ ] **Step 4: Create `list29-2.component.ts`** (identical to `list29-1` except `edit()` target) + +```ts +import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Input, Output, OnChanges, SimpleChanges } from '@angular/core'; +import { PageEvent } from '@angular/material/paginator'; +import { Router } from '@angular/router'; +import { BaseList } from 'src/app/core/base/base-list'; + +@Component({ + selector: 'app-list29-2', + templateUrl: './list29-2.component.html', + styleUrls: ['./list29-2.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class List29_2Component extends BaseList implements OnInit, OnChanges { + + @Input() dataSource: any = []; + @Output() onchange = new EventEmitter(); + @Output() onedit = new EventEmitter(); + @Output() ondelete = new EventEmitter(); + @Output() onexcel = new EventEmitter(); + data + + constructor( + public router: Router + ) { + super(); + } + + ngOnChanges(changes: SimpleChanges): void { + if ('dataSource' in changes && changes?.dataSource?.currentValue) { + this.data = this.dataSource + this.dataSource = this.updateMatTable(this.dataSource ? this.dataSource : []); + } + } + + ngOnInit(): void { + } + + edit(val) { + this.router.navigate(['app/agency-report-out-of-plan-edit-send/edit-report', val.agency_report_uid]) + } + + delete(val) { + this.ondelete.emit(val) + } + + change(event: PageEvent) { + let page: number = event.pageIndex + 1 + let table: any = { + page: page, + size: event.pageSize + } + this.onchange.emit(table) + } + + excel(val) { + this.onexcel.emit(val) + } + +} +``` + +- [ ] **Step 5: Create `list29-2.component.scss`** + +```scss +``` +(empty file) + +- [ ] **Step 6: Create `list29-2.component.html`** (identical to `list29-1.component.html`) + +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ประเภท + นอกแผน + ปีงบประมาณ + {{x.budget_year_name_th}} + ชื่อโครงการ + {{x.project_name_th}} + ผลผลิต + {{x.text_6}} + ด้าน + {{x.text_7}} + ชื่อหน่วยงาน + {{x.responsible_faculty_name_th}} + ดูรายงานผล + create +
+
+ +``` + +- [ ] **Step 7: Create `list29-3.component.ts`** (identical, `edit()` targets the "pass" route) + +```ts +import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Input, Output, OnChanges, SimpleChanges } from '@angular/core'; +import { PageEvent } from '@angular/material/paginator'; +import { Router } from '@angular/router'; +import { BaseList } from 'src/app/core/base/base-list'; + +@Component({ + selector: 'app-list29-3', + templateUrl: './list29-3.component.html', + styleUrls: ['./list29-3.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class List29_3Component extends BaseList implements OnInit, OnChanges { + + @Input() dataSource: any = []; + @Output() onchange = new EventEmitter(); + @Output() onedit = new EventEmitter(); + @Output() ondelete = new EventEmitter(); + @Output() onexcel = new EventEmitter(); + data + + constructor( + public router: Router + ) { + super(); + } + + ngOnChanges(changes: SimpleChanges): void { + if ('dataSource' in changes && changes?.dataSource?.currentValue) { + this.data = this.dataSource + this.dataSource = this.updateMatTable(this.dataSource ? this.dataSource : []); + } + } + + ngOnInit(): void { + } + + edit(val) { + this.router.navigate(['app/agency-report-out-of-plan-edit-pass/edit-report', val.agency_report_uid]) + } + + delete(val) { + this.ondelete.emit(val) + } + + change(event: PageEvent) { + let page: number = event.pageIndex + 1 + let table: any = { + page: page, + size: event.pageSize + } + this.onchange.emit(table) + } + + excel(val) { + this.onexcel.emit(val) + } + +} +``` + +- [ ] **Step 8: Create `list29-3.component.scss`** + +```scss +``` +(empty file) + +- [ ] **Step 9: Create `list29-3.component.html`** + +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ประเภท + นอกแผน + ปีงบประมาณ + {{x.budget_year_name_th}} + ชื่อโครงการ + {{x.project_name_th}} + ผลผลิต + {{x.text_6}} + ด้าน + {{x.text_7}} + ชื่อหน่วยงาน + {{x.responsible_faculty_name_th}} + ดูรายงานผล + create +
+
+ +``` + +- [ ] **Step 10: Register the 3 components in the module** + +Find, in `request-budget-statistics.module.ts`: +```ts +import { List16_1Component } from './presenter/list/request-budget-statistics-list/list16-1/list16-1.component'; +import { List16_2Component } from './presenter/list/request-budget-statistics-list/list16-2/list16-2.component'; +import { List16_3Component } from './presenter/list/request-budget-statistics-list/list16-3/list16-3.component'; +import { List20Component } from './presenter/list/request-budget-statistics-list/list20/list20.component'; +``` +Change to: +```ts +import { List16_1Component } from './presenter/list/request-budget-statistics-list/list16-1/list16-1.component'; +import { List16_2Component } from './presenter/list/request-budget-statistics-list/list16-2/list16-2.component'; +import { List16_3Component } from './presenter/list/request-budget-statistics-list/list16-3/list16-3.component'; +import { List29_1Component } from './presenter/list/request-budget-statistics-list/list29-1/list29-1.component'; +import { List29_2Component } from './presenter/list/request-budget-statistics-list/list29-2/list29-2.component'; +import { List29_3Component } from './presenter/list/request-budget-statistics-list/list29-3/list29-3.component'; +import { List20Component } from './presenter/list/request-budget-statistics-list/list20/list20.component'; +``` + +Find: +```ts + List16_1Component, + List16_2Component, + List16_3Component, + List20Component, +``` +Change to: +```ts + List16_1Component, + List16_2Component, + List16_3Component, + List29_1Component, + List29_2Component, + List29_3Component, + List20Component, +``` + +- [ ] **Step 11: Build to confirm no errors** + +Run: `cd /Users/nut.looknut/Project/rmutr/rmutr-web && ng build --configuration=production` +Expected: build succeeds with no new errors. + +- [ ] **Step 12: Commit** + +```bash +git add rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-1/ rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-2/ rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list29-3/ rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/request-budget-statistics.module.ts +git commit -m "feat: add list29-1/2/3 review tab components for out-of-plan reports" +``` + +--- + +### Task 3: Wire the new reviewer list end-to-end + fix `check-project-report` leak + +**Files:** +- Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/container/request-other-expenses/request-budget-statistics.container.ts` +- Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/container/request-other-expenses/request-budget-statistics.container.html` +- Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/request-budget-statistics-list.component.ts` +- Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/request-budget-statistics-list.component.html` + +**Interfaces:** +- Consumes: `app-list29-1/2/3` selectors (Task 2), route path `check-project-report-out-of-plan` (Task 1, `type: 29`) +- Produces: `dataSource29_1$/29_2$/29_3$` observables on the container — final task in this chain, nothing downstream depends on these further. + +- [ ] **Step 1: Add the 3 new dataSource properties** + +Find, in `request-budget-statistics.container.ts`: +```ts + dataSource16_1$ = new Observable() + dataSource16_2$ = new Observable() + dataSource16_3$ = new Observable() + dataSourceBetweenYear$ = new Observable() +``` +Change to: +```ts + dataSource16_1$ = new Observable() + dataSource16_2$ = new Observable() + dataSource16_3$ = new Observable() + dataSource29_1$ = new Observable() + dataSource29_2$ = new Observable() + dataSource29_3$ = new Observable() + dataSourceBetweenYear$ = new Observable() +``` + +- [ ] **Step 2: Fix `getAll()`'s initial (unfiltered) load — exclude out-of-plan from 16, include only out-of-plan in 29** + +Find, in `getAll()`: +```ts + this.dataSource16_1$ = this.AgencyReportSV.queryString(`?status_id=1`) + this.dataSource16_2$ = this.AgencyReportSV.queryString(`?status_id=2`) + this.dataSource16_3$ = this.AgencyReportSV.queryString(`?status_id=3`) + this.dataSourceBetweenYear$ = this.requestBudgetSV.queryString(`?is_between_year=true`) +``` +Change to: +```ts + this.dataSource16_1$ = this.AgencyReportSV.queryString(`?status_id=1`).pipe(map((data: any[]) => (data || []).filter(d => !d.out_of_plan_report_detail_uid))) + this.dataSource16_2$ = this.AgencyReportSV.queryString(`?status_id=2`).pipe(map((data: any[]) => (data || []).filter(d => !d.out_of_plan_report_detail_uid))) + this.dataSource16_3$ = this.AgencyReportSV.queryString(`?status_id=3`).pipe(map((data: any[]) => (data || []).filter(d => !d.out_of_plan_report_detail_uid))) + this.dataSource29_1$ = this.AgencyReportSV.queryString(`?status_id=1`).pipe(map((data: any[]) => (data || []).filter(d => !!d.out_of_plan_report_detail_uid))) + this.dataSource29_2$ = this.AgencyReportSV.queryString(`?status_id=2`).pipe(map((data: any[]) => (data || []).filter(d => !!d.out_of_plan_report_detail_uid))) + this.dataSource29_3$ = this.AgencyReportSV.queryString(`?status_id=3`).pipe(map((data: any[]) => (data || []).filter(d => !!d.out_of_plan_report_detail_uid))) + this.dataSourceBetweenYear$ = this.requestBudgetSV.queryString(`?is_between_year=true`) +``` +(`map` is already imported at the top of this file — used by many other branches — no new import needed.) + +- [ ] **Step 3: Fix the `onsearch()` query branch for `typeUrl == 16` and add the `typeUrl == 29` branch** + +Find, in `onsearch()`: +```ts + else if(this.typeUrl == 16){ + this.dataSource16_1$ = this.AgencyReportSV.queryString(`?status_id=1&${queryStr}`) + this.dataSource16_2$ = this.AgencyReportSV.queryString(`?status_id=2&${queryStr}`) + this.dataSource16_3$ = this.AgencyReportSV.queryString(`?status_id=3&${queryStr}`) + + } +``` +Change to: +```ts + else if(this.typeUrl == 16){ + const excludeOutOfPlan = (data: any[]) => (data || []).filter(d => !d.out_of_plan_report_detail_uid) + this.dataSource16_1$ = this.AgencyReportSV.queryString(`?status_id=1&${queryStr}`).pipe(map(excludeOutOfPlan)) + this.dataSource16_2$ = this.AgencyReportSV.queryString(`?status_id=2&${queryStr}`).pipe(map(excludeOutOfPlan)) + this.dataSource16_3$ = this.AgencyReportSV.queryString(`?status_id=3&${queryStr}`).pipe(map(excludeOutOfPlan)) + + } + else if(this.typeUrl == 29){ + const onlyOutOfPlan = (data: any[]) => (data || []).filter(d => !!d.out_of_plan_report_detail_uid) + this.dataSource29_1$ = this.AgencyReportSV.queryString(`?status_id=1&${queryStr}`).pipe(map(onlyOutOfPlan)) + this.dataSource29_2$ = this.AgencyReportSV.queryString(`?status_id=2&${queryStr}`).pipe(map(onlyOutOfPlan)) + this.dataSource29_3$ = this.AgencyReportSV.queryString(`?status_id=3&${queryStr}`).pipe(map(onlyOutOfPlan)) + } +``` + +- [ ] **Step 4: Bind the 3 new observables in the container template** + +Find, in `request-budget-statistics.container.html`: +```html +[dataSource16_1]="dataSource16_1$ | async" +[dataSource16_2]="dataSource16_2$ | async" +[dataSource16_3]="dataSource16_3$ | async" +``` +Change to: +```html +[dataSource16_1]="dataSource16_1$ | async" +[dataSource16_2]="dataSource16_2$ | async" +[dataSource16_3]="dataSource16_3$ | async" +[dataSource29_1]="dataSource29_1$ | async" +[dataSource29_2]="dataSource29_2$ | async" +[dataSource29_3]="dataSource29_3$ | async" +``` + +- [ ] **Step 5: Add the 3 new `@Input()`s to the presenter list component** + +Find, in `request-budget-statistics-list.component.ts`: +```ts + @Input() dataSource16_1: any = []; + @Input() dataSource16_2: any = []; + @Input() dataSource16_3: any = []; +``` +Change to: +```ts + @Input() dataSource16_1: any = []; + @Input() dataSource16_2: any = []; + @Input() dataSource16_3: any = []; + @Input() dataSource29_1: any = []; + @Input() dataSource29_2: any = []; + @Input() dataSource29_3: any = []; +``` + +- [ ] **Step 6: Add the `typeUrl == 29` tab block to the presenter list template** + +Find, in `request-budget-statistics-list.component.html`: +```html + + + + + + + + + + + + + + + + + +``` +Change to: +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +- [ ] **Step 7: Build to confirm no errors** + +Run: `cd /Users/nut.looknut/Project/rmutr/rmutr-web && ng build --configuration=production` +Expected: build succeeds with no new errors. + +- [ ] **Step 8: Commit** + +```bash +git add rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/container/request-other-expenses/request-budget-statistics.container.ts rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/container/request-other-expenses/request-budget-statistics.container.html rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/request-budget-statistics-list.component.ts rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/request-budget-statistics-list.component.html +git commit -m "feat: wire out-of-plan reviewer list (typeUrl 29) and stop out-of-plan reports leaking into check-project-report" +``` + +--- + +### Task 4: Report form — lock, badge, review buttons, `close()` routing + +**Files:** +- Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/form/agency-report-form/agency-report-form.component.ts` +- Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/form/agency-report-form/agency-report-form.component.html` + +**Interfaces:** +- Consumes: `typeUrl` values `29161`/`29162`/`29163` (Task 1), `agency_report.out_of_plan_report_detail_uid`/`status_id` (existing fields, no new backend needed) +- Produces: `isLocked: boolean` class field — the template reads it to hide the save button and show a locked banner. `close()` now routes `29161/29162/29163` back to `check-project-report-out-of-plan`. + +- [ ] **Step 1: Add the `isLocked` field** + +Find: +```ts + urlPath + typeUrl + tabIndex +``` +Change to: +```ts + urlPath + typeUrl + tabIndex + isLocked: boolean = false +``` + +- [ ] **Step 2: Route the 3 new review typeUrls through the existing `typeUrl == 282` load branch, and lock the form when the loaded report is already submitted or done** + +Find the full `else if (this.typeUrl == 282)` block: +```ts + // type 282: เปิดฟอร์มจากรายงานที่มีอยู่แล้ว → โหลด agency_report ตรงๆ แล้ว state='edit' + else if (this.typeUrl == 282) { + this.AgencyReportSV.get(this.uniquekey).pipe( + tap((x: any) => { + this.form.patchValue(x) + this.change_year(x.budget_year_name_th) + if (x.agency_report_items) { + const formArray = this.form.get('agency_report_items') as FormArray + x.agency_report_items.forEach((agency_report_item) => { + formArray.push(this.agency_report_item_form(agency_report_item)) + }) + } + if (x.agency_report_details) { + x.agency_report_details.forEach((agency_report_detail) => { + let form_agency_report_details = this.form.get('agency_report_details') as FormArray + form_agency_report_details.push(this.agency_report_details_form(agency_report_detail)) + }) + const firstIssue = x.agency_report_details.find(d => d.type == 1 && d.text_1) + if (firstIssue) this.change_budget_strategy_faculty_strategic_edit(firstIssue.text_1) + } + if (x.agency_report_popularities) { + x.agency_report_popularities.forEach((p) => { + const arr = this.form.get('agency_report_popularities') as FormArray + arr.push(this.fb.group(p)) + }) + this.cdRef.detectChanges() + } + Swal.close() + }), + catchError(err => { + Swal.close() + return of(null) + }) + ).subscribe() + } +``` +Change to: +```ts + // type 282/29161/29162/29163: เปิดฟอร์มจากรายงานที่มีอยู่แล้ว → โหลด agency_report ตรงๆ แล้ว state='edit' + // (29161/29162/29163 = งานแผนเปิดดูจากเมนู "ตรวจสอบรายงานผล(นอกแผน)" — ใช้ loader เดียวกับ 282) + else if (this.typeUrl == 282 || this.typeUrl == 29161 || this.typeUrl == 29162 || this.typeUrl == 29163) { + this.AgencyReportSV.get(this.uniquekey).pipe( + tap((x: any) => { + this.form.patchValue(x) + this.change_year(x.budget_year_name_th) + if (x.agency_report_items) { + const formArray = this.form.get('agency_report_items') as FormArray + x.agency_report_items.forEach((agency_report_item) => { + formArray.push(this.agency_report_item_form(agency_report_item)) + }) + } + if (x.agency_report_details) { + x.agency_report_details.forEach((agency_report_detail) => { + let form_agency_report_details = this.form.get('agency_report_details') as FormArray + form_agency_report_details.push(this.agency_report_details_form(agency_report_detail)) + }) + const firstIssue = x.agency_report_details.find(d => d.type == 1 && d.text_1) + if (firstIssue) this.change_budget_strategy_faculty_strategic_edit(firstIssue.text_1) + } + if (x.agency_report_popularities) { + x.agency_report_popularities.forEach((p) => { + const arr = this.form.get('agency_report_popularities') as FormArray + arr.push(this.fb.group(p)) + }) + this.cdRef.detectChanges() + } + if (x.out_of_plan_report_detail_uid && (x.status_id === 1 || x.status_id === 3)) { + this.isLocked = true + this.form.disable() + } + Swal.close() + }), + catchError(err => { + Swal.close() + return of(null) + }) + ).subscribe() + } +``` + +- [ ] **Step 3: Route `close()` for the 3 new review typeUrls back to the new menu** + +Find: +```ts + close() { + if(this.typeUrl == 202161 || this.typeUrl == 202163 ){ + this.router.navigate(['app/check-project-report']) + } + else if(this.typeUrl == 202162){ + this.router.navigate(['app/send-edit-agency-report']) + } + else if(this.typeUrl == 281 || this.typeUrl == 282){ + this.router.navigate(['app/agency-out-of-plan/out-of-plan']) + } + else{ + this.router.navigate(['app/agency-report']) + } + + } +``` +Change to: +```ts + close() { + if(this.typeUrl == 202161 || this.typeUrl == 202163 ){ + this.router.navigate(['app/check-project-report']) + } + else if(this.typeUrl == 202162){ + this.router.navigate(['app/send-edit-agency-report']) + } + else if(this.typeUrl == 29161 || this.typeUrl == 29162 || this.typeUrl == 29163){ + this.router.navigate(['app/check-project-report-out-of-plan']) + } + else if(this.typeUrl == 281 || this.typeUrl == 282){ + this.router.navigate(['app/agency-out-of-plan/out-of-plan']) + } + else{ + this.router.navigate(['app/agency-report']) + } + + } +``` + +- [ ] **Step 4: Add the "โครงการ"/"นอกแผน" badge at the top of the form** + +Find, at the very top of `agency-report-form.component.html`: +```html +
+
+``` +Change to: +```html + +
+ นอกแผน + โครงการ +
+
+``` + +- [ ] **Step 5: Gate the bottom action buttons for the 3 new review typeUrls + show the locked banner** + +Find: +```html + + + + + + + + + +
+``` +Change to: +```html + รายงานนี้ถูกล็อก ไม่สามารถแก้ไขได้ + + + + + + + + + +
+``` +`save_pass()` sets `status_id = 3` and PUTs; `save_send()` sets `status_id = 2` and PUTs — both already exist unchanged (`agency-report-form.component.ts:4116-4179`) and both call `getCleanedFormValue()` → `form.getRawValue()`, which includes disabled-control values, so they work correctly even when `isLocked` has called `form.disable()`. + +- [ ] **Step 6: Build to confirm no errors** + +Run: `cd /Users/nut.looknut/Project/rmutr/rmutr-web && ng build --configuration=production` +Expected: build succeeds with no new errors. + +- [ ] **Step 7: Commit** + +```bash +git add rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/form/agency-report-form/agency-report-form.component.ts rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/form/agency-report-form/agency-report-form.component.html +git commit -m "feat: lock out-of-plan report form once submitted, add project/out-of-plan badge, wire review actions for new typeUrls" +``` + +--- + +### Task 5: Out-of-plan list — checkbox, status badge, "ส่งงานแผน" button + +**Files:** +- Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/form/agency-out-of-plan-form/agency-out-of-plan-form.component.ts` +- Modify: `/Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/form/agency-out-of-plan-form/agency-out-of-plan-form.component.html` + +**Interfaces:** +- Consumes: `AgencyReportService.updateStatusAgency(data)` (existing, `agency-report.service.ts`, POSTs to `update_status/1`) +- Produces: nothing further downstream — this is the last functional task; sent rows become visible in the Task 3 reviewer list purely because they now match `status_id=1 && out_of_plan_report_detail_uid != null`. + +- [ ] **Step 1: Track `_status_id` alongside `_agency_report_uid`, add `selection`, and the send handler** + +Find: +```ts +import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; +import { Router } from '@angular/router'; +import { Observable, of, forkJoin } from 'rxjs'; +import { catchError, concatMap, filter, map, switchMap, tap } from 'rxjs/operators'; +import * as XLSX from 'xlsx-js-style'; +import { SweetalertService } from 'src/app/core/service/sweetalert/sweetalert'; +import { OutOfPlanReportDetailService } from 'src/app/core/service/request-budget/out-of-plan-report-detail.service'; +import { OutOfPlanReportPDetailService } from 'src/app/core/service/request-budget/out-of-plan-report-p-detail.service'; +import { OutOfPlanReportADetailService } from 'src/app/core/service/request-budget/out-of-plan-report-a-detail.service'; +import { AgencyReportService } from 'src/app/core/service/request-budget/agency-report.service'; +``` +Change to: +```ts +import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; +import { Router } from '@angular/router'; +import { SelectionModel } from '@angular/cdk/collections'; +import { Observable, of, forkJoin, throwError } from 'rxjs'; +import { catchError, concatMap, filter, map, switchMap, tap } from 'rxjs/operators'; +import * as XLSX from 'xlsx-js-style'; +import { SweetalertService } from 'src/app/core/service/sweetalert/sweetalert'; +import { OutOfPlanReportDetailService } from 'src/app/core/service/request-budget/out-of-plan-report-detail.service'; +import { OutOfPlanReportPDetailService } from 'src/app/core/service/request-budget/out-of-plan-report-p-detail.service'; +import { OutOfPlanReportADetailService } from 'src/app/core/service/request-budget/out-of-plan-report-a-detail.service'; +import { AgencyReportService } from 'src/app/core/service/request-budget/agency-report.service'; +``` + +Find: +```ts + isExporting = false + details: any[] = [] + private detailsToDelete: string[] = [] + private pDetailsToDelete: string[] = [] + private aDetailsToDelete: string[] = [] + loading = true +``` +Change to: +```ts + isExporting = false + details: any[] = [] + private detailsToDelete: string[] = [] + private pDetailsToDelete: string[] = [] + private aDetailsToDelete: string[] = [] + loading = true + selection = new SelectionModel(true, []) +``` + +Find, inside `load()`: +```ts + // สร้าง Map: out_of_plan_report_detail_uid → agency_report_uid + const agencyByDetail = new Map() + for (const ar of agencyReports || []) { + const k = ar?.out_of_plan_report_detail_uid + if (!k) continue + agencyByDetail.set(k, ar.agency_report_uid) + } +``` +Change to: +```ts + // สร้าง Map: out_of_plan_report_detail_uid → { agency_report_uid, status_id } + const agencyByDetail = new Map() + for (const ar of agencyReports || []) { + const k = ar?.out_of_plan_report_detail_uid + if (!k) continue + agencyByDetail.set(k, { agency_report_uid: ar.agency_report_uid, status_id: ar.status_id }) + } +``` + +Find: +```ts + this.allDetails = (details || []).map(d => { + const uid = d.out_of_plan_report_detail_uid + const pList = (pByDetail.get(uid) || []).sort((x, y) => (x.sequence_no ?? 0) - (y.sequence_no ?? 0)) + const aList = (aByDetail.get(uid) || []).sort((x, y) => (x.sequence_no ?? 0) - (y.sequence_no ?? 0)) + return { + ...d, + _agency_report_uid: agencyByDetail.get(uid) || null, + out_of_plan_report_p_details: pList.length > 0 ? pList : [{ sequence_no: 1 }], + out_of_plan_report_a_details: aList.length > 0 ? aList : [{ sequence_no: 1 }], + } + }) + this.details = [...this.allDetails] + this.loading = false + this.cdRef.detectChanges() + }), + catchError(err => { + this.swSV.errText(err?.error?.description || 'โหลดข้อมูลไม่สำเร็จ') + this.loading = false + this.cdRef.detectChanges() + return of(null) + }) + ).subscribe() + } +``` +Change to: +```ts + this.allDetails = (details || []).map(d => { + const uid = d.out_of_plan_report_detail_uid + const pList = (pByDetail.get(uid) || []).sort((x, y) => (x.sequence_no ?? 0) - (y.sequence_no ?? 0)) + const aList = (aByDetail.get(uid) || []).sort((x, y) => (x.sequence_no ?? 0) - (y.sequence_no ?? 0)) + const agency = agencyByDetail.get(uid) + return { + ...d, + _agency_report_uid: agency?.agency_report_uid || null, + _status_id: agency ? agency.status_id : null, + out_of_plan_report_p_details: pList.length > 0 ? pList : [{ sequence_no: 1 }], + out_of_plan_report_a_details: aList.length > 0 ? aList : [{ sequence_no: 1 }], + } + }) + this.details = [...this.allDetails] + this.selection.clear() + this.loading = false + this.cdRef.detectChanges() + }), + catchError(err => { + this.swSV.errText(err?.error?.description || 'โหลดข้อมูลไม่สำเร็จ') + this.loading = false + this.cdRef.detectChanges() + return of(null) + }) + ).subscribe() + } + + private sendableDetails(): any[] { + return (this.details || []).filter(d => this.isSendable(d)) + } + + isSendable(detail: any): boolean { + return !!detail?._agency_report_uid && detail?._status_id !== 1 && detail?._status_id !== 3 + } + + isAllSelected(): boolean { + const sendable = this.sendableDetails() + return sendable.length > 0 && this.selection.selected.length === sendable.length + } + + masterToggle(): void { + if (this.isAllSelected()) { + this.selection.clear() + return + } + this.selection.select(...this.sendableDetails()) + } + + sendToPlan(): void { + if (this.selection.selected.length === 0) return + this.swSV.confirmSave('ต้องการส่งรายการที่เลือกไปงานแผนหรือไม่?').pipe( + filter(x => x.isConfirmed), + concatMap(() => this.agencyReportSV.updateStatusAgency( + this.selection.selected.map(d => ({ agency_report_uid: d._agency_report_uid })) + ).pipe( + catchError(err => { + this.swSV.errText(err?.error?.description || 'ส่งไม่สำเร็จ') + return throwError(err) + }) + )), + tap(() => this.swSV.updateSuccess()), + tap(() => this.load()), + ).subscribe() + } +``` + +- [ ] **Step 2: Add the checkbox column, status badge column, and "ส่งงานแผน" toolbar button** + +Find, in `agency-out-of-plan-form.component.html`, the header row (``): +```html + + + ลำดับ +``` +Change to: +```html + + + + + + + สถานะ + ลำดับ +``` + +Find the first-P-row block that renders the rowspan-ed shared cells (the block starting right after `` and ``): +```html + + + {{i + 1}} + +``` +Change to: +```html + + + + + + + ยังไม่ทำรายงาน + ร่าง + รอตรวจสอบ + ส่งแก้ไข + ตรวจสอบแล้ว + + + {{i + 1}} + +``` + +Find, in the bottom actions bar: +```html +
+ +
+``` +Change to: +```html +
+
+ + +
+
+``` + +- [ ] **Step 3: Build to confirm no errors** + +Run: `cd /Users/nut.looknut/Project/rmutr/rmutr-web && ng build --configuration=production` +Expected: build succeeds with no new errors. + +- [ ] **Step 4: Commit** + +```bash +git add rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/form/agency-out-of-plan-form/agency-out-of-plan-form.component.ts rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/form/agency-out-of-plan-form/agency-out-of-plan-form.component.html +git commit -m "feat: add checkbox, status badge, and send-to-planning button to out-of-plan list" +``` + +--- + +### Task 6: End-to-end manual verification + +**Files:** none (verification only) + +**Interfaces:** none + +- [ ] **Step 1: Start the dev server** + +Run: `cd /Users/nut.looknut/Project/rmutr/rmutr-web && npm start` + +- [ ] **Step 2: Draft → fill report → check status badge** + +Open "การบริหารและรายงานผล > หน่วยงานทำรายงานผล(นอกแผน)" → open (or add) a project row → click the report icon to fill in the report → save. +Expected: back on the list, the row shows status badge "ร่าง", checkbox enabled. + +- [ ] **Step 3: Send to planning** + +Tick the checkbox on 1-2 rows with badge "ร่าง" → click "ส่งงานแผน" → confirm the dialog. +Expected: success toast, badge changes to "รอตรวจสอบ", checkbox now disabled/unchecked. + +- [ ] **Step 4: Confirm the form is locked** + +Click the report icon on a now-"รอตรวจสอบ" row. +Expected: form loads with all fields disabled, red text "รายงานนี้ถูกล็อก ไม่สามารถแก้ไขได้" visible near the bottom, no "บันทึก" button, badge at top reads "นอกแผน". + +- [ ] **Step 5: Reviewer sees it in the new menu** + +Open "การบริหารและรายงานผล > ตรวจสอบรายงานผล(นอกแผน)" → tab "รอตรวจสอบ". +Expected: the sent report(s) appear, each row showing badge "นอกแผน". Search by ปีงบประมาณ and by ชื่อโครงการ (top search panel) and confirm filtering works. + +- [ ] **Step 6: Send back for revision** + +Click the edit icon on a row in "รอตรวจสอบ" → click "ส่งแก้ไข". +Expected: navigates back to "ตรวจสอบรายงานผล(นอกแผน)"; the item now appears in tab "ส่งแก้ไข". + +- [ ] **Step 7: Agency edits and resends** + +Back in "หน่วยงานทำรายงานผล(นอกแผน)", confirm the same row now shows badge "ส่งแก้ไข" and checkbox is enabled again. Open the report — confirm fields are editable (not locked) — make a change, save. Tick the checkbox, click "ส่งงานแผน" again. +Expected: badge returns to "รอตรวจสอบ". + +- [ ] **Step 8: Approve** + +In "ตรวจสอบรายงานผล(นอกแผน)" tab "รอตรวจสอบ", open the report → click "ตรวจสอบผ่าน". +Expected: navigates back to the menu; item now appears in tab "ตรวจสอบแล้ว". Reopening it from that tab shows the form locked (read-only). + +- [ ] **Step 9: Regression check — regular (in-plan) report flow untouched** + +Open "การบริหารและรายงานผล > ตรวจสอบรายงานผลโครงการ" (the original, pre-existing menu) → confirm out-of-plan reports sent in earlier steps do **not** appear anywhere in its 3 tabs, and any pre-existing regular (non-out-of-plan) reports still appear as before. Open a regular report from this menu → confirm the header badge reads "โครงการ" and "ตรวจสอบผ่าน"/"ส่งแก้ไข" buttons still work exactly as before (unaffected by the `isLocked`/badge additions, since those are gated on `out_of_plan_report_detail_uid`/new typeUrl values only).