Files
rmutr/docs/superpowers/plans/2026-07-06-between-year-draft-send.md
Nut.ไปเรื่อยandClaude Sonnet 5 4471b224cf docs: add remaining implementation plans
Adds plan docs that were pending in the working tree, matching the
existing docs/superpowers/plans/ pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mf6qFtiQoRuSh9zQSPF1wS
2026-09-24 19:32:52 +07:00

43 KiB

Between-Year Draft → Send-to-Planning 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 draft/send-to-planning workflow to the "คำขอระหว่างปี" (between-year budget request) menu — save as draft, then explicitly select rows and click "ส่งกองนโยบายและแผน" to send them to the planning office, locking them from further edits.

Architecture: No new tables/endpoints. Everything hinges on the existing request_budget.status_id column (0 = draft, 1 = sent) on the SAME record. The list already shows both states (once we remove a stale query filter); a new checkbox + bulk-send button flips status_id 0→1 via the existing generic RequestBudgetService.updateMany(). The list-level UI lives in list-between-year.component.ts/html; the record-level form lives in a large shared component (RequestOtherExpensesFormComponent) reused by many other project-proposal types, so every change there is gated behind this.typeUrl == 21 (the between-year route's type) to avoid touching other flows.

Tech Stack: Angular 17, Angular Material (mat-table, mat-checkbox), Angular CDK SelectionModel, RxJS, SweetAlert2 (via SweetalertService).

Global Constraints

  • Scope is strictly the "คำขอระหว่างปี" menu (typeUrl == 21 / is_between_year). Do not change behavior for any other typeUrl value in shared files (request-budget-statistics.container.ts, request-budget-statistics-list.component.ts/html, request-other-expenses-form.component.ts/html) — every new branch must be explicitly guarded by typeUrl == 21.
  • This repo has no meaningful unit-test culture for these Angular components (only CLI-boilerplate .spec.ts files exist project-wide; none for the files touched here). Per-task verification therefore 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 (dev server with HMR) — this matches how this project has actually been verified throughout this session (Playwright-driven manual checks), not fabricated unit tests.
  • Follow existing code conventions exactly: raw Swal.fire(...) in request-other-expenses-form.component.ts (it does NOT use SweetalertService), vs. this.swSV.confirmSave()/.updateSuccess()/.errText() in request-budget-statistics.container.ts (it DOES use SweetalertService). Don't introduce a new pattern in either file.
  • BaseService.updateMany(data) (PUT {fullUrl}/s) is the existing generic bulk-update method — reuse it, don't add a new API method.

Task 1: Fix stale status_id=1 filter blocking drafts from the list

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

Interfaces:

  • Consumes: nothing new

  • Produces: dataSourceBetweenYear$ now returns both draft (status_id=0) and sent (status_id=1) rows — later tasks (checkbox/status badge in Task 4) rely on both being present in the same list.

  • Step 1: Read the current line to confirm context

The getAll() method currently has (line 199):

    this.dataSourceBetweenYear$ = this.requestBudgetSV.queryString(`?is_between_year=true&status_id=1`)

This hardcodes status_id=1, meaning newly-created drafts (status_id=0, added by Task 2) would never show up in the list at all. The search path (onsearch(), same file, typeUrl == 21 branch) already has no such filter — this line is inconsistent with it and must be fixed.

  • Step 2: Edit the line

Change:

    this.dataSourceBetweenYear$ = this.requestBudgetSV.queryString(`?is_between_year=true&status_id=1`)

to:

    this.dataSourceBetweenYear$ = this.requestBudgetSV.queryString(`?is_between_year=true`)
  • 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/container/request-other-expenses/request-budget-statistics.container.ts
git commit -m "fix: stop filtering out draft between-year requests from the list"

Task 2: Set status_id=0 on create + lock the form once sent (shared form component)

Files:

  • Modify: /Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-other-expenses/presenter/form/request-other-expenses-form/request-other-expenses-form.component.ts

Interfaces:

  • Consumes: this.typeUrl (already set at line 391 from route data; between-year route resolves this to 21, confirmed live via browser console), this.state ('add'/'edit', set by BaseFormV2)

  • Produces: isLocked: boolean class field — Task 3 (template) reads this to hide the save button and show a locked banner.

  • Step 1: Add the isLocked field

Find (around line 199):

  is_plan_log: boolean = false

Change to:

  is_plan_log: boolean = false
  isLocked: boolean = false
  • Step 2: Set status_id = 0 on create, scoped to typeUrl == 21

Find, at the top of save() (around line 4455-4458):

  save() {
    console.log('ททท',this.request_budget_type_url)
    console.log(this.typeUrl)

    if(this.typeUrl == 2001){

Change to:

  save() {
    console.log('ททท',this.request_budget_type_url)
    console.log(this.typeUrl)

    if (this.typeUrl == 21 && this.state === 'add') {
      this.form.get('status_id').setValue(0)
    }

    if(this.typeUrl == 2001){

This only ever fires for the between-year route (typeUrl == 21) on first creation, leaving every other typeUrl branch (including the typeUrl == 2001 review flow right below, which manages its own status_id independently) untouched. On 'edit' saves of an existing draft, status_id is left as whatever was loaded (already 0), so this is a no-op there too.

  • Step 3: Lock the form when an already-sent between-year record loads

Find, in setUpform(res) (around line 975-990):

  setUpform(res) {
    console.log(res)
    this.change_year(res.budget_year_uid)
    if (res.budget_strategy_uid) this.change_budget_strategy_edit(res.budget_strategy_uid)
    if (res.budget_strategy_target_uid) this.change_budget_strategy_target_edit(res.budget_strategy_target_uid)
    if (res.budget_reform_uid) this.change_budget_reform_edit(res.budget_reform_uid)
    if (res.budget_reform_target_uid) this.change_budget_reform_target_edit(res.budget_reform_target_uid)
    if (res.budget_economic_uid) this.change_budget_economic_edit(res.budget_economic_uid)
    if (res.budget_economic_target_uid) this.change_budget_economic_target_edit(res.budget_economic_target_uid)
    if (res.budget_strategy_faculty_uid) this.change_budget_strategy_faculty_strategic_edit(res.budget_strategy_faculty_uid)
    if (res.budget_unit_uid) this.change_budget_unit_edit(res.budget_unit_uid)
    if (res.service_insurance_level_uid) this.on_search_Insurance_fac(res.service_insurance_level_uid)
    if (res.service_insurance_faculty_uid) this.on_search_Insurance(res.service_insurance_faculty_uid)
    this.click_faculty_edit(res)
    this.form.patchValue(res)

Change the last line (this.form.patchValue(res)) to add the lock check right after it:

    this.click_faculty_edit(res)
    this.form.patchValue(res)

    if (this.typeUrl == 21) {
      this.isLocked = res.status_id !== 0
      if (this.isLocked) {
        this.form.disable()
      }
    }

setUpform is shared by every project type, so this must stay guarded by typeUrl == 21 — for any other type, isLocked stays false and form.disable() never runs, preserving current behavior exactly.

  • Step 4: 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 5: Commit
git add rmutr-web/src/app/feature/budget-request/request/request-other-expenses/presenter/form/request-other-expenses-form/request-other-expenses-form.component.ts
git commit -m "feat: save between-year requests as draft (status_id=0) and lock the form once sent"

Task 3: Split the save button into "บันทึก" / "บันทึกฉบับร่าง" and show a locked banner

Files:

  • Modify: /Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-other-expenses/presenter/form/request-other-expenses-form/request-other-expenses-form.component.html:876-877

Interfaces:

  • Consumes: typeUrl, isLocked (from Task 2), save() (unchanged method — same call for both buttons)

  • Produces: nothing new for later tasks (this is the leaf-most template change)

  • Step 1: Edit the button block

Find (lines 876-877):

        <button mat-raised-button cdkFocusInitial (click)="close_request_budget()">ยกเลิก</button>&nbsp;&nbsp;&nbsp;
        <button mat-raised-button class="bg-bpi-primary-color" (click)="save()" *ngIf="typeUrl != 2001" color="primary">บันทึก</button>

Change to:

        <span *ngIf="typeUrl == 21 && isLocked" style="color:#ef4444;font-weight:600;margin-right:12px;">ส่งกองนโยบายและแผนแล้ว — ไม่สามารถแก้ไขได้</span>
        <button mat-raised-button cdkFocusInitial (click)="close_request_budget()">ยกเลิก</button>&nbsp;&nbsp;&nbsp;
        <button mat-raised-button class="bg-bpi-primary-color" (click)="save()" *ngIf="typeUrl != 2001 && typeUrl != 21" color="primary">บันทึก</button>
        <button mat-raised-button class="bg-bpi-primary-color" (click)="save()" *ngIf="typeUrl == 21 && !isLocked" color="primary">บันทึกฉบับร่าง</button>

The original button's *ngIf now excludes typeUrl == 21 (so between-year never shows the generic "บันทึก" label), and a new button shows "บันทึกฉบับร่าง" only for between-year and only while unlocked (draft). When locked, neither save button renders — only the banner and "ยกเลิก".

  • Step 2: 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 3: Manual verification

Run: cd /Users/nut.looknut/Project/rmutr/rmutr-web && npm start, then in a browser:

  • Open the "คำขอระหว่างปี" menu → "เพิ่มคำขอระหว่างปี" (add). Confirm the button reads "บันทึกฉบับร่าง" (not "บันทึก").

  • Save it, then edit that same record. Confirm "บันทึกฉบับร่าง" still shows (not locked yet — Task 5 is what flips it to sent).

  • Step 4: Commit

git add rmutr-web/src/app/feature/budget-request/request/request-other-expenses/presenter/form/request-other-expenses-form/request-other-expenses-form.component.html
git commit -m "feat: label between-year save button as draft and show locked banner when sent"

Task 4: Checkbox column, status badge, and locked delete icon in the list

Files:

  • Modify: /Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list-between-year/list-between-year.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/list-between-year/list-between-year.component.html
  • Modify: /Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list-between-year/list-between-year.component.scss

Interfaces:

  • Consumes: dataSource input rows now include both status_id: 0 (draft) and status_id: 1 (sent) rows (Task 1), each row has is_ok/is_modify/status_id fields already returned by the API.

  • Produces: @Output() onsend = new EventEmitter<any[]>() — emits the array of selected draft rows when "ส่งกองนโยบายและแผน" is clicked. Task 5 wires this output up through the presenter list and container.

  • Step 1: Add SelectionModel and the send output to the component class

Find the full current file:

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';
import { RequestPlanUidService } from 'src/app/core/service/request-budget/request-plan-uid.service';

@Component({
  selector: 'app-list-between-year',
  templateUrl: './list-between-year.component.html',
  styleUrls: ['./list-between-year.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListBetweenYearComponent 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(
    private router: Router,
    private RequestPlanUidSV: RequestPlanUidService,
  ) {
    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 {
  }

  add() {
    this.RequestPlanUidSV.is_between_year$.next(true);
    this.RequestPlanUidSV.request_budget_type$.next('ae9afcdb-a8d6-4be6-b440-931e7a27fa73');
    this.router.navigate(['app/request-budget-between-year/add-between-year/add']);
  }

  edit(val) {
    this.router.navigate(['app/request-budget-between-year/add-between-year/edit', val.request_budget_uid]);
  }

  delete(val) {
    this.ondelete.emit(val);
  }

  change(event: PageEvent) {
    const page: number = event.pageIndex + 1;
    const table: any = { page, size: event.pageSize };
    this.onchange.emit(table);
  }

  excel(val) {
    this.onexcel.emit(val);
  }

}

Replace it entirely with:

import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Input, Output, OnChanges, SimpleChanges } from '@angular/core';
import { SelectionModel } from '@angular/cdk/collections';
import { PageEvent } from '@angular/material/paginator';
import { Router } from '@angular/router';
import { BaseList } from 'src/app/core/base/base-list';
import { RequestPlanUidService } from 'src/app/core/service/request-budget/request-plan-uid.service';

@Component({
  selector: 'app-list-between-year',
  templateUrl: './list-between-year.component.html',
  styleUrls: ['./list-between-year.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListBetweenYearComponent extends BaseList implements OnInit, OnChanges {

  @Input() dataSource: any = [];
  @Output() onchange = new EventEmitter();
  @Output() onedit = new EventEmitter();
  @Output() ondelete = new EventEmitter();
  @Output() onexcel = new EventEmitter();
  @Output() onsend = new EventEmitter();
  selection = new SelectionModel<any>(true, []);
  data

  constructor(
    private router: Router,
    private RequestPlanUidSV: RequestPlanUidService,
  ) {
    super();
  }

  ngOnChanges(changes: SimpleChanges): void {
    if ('dataSource' in changes && changes?.dataSource?.currentValue) {
      this.data = this.dataSource;
      this.dataSource = this.updateMatTable(this.dataSource ? this.dataSource : []);
      this.selection.clear();
    }
  }

  ngOnInit(): void {
  }

  add() {
    this.RequestPlanUidSV.is_between_year$.next(true);
    this.RequestPlanUidSV.request_budget_type$.next('ae9afcdb-a8d6-4be6-b440-931e7a27fa73');
    this.router.navigate(['app/request-budget-between-year/add-between-year/add']);
  }

  edit(val) {
    this.router.navigate(['app/request-budget-between-year/add-between-year/edit', val.request_budget_uid]);
  }

  delete(val) {
    this.ondelete.emit(val);
  }

  change(event: PageEvent) {
    const page: number = event.pageIndex + 1;
    const table: any = { page, size: event.pageSize };
    this.onchange.emit(table);
  }

  excel(val) {
    this.onexcel.emit(val);
  }

  private draftRows(): any[] {
    return (this.data || []).filter((x: any) => x.status_id === 0);
  }

  isAllSelected(): boolean {
    const draft = this.draftRows();
    return draft.length > 0 && this.selection.selected.length === draft.length;
  }

  masterToggle(): void {
    if (this.isAllSelected()) {
      this.selection.clear();
      return;
    }
    this.selection.select(...this.draftRows());
  }

  checkboxLabel(row?: any): string {
    if (!row) {
      return `${this.isAllSelected() ? 'deselect' : 'select'} all`;
    }
    return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row`;
  }

  sendToStrategic(): void {
    this.onsend.emit(this.selection.selected);
  }

}

Selection is cleared on every dataSource change (Angular gives new row object instances on every refresh, so stale selected references would otherwise linger); only draft (status_id === 0) rows are selectable, matching the same "already-sent rows can't be selected" pattern used by list6/list24 elsewhere in this codebase.

  • Step 2: Add the checkbox column, fix the status column, lock the delete icon, add the send button

Find the full current file:

<div class="list-card">
    <div class="toolbar">
        <button mat-raised-button color="primary" class="btn-add" (click)="add()">
            <mat-icon>add</mat-icon>
            เพิ่มคำขอระหว่างปี
        </button>
    </div>

    <div class="table-wrap">
        <table mat-table [dataSource]="dataSource" class="list-table">
            <tr mat-header-row
                *matHeaderRowDef="['action','budget_year_name_th','budget_location_name_th','faculty_name_th','project_name_th','budget_project_name_th','budget_topic_name_th','request_type','total_budget']">
            </tr>
            <tr mat-row
                *matRowDef="let myRowData; columns: ['action','budget_year_name_th','budget_location_name_th','faculty_name_th','project_name_th','budget_project_name_th','budget_topic_name_th','request_type','total_budget']">
            </tr>
            <tr mat-footer-row
                *matFooterRowDef="['action','budget_year_name_th','budget_location_name_th','faculty_name_th','project_name_th','budget_project_name_th','budget_topic_name_th','request_type','total_budget']">
            </tr>

            <!-- Action -->
            <ng-container matColumnDef="action">
                <th mat-header-cell *matHeaderCellDef class="col-action"></th>
                <td mat-cell *matCellDef="let x" class="col-action">
                    <span class="material-icons action-icon" (click)="edit(x)" title="แก้ไข">edit</span>
                    <span class="material-icons action-icon action-icon--delete" (click)="delete(x)" title="ลบ">delete</span>
                </td>
                <td mat-footer-cell *matFooterCellDef></td>
            </ng-container>

            <!-- ปีงบประมาณ -->
            <ng-container matColumnDef="budget_year_name_th">
                <th mat-header-cell *matHeaderCellDef class="col-year">ปีงบฯ</th>
                <td mat-cell *matCellDef="let x" class="col-year">
                    <span class="badge-year">{{x.budget_year_name_th}}</span>
                </td>
                <td mat-footer-cell *matFooterCellDef></td>
            </ng-container>

            <!-- พื้นที่ -->
            <ng-container matColumnDef="budget_location_name_th">
                <th mat-header-cell *matHeaderCellDef class="col-location">พื้นที่</th>
                <td mat-cell *matCellDef="let x" class="col-location">
                    <span class="text-clip">{{x.budget_location_name_th}}</span>
                </td>
                <td mat-footer-cell *matFooterCellDef></td>
            </ng-container>

            <!-- หน่วยงาน/คณะ -->
            <ng-container matColumnDef="faculty_name_th">
                <th mat-header-cell *matHeaderCellDef class="col-faculty">หน่วยงาน/คณะ</th>
                <td mat-cell *matCellDef="let x" class="col-faculty">
                    <span class="text-clip">{{x.responsible_faculty_name_th}}</span>
                </td>
                <td mat-footer-cell *matFooterCellDef></td>
            </ng-container>

            <!-- ชื่อโครงการ -->
            <ng-container matColumnDef="project_name_th">
                <th mat-header-cell *matHeaderCellDef class="col-project">ชื่อโครงการ</th>
                <td mat-cell *matCellDef="let x" class="col-project">
                    <span class="text-clip project-name">{{x.project_name_th}}</span>
                </td>
                <td mat-footer-cell *matFooterCellDef></td>
            </ng-container>

            <!-- ผลผลิต -->
            <ng-container matColumnDef="budget_project_name_th">
                <th mat-header-cell *matHeaderCellDef class="col-project-type">ผลผลิต</th>
                <td mat-cell *matCellDef="let x" class="col-project-type">
                    <span class="text-clip">{{x.budget_project_name_th}}</span>
                </td>
                <td mat-footer-cell *matFooterCellDef></td>
            </ng-container>

            <!-- ด้าน -->
            <ng-container matColumnDef="budget_topic_name_th">
                <th mat-header-cell *matHeaderCellDef class="col-topic">ด้าน</th>
                <td mat-cell *matCellDef="let x" class="col-topic">
                    <span class="text-clip">{{x.budget_topic_name_th}}</span>
                </td>
                <td mat-footer-cell *matFooterCellDef></td>
            </ng-container>

            <!-- ประเภทคำขอ -->
            <ng-container matColumnDef="request_type">
                <th mat-header-cell *matHeaderCellDef class="col-request-type">ประเภทคำขอ</th>
                <td mat-cell *matCellDef="let x" class="col-request-type">
                    <span class="badge badge-between">คำขอระหว่างปี</span>
                </td>
                <td mat-footer-cell *matFooterCellDef></td>
            </ng-container>

            <!-- สถานะ -->
            <!-- <ng-container matColumnDef="status">
                <th mat-header-cell *matHeaderCellDef class="col-status">สถานะ</th>
                <td mat-cell *matCellDef="let x" class="col-status">
                    <span *ngIf="x.is_ok == true" class="badge badge-success">ตรวจสอบแล้ว</span>
                    <span *ngIf="x.is_modify == true" class="badge badge-warning">ส่งแก้ไข</span>
                    <span *ngIf="!x.is_ok && !x.is_modify && x.status_id == 1" class="badge badge-pending">รอตรวจสอบ</span>
                    <span *ngIf="x.status_id == 0" class="badge badge-draft">ร่าง</span>
                </td>
                <td mat-footer-cell *matFooterCellDef class="footer-label">จำนวนเงินรวม</td>
            </ng-container> -->

            <!-- วงเงิน -->
            <ng-container matColumnDef="total_budget">
                <th mat-header-cell *matHeaderCellDef class="col-amount">วงเงิน (บาท)</th>
                <td mat-cell *matCellDef="let x" class="col-amount">
                    {{x.total_budget | number:'1.2-2'}}
                </td>
                <td mat-footer-cell *matFooterCellDef class="col-amount footer-total">
                    {{data | total:'total_budget' | number:'1.2-2'}}
                </td>
            </ng-container>
        </table>
    </div>

    <mat-paginator [pageSizeOptions]="[20, 50, 100]" showFirstLastButtons (page)="change($event)"></mat-paginator>
</div>

Replace it entirely with:

<div class="list-card">
    <div class="toolbar">
        <button mat-raised-button color="primary" class="btn-add" (click)="add()">
            <mat-icon>add</mat-icon>
            เพิ่มคำขอระหว่างปี
        </button>
        <button mat-raised-button color="accent" class="btn-send" [disabled]="selection.selected.length === 0" (click)="sendToStrategic()">
            <mat-icon>send</mat-icon>
            ส่งกองนโยบายและแผน
        </button>
    </div>

    <div class="table-wrap">
        <table mat-table [dataSource]="dataSource" class="list-table">
            <tr mat-header-row
                *matHeaderRowDef="['checkbox','action','budget_year_name_th','budget_location_name_th','faculty_name_th','project_name_th','budget_project_name_th','budget_topic_name_th','request_type','status','total_budget']">
            </tr>
            <tr mat-row
                *matRowDef="let myRowData; columns: ['checkbox','action','budget_year_name_th','budget_location_name_th','faculty_name_th','project_name_th','budget_project_name_th','budget_topic_name_th','request_type','status','total_budget']">
            </tr>
            <tr mat-footer-row
                *matFooterRowDef="['checkbox','action','budget_year_name_th','budget_location_name_th','faculty_name_th','project_name_th','budget_project_name_th','budget_topic_name_th','request_type','status','total_budget']">
            </tr>

            <!-- เลือก -->
            <ng-container matColumnDef="checkbox">
                <th mat-header-cell *matHeaderCellDef class="col-checkbox">
                    <mat-checkbox
                        (change)="$event ? masterToggle() : null"
                        [checked]="selection.hasValue() && isAllSelected()"
                        [indeterminate]="selection.hasValue() && !isAllSelected()">
                    </mat-checkbox>
                </th>
                <td mat-cell *matCellDef="let x" class="col-checkbox">
                    <mat-checkbox
                        (click)="$event.stopPropagation()"
                        (change)="$event ? selection.toggle(x) : null"
                        [checked]="selection.isSelected(x)"
                        [aria-label]="checkboxLabel(x)"
                        [disabled]="x.status_id !== 0">
                    </mat-checkbox>
                </td>
                <td mat-footer-cell *matFooterCellDef></td>
            </ng-container>

            <!-- Action -->
            <ng-container matColumnDef="action">
                <th mat-header-cell *matHeaderCellDef class="col-action"></th>
                <td mat-cell *matCellDef="let x" class="col-action">
                    <span class="material-icons action-icon" (click)="edit(x)" title="แก้ไข">edit</span>
                    <span *ngIf="x.status_id === 0" class="material-icons action-icon action-icon--delete" (click)="delete(x)" title="ลบ">delete</span>
                    <span *ngIf="x.status_id !== 0" class="material-icons action-icon action-icon--disabled" title="ส่งไปงานแผนแล้ว ลบไม่ได้">delete</span>
                </td>
                <td mat-footer-cell *matFooterCellDef></td>
            </ng-container>

            <!-- ปีงบประมาณ -->
            <ng-container matColumnDef="budget_year_name_th">
                <th mat-header-cell *matHeaderCellDef class="col-year">ปีงบฯ</th>
                <td mat-cell *matCellDef="let x" class="col-year">
                    <span class="badge-year">{{x.budget_year_name_th}}</span>
                </td>
                <td mat-footer-cell *matFooterCellDef></td>
            </ng-container>

            <!-- พื้นที่ -->
            <ng-container matColumnDef="budget_location_name_th">
                <th mat-header-cell *matHeaderCellDef class="col-location">พื้นที่</th>
                <td mat-cell *matCellDef="let x" class="col-location">
                    <span class="text-clip">{{x.budget_location_name_th}}</span>
                </td>
                <td mat-footer-cell *matFooterCellDef></td>
            </ng-container>

            <!-- หน่วยงาน/คณะ -->
            <ng-container matColumnDef="faculty_name_th">
                <th mat-header-cell *matHeaderCellDef class="col-faculty">หน่วยงาน/คณะ</th>
                <td mat-cell *matCellDef="let x" class="col-faculty">
                    <span class="text-clip">{{x.responsible_faculty_name_th}}</span>
                </td>
                <td mat-footer-cell *matFooterCellDef></td>
            </ng-container>

            <!-- ชื่อโครงการ -->
            <ng-container matColumnDef="project_name_th">
                <th mat-header-cell *matHeaderCellDef class="col-project">ชื่อโครงการ</th>
                <td mat-cell *matCellDef="let x" class="col-project">
                    <span class="text-clip project-name">{{x.project_name_th}}</span>
                </td>
                <td mat-footer-cell *matFooterCellDef></td>
            </ng-container>

            <!-- ผลผลิต -->
            <ng-container matColumnDef="budget_project_name_th">
                <th mat-header-cell *matHeaderCellDef class="col-project-type">ผลผลิต</th>
                <td mat-cell *matCellDef="let x" class="col-project-type">
                    <span class="text-clip">{{x.budget_project_name_th}}</span>
                </td>
                <td mat-footer-cell *matFooterCellDef></td>
            </ng-container>

            <!-- ด้าน -->
            <ng-container matColumnDef="budget_topic_name_th">
                <th mat-header-cell *matHeaderCellDef class="col-topic">ด้าน</th>
                <td mat-cell *matCellDef="let x" class="col-topic">
                    <span class="text-clip">{{x.budget_topic_name_th}}</span>
                </td>
                <td mat-footer-cell *matFooterCellDef></td>
            </ng-container>

            <!-- ประเภทคำขอ -->
            <ng-container matColumnDef="request_type">
                <th mat-header-cell *matHeaderCellDef class="col-request-type">ประเภทคำขอ</th>
                <td mat-cell *matCellDef="let x" class="col-request-type">
                    <span class="badge badge-between">คำขอระหว่างปี</span>
                </td>
                <td mat-footer-cell *matFooterCellDef></td>
            </ng-container>

            <!-- สถานะ -->
            <ng-container matColumnDef="status">
                <th mat-header-cell *matHeaderCellDef class="col-status">สถานะ</th>
                <td mat-cell *matCellDef="let x" class="col-status">
                    <span *ngIf="x.is_ok == true" class="badge badge-success">ตรวจสอบแล้ว</span>
                    <span *ngIf="x.is_modify == true" class="badge badge-warning">ส่งแก้ไข</span>
                    <span *ngIf="!x.is_ok && !x.is_modify && x.status_id == 1" class="badge badge-pending">ส่งงานแผน</span>
                    <span *ngIf="x.status_id == 0" class="badge badge-draft">ร่าง</span>
                </td>
                <td mat-footer-cell *matFooterCellDef></td>
            </ng-container>

            <!-- วงเงิน -->
            <ng-container matColumnDef="total_budget">
                <th mat-header-cell *matHeaderCellDef class="col-amount">วงเงิน (บาท)</th>
                <td mat-cell *matCellDef="let x" class="col-amount">
                    {{x.total_budget | number:'1.2-2'}}
                </td>
                <td mat-footer-cell *matFooterCellDef class="col-amount footer-total">
                    {{data | total:'total_budget' | number:'1.2-2'}}
                </td>
            </ng-container>
        </table>
    </div>

    <mat-paginator [pageSizeOptions]="[20, 50, 100]" showFirstLastButtons (page)="change($event)"></mat-paginator>
</div>
  • Step 3: Add the checkbox column width and locked-icon styles

Find, in the .scss file:

.action-icon {
    font-size: 17px !important;
    width: 17px !important;
    height: 17px !important;
    color: #f8a300;
    cursor: pointer;
    display: inline-block;
    margin: 0 4px;
    transition: opacity 0.15s;
    vertical-align: middle;

    &:hover { opacity: 0.7; }

    &--delete {
        color: #ef4444;
    }
}

Change to:

.action-icon {
    font-size: 17px !important;
    width: 17px !important;
    height: 17px !important;
    color: #f8a300;
    cursor: pointer;
    display: inline-block;
    margin: 0 4px;
    transition: opacity 0.15s;
    vertical-align: middle;

    &:hover { opacity: 0.7; }

    &--delete {
        color: #ef4444;
    }

    &--disabled {
        color: #d1d5db;
        cursor: not-allowed;
    }
}

.col-checkbox {
    width: 44px !important;
    min-width: 44px !important;
    max-width: 44px !important;
    text-align: center !important;
}

Also find, near the top of the file:

.btn-add {
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: 13px;
    font-weight: 600;
}

Change to:

.btn-add {
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: 13px;
    font-weight: 600;
}

.btn-send {
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: 13px;
    font-weight: 600;
}
  • Step 4: 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 5: Commit
git add rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list-between-year/
git commit -m "feat: add checkbox selection, status badge, and locked delete icon to between-year list"

Task 5: Wire the send action end-to-end and implement the bulk status update

Files:

  • 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
  • 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/container/request-other-expenses/request-budget-statistics.container.ts

Interfaces:

  • Consumes: list-between-year's @Output() onsend (Task 4) — emits any[] of selected draft rows.

  • Produces: nothing further downstream — this is the final task; the sent rows automatically appear in the existing "ตรวจสอบคำเสนอขอโครงการ" menu (check-project-proposal) because that menu already queries status_id=1 on the same table (request_budget_and_strategic), unchanged by this plan.

  • Step 1: Add the output + forwarding method to the presenter list component

Find, in request-budget-statistics-list.component.ts:

  @Output() onsave22 = new EventEmitter();
  @Output() onsendPlan22 = new EventEmitter();

Change to:

  @Output() onsave22 = new EventEmitter();
  @Output() onsendPlan22 = new EventEmitter();
  @Output() onsendBetweenYear = new EventEmitter();

Find:

  save22(val){
    this.onsave22.emit(val)
  }

  sendPlan22(val){
    this.onsendPlan22.emit(val)
  }

Change to:

  save22(val){
    this.onsave22.emit(val)
  }

  sendPlan22(val){
    this.onsendPlan22.emit(val)
  }

  sendBetweenYear(val){
    this.onsendBetweenYear.emit(val)
  }
  • Step 2: Wire the child (onsend) into the new method in the presenter list's template

Find, in request-budget-statistics-list.component.html:

<ng-container *ngIf="typeUrl == 21">
    <app-list-between-year
        [dataSource]="dataSourceBetweenYear"
        (ondelete)="onDelete($event)"
        (onchange)="change($event)"
        (onexcel)="excel($event)"
    ></app-list-between-year>
</ng-container>

Change to:

<ng-container *ngIf="typeUrl == 21">
    <app-list-between-year
        [dataSource]="dataSourceBetweenYear"
        (ondelete)="onDelete($event)"
        (onchange)="change($event)"
        (onexcel)="excel($event)"
        (onsend)="sendBetweenYear($event)"
    ></app-list-between-year>
</ng-container>
  • Step 3: Wire the presenter list's (onsendBetweenYear) into the container's template

Find, in request-budget-statistics.container.html:

(onedit)="onedit($event)"
(onedit_request_budget)="onedit_request_budget($event)"
(ondelete)="ondelete($event)"
(onchange)="onchange($event)" 

Change to:

(onedit)="onedit($event)"
(onedit_request_budget)="onedit_request_budget($event)"
(ondelete)="ondelete($event)"
(onchange)="onchange($event)" 
(onsendBetweenYear)="sendBetweenYearToStrategic($event)"
  • Step 4: Implement the bulk-send handler in the container

Find, in request-budget-statistics.container.ts, the ondelete(value) method:

  ondelete(value){
    // console.log(value)
    this.swSV.confirm().pipe(
      filter(x => x.isConfirmed),
      concatMap(() => {
        const delete$ = (this.typeUrl == 7 || this.typeUrl == 22 || this.typeUrl == 9 || this.typeUrl == 11)
          ? this.requestBudgetAnalysis5SV.deleteData(value.request_budget_analysis_5_uid)
          : this.requestBudgetSV.deleteData(value.request_budget_uid)
        return delete$.pipe(
          tap(() => this.swSV.removeSuccess()),
          tap(() => this.ngOnInit()),
          tap(() => this.cdRef.detectChanges())
        )
      })
    ).subscribe()
  }

Add a new method directly after it:

  ondelete(value){
    // console.log(value)
    this.swSV.confirm().pipe(
      filter(x => x.isConfirmed),
      concatMap(() => {
        const delete$ = (this.typeUrl == 7 || this.typeUrl == 22 || this.typeUrl == 9 || this.typeUrl == 11)
          ? this.requestBudgetAnalysis5SV.deleteData(value.request_budget_analysis_5_uid)
          : this.requestBudgetSV.deleteData(value.request_budget_uid)
        return delete$.pipe(
          tap(() => this.swSV.removeSuccess()),
          tap(() => this.ngOnInit()),
          tap(() => this.cdRef.detectChanges())
        )
      })
    ).subscribe()
  }

  sendBetweenYearToStrategic(value: any[]){
    const selected = (value || []).filter(v => v?.status_id === 0)
    if (selected.length === 0) {
      this.swSV.errText('กรุณาเลือกรายการที่ต้องการส่ง')
      return
    }
    this.swSV.confirmSave('ต้องการส่งรายการที่เลือกไปกองนโยบายและแผนหรือไม่?').pipe(
      filter(x => x.isConfirmed),
      concatMap(() => {
        const putData = selected.map(v => ({ ...v, status_id: 1, is_examine: true }))
        return this.requestBudgetSV.updateMany(putData).pipe(
          catchError(err => {
            this.swSV.errText(err?.error?.description || 'ส่งไม่สำเร็จ')
            return throwError(err)
          })
        )
      }),
      tap(() => this.swSV.updateSuccess()),
      tap(() => this.getAll()),
      tap(() => this.cdRef.detectChanges())
    ).subscribe()
  }

filter, concatMap, tap, catchError, and throwError are already imported at the top of this file (used throughout the other handlers above), so no new imports are needed.

  • Step 5: 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 6: Commit
git add 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 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/container/request-other-expenses/request-budget-statistics.container.ts
git commit -m "feat: wire checkbox send action to bulk-update between-year requests to sent status"

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 creation

In the browser, open "แผนงานและโครงการ > คำขอระหว่างปี" → "เพิ่มคำขอระหว่างปี" → fill required fields → click "บันทึกฉบับร่าง". Expected: record appears in the list with badge "ร่าง", checkbox enabled, both "แก้ไข" and "ลบ" icons active.

  • Step 3: Edit a draft

Click "แก้ไข" on the drafted row. Expected: form loads fully editable, button still reads "บันทึกฉบับร่าง", no locked banner.

  • Step 4: Send to planning

Back in the list, tick the checkbox on 1-2 draft rows → click "ส่งกองนโยบายและแผน" → confirm the dialog. Expected: success toast, rows now show badge "ส่งงานแผน", checkbox greyed out and unchecked, "ลบ" icon greyed out/disabled, "แก้ไข" icon still clickable.

  • Step 5: View a sent (locked) record

Click "แก้ไข" on a now-sent row. Expected: form loads with all fields disabled, red banner "ส่งกองนโยบายและแผนแล้ว — ไม่สามารถแก้ไขได้" visible, no save button, only "ยกเลิก".

  • Step 6: Confirm it reached the planning menu

Open "แผนงานและโครงการ > ตรวจสอบคำเสนอขอโครงการ" → tab "จัดการคำเสนอขอ". Expected: the just-sent project(s) appear there (this is existing, unmodified behavior — confirms status_id=1 propagation works end-to-end).

  • Step 7: Regression check on an unrelated menu

Open "แผนงานและโครงการ > ต้นฉบับเสนอโครงการ ง.5" (typeUrl 1, uses the same shared form component) → add or edit a record. Expected: behaves exactly as before — plain "บันทึก" button (not "บันทึกฉบับร่าง"), no locked banner, saves normally. This confirms the typeUrl == 21 guards in Task 2/3 didn't leak into other project types.