From 2b01bec26219d8c6d449dd5f65d0b2dffa045a84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nut=2E=E0=B9=84=E0=B8=9B=E0=B9=80=E0=B8=A3=E0=B8=B7?= =?UTF-8?q?=E0=B9=88=E0=B8=AD=E0=B8=A2?= Date: Wed, 17 Jun 2026 12:08:13 +0700 Subject: [PATCH] docs: add disbursement plan research implementation plan --- .../2026-06-17-disbursement-plan-research.md | 641 ++++++++++++++++++ 1 file changed, 641 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-17-disbursement-plan-research.md diff --git a/docs/superpowers/plans/2026-06-17-disbursement-plan-research.md b/docs/superpowers/plans/2026-06-17-disbursement-plan-research.md new file mode 100644 index 0000000..77b2084 --- /dev/null +++ b/docs/superpowers/plans/2026-06-17-disbursement-plan-research.md @@ -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( + 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 เกิดขึ้น