From da58e1aac8efd401d47cecc1835ec513d01fb049 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, 5 Aug 2026 11:43:08 +0700 Subject: [PATCH] docs: add design spec for native income-budget-report (replacing SyncFusion) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design for replacing the SyncFusion spreadsheet on income-budget-report-income1 and its 6 sibling menus with a native Angular tree component. Covers the 3 structurally-distinct document groups found in the real production files (draft rounds 1-3, allocation/adjustment 4-6, material-cost matrix type 10), the shift from source-locked-vs-editable data (ร.2/ร.4/ร.5/ร.6 as read-only source, only committee-adjusted amounts editable), and the new income_budget_report/income_budget_report_line data model replacing the .xlsx-blob storage. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_011R9j5gaw2qsoh9ypWQeXxi --- ...8-05-income-budget-report-native-design.md | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-05-income-budget-report-native-design.md diff --git a/docs/superpowers/specs/2026-08-05-income-budget-report-native-design.md b/docs/superpowers/specs/2026-08-05-income-budget-report-native-design.md new file mode 100644 index 0000000..c516c32 --- /dev/null +++ b/docs/superpowers/specs/2026-08-05-income-budget-report-native-design.md @@ -0,0 +1,147 @@ +# แทนที่ SyncFusion Spreadsheet ในรายงานงบประมาณเงินรายได้ ด้วยหน้าเว็บ native + +## บริบท + +หน้า `/app/income-budget-report-income1/add` (และเมนูพี่น้องอีก 6 เมนู) ใช้ SyncFusion Spreadsheet (`@syncfusion/ej2-angular-spreadsheet`) เปิดไฟล์ `.xlsx` ที่ backend generate มาให้ ผู้ใช้แก้ไขในสเปรดชีตที่มองเห็นได้ทุกเซลล์อิสระ แล้ว "บันทึก" กลับเป็นไฟล์ `.xlsx` ใหม่เก็บไว้เป็นหลักฐาน (`revenue_draft_committee_file`) + +ปัญหาที่ต้องแก้: +1. UX สเปรดชีตไม่เหมาะกับผู้ใช้ที่ไม่คุ้น Excel, formula/สูตรผูกกับ "สีเซลล์" ที่ซ่อนอยู่ในไฟล์ ตรวจสอบ/แก้ไขยาก +2. **ไม่มีการล็อกข้อมูล** — ตำแหน่ง/จำนวนอัตรา/รายการครุภัณฑ์ที่ควรมาจากฟอร์มต้นทาง (ร.2/ร.4/ร.5/ร.6) กลับแก้ไขได้อิสระในหน้านี้เหมือนตัวเลขเงิน เพราะสเปรดชีตไม่แยกเซลล์ locked/unlocked + +7 เมนูที่ใช้ component เดียวกัน (`income-budget-report.component.ts`, แยกพฤติกรรมด้วย route data `type`) แท้จริงแบ่งเป็น **3 กลุ่มเอกสารที่โครงสร้างต่างกัน** (ยืนยันด้วยการดาวน์โหลดไฟล์จริงจาก production มาตรวจ ไม่ใช่การเดา): + +| กลุ่ม | type_id | ชื่อเมนู | โครงสร้างไฟล์จริง | +|---|---|---|---| +| **A — ร่างเงินรายได้** | 1→2→3 | ร่างเงินรายได้ เพื่อประชุม คกก.ร่างเงินรายได้ / คกก.การเงิน / สภา | 1 ชีท ("Page1"), tree เดียว รายการบุคลากร→งบบุคลากร/งบดำเนินงาน→แผนงาน→ผลผลิต→งบเงินอุดหนุน/งบลงทุน/งบรายจ่ายอื่นๆ ยืนยันไฟล์จริงทั้ง 3 type ตรงกัน | +| **B — จัดสรร/ปรับแผนเงินรายได้** | 4→5→6 | จัดสรรงบประมาณเงินรายได้ / ปรับแผนเงินรายได้ (ประมาณการรายจ่าย) / ปรับแผนเงินรายได้ | workbook 5 ชีท แยกตาม "ผลผลิต" + สรุป มีช่องกรอกอิสระ ("ตำแหน่ง......(ระบุ)") แต่ **ใช้ logic คำนวณผลรวมแบบเดียวกับกลุ่ม A** (สีเซลล์ + สูตร SUM รูปแบบเดียวกัน) ยืนยันไฟล์จริง type 4,5 (type 6 ไม่มีข้อมูลจริงในระบบ อนุมานจาก chain ต่อจาก 5) | +| **C — วิเคราะห์ค่าวัสดุการศึกษา** | 10 | ตารางและรายงานคำนวณค่าวัสดุการศึกษา | matrix แยกตามพื้นที่วิทยาเขต ไม่ใช่ tree เลย | + +**Root cause ของปัญหาที่ 2:** ตำแหน่ง/รายการที่ควรมาจากฟอร์มต้นทางจริง ๆ **ก็ดึงมาจากต้นทางอยู่แล้วในทางเทคนิค** — +- ร.2 (คำชี้แจงงบบุคลากร) เก็บใน `personnel_statement` + `personnel_statement_detail`/`_2` (`rmutr-api/Modules/ReportSalary/Databases/Models/`) ดึงผ่าน `GET /api/report/personnel/hr/budget_expenditure_report_from_revenue_v3/{view}` (`Personnel.cs:2195`) +- ร.4 (งบครุภัณฑ์) = `invest_asset_request_information`, ร.5 (งบที่ดิน/สิ่งก่อสร้าง) = `invest_construct_request_information`, ร.6 (เงินอุดหนุน) = `request_budget_income` — ทั้งสามดึงรวมกันแล้วผ่าน `GET /api/budget_progress/budget_progress/summary_expense/{budget_year_uid}/{faculty_uid}` (`BudgetProgress.cs:409`) ซึ่งมี tree renderer ต้นแบบอยู่แล้วที่ `manage-budget-request-expense-list.component.html` + +แต่เพราะ SyncFusion เปิดให้แก้ทุกเซลล์อิสระ ข้อมูลที่ดึงมาจึง **ไม่ถูกล็อก** อีกต่อไปหลังโหลดเข้าสเปรดชีต — นี่คือรากของปัญหา ไม่ใช่การขาดต้นทางข้อมูล + +## ขอบเขต + +- แทนที่ SyncFusion Spreadsheet ด้วย native Angular tree component สำหรับ **กลุ่ม A และ B** (ใช้ "เครื่องยนต์" เดียวกัน — ดูเหตุผลในหัวข้อสถาปัตยกรรม) +- กลุ่ม C (type 10) แยก component matrix ต่างหาก ใช้ style/token ชุดเดียวกันเพื่อความสม่ำเสมอ แต่ไม่ใช้ tree schema/engine เดียวกับ A/B +- เปลี่ยนการเก็บข้อมูลจาก "ไฟล์ .xlsx เป็นหลักฐาน" → "ข้อมูลโครงสร้างใน DB เป็นหลัก, .xlsx generate ตอนดาวน์โหลดเท่านั้น" +- Routing เดิมทั้ง 7 เมนูไม่เปลี่ยน (`income-budget-report-income1/2/3`, `allocate-income-budget`, `income-expense-estimates(-02)`, `educational-materials`) — เปลี่ยนแค่เนื้อในของ component ที่ render +- **ไม่แตะ** ฟอร์มต้นทาง ร.2/ร.4/ร.5/ร.6 เอง (`estimated-income-form`, `statement-invest-asset`, `statement-invest-construct`, `statement-request`) — หน้านี้อ่านข้อมูลจากตารางเหล่านั้นเท่านั้น ไม่เขียนกลับ + +## สถาปัตยกรรม + +``` +ต้นทางข้อมูล (อ่านอย่างเดียว) หน้ารายงานใหม่ (native web) +───────────────────────── ────────────────────────── +ร.2 personnel_statement ──┐ +ร.4 invest_asset_request_info ──┤ Backend: endpoint รวบรวมใหม่ +ร.5 invest_construct_request_info ──┼───▶ (ต่อยอด summary_expense เดิม +ร.6 request_budget_income ──┘ + เพิ่ม ร.2) + │ + ▼ + income_budget_report (ใหม่) + + income_budget_report_line + │ + ▼ + Angular: tree component ใช้ร่วม + กลุ่ม A (1 ต้นไม้ใหญ่) + กลุ่ม B (5 แท็บ) +``` + +หน้ารายงานนี้เปลี่ยนบทบาทจาก "ฟอร์มกรอกข้อมูล" เป็น **"หน้ารวบรวม+ทบทวนตัวเลข"** — ตำแหน่ง/รายการครุภัณฑ์/โครงการ แสดง read-only จากฟอร์มต้นทาง ส่วนที่แก้ไขได้จริงคือ "จำนวนเงินที่คณะกรรมการปรับ" ต่อรายการ เก็บแยกเป็น snapshot ของตัวเอง ไม่เขียนทับข้อมูลต้นทาง + +โครงสร้าง tree (รายการบุคลากร→งบบุคลากร→...) เป็น **config คงที่ต่อ `type_id`** (ไฟล์ TypeScript ฝั่ง frontend + ค่าคู่กันฝั่ง backend สำหรับคำนวณตอน export) ไม่ใช่ตารางใน DB — เพราะเป็นแบบฟอร์มราชการที่แทบไม่เปลี่ยนโครงสร้าง ถ้าในอนาคตต้องการให้แก้ schema ผ่าน UI ได้ ค่อยย้ายเป็น DB-driven ทีหลัง (YAGNI) + +## Data Model + +``` +income_budget_report income_budget_report_line +────────────────────── ────────────────────────── +income_budget_report_uid (PK) income_budget_report_line_uid (PK) +type_id (1-6, 10) income_budget_report_uid (FK) +budget_year_uid node_key เช่น "pb-temp-old", "out1-invest-equip" +faculty_uid, budget_location_uid source_type: personnel_detail | personnel_detail_2 | +sector_name_th invest_asset | invest_construct | +parent_uid ← report รอบก่อนหน้า (1→2→3, 4→5→6) request_budget_income | manual +status_id source_uid (uid ต้นทาง, null = รายการพิมพ์เอง) ++ audit fields มาตรฐาน (base_table) label_th (snapshot ชื่อรายการตอนดึงมา) + amount ← แก้ไขได้ที่นี่เท่านั้น + original_amount (ค่าตอนดึงมา, ไว้เทียบ/audit) + sequence_no +``` + +**การสร้าง/ต่อรอบ:** +- **รอบแรก (type 1, 4):** backend query ร.2/4/5/6 ตาม `faculty_uid` + `budget_year_uid` (ต่อยอด `summary_expense` + `budget_expenditure_report_from_revenue_v3`) → generate `income_budget_report_line` ให้อัตโนมัติ, `amount = original_amount` = ค่าจากต้นทาง, จับคู่ `node_key` ตาม config ของ type นั้น +- **ต่อรอบ (type 2,3 / 5,6):** **clone** header+lines จากรอบก่อนหน้าตรงๆ (ตามที่ยืนยัน) — ไม่ query ต้นทางซ้ำ เพราะรอบถัดไปคือ "แก้ต่อจากมติที่ประชุมรอบก่อน" ไม่ใช่ดึงข้อมูลสดใหม่ ถ้าต้นทาง (ร.2/4/5/6) ถูกแก้ไขหลังจาก snapshot ไปแล้ว **จะไม่ไหลย้อนกลับมาอัตโนมัติ** — เป็นการตัดสินใจโดยตั้งใจเพื่อความนิ่งของมติที่ประชุมแต่ละรอบ ต้องมี UI แจ้งผู้ใช้ให้ชัดเจน +- **รายการที่ `source_uid = null`** (เช่น "อัตราใหม่" ที่ยังไม่มีใน ร.2 จริง, หรือช่องกรอกอิสระ "ระบุ..." ในกลุ่ม B) → แก้ไขได้ทั้งชื่อและจำนวนเงิน +- **รายการที่มี `source_uid`** → ชื่อ/ตำแหน่ง/จำนวนอัตรา **ล็อกเป็น read-only** แก้ได้แค่ `amount` + +กลุ่ม C (type 10) โครงสร้างเป็น matrix ไม่ใช่ tree — ใช้ตารางแยก `material_cost_report` เรียบง่ายกว่า ไม่ผูกกับ schema ข้างต้น (รายละเอียดโครงสร้างกลุ่ม C จะออกแบบในรอบถัดไปเมื่อเริ่มลงมือ เพราะเป็น sub-project ที่ independent จาก A/B) + +## Backend API (เพิ่มใหม่) + +ต่อยอด pattern `BaseUidController` มาตรฐานของระบบ: + +``` +GET /api/setting/income_budget_report/{uid} → โหลด report + lines (BaseUidController มาตรฐาน) +POST /api/setting/income_budget_report/create_draft → สร้างรอบแรก (type 1,4): query ร.2/4/5/6 + ตาม faculty+ปี → generate lines +POST /api/setting/income_budget_report/{uid}/clone_next_round → สร้างรอบถัดไป (type 2,3,5,6): clone + header+lines จาก uid เดิม, type_id+1 +PUT /api/setting/income_budget_report/lines → บันทึกจำนวนเงินที่แก้ (batch update amount) +GET /api/setting/income_budget_report/{uid}/export/xlsx → generate .xlsx ตอนดาวน์โหลด (EPPlus/ + ClosedXML จาก tree config + ข้อมูลจริงใน DB) +``` + +`IncomeBudgetReportRollupService` (C# ใหม่) เดินตาม tree config เดียวกับฝั่ง frontend สรุปยอดแต่ละ node จาก children — ใช้ทั้งตอน export Excel และตอนส่งข้อมูลกลับให้ Angular แสดง แทนที่ logic เดิมที่ไล่หาสีเซลล์ (`Personnel.cs:6089-6363`, endpoint `/report/personnel/calcurate`) ทั้งหมด + +## Frontend Component Structure + +``` +shared/components/budget-report-tree/ generic recursive tree (ต่อยอดจาก mockup ที่อนุมัติแล้ว) + budget-report-tree-node.component.ts รับ @Input node config + data, render เอง + เรียกตัวเองซ้ำ + คำนวณ sum ฝั่ง client แบบ real-time เหมือน mockup + +feature/income/report-config/ config คงที่ต่อ type (TypeScript const) + income-report-type1.config.ts tree schema กลุ่ม A + income-report-type4.config.ts tree schema กลุ่ม B (5 sub-tree ตามผลผลิต) + +feature/income/draft/income-budget-report1/ แทนที่เนื้อในของ component เดิมทั้งไฟล์ + income-budget-report.component.ts โหลด config ตาม type_id (route data), โหลด/สร้าง/clone + report, bind เข้า budget-report-tree, ปุ่มบันทึก/ส่งออก + (กลุ่ม B ครอบด้วย mat-tab-group 5 แท็บตามผลผลิต) + +feature/income/material-cost-report/ กลุ่ม C แยกต่างหาก (matrix component ใหม่ทั้งหมด) +``` + +Routing เดิมไม่เปลี่ยน — component เดิมยังถูกเรียกจากทั้ง 7 เมนูเหมือนเดิม แค่เนื้อในเปลี่ยนจาก SyncFusion เป็น tree component (กลุ่ม A/B) หรือ matrix component ใหม่ (กลุ่ม C, `educational-materials` type=10) + +## Data Flow + +1. เปิดหน้า `/add` → เช็คว่ามี report ของ หน่วยงาน+ปี+type นี้อยู่แล้วหรือยัง ถ้ายัง → กด "สร้างรายงาน" → `create_draft` +2. แก้จำนวนเงินที่ปรับได้ → คำนวณยอดรวมสดฝั่ง client (เหมือน mockup) → กด "บันทึก" → `PUT lines` แบบ batch +3. ประชุมรอบนี้เสร็จ → กด "ส่งต่อรอบถัดไป" → `clone_next_round` → เปิดหน้า type ถัดไปพร้อมข้อมูล clone มาแล้ว +4. กด "ส่งออก Excel" ได้ทุกจุด → generate ไฟล์ตามข้อมูล ณ ขณะนั้น + +## Error Handling + +- ยังไม่มีข้อมูลจาก ร.2/4/5/6 เลย → โชว์ tree เปล่าพร้อมข้อความ "ยังไม่มีข้อมูลจาก ร.2 กรุณากรอกฟอร์มก่อน" ลิงก์ไปหน้านั้น +- กัน "ส่งต่อรอบถัดไป" ซ้ำซ้อน — เช็คว่ามี report ที่ `parent_uid` ชี้มาที่ตัวปัจจุบันอยู่แล้วหรือยังก่อนอนุญาตให้ clone อีกครั้ง +- แก้ ร.2/4/5/6 ต้นทางหลัง snapshot ไปแล้ว ไม่ไหลย้อนกลับอัตโนมัติ (ดูหัวข้อ Data Model) — ต้องมี UI แจ้งชัดเจน + +## Testing + +ระบบนี้ไม่มี automated test (ตรวจสอบด้วยมือทั้งระบบตาม convention เดิม) — verify โดยเทียบยอดรวมกับไฟล์ Excel จริงที่ดาวน์โหลดมาตรวจสอบแล้วระหว่างขั้นตอนออกแบบ (มี type 1,2,3,4,5,10 อยู่ในมือ) ให้ตัวเลขตรงกันทุก node ก่อนถือว่าใช้ได้ + +## Rollout + +ระบบ production ที่ใช้งานจริงกับการประชุมจริงของมหาวิทยาลัย — deploy เป็นฟีเจอร์คู่ขนาน ไม่ทับของเดิมทันที: ทดสอบกับ 1 หน่วยงานจริงก่อน 1 รอบประชุม เทียบผลกับของเดิมให้ตรงกัน ก่อนค่อยเปิดใช้แทนของเดิมทั้งระบบ + +## จุดที่ยังไม่ชัด / ต้องตรวจเพิ่มตอน implementation + +- **ร.7**: หาไม่เจอในระบบเลยทั้ง frontend/backend ไม่ทราบว่าเคยมีหรือถูกยุบรวมกับฟอร์มอื่น — ไม่กระทบ scope นี้เพราะไม่มีข้อมูลอ้างอิงในไฟล์ Excel เดิมที่ตรวจแล้ว +- `budget_progress_id == 2` ใน `GetSummaryExpense` (`BudgetProgress.cs:419`) ความหมายจริงยังไม่ยืนยัน (draft/submitted?) กระทบว่าจะดึงข้อมูลสถานะไหนมาแสดง +- `request_budget_income` ใน `GetSummaryExpense` ไม่ได้ filter `type_id` (ต่างจาก `Request.cs:651` ที่ filter `type_id == 1`) — อาจดึงมาทั้ง "งบเงินอุดหนุน" และ "งบรายจ่ายอื่นๆ" ปนกัน ต้อง filter เพิ่มถ้าต้องการเฉพาะ ร.6 +- โครงสร้าง tree schema ของ type 2,3,5,6 (chain ต่อจาก 1,4) ยังไม่ได้ตรวจไฟล์จริงเทียบเท่า type 1,4 — type 3 ยืนยันแล้วว่าตรงกับ type 1/2, type 5 ยืนยันแล้วว่าตรงกับ type 4, **type 6 ไม่มีข้อมูลจริงในระบบให้ตรวจ** อนุมานจาก chain เท่านั้น ต้องระวังตอน implement +- กลุ่ม C (type 10) ยังไม่ได้ออกแบบ data model โดยละเอียด (matrix by campus) — เป็น sub-project แยกที่จะ spec เพิ่มเมื่อเริ่มลงมือ