docs: add disbursement plan research implementation plan

This commit is contained in:
Nut.ไปเรื่อย
2026-06-17 12:08:13 +07:00
parent 5a94f2481f
commit 2b01bec262
@@ -0,0 +1,641 @@
# Disbursement Plan Research 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:** เพิ่มปุ่มไอคอนในตาราง `agency-report-research` (typeUrl=27) ต่อโครงการ เพื่อกรอก/แก้ไข "แผนการเบิกจ่ายเป็นงวด" 3 งวดผ่าน MatDialog popup พร้อมบันทึกลง DB
**Architecture:** เพิ่ม 6 field ใน `change_project_research_detail` model (backend) + EF Core migration + สร้าง Angular MatDialog component + ต่อ EventEmitter chain จาก `list20-research``request-budget-statistics-list` → container
**Tech Stack:** ASP.NET Core (EF Core migrations), Angular 13+, Angular Material (MatDialog, MatDatepicker), TypeScript, RxJS
## Global Constraints
- Backend schema: `BUDGET` (PostgreSQL)
- Angular ChangeDetectionStrategy: `OnPush` ทุก component ใหม่
- PUT endpoint: `PUT /api/request_budget/change_project_research_detail/s` (updateMany)
- รวมเบิกจ่ายทั้งสิ้น = คำนวณใน frontend เท่านั้น ไม่เก็บใน DB
- ใช้ `SweetalertService` (`swSV`) สำหรับ confirmSave / updateSuccess / errText ทุก save action
- ชื่อ dialog selector: `app-disbursement-plan-research-dialog`
---
## File Map
| File | Action |
|------|--------|
| `rmutr-api/Modules/RequestBudgets/Databases/Models/change_project_research_detail.cs` | Modify — เพิ่ม 6 field |
| `rmutr-api/Migrations/20260617000002_disbursement_plan_research.cs` | Create — EF migration |
| `rmutr-web/.../presenter/popup/disbursement-plan-research-dialog/disbursement-plan-research-dialog.component.ts` | Create |
| `rmutr-web/.../presenter/popup/disbursement-plan-research-dialog/disbursement-plan-research-dialog.component.html` | Create |
| `rmutr-web/.../presenter/popup/disbursement-plan-research-dialog/disbursement-plan-research-dialog.component.scss` | Create |
| `rmutr-web/.../list20-research/list20-research.component.ts` | Modify — เพิ่ม Output |
| `rmutr-web/.../list20-research/list20-research.component.html` | Modify — เพิ่มคอลัมน์ + ปุ่ม |
| `rmutr-web/.../request-budget-statistics-list.component.ts` | Modify — เพิ่ม Output + relay method |
| `rmutr-web/.../request-budget-statistics-list.component.html` | Modify — bind event บน app-list20-research |
| `rmutr-web/.../request-budget-statistics.container.ts` | Modify — เพิ่ม handler + เปิด dialog |
| `rmutr-web/.../request-budget-statistics.container.html` | Modify — bind event บน list component |
| `rmutr-web/.../request-budget-statistics.module.ts` | Modify — import + declare component |
---
### Task 1: Backend — เพิ่ม field ใน model + migration
**Files:**
- Modify: `rmutr-api/Modules/RequestBudgets/Databases/Models/change_project_research_detail.cs`
- Create: `rmutr-api/Migrations/20260617000002_disbursement_plan_research.cs`
**Interfaces:**
- Produces: PUT `/api/request_budget/change_project_research_detail/s` รองรับ `disbursement_1_amount`, `disbursement_1_date`, `disbursement_2_amount`, `disbursement_2_date`, `disbursement_last_amount`, `disbursement_last_date`
- [ ] **Step 1: เพิ่ม 6 field ใน model**
เปิด `rmutr-api/Modules/RequestBudgets/Databases/Models/change_project_research_detail.cs`
เพิ่ม field ก่อน `[NotMapped]` ท้าย class `change_project_research_detail`:
```csharp
public decimal? disbursement_1_amount { get; set; }
public DateTime? disbursement_1_date { get; set; }
public decimal? disbursement_2_amount { get; set; }
public DateTime? disbursement_2_date { get; set; }
public decimal? disbursement_last_amount { get; set; }
public DateTime? disbursement_last_date { get; set; }
```
ตำแหน่งที่ถูกต้อง: ก่อนบรรทัด `[NotMapped] public string budget_unit_name_th { get; set; }`
- [ ] **Step 2: สร้าง migration file**
สร้างไฟล์ `rmutr-api/Migrations/20260617000002_disbursement_plan_research.cs`:
```csharp
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace rmutr_budget_api.Migrations
{
public partial class disbursement_plan_research : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "disbursement_1_amount",
schema: "BUDGET",
table: "change_project_research_detail",
type: "numeric",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "disbursement_1_date",
schema: "BUDGET",
table: "change_project_research_detail",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "disbursement_2_amount",
schema: "BUDGET",
table: "change_project_research_detail",
type: "numeric",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "disbursement_2_date",
schema: "BUDGET",
table: "change_project_research_detail",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "disbursement_last_amount",
schema: "BUDGET",
table: "change_project_research_detail",
type: "numeric",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "disbursement_last_date",
schema: "BUDGET",
table: "change_project_research_detail",
type: "timestamp with time zone",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(name: "disbursement_1_amount", schema: "BUDGET", table: "change_project_research_detail");
migrationBuilder.DropColumn(name: "disbursement_1_date", schema: "BUDGET", table: "change_project_research_detail");
migrationBuilder.DropColumn(name: "disbursement_2_amount", schema: "BUDGET", table: "change_project_research_detail");
migrationBuilder.DropColumn(name: "disbursement_2_date", schema: "BUDGET", table: "change_project_research_detail");
migrationBuilder.DropColumn(name: "disbursement_last_amount", schema: "BUDGET", table: "change_project_research_detail");
migrationBuilder.DropColumn(name: "disbursement_last_date", schema: "BUDGET", table: "change_project_research_detail");
}
}
}
```
- [ ] **Step 3: build + run migration**
```bash
cd rmutr-api
dotnet build
dotnet ef database update
```
Expected: build สำเร็จ, migration applied ไม่มี error
- [ ] **Step 4: commit**
```bash
git add rmutr-api/Modules/RequestBudgets/Databases/Models/change_project_research_detail.cs \
rmutr-api/Migrations/20260617000002_disbursement_plan_research.cs
git commit -m "feat: add disbursement plan fields to change_project_research_detail"
```
---
### Task 2: Frontend — สร้าง DisbursementPlanResearchDialogComponent
**Files:**
- Create: `rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/popup/disbursement-plan-research-dialog/disbursement-plan-research-dialog.component.ts`
- Create: `rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/popup/disbursement-plan-research-dialog/disbursement-plan-research-dialog.component.html`
- Create: `rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/popup/disbursement-plan-research-dialog/disbursement-plan-research-dialog.component.scss`
**Interfaces:**
- Consumes: `MAT_DIALOG_DATA` = `change_project_research_detail` row (any) ที่มี field: `change_project_research_detail_uid`, `project_name_th`, `disbursement_1_amount`, `disbursement_1_date`, `disbursement_2_amount`, `disbursement_2_date`, `disbursement_last_amount`, `disbursement_last_date`
- Consumes: `ChangeProjectResearchDetailService.updateMany(data[])` → PUT `/api/request_budget/change_project_research_detail/s`
- Produces: `dialogRef.close()` หลัง save สำเร็จ
- [ ] **Step 1: สร้าง TypeScript component**
สร้างไฟล์ `disbursement-plan-research-dialog.component.ts`:
```typescript
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { of } from 'rxjs';
import { filter, concatMap, tap, catchError } from 'rxjs/operators';
import { ChangeProjectResearchDetailService } from 'src/app/core/service/request-budget/change-project-research-detail.service';
import { SweetalertService } from 'src/app/core/service/sweetalert/sweetalert';
@Component({
selector: 'app-disbursement-plan-research-dialog',
templateUrl: './disbursement-plan-research-dialog.component.html',
styleUrls: ['./disbursement-plan-research-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DisbursementPlanResearchDialogComponent implements OnInit {
form: any = {
disbursement_1_amount: null,
disbursement_1_date: null,
disbursement_2_amount: null,
disbursement_2_date: null,
disbursement_last_amount: null,
disbursement_last_date: null,
}
constructor(
public dialogRef: MatDialogRef<DisbursementPlanResearchDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private cdRef: ChangeDetectorRef,
private swSV: SweetalertService,
private changeProjectResearchDetailSV: ChangeProjectResearchDetailService,
) {}
ngOnInit(): void {
if (this.data) {
this.form = {
disbursement_1_amount: this.data.disbursement_1_amount ?? null,
disbursement_1_date: this.data.disbursement_1_date ?? null,
disbursement_2_amount: this.data.disbursement_2_amount ?? null,
disbursement_2_date: this.data.disbursement_2_date ?? null,
disbursement_last_amount: this.data.disbursement_last_amount ?? null,
disbursement_last_date: this.data.disbursement_last_date ?? null,
}
}
this.cdRef.detectChanges()
}
get total(): number {
const a1 = parseFloat(this.form.disbursement_1_amount) || 0
const a2 = parseFloat(this.form.disbursement_2_amount) || 0
const aL = parseFloat(this.form.disbursement_last_amount) || 0
return a1 + a2 + aL
}
cancel(): void {
this.dialogRef.close()
}
save(): void {
const payload = [{
...this.data,
disbursement_1_amount: this.form.disbursement_1_amount,
disbursement_1_date: this.form.disbursement_1_date,
disbursement_2_amount: this.form.disbursement_2_amount,
disbursement_2_date: this.form.disbursement_2_date,
disbursement_last_amount: this.form.disbursement_last_amount,
disbursement_last_date: this.form.disbursement_last_date,
}]
this.swSV.confirmSave().pipe(
filter(x => x.isConfirmed),
concatMap(() => this.changeProjectResearchDetailSV.updateMany(payload).pipe(
catchError(err => {
this.swSV.errText(err?.error?.description || 'บันทึกไม่สำเร็จ')
return of(null)
})
)),
filter(res => res !== null),
tap(() => this.swSV.updateSuccess()),
tap(() => this.dialogRef.close('saved')),
).subscribe()
}
}
```
- [ ] **Step 2: สร้าง HTML template**
สร้างไฟล์ `disbursement-plan-research-dialog.component.html`:
```html
<h2 mat-dialog-title>แผนการเบิกจ่ายเงินเป็นงวด</h2>
<mat-dialog-content>
<div class="project-info mb-3">
<span class="info-label">ชื่อโครงการ:</span>
<span class="info-value">{{ data?.project_name_th }}</span>
</div>
<div class="table-scroll">
<table class="table-excel" style="table-layout:fixed; min-width:700px;">
<colgroup>
<col style="width:130px">
<col style="width:140px">
<col style="width:130px">
<col style="width:140px">
<col style="width:130px">
<col style="width:140px">
<col style="width:140px">
</colgroup>
<thead>
<tr class="tr_class">
<th class="th_class" colspan="6">แผนการเบิกจ่ายเงิน</th>
<th class="th_class" rowspan="3">รวมเบิกจ่าย<br>ทั้งสิ้น (บาท)</th>
</tr>
<tr class="tr_class">
<th class="th_class" colspan="2">งวดที่ 1</th>
<th class="th_class" colspan="2">งวดที่ 2</th>
<th class="th_class" colspan="2">งวดสุดท้าย</th>
</tr>
<tr class="tr_class">
<th class="th_class">จำนวนเงิน (บาท)</th>
<th class="th_class">ว/ด/ป</th>
<th class="th_class">จำนวนเงิน (บาท)</th>
<th class="th_class">ว/ด/ป</th>
<th class="th_class">จำนวนเงิน (บาท)</th>
<th class="th_class">ว/ด/ป</th>
</tr>
</thead>
<tbody>
<tr class="tr_p">
<td class="td_class">
<input type="text" appCurrencyInputMask [(ngModel)]="form.disbursement_1_amount" class="td-input">
</td>
<td class="td_class">
<mat-form-field style="width:100%;">
<input matInput (click)="dp1.open()" readonly [matDatepicker]="dp1" [(ngModel)]="form.disbursement_1_date" />
<mat-datepicker-toggle matSuffix [for]="dp1"></mat-datepicker-toggle>
<mat-datepicker #dp1></mat-datepicker>
</mat-form-field>
</td>
<td class="td_class">
<input type="text" appCurrencyInputMask [(ngModel)]="form.disbursement_2_amount" class="td-input">
</td>
<td class="td_class">
<mat-form-field style="width:100%;">
<input matInput (click)="dp2.open()" readonly [matDatepicker]="dp2" [(ngModel)]="form.disbursement_2_date" />
<mat-datepicker-toggle matSuffix [for]="dp2"></mat-datepicker-toggle>
<mat-datepicker #dp2></mat-datepicker>
</mat-form-field>
</td>
<td class="td_class">
<input type="text" appCurrencyInputMask [(ngModel)]="form.disbursement_last_amount" class="td-input">
</td>
<td class="td_class">
<mat-form-field style="width:100%;">
<input matInput (click)="dpL.open()" readonly [matDatepicker]="dpL" [(ngModel)]="form.disbursement_last_date" />
<mat-datepicker-toggle matSuffix [for]="dpL"></mat-datepicker-toggle>
<mat-datepicker #dpL></mat-datepicker>
</mat-form-field>
</td>
<td class="td_class text-right total-cell">
{{ total | number:'1.2-2' }}
</td>
</tr>
</tbody>
</table>
</div>
<div class="total-summary mt-3">
<span>รวมเบิกจ่ายทั้งสิ้น:</span>
<strong>{{ total | number:'1.2-2' }} บาท</strong>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-stroked-button (click)="cancel()">ยกเลิก</button>
<button mat-raised-button class="buttonColorSave ml-2" (click)="save()">บันทึก</button>
</mat-dialog-actions>
```
- [ ] **Step 3: สร้าง SCSS**
สร้างไฟล์ `disbursement-plan-research-dialog.component.scss`:
```scss
.project-info {
background: #f3f8ff;
border-left: 4px solid #1976d2;
padding: 8px 12px;
border-radius: 0 4px 4px 0;
.info-label { font-size: 12px; color: #666; margin-right: 6px; }
.info-value { font-size: 14px; font-weight: 600; color: #1976d2; }
}
.table-scroll { overflow-x: auto; }
.td-input {
width: 100%;
border: none;
padding: 6px 4px;
text-align: right;
font-size: 13px;
background: transparent;
outline: none;
font-family: inherit;
&:focus { background: #fff8e1; }
}
.total-cell {
background: #fff3e0;
font-weight: 700;
font-size: 14px;
color: #e65100;
}
.total-summary {
text-align: right;
font-size: 14px;
strong { color: #e65100; margin-left: 8px; font-size: 16px; }
}
```
- [ ] **Step 4: commit**
```bash
git add rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/popup/disbursement-plan-research-dialog/
git commit -m "feat: create DisbursementPlanResearchDialogComponent"
```
---
### Task 3: Frontend — ต่อ EventEmitter chain (list → list-wrapper → container)
**Files:**
- Modify: `rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list20-research/list20-research.component.ts`
- Modify: `rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list20-research/list20-research.component.html`
- Modify: `rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/request-budget-statistics-list.component.ts`
- Modify: `rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/request-budget-statistics-list.component.html`
**Interfaces:**
- Produces: `@Output() onDisbursementPlan` emit ค่า `change_project_research_detail` row (any) ทั้ง object
- [ ] **Step 1: เพิ่ม Output ใน list20-research.component.ts**
เปิด `list20-research.component.ts` — เพิ่ม `@Output() onDisbursementPlan` ต่อจาก `@Output() onedit`:
```typescript
@Output() onDisbursementPlan = new EventEmitter<any>();
```
เพิ่ม method ใน class:
```typescript
openDisbursement(val: any) {
this.onDisbursementPlan.emit(val);
}
```
- [ ] **Step 2: เพิ่มคอลัมน์ + ปุ่มใน list20-research.component.html**
**2a) เพิ่ม col ใน colgroup** — เพิ่มก่อน `<col style="width:60px">` (คอลัมน์เครื่องมือเดิม):
```html
<col style="width:80px">
```
**2b) เพิ่ม th "แผนเบิกจ่าย"** ใน header row ที่ 1 ก่อน `<th class="th_class" rowspan="5">เครื่องมือ</th>`:
```html
<th class="th_class" rowspan="5">แผน<br>เบิกจ่าย</th>
```
**2c) เพิ่ม td badge + ปุ่มใน tbody** — ใน block `<ng-container *ngIf="pIdx === 0">` ที่ห่อ td เครื่องมือ เพิ่ม td badge ก่อน td เครื่องมือ:
```html
<td class="td_class text-center"
[attr.rowspan]="(detail.change_project_research_p_details?.length || 1) + 1">
<span *ngIf="detail.disbursement_1_amount"
style="background:#e8f5e9;color:#388e3c;padding:2px 6px;border-radius:10px;font-size:11px;white-space:nowrap;">
✓ กรอกแล้ว
</span>
<span *ngIf="!detail.disbursement_1_amount"
style="background:#f5f5f5;color:#9e9e9e;padding:2px 6px;border-radius:10px;font-size:11px;white-space:nowrap;">
ยังไม่กรอก
</span>
</td>
```
**2d) เพิ่มปุ่ม 💰 ใน td เครื่องมือ** — เพิ่มหลังปุ่ม edit ที่มีอยู่แล้ว:
```html
<button mat-icon-button
[style.color]="detail.disbursement_1_amount ? '#388e3c' : '#81c784'"
(click)="openDisbursement(detail)"
title="แผนการเบิกจ่าย"
style="width:28px;height:28px;line-height:28px;">
<mat-icon style="font-size:18px;width:18px;height:18px;line-height:18px;">account_balance_wallet</mat-icon>
</button>
```
- [ ] **Step 3: เพิ่ม Output ใน request-budget-statistics-list.component.ts**
เปิด `request-budget-statistics-list.component.ts` — เพิ่มต่อจาก `@Output() onEditAgencyReportResearch = new EventEmitter()`:
```typescript
@Output() onDisbursementPlan = new EventEmitter<any>();
```
เพิ่ม relay method ใน class (ต่อจาก `onEditAgencyReportResearch_`):
```typescript
onDisbursementPlan_(val: any) {
this.onDisbursementPlan.emit(val);
}
```
- [ ] **Step 4: bind event ใน request-budget-statistics-list.component.html**
เปิด `request-budget-statistics-list.component.html` หา block:
```html
<app-list20-research
[dataSource]="dataSource20Research"
(onedit)="onEditAgencyReportResearch_($event)"
(onchange)="change($event)"
></app-list20-research>
```
แก้เป็น:
```html
<app-list20-research
[dataSource]="dataSource20Research"
(onedit)="onEditAgencyReportResearch_($event)"
(onDisbursementPlan)="onDisbursementPlan_($event)"
(onchange)="change($event)"
></app-list20-research>
```
- [ ] **Step 5: commit**
```bash
git add rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/
git commit -m "feat: add onDisbursementPlan event chain to list20-research"
```
---
### Task 4: Frontend — Container handler + Module registration
**Files:**
- Modify: `rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/container/request-other-expenses/request-budget-statistics.container.ts`
- Modify: `rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/container/request-other-expenses/request-budget-statistics.container.html`
- Modify: `rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/request-budget-statistics.module.ts`
**Interfaces:**
- Consumes: `onDisbursementPlan` emit จาก `request-budget-statistics-list` (Task 3)
- Consumes: `DisbursementPlanResearchDialogComponent` (Task 2)
- Consumes: `this.dialog.open(...)` pattern เดิมในไฟล์ container
- [ ] **Step 1: เพิ่ม import ใน container.ts**
เปิด `request-budget-statistics.container.ts` — เพิ่ม import ต่อจาก CopyPopupComponent:
```typescript
import { DisbursementPlanResearchDialogComponent } from '../../presenter/popup/disbursement-plan-research-dialog/disbursement-plan-research-dialog.component';
```
- [ ] **Step 2: เพิ่ม method `onDisbursementPlan` ใน container.ts**
เพิ่ม method ใน class ต่อจาก `onEditAgencyReportResearch`:
```typescript
onDisbursementPlan(detail: any): void {
const dialogRef = this.dialog.open(DisbursementPlanResearchDialogComponent, {
width: '820px',
disableClose: true,
data: detail,
});
dialogRef.afterClosed().pipe(
tap((result) => {
if (result === 'saved') {
this.getAll();
this.cdRef.detectChanges();
}
})
).subscribe();
}
```
- [ ] **Step 3: bind event ใน container.html**
เปิด `request-budget-statistics.container.html` — หาบรรทัด:
```
(onEditAgencyReportResearch)="onEditAgencyReportResearch($event)"
```
เพิ่มต่อท้าย (บรรทัดถัดไป):
```html
(onDisbursementPlan)="onDisbursementPlan($event)"
```
- [ ] **Step 4: เพิ่ม import + declare ใน module**
เปิด `request-budget-statistics.module.ts` — เพิ่ม import ต่อจาก `SendApproveTargetPopupComponent`:
```typescript
import { DisbursementPlanResearchDialogComponent } from './presenter/popup/disbursement-plan-research-dialog/disbursement-plan-research-dialog.component';
```
ใน `declarations` array — เพิ่มต่อจาก `SendApproveTargetPopupComponent,`:
```typescript
DisbursementPlanResearchDialogComponent,
```
- [ ] **Step 5: build Angular เพื่อตรวจ compile error**
```bash
cd rmutr-web
ng build --configuration development 2>&1 | tail -20
```
Expected: ไม่มี error
- [ ] **Step 6: commit**
```bash
git add rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/container/ \
rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/request-budget-statistics.module.ts
git commit -m "feat: wire disbursement plan dialog into container and module"
```
---
### Task 5: Manual Verification
- [ ] **Step 1: start dev server**
```bash
cd rmutr-web
ng serve --port 4200
```
- [ ] **Step 2: ไปที่หน้า**
เปิด `http://localhost:4200/app/agency-report-research`
- [ ] **Step 3: ตรวจสอบ list**
- [ ] ตารางมีคอลัมน์ "แผนเบิกจ่าย" แสดง badge "ยังไม่กรอก" สำหรับโครงการที่ยังไม่มีข้อมูล
- [ ] มีไอคอน 💰 (account_balance_wallet) ในคอลัมน์เครื่องมือทุกแถว
- [ ] **Step 4: กรอกข้อมูลใหม่**
- [ ] กดปุ่ม 💰 บนโครงการใดก็ได้
- [ ] Dialog เปิดขึ้น แสดงชื่อโครงการถูกต้อง
- [ ] กรอก: งวด 1 = 50000, วันที่ = 2567-11-01, งวด 2 = 30000, วันที่ = 2568-01-15, งวดสุดท้าย = 20000, วันที่ = 2568-03-31
- [ ] "รวมเบิกจ่ายทั้งสิ้น" แสดง 100,000.00 ทันที (real-time)
- [ ] กด "บันทึก" → confirm dialog → success toast
- [ ] ตาราง reload → badge เปลี่ยนเป็น "✓ กรอกแล้ว"
- [ ] **Step 5: แก้ไขข้อมูล**
- [ ] กดปุ่ม 💰 อีกครั้ง → dialog เปิดพร้อมข้อมูลเดิม
- [ ] แก้ไขงวด 1 เป็น 60000 → รวม = 110,000.00
- [ ] กด "บันทึก" → สำเร็จ
- [ ] verify ใน DB ว่า `disbursement_1_amount = 60000`
- [ ] **Step 6: ตรวจสอบ cancel**
- [ ] กด "ยกเลิก" → dialog ปิด ไม่มี save เกิดขึ้น