# แทนที่ 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 เพิ่มเมื่อเริ่มลงมือ