# 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( name: "disbursement_1_amount", schema: "BUDGET", table: "change_project_research_detail", type: "numeric", nullable: true); migrationBuilder.AddColumn( name: "disbursement_1_date", schema: "BUDGET", table: "change_project_research_detail", type: "timestamp with time zone", nullable: true); migrationBuilder.AddColumn( name: "disbursement_2_amount", schema: "BUDGET", table: "change_project_research_detail", type: "numeric", nullable: true); migrationBuilder.AddColumn( name: "disbursement_2_date", schema: "BUDGET", table: "change_project_research_detail", type: "timestamp with time zone", nullable: true); migrationBuilder.AddColumn( name: "disbursement_last_amount", schema: "BUDGET", table: "change_project_research_detail", type: "numeric", nullable: true); migrationBuilder.AddColumn( 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, @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

แผนการเบิกจ่ายเงินเป็นงวด

ชื่อโครงการ: {{ data?.project_name_th }}
แผนการเบิกจ่ายเงิน รวมเบิกจ่าย
ทั้งสิ้น (บาท)
งวดที่ 1 งวดที่ 2 งวดสุดท้าย
จำนวนเงิน (บาท) ว/ด/ป จำนวนเงิน (บาท) ว/ด/ป จำนวนเงิน (บาท) ว/ด/ป
{{ total | number:'1.2-2' }}
รวมเบิกจ่ายทั้งสิ้น: {{ total | number:'1.2-2' }} บาท
``` - [ ] **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(); ``` เพิ่ม method ใน class: ```typescript openDisbursement(val: any) { this.onDisbursementPlan.emit(val); } ``` - [ ] **Step 2: เพิ่มคอลัมน์ + ปุ่มใน list20-research.component.html** **2a) เพิ่ม col ใน colgroup** — เพิ่มก่อน `` (คอลัมน์เครื่องมือเดิม): ```html ``` **2b) เพิ่ม th "แผนเบิกจ่าย"** ใน header row ที่ 1 ก่อน `เครื่องมือ`: ```html แผน
เบิกจ่าย ``` **2c) เพิ่ม td badge + ปุ่มใน tbody** — ใน block `` ที่ห่อ td เครื่องมือ เพิ่ม td badge ก่อน td เครื่องมือ: ```html ✓ กรอกแล้ว ยังไม่กรอก ``` **2d) เพิ่มปุ่ม 💰 ใน td เครื่องมือ** — เพิ่มหลังปุ่ม edit ที่มีอยู่แล้ว: ```html ``` - [ ] **Step 3: เพิ่ม Output ใน request-budget-statistics-list.component.ts** เปิด `request-budget-statistics-list.component.ts` — เพิ่มต่อจาก `@Output() onEditAgencyReportResearch = new EventEmitter()`: ```typescript @Output() onDisbursementPlan = new EventEmitter(); ``` เพิ่ม 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 ``` แก้เป็น: ```html ``` - [ ] **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 เกิดขึ้น