Files
rmutr/docs/superpowers/specs/2026-07-21-out-of-plan-report-review-design.md
T
Nut.ไปเรื่อย d24f73d119 docs: add out-of-plan report submit/review design spec
Design for adding a "ส่งงานแผน" submit flow to the out-of-plan report
list plus a new reviewer menu "ตรวจสอบรายงานผล(นอกแผน)", mirroring the
existing check-project-report status_id flow with no backend changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01613NaY9LAvy2wFXEeyE5eR
2026-07-21 22:58:37 +07:00

20 KiB

รายงานผล(นอกแผน) — ส่งงานแผน / เมนูตรวจสอบ

บริบท

หน้า "หน่วยงานทำรายงานผล(นอกแผน)" (/app/agency-out-of-plan/out-of-plan, type: 28) ให้หน่วยงานกรอกรายงานผลโครงการนอกแผนเป็นตารางรายการ (out_of_plan_report_detail) แต่ละแถวเชื่อมกับรายงาน (agency_report, ผ่าน out_of_plan_report_detail_uid) ที่กรอกในฟอร์มแยก (agency-report-form.component.ts, เปิดผ่าน openReport() ที่ agency-out-of-plan-form.component.ts:107-117)

ปัจจุบันไม่มีขั้นตอนส่งงานแผน สำหรับรายงานนอกแผน — ฟอร์มมีแต่ปุ่ม "บันทึก" (save() case 281/282 ที่ agency-report-form.component.ts:4015-4045) ซึ่งไม่แตะ status_id เลย ต่างจากรายงานในแผนปกติที่มี flow ผ่านเมนู "ตรวจสอบรายงานผลโครงการ" (check-project-report, type: 16) อยู่แล้ว:

  • 3 แท็บ กรองด้วย agency_report.status_id: รอตรวจสอบ (1) / ส่งแก้ไข (2) / ตรวจสอบแล้ว (3) — request-budget-statistics-list.component.html:297-330, query ที่ request-budget-statistics.container.ts:401-406
  • ปุ่มตรวจสอบ "ส่งกลับแก้ไข" (save_send() → status 2) และ "ตรวจสอบแล้ว" (save_pass() → status 3) มีอยู่แล้วใน agency-report-form.component.ts:4116-4179 (ใช้ร่วมกันได้ทุก typeUrl เพราะไม่ได้ gate ด้วย typeUrl ภายในเมธอด)
  • Backend endpoint POST /request_budget/agency_report/update_status/{status_id} (rmutr-api/Modules/RequestBudgets/Controllers/AgencyReport.cs:72-87) รับ list ของ t_agency_report แล้วตั้ง status_id ให้ทุกตัว — รองรับทั้ง 1/2/3 อยู่แล้วเพราะ status_id เป็น route parameter ทั่วไป ไม่ผูกกับค่าใดค่าหนึ่ง

เป้าหมายงานนี้คือต่อยอด pattern เดิมที่มีอยู่แล้ว มาสโคปเฉพาะรายงานที่มี out_of_plan_report_detail_uid != null โดยไม่ต้องแก้ backend

ข้อจำกัดสำคัญ: filter mechanism ทั่วไปของ BaseUidController/EntityUidService (rmutr-api/Databases/Features/EntityUidService.cs:125-128) รองรับแค่ equality/substring ต่อ field เท่านั้น ไม่รองรับ "field IS NOT NULL" ผ่าน query string ดังนั้นการแยกรายงานนอกแผนออกจากรายงานในแผนต้องกรองฝั่ง client (Angular) หลัง fetch — ตรงกับ pattern ที่ agency-out-of-plan-form.component.ts:54-94 ทำอยู่แล้วสำหรับ field เดียวกันนี้ และตรงกับ pattern การกรอง client-side ด้วย map(data => data.filter(...)) ที่มีอยู่แล้วหลายจุดใน request-budget-statistics.container.ts (เช่น typeUrl == 14/15/20/26/27)

ขอบเขต

  • ฝั่งหน่วยงาน: หน้า agency-out-of-plan/out-of-plan (list) + ฟอร์มรายงาน (agency-report-form.component.ts เฉพาะ typeUrl ที่เกี่ยวกับนอกแผน)
  • เมนูใหม่: "ตรวจสอบรายงานผล(นอกแผน)" — list 3 แท็บ + routing + menu entry
  • ป้าย "โครงการ" / "นอกแผน" บนหัวฟอร์มรายงาน (ทุก typeUrl ที่ใช้ฟอร์มนี้ ไม่ใช่แค่นอกแผน) และบนเมนูใหม่
  • ไม่แตะ UI/เมนู check-project-report เดิม ไม่แตะ backend ไม่แตะ flow ของรายงานในแผนปกติ — ยกเว้น เพิ่ม client-side filter บรรทัดเดียวใน typeUrl == 16 query (container เดียวกับที่แก้ในงานนี้อยู่แล้ว) เพื่อไม่ให้รายงานนอกแผนไปโผล่ซ้ำในเมนูเดิม (ดูเหตุผลข้อ 7)

