Files
rmutr/docs/superpowers/plans/2026-07-06-between-year-draft-send.md
T
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

966 lines
43 KiB
Markdown

# 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):
```ts
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:
```ts
this.dataSourceBetweenYear$ = this.requestBudgetSV.queryString(`?is_between_year=true&status_id=1`)
```
to:
```ts
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**
```bash
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):
```ts
is_plan_log: boolean = false
```
Change to:
```ts
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):
```ts
save() {
console.log('ททท',this.request_budget_type_url)
console.log(this.typeUrl)
if(this.typeUrl == 2001){
```
Change to:
```ts
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):
```ts
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:
```ts
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**
```bash
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):
```html
<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:
```html
<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**
```bash
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:
```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';
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:
```ts
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:
```html
<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:
```html
<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:
```scss
.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:
```scss
.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:
```scss
.btn-add {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
font-weight: 600;
}
```
Change to:
```scss
.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**
```bash
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`:
```ts
@Output() onsave22 = new EventEmitter();
@Output() onsendPlan22 = new EventEmitter();
```
Change to:
```ts
@Output() onsave22 = new EventEmitter();
@Output() onsendPlan22 = new EventEmitter();
@Output() onsendBetweenYear = new EventEmitter();
```
Find:
```ts
save22(val){
this.onsave22.emit(val)
}
sendPlan22(val){
this.onsendPlan22.emit(val)
}
```
Change to:
```ts
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`:
```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:
```html
<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`:
```html
(onedit)="onedit($event)"
(onedit_request_budget)="onedit_request_budget($event)"
(ondelete)="ondelete($event)"
(onchange)="onchange($event)"
```
Change to:
```html
(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:
```ts
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:
```ts
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**
```bash
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.