Files
rmutr/docs/superpowers/specs/2026-07-22-expense-project-research-history-design.md
Nut.ไปเรื่อย a5de73a39c docs: add design spec for expense-project-research edit-history view
Ports the already-built change-history diff/session UI (backend + shared
Angular logic) from change-project-research-form to expense-project-research,
showing project detail fields (name, plan, output, etc.) instead of a/p
metrics per the requested screenshot layout.

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

17 KiB

ประวัติการแก้ไขโครงการวิจัย — หน้า expense-project-research

บริบท

เมนู "เปลี่ยนแปลงโครงการ(วิจัย)" (change-project-research-form/list-all, typeUrl: 12) ให้แก้ไขข้อมูลโครงการวิจัย กดปุ่ม "ส่งข้อมูล" (sendResearchDataListAll()) แล้วรายการจะไปโผล่ที่เมนู "หน่วยงานทำรายงานผล(วิจัย)" — route expense-project-research (typeUrl: 26, status_id=2) แสดงด้วย List14ResearchComponent

ฟีเจอร์ "ดูประวัติการแก้ไข" มีอยู่แล้วสมบูรณ์ทั้ง backend และ frontend — แต่ใช้งานได้เฉพาะที่หน้า change-project-research-form/list-all (typeUrl 12) เท่านั้น ผ่าน component ใหญ่ตัวเดียวที่ใช้ร่วมกันหลายเมนู make-year-plant-form.component.ts:

  • Backend: ChangeProjectResearchService.UpdateEntity() (rmutr-api/Modules/RequestBudgets/Services/ChangeProjectResearch.cs:25-89) ทุกครั้งที่บันทึก จะ diff field ที่เปลี่ยนผ่าน DiffDetail() (บรรทัด 119-181) เขียนลงตาราง t_change_project_research_history ถ้ายังไม่เคยมี history ของ detail นี้มาก่อน จะสร้าง snapshot ต้นฉบับก่อนด้วย BuildOriginalSnapshot() (บรรทัด 186+, ใส่ change_remark = "ต้นฉบับ") — endpoint อ่าน: GET api/request_budget/change_project_research_detail/{uid}/history
  • DiffDetail()/BuildOriginalSnapshot() diff ทั้งฟิลด์รายละเอียดโครงการ (project_name_th, budget_project_name_th, budget_plan_name_th, budget_topic_name_th, budget_strategy_name_th, responsible_faculty_name_th, budget_come_from, budget_location_name_th) และ ฟิลด์ตัวชี้วัด a_1-a_13/p_1-p_13 — แต่หน้าจอเดิม (typeUrl 12) เอามาแสดงแค่กลุ่ม a/p เท่านั้น (ดูตารางในแถวขยายที่ make-year-plan-form.component.html:1406-1487) ฟิลด์รายละเอียดโครงการที่ diff ไว้ ไม่เคยถูกแสดงที่ไหนเลย
  • Frontend logic ที่มีอยู่แล้วและนำมาใช้ซ้ำได้ตรงๆ ใน make-year-plant-form.component.ts: buildHistorySessions() (บรรทัด 791-805, group ประวัติเป็น session ตาม timestamp แล้วเรียง [...edits, ...originals] — ต้นฉบับอยู่ท้ายเสมอ), countEditSessions() (บรรทัด 670-678), toggleResearchHistory()/getResearchEditCount()/isResearchHistoryLoading()/getResearchHistorySessions() (บรรทัด 2694-2726)

หน้า expense-project-research (typeUrl 26) แสดงด้วยคนละ component (List14ResearchComponent, .../list14-research/list14-research.component.ts) ซึ่งไม่มี history เลย — ไม่ fetch, ไม่มี state, ไม่มี UI (ยืนยันด้วย grep ไม่พบคำว่า "history"/"เปลี่ยนแปลง" ในไฟล์นี้)