การไหลของสถานะ (บน agency_report.status_id)

status_id ป้าย ใครทำ / เกิดอะไรขึ้น
0 / null ร่าง หน่วยงานกรอก/แก้ไขได้ปกติ (ฟอร์ม enable)
1 รอตรวจสอบ หลังกด "ส่งงานแผน" — ฟอร์มล็อก (form.disable()) จนกว่าจะถูกส่งกลับ
2 ส่งแก้ไข งานแผนกด "ส่งกลับแก้ไข" (save_send()) — ฟอร์มปลดล็อกให้หน่วยงานแก้ไขอีกครั้ง
3 ตรวจสอบแล้ว งานแผนกด "ตรวจสอบแล้ว" (save_pass()) — จบ flow, ฟอร์มยังคงล็อก

การเปลี่ยนแปลงที่ต้องทำ

1. ฝั่งหน่วยงาน — list agency-out-of-plan-form.component.ts / .html

  • เพิ่ม selection = new SelectionModel<any>(true, []) (import @angular/cdk/collections) ตาม pattern list-between-year
  • เพิ่มคอลัมน์ checkbox หน้าสุด: เช็คได้เฉพาะแถวที่ _agency_report_uid ไม่ null (กรอกรายงานแล้ว) และ status_id เป็น 0/null เท่านั้น (ยังไม่เคยส่ง) แถวอื่น disable checkbox ไว้ล่วงหน้า — ต้องดึง status_id ของ agency_report มาเก็บใน allDetails/details ตอน load() ด้วย (ปัจจุบันเก็บแค่ _agency_report_uid ที่บรรทัด 89 — เพิ่ม _status_id: agencyByDetail.get(uid)?.status_id โดยเปลี่ยน map จาก uid string เป็น object {uid, status_id})
  • เพิ่มคอลัมน์/badge สถานะแสดง "ร่าง"/"รอตรวจสอบ"/"ส่งแก้ไข"/"ตรวจสอบแล้ว" ตามตาราง state flow ด้านบน (ใช้ _status_id ที่เพิ่มมา)
  • เพิ่มปุ่ม toolbar "ส่งงานแผน": disabled เมื่อ selection.selected.length === 0, คลิกแล้ว confirm ผ่าน swSV.confirmSave() (pattern เดียวกับที่อื่นในไฟล์นี้) แล้วเรียก agencyReportSV.updateStatusAgency(selection.selected.map(s => ({agency_report_uid: s._agency_report_uid}))) (service method ที่มีอยู่แล้ว agency-report.service.ts ยิง POST update_status/1) สำเร็จแล้ว load() ใหม่ + เคลียร์ selection

2. ฝั่งหน่วยงาน — ฟอร์ม agency-report-form.component.ts (typeUrl 281/282)

  • ตอนโหลด (ngOnInit, ภายใน branch typeUrl == 282 ที่บรรทัด 527-559 — branch 281 เป็นรายงานใหม่ ไม่มีปัญหานี้เพราะ status_id ถูก set เป็น 0 เสมอที่บรรทัด 512): หลัง patchValue(x) เช็คถ้า x.status_id === 1 || x.status_id === 3this.form.disable() และตั้ง flag isLocked = true (component property ใหม่)
  • Template: ถ้า isLocked ซ่อนปุ่ม "บันทึก" ปกติ (ฟิลด์ทั้งหมด disable อยู่แล้วจาก form.disable())

3. ป้าย "โครงการ" / "นอกแผน" บนฟอร์ม

  • agency-report-form.component.html หัวฟอร์ม: เพิ่ม badge/chip อ่านจาก form.value.out_of_plan_report_detail_uid — มีค่า → "นอกแผน", ไม่มี (null) → "โครงการ" ใช้ได้กับทุก typeUrl เพราะ field นี้มีอยู่แล้วใน agency_report (จาก migration 20260624000002_AddOutOfPlanLinkToAgencyReport.cs) และฟอร์มนี้ใช้ร่วมกันทั้งสองประเภทรายงาน

4. Routing ใหม่ (app-routing.module.ts)

เพิ่มต่อจากกลุ่ม route เดิมของ out-of-plan (ใกล้บรรทัด 2945-2971) และของ check-project-report review (ใกล้บรรทัด 2991-3016):

