26 KiB
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:
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:
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
cd rmutr-api
dotnet build
dotnet ef database update
Expected: build สำเร็จ, migration applied ไม่มี error
- Step 4: commit
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_detailrow (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:
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:
<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:
.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
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() onDisbursementPlanemit ค่าchange_project_research_detailrow (any) ทั้ง object -
Step 1: เพิ่ม Output ใน list20-research.component.ts
เปิด list20-research.component.ts — เพิ่ม @Output() onDisbursementPlan ต่อจาก @Output() onedit:
@Output() onDisbursementPlan = new EventEmitter<any>();
เพิ่ม method ใน class:
openDisbursement(val: any) {
this.onDisbursementPlan.emit(val);
}
- Step 2: เพิ่มคอลัมน์ + ปุ่มใน list20-research.component.html
2a) เพิ่ม col ใน colgroup — เพิ่มก่อน <col style="width:60px"> (คอลัมน์เครื่องมือเดิม):
<col style="width:80px">
2b) เพิ่ม th "แผนเบิกจ่าย" ใน header row ที่ 1 ก่อน <th class="th_class" rowspan="5">เครื่องมือ</th>:
<th class="th_class" rowspan="5">แผน<br>เบิกจ่าย</th>
2c) เพิ่ม td badge + ปุ่มใน tbody — ใน block <ng-container *ngIf="pIdx === 0"> ที่ห่อ td เครื่องมือ เพิ่ม td badge ก่อน td เครื่องมือ:
<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 ที่มีอยู่แล้ว:
<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():
@Output() onDisbursementPlan = new EventEmitter<any>();
เพิ่ม relay method ใน class (ต่อจาก onEditAgencyReportResearch_):
onDisbursementPlan_(val: any) {
this.onDisbursementPlan.emit(val);
}
- Step 4: bind event ใน request-budget-statistics-list.component.html
เปิด request-budget-statistics-list.component.html หา block:
<app-list20-research
[dataSource]="dataSource20Research"
(onedit)="onEditAgencyReportResearch_($event)"
(onchange)="change($event)"
></app-list20-research>
แก้เป็น:
<app-list20-research
[dataSource]="dataSource20Research"
(onedit)="onEditAgencyReportResearch_($event)"
(onDisbursementPlan)="onDisbursementPlan_($event)"
(onchange)="change($event)"
></app-list20-research>
- Step 5: commit
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:
onDisbursementPlanemit จาก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:
import { DisbursementPlanResearchDialogComponent } from '../../presenter/popup/disbursement-plan-research-dialog/disbursement-plan-research-dialog.component';
- Step 2: เพิ่ม method
onDisbursementPlanใน container.ts
เพิ่ม method ใน class ต่อจาก onEditAgencyReportResearch:
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)"
เพิ่มต่อท้าย (บรรทัดถัดไป):
(onDisbursementPlan)="onDisbursementPlan($event)"
- Step 4: เพิ่ม import + declare ใน module
เปิด request-budget-statistics.module.ts — เพิ่ม import ต่อจาก SendApproveTargetPopupComponent:
import { DisbursementPlanResearchDialogComponent } from './presenter/popup/disbursement-plan-research-dialog/disbursement-plan-research-dialog.component';
ใน declarations array — เพิ่มต่อจาก SendApproveTargetPopupComponent,:
DisbursementPlanResearchDialogComponent,
- Step 5: build Angular เพื่อตรวจ compile error
cd rmutr-web
ng build --configuration development 2>&1 | tail -20
Expected: ไม่มี error
- Step 6: commit
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
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 เกิดขึ้น