# รายงานผล(นอกแผน) — ส่งงานแผน / เมนูตรวจสอบ ## บริบท หน้า "หน่วยงานทำรายงานผล(นอกแผน)" (`/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(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 === 3` → `this.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): ```ts { 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): ```ts { 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`: เพิ่ม `` มิเรอร์ 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 ไม่รองรับ (ดูบริบทด้านบน): ```ts 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 แบบเดียวกัน (กรอง**ออก**) ในบล็อกเดิม: ```ts 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)) } ``` ค้นหา (ปีงบประมาณ/ชื่อโครงการ/แผนงบประมาณ/หน่วยงาน) ใช้ `` เดิมที่ 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()` (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 == 29163` → `router.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 ที่ใช้ร่วมกัน)