เป้าหมายงานนี้: พอร์ตความสามารถ "ดูประวัติการแก้ไข" มาที่หน้า expense-project-research โดยแสดงฟิลด์รายละเอียดโครงการแทนกลุ่ม a/p (ตามภาพตัวอย่างที่ผู้ใช้ให้ไว้ — คอลัมน์ วันเวลาที่แก้ไข, แผนงาน, ชื่อโครงการ, ผลผลิต, ประเด็นยุทธศาสตร์, ลักษณะโครงการ ฯลฯ) ไม่ต้องแก้ backend เลยเพราะข้อมูลถูก diff ไว้ครบอยู่แล้ว

ขอบเขต

  • แก้เฉพาะฝั่ง frontend (rmutr-web), เฉพาะ List14ResearchComponent + extract util ใหม่จาก make-year-plant-form.component.ts
  • ไม่แตะ backend/endpoint ใดๆ (ข้อมูลพร้อมใช้อยู่แล้ว)
  • ไม่แตะพฤติกรรมเดิมของหน้า change-project-research-form/list-all (typeUrl 12) — ยกเว้น refactor ดึง buildHistorySessions()/countEditSessions() ออกเป็น shared util (พฤติกรรมต้องเหมือนเดิมทุกประการ ไม่ใช่ redesign)
  • ไม่รวมฟิลด์ตัวชี้วัด a/p ในตารางประวัติของหน้านี้ (ต่างจากหน้าเดิม) — ตามที่ผู้ใช้ยืนยันจากภาพตัวอย่าง

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

1. Extract shared util — ไฟล์ใหม่ src/app/core/utils/change-history-session.util.ts

  • ย้าย buildHistorySessions() (ปัจจุบัน private method ที่ make-year-plant-form.component.ts:791-805) ออกมาเป็น exported pure function ชื่อเดิม รับ records: any[] คืน Array<{datetime: string, isOriginal: boolean, vals: Record<string,string>}> — คัดลอก logic มาทั้งหมดโดยไม่แก้ไข (group ด้วย created_datetime.substring(0,19), แถว change_remark === 'ต้นฉบับ' แยกกลุ่มเป็น __original__, คืนค่า [...edits, ...originals])
  • ย้าย countEditSessions() (บรรทัด 670-678) ออกมาเป็น exported pure function ชื่อเดิม เช่นกัน
  • แก้ make-year-plant-form.component.ts: import 2 ฟังก์ชันนี้จาก util ใหม่ ลบ private method เดิมทิ้ง แล้วแก้จุดเรียกทั้งหมด (this.buildHistorySessions(...)buildHistorySessions(...), this.countEditSessions(...)countEditSessions(...)) — จุดเรียกใช้งานจริงรวม 8 จุด: buildHistorySessions ที่บรรทัด 749, 832, 2712, 2758 และ countEditSessions ที่บรรทัด 267, 746, 2660, 2755 พฤติกรรมต้องเหมือนเดิม 100%

2. List14ResearchComponent (list14-research.component.ts) — เพิ่ม history state + fetch

  • inject ChangeProjectResearchDetailService (มีอยู่แล้ว getHistory(uid) ที่ change-project-research-detail.service.ts:14) และ ChangeDetectorRef (component ใช้ ChangeDetectionStrategy.OnPush อยู่แล้วที่บรรทัด 9 — ต้องเรียก detectChanges() เองทุกจุดที่ callback แบบ async อัปเดต state)
  • เพิ่ม properties: editCountMap = new Map<string, number>(), historySessionsMap = new Map<string, Array<{datetime,isOriginal,vals}>>(), historyLoadingUids = new Set<string>(), expandedHistoryUid: string | null = null
  • ใน ngOnChanges หลัง set this.details = [...this.dataSource] (บรรทัด 26-27): ถ้ามีข้อมูล, forkJoin เรียก getHistory(d.change_project_research_detail_uid) ทุกแถว (catchError(() => of([])) ต่อรายการ) แล้วคำนวณ editCountMap ด้วย countEditSessions() จาก util ใหม่ — ยังไม่ build session เต็ม ตอนนี้ (lazy โหลดตอน toggle เพื่อลด payload ตอนโหลดหน้าแรก เหมือน pattern เดิมที่ toggleResearchHistory() ทำ)
  • เพิ่มเมธอด มิเรอร์ toggleResearchHistory/isResearchHistoryLoading/getResearchEditCount/getResearchHistorySessions จาก make-year-plant-form.component.ts:2694-2726 ทุกตัวอักษร (เปลี่ยนแค่ชื่อ map/property ให้ตรงกับของ component นี้, เรียก util ที่ extract แทน this. method เดิม):
    • toggleHistory(uid: string)
    • isHistoryLoading(uid: string): boolean
    • getEditCount(uid: string): number
    • getHistorySessions(uid: string)