{
  path: 'check-project-report-out-of-plan',
  loadChildren: () => import('.../request-budget-statistics.module').then(m => m.RequestBudgetStatisticsModule),
  data: { menuName: `การบริหารและรายงานผล${seperation}ตรวจสอบรายงานผล(นอกแผน)`, type: 29 }
},
{
  path: 'agency-report-out-of-plan-edit-wait',   // เปิดจากแท็บ "รอตรวจสอบ"
  data: { ..., type: 29161 }
},
{
  path: 'agency-report-out-of-plan-edit-send',   // เปิดจากแท็บ "ส่งแก้ไข"
  data: { ..., type: 29162 }
},
{
  path: 'agency-report-out-of-plan-edit-pass',   // เปิดจากแท็บ "ตรวจสอบแล้ว"
  data: { ..., type: 29163 }
},

(เลข type 29/29161/29162/29163 ยืนยันแล้วว่าไม่ชนกับ type ที่มีอยู่เดิมทั้งหมดใน app-routing.module.ts)

5. Menu entry (navigator.ts)

เพิ่มต่อจาก entry agency-report-out-of-plan (บรรทัด 1111-1117):

{
  id: 'check-project-report-out-of-plan',
  code: 'administer-011-007-3',
  title: 'ตรวจสอบรายงานผล(นอกแผน)',
  type: 'basic',
  icon: 'fiber_manual_record',
  link: '/app/check-project-report-out-of-plan',
},

6. List 3 แท็บใหม่ (มิเรอร์ list16-1/2/3)

  • สร้าง component ใหม่ list29-1, list29-2, list29-3 (copy โครงสร้างจาก list16-1/2/3 รวม SelectionModel/BaseList pattern เดิม) — ต่างกันแค่ edit(val) navigate ไปที่ route ใหม่ตามข้อ 4 (agency-report-out-of-plan-edit-wait/edit-report/:uid เป็นต้น) และแสดง badge "นอกแผน" กำกับทุกแถว (แม้ในลิสต์นี้จะเป็นนอกแผนทั้งหมดอยู่แล้ว — ใส่เพื่อความสอดคล้องกับหัวฟอร์ม)
  • request-budget-statistics-list.component.html: เพิ่ม <ng-container *ngIf="typeUrl == 29"> มิเรอร์ block ของ typeUrl == 16 (บรรทัด 297-330) แต่ผูกกับ dataSource29_1/2/3 และ component app-list29-1/2/3

7. Query data (request-budget-statistics.container.ts)

เพิ่ม branch ใหม่ต่อจาก typeUrl == 16 (บรรทัด 401-406) — filter "not null" ทำฝั่ง client เพราะ backend ไม่รองรับ (ดูบริบทด้านบน):

else if(this.typeUrl == 29){
  const filterOutOfPlan = (data: any[]) => (data || []).filter(d => !!d.out_of_plan_report_detail_uid)
  this.dataSource29_1$ = this.AgencyReportSV.queryString(`?status_id=1&${queryStr}`).pipe(map(filterOutOfPlan))
  this.dataSource29_2$ = this.AgencyReportSV.queryString(`?status_id=2&${queryStr}`).pipe(map(filterOutOfPlan))
  this.dataSource29_3$ = this.AgencyReportSV.queryString(`?status_id=3&${queryStr}`).pipe(map(filterOutOfPlan))
}

ต้องแก้ควบคู่กัน: query ของ typeUrl == 16 (บรรทัด 401-406) ปัจจุบันไม่กรอง out_of_plan_report_detail_uid เลย — เพราะ agency_report เป็นตารางเดียวกันทั้งรายงานในแผนและนอกแผน (แยกกันแค่ field นี้) เมื่อรายงานนอกแผนถูก "ส่งงานแผน" (status_id=1) มันจะตรงเงื่อนไข query เดิมของ check-project-report ด้วยเช่นกัน ทำให้ไปโผล่ซ้ำสองเมนู ต้องเพิ่ม client-side filter แบบเดียวกัน (กรองออก) ในบล็อกเดิม:

else if(this.typeUrl == 16){
  const excludeOutOfPlan = (data: any[]) => (data || []).filter(d => !d.out_of_plan_report_detail_uid)
  this.dataSource16_1$ = this.AgencyReportSV.queryString(`?status_id=1&${queryStr}`).pipe(map(excludeOutOfPlan))
  this.dataSource16_2$ = this.AgencyReportSV.queryString(`?status_id=2&${queryStr}`).pipe(map(excludeOutOfPlan))
  this.dataSource16_3$ = this.AgencyReportSV.queryString(`?status_id=3&${queryStr}`).pipe(map(excludeOutOfPlan))
}

ค้นหา (ปีงบประมาณ/ชื่อโครงการ/แผนงบประมาณ/หน่วยงาน) ใช้ <app-request-budget-statistics-search> เดิมที่ container ผูกไว้แล้วแบบไม่ขึ้นกับ typeUrl (request-budget-statistics.container.html:1-12) — ไม่ต้องเพิ่ม UI ค้นหาใหม่ field ที่มีอยู่แล้ว (request-budget-statistics.search.component.ts:26-32) ครอบคลุมสิ่งที่ผู้ใช้ขอ (ปีงบประมาณ, ชื่อโครงการ) และเกินไปอีก (แผนงบประมาณ, หน่วยงานรับผิดชอบ)

ต้องเพิ่ม dataSource29_1$/29_2$/29_3$ = new Observable<any>() (properties, มิเรอร์บรรทัด 75-77) และค่าเริ่มต้นตอน getAll() (มิเรอร์บรรทัด 196-198)

8. ปุ่มตรวจสอบในฟอร์ม (agency-report-form.component.html)

ปุ่ม "ส่งกลับแก้ไข"/"ตรวจสอบแล้ว" มีอยู่แล้ว gate ด้วย *ngIf="typeUrl == 202161" (ตำแหน่งใกล้บรรทัด 435-442) — เพิ่ม || typeUrl == 29161 เข้าไปในเงื่อนไขเดิม (ใช้เมธอด save_send()/save_pass() เดิม ไม่ต้อง fork ใหม่)

9. close() (agency-report-form.component.ts:3811-3825)

เพิ่ม branch: typeUrl == 29161 || typeUrl == 29162 || typeUrl == 29163router.navigate(['app/check-project-report-out-of-plan'])

Error handling

  • กดส่งงานแผนโดยไม่เลือกอะไร: ปุ่ม disabled ไว้แล้ว
  • API update_status/save_send/save_pass ล้มเหลว: error ผ่าน pattern Swal.fire(err.error.description, '', 'error') เดิมของไฟล์ ไม่ rollback selection
  • guard การเลือกแถวส่งซ้ำเป็น client-side เท่านั้น (checkbox disable ไว้ล่วงหน้า) — ไม่มี guard ฝั่ง backend เพิ่มเติม สอดคล้องกับ pattern เดิมของโปรเจกต์ (ดู between-year-draft-send-design.md)

Testing (manual/browser)

  1. หน่วยงานกรอกรายงานนอกแผนใหม่ → บันทึก → เห็นสถานะ "ร่าง" ในลิสต์ ยังแก้ไข/ลบได้
  2. ติ๊ก checkbox แถวร่างที่มีรายงานแล้ว 1-2 แถว → กด "ส่งงานแผน" → สถานะเปลี่ยนเป็น "รอตรวจสอบ" → เปิดฟอร์มดูอีกครั้ง ยืนยัน field ทั้งหมด disable
  3. ยืนยัน checkbox ของแถวที่ส่งแล้วไม่สามารถติ๊กเลือกซ้ำได้ และแถวที่ยังไม่มีรายงาน (_agency_report_uid null) ก็เลือกไม่ได้เช่นกัน
  4. เข้าเมนูใหม่ "ตรวจสอบรายงานผล(นอกแผน)" ในฐานะงานแผน → เห็นรายการที่ส่งมาในแท็บ "รอตรวจสอบ" พร้อม badge "นอกแผน" → ค้นหาด้วยปีงบประมาณ และค้นหาด้วยชื่อโครงการ ยืนยันกรองถูกต้อง
  5. เปิดรายงานจากแท็บ "รอตรวจสอบ" → กด "ส่งกลับแก้ไข" → ยืนยันสถานะเป็น "ส่งแก้ไข" และกลับไปเมนูตรวจสอบนอกแผน (ไม่ใช่เมนู check-project-report เดิม)
  6. หน่วยงานเปิดรายงานที่ถูกส่งกลับ (status 2) → ยืนยันฟอร์มแก้ไขได้อีกครั้ง → แก้ไข+ส่งงานแผนใหม่ (ทำซ้ำข้อ 2)
  7. งานแผนกด "ตรวจสอบแล้ว" ในแท็บ "รอตรวจสอบ" → ยืนยันสถานะเป็น "ตรวจสอบแล้ว" ย้ายไปแท็บ "ตรวจสอบแล้ว"
  8. ยืนยันรายการนอกแผน (ทุกสถานะ) ไม่ ไปโผล่ในเมนู check-project-report เดิม หลังเพิ่ม client-side filter ตามข้อ 7 (ก่อนแก้จะโผล่ซ้ำแน่นอน เพราะ query เดิมกรองแค่ status_id) และยืนยันว่ารายงานในแผนปกติยังโผล่ใน check-project-report ตามเดิมไม่หายไป (regression check)
  9. เปิดรายงานในแผนปกติ (ไม่ใช่นอกแผน) ผ่านเมนูเดิม → ยืนยันหัวฟอร์มขึ้นป้าย "โครงการ" ถูกต้อง (regression check สำหรับข้อ 3 ที่แก้ template ที่ใช้ร่วมกัน)