3. Field set ของตารางประวัติ — ไม่ใช้ a/p

Mapping คงที่ (field_key ตรงกับที่ backend เขียนจริงใน DiffDetail()/BuildOriginalSnapshot()):

field_key หัวคอลัมน์
budget_plan_name_th แผนงาน
project_name_th ชื่อโครงการ
budget_project_name_th ผลผลิต
budget_strategy_name_th ประเด็นยุทธศาสตร์
budget_topic_name_th ลักษณะโครงการ
responsible_faculty_name_th หน่วยงานที่รับผิดชอบ
budget_come_from แหล่งที่มาของเงิน

ลำดับคอลัมน์อ้างอิงหัวตารางหลักของ list14-research.component.html:29-37 ให้ตรงกัน (ต่อจากคอลัมน์ "วันเวลาที่แก้ไข" ที่นำหน้าเสมอ)

ไม่รวม budget_location_name_th แม้ backend diff ไว้ — เพราะไม่มีคอลัมน์นี้แสดงในตารางหลักของหน้านี้อยู่แล้ว (นอกสโคป ถ้าต้องการภายหลังแจ้งเพิ่มได้) และไม่รวม a_1-a_13/p_1-p_13 ตามที่ตกลงกันไว้

4. Template list14-research.component.html — ไอคอน + badge + แถวขยาย

  • คอลัมน์ "ลำดับ" (บรรทัด 81, ปัจจุบันแสดงแค่ {{i+1}} เฉยๆ): เปลี่ยนเป็นโครงสร้างเดียวกับ seq-cell ที่ make-year-plan-form.component.html:1268-1288 แต่ไม่มีปุ่มบันทึก (หน้านี้ read-only ไม่มีฟอร์มแก้ไข) — แสดงเลขลำดับ + ปุ่มไอคอน history (*ngIf="getEditCount(detail.change_project_research_detail_uid) > 0", สลับ history/expand_less ตามสถานะเปิด, title="ดูประวัติการแก้ไข") + badge เปลี่ยนแปลง #{{getEditCount(...)}}
  • หลังแถว tr_a (บรรทัด 116-131) เพิ่มแถวขยาย <tr *ngIf="expandedHistoryUid === detail.change_project_research_detail_uid"><td colspan="24" ...> (24 = จำนวนคอลัมน์ทั้งหมดตาม <colgroup> บรรทัด 3-27) — โครงสร้างเดียวกับ make-year-plan-form.component.html:1406-1487:
    • หัวข้อ "ประวัติการแก้ไข", loading spinner ระหว่าง isHistoryLoading(uid)
    • ตาราง 1 session = 1 แถว (ต่างจากต้นฉบับที่แยก P/A เป็น 2 แถวย่อย เพราะ field ชุดใหม่นี้เป็นค่าเดียวต่อ detail ไม่ผูกกับ P-row) หัวตาราง: วันเวลาที่แก้ไข + 7 คอลัมน์ตามข้อ 3
    • แถว "ต้นฉบับ" ใช้ badge สีส้มเดิม (background:#ff8f00) แทนวันที่ — อยู่แถวสุดท้ายเสมอ (การันตีจาก buildHistorySessions())
    • ไฮไลต์เซลล์ background:#fff8e1 เมื่อ !session.isOriginal && session.vals['<field_key>'] != null (กติกาเดิมเป๊ะ — ถ้า field ไม่ได้ถูกแก้ใน session นั้น field_key จะไม่อยู่ใน vals เลย เพราะ DiffDetail() สร้างแถว history เฉพาะ field ที่ค่าเปลี่ยนจริงเท่านั้น)
    • แถว empty state ไม่มีประวัติการแก้ไข เมื่อ getHistorySessions(uid).length === 0

Error handling

  • getHistory() ล้มเหลว (ทั้งตอนโหลดหน้าแรกและตอน toggle): catchError(() => of([])) เหมือนทุกจุดในโปรเจกต์ — แถวนั้นจะไม่มี badge/ไอคอน (เหมือนไม่มีประวัติ) ไม่มี error popup รบกวน (หน้านี้เป็น read-only report ไม่ใช่ฟอร์มที่ต้อง block การทำงาน)
  • ไม่มีแถวไหนมีประวัติเลยทั้งหน้า: ไม่แสดงไอคอน/badge (ปกติ ไม่ต้องมี state พิเศษ)

Known limitation (สืบทอดจากของเดิม ไม่ใช่สิ่งที่งานนี้สร้างขึ้น)

  • budget_location_name_th ถูก diff ไว้ใน backend แต่ยังไม่มีหน้าไหนแสดงเลย (ทั้งของเดิมและงานนี้) — คงสถานะเดิม
  • field p_1-p_13 ของหลายแถว P ในรายละเอียดเดียวกัน backend ใช้ field_key ซ้ำกัน ("p_1" ทุกแถว) ทำให้ session ประวัติที่มี P มากกว่า 1 แถวจะเห็นค่าทับกันถ้าดูผ่านตาราง a/p เดิม — ไม่กระทบงานนี้เพราะไม่แสดง a/p ในตารางประวัติของหน้านี้

Testing (manual/browser)

  1. เข้าเมนู "เปลี่ยนแปลงโครงการ(วิจัย)" (change-project-research-form/list-all) แก้ไขโครงการวิจัยที่มีสถานะรอส่ง (เปลี่ยน "ชื่อโครงการ" และ "แผนงาน") กด "ส่งข้อมูล"
  2. เข้าเมนู expense-project-research — ยืนยันแถวโครงการที่เพิ่งแก้ไขมีไอคอน history + badge "เปลี่ยนแปลง #1" ในคอลัมน์ลำดับ
  3. กดไอคอน — ยืนยันแถวขยายออกมาแสดง 2 แถว (session ที่เพิ่งแก้ + ต้นฉบับ) ต้นฉบับอยู่ล่างสุดเสมอ คอลัมน์ "ชื่อโครงการ"/"แผนงาน" ไฮไลต์สีเหลืองเฉพาะแถว session ที่แก้จริง ไม่ไฮไลต์ในแถวต้นฉบับ คอลัมน์อื่นที่ไม่ได้แก้ (เช่น ผลผลิต) ว่างเปล่าไม่ไฮไลต์
  4. แก้ไขโครงการเดิมซ้ำอีกครั้งที่เมนู change-project-research-form/list-all (คนละ field เช่น "ผลผลิต") — กลับมาที่ expense-project-research ยืนยัน badge เปลี่ยนเป็น "#2" และแถวขยายมี 3 แถว (2 session + ต้นฉบับ) เรียงใหม่→เก่า ต้นฉบับยังคงอยู่ล่างสุด
  5. โครงการที่ไม่เคยถูกแก้ไขเลย — ยืนยันไม่มีไอคอน/badge ในคอลัมน์ลำดับ
  6. เปิดหน้า change-project-research-form/list-all (typeUrl 12) อีกครั้งหลัง refactor util — ยืนยัน badge/แถวขยาย/ไฮไลต์ของกลุ่ม a/p ยังทำงานเหมือนเดิมทุกจุด (regression check การ extract util)
  7. throttle network แล้วกดไอคอน history ที่หน้า expense-project-research — เห็น spinner "กำลังโหลด..." ก่อนแถวข้อมูลมา