Files
rmutr/docs/superpowers/plans/2026-07-22-expense-project-research-history.md
T
2026-07-22 18:33:05 +07:00

31 KiB

Expense-Project-Research Edit-History View Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add a "ดูประวัติการแก้ไข" (view edit history) affordance — icon, edit-count badge, expandable session table — to the expense-project-research list (List14ResearchComponent), showing project detail-field diffs (ชื่อโครงการ, แผนงาน, ผลผลิต, ประเด็นยุทธศาสตร์, ลักษณะโครงการ, หน่วยงานที่รับผิดชอบ, แหล่งที่มาของเงิน) with the pre-edit original always pinned last.

Architecture: No backend changes — GET /request_budget/change_project_research_detail/{uid}/history already returns field-level diff rows including a synthetic "ต้นฉบับ" (original) snapshot. Two pure functions (buildHistorySessions, countEditSessions) that already implement the "group into sessions, original last" logic are extracted out of the large shared make-year-plant-form.component.ts into a new util module, then reused by List14ResearchComponent, which gets its own history-fetch/expand-row state (independent of the shared form component — different page, different data shape, no P/A metrics table). The visible history table shows a different field set than the existing (typeUrl 12) implementation: one row per edit session with the 7 project-detail columns instead of per-P/A-row numeric columns.

Tech Stack: Angular 17, Angular Material (mat-icon, mat-icon-button, mat-spinner), RxJS (forkJoin, catchError).

Global Constraints

  • No rmutr-api changes — the history endpoint and diff data already exist and cover every field this plan displays.
  • This repo has no unit-test culture for these Angular components. Per-task verification uses ng build --configuration=production (from /Users/nut.looknut/Project/rmutr/rmutr-web) to catch compile/template errors; the final task is a manual browser check via npm start.
  • Extracting buildHistorySessions/countEditSessions out of make-year-plant-form.component.ts must not change behavior for the existing typeUrl 12 (change-project-research-form/list-all) history view — same output for the same input, verified by a regression check in the final task.
  • List14ResearchComponent uses ChangeDetectionStrategy.OnPush — every async state update must be followed by this.cdRef.detectChanges(), matching the pattern already used in make-year-plant-form.component.ts.
  • New history table shows only these 7 field_keys (plus datetime): 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. Do not add a_1-a_13/p_1-p_13 or budget_location_name_th — out of scope per the approved design spec.
  • Failed getHistory() calls degrade silently (catchError(() => of([]))) — no Swal error popups on this read-only report page.

Task 1: Extract buildHistorySessions/countEditSessions into a shared util

Files:

  • Create: /Users/nut.looknut/Project/rmutr/rmutr-web/src/app/core/utils/change-history-session.util.ts
  • Modify: /Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/form/make-year-plan-form/make-year-plant-form.component.ts

Interfaces:

  • Consumes: nothing new

  • Produces: buildHistorySessions(records: any[]): HistorySession[] and countEditSessions(history: any[]): number, plus the HistorySession interface ({datetime: string, isOriginal: boolean, vals: Record<string,string>}) — Task 2 imports all three.

  • Step 1: Create the util file

export interface HistorySession {
  datetime: string
  isOriginal: boolean
  vals: Record<string, string>
}

export function buildHistorySessions(records: any[]): HistorySession[] {
  const sessionMap = new Map<string, HistorySession>()
  for (const r of records) {
    const isOriginal = r.change_remark === 'ต้นฉบับ'
    const key = isOriginal ? '__original__' : (r.created_datetime ?? '').substring(0, 19)
    if (!sessionMap.has(key)) {
      sessionMap.set(key, { datetime: r.created_datetime, isOriginal, vals: {} })
    }
    sessionMap.get(key)!.vals[r.field_key] = r.new_value
  }
  const entries = Array.from(sessionMap.values())
  const originals = entries.filter(e => e.isOriginal)
  const edits = entries.filter(e => !e.isOriginal)
  return [...edits, ...originals]
}

export function countEditSessions(history: any[]): number {
  if (!history?.length) return 0
  const keys = new Set(
    history
      .filter(h => h.change_remark !== 'ต้นฉบับ')
      .map(h => (h.created_datetime ?? '').substring(0, 19))
  )
  return keys.size
}
  • Step 2: Add the import to make-year-plant-form.component.ts

Find, at the top of the file:

import { ChangeProjectService } from 'src/app/core/service/request-budget/change-project.service';

Change to:

import { ChangeProjectService } from 'src/app/core/service/request-budget/change-project.service';
import { buildHistorySessions, countEditSessions } from 'src/app/core/utils/change-history-session.util';
  • Step 3: Remove the private countEditSessions method

Find:

  private countEditSessions(history: any[]): number {
    if (!history?.length) return 0
    const keys = new Set(
      history
        .filter(h => h.change_remark !== 'ต้นฉบับ')
        .map(h => (h.created_datetime ?? '').substring(0, 19))
    )
    return keys.size
  }

  getEditCount(uid: string): number {

Change to:

  getEditCount(uid: string): number {
  • Step 4: Remove the private buildHistorySessions method

Find:

  private buildHistorySessions(records: any[]): Array<{datetime: string, isOriginal: boolean, vals: Record<string, string>}> {
    const sessionMap = new Map<string, {datetime: string, isOriginal: boolean, vals: Record<string, string>}>()
    for (const r of records) {
      const isOriginal = r.change_remark === 'ต้นฉบับ'
      const key = isOriginal ? '__original__' : (r.created_datetime ?? '').substring(0, 19)
      if (!sessionMap.has(key)) {
        sessionMap.set(key, { datetime: r.created_datetime, isOriginal, vals: {} })
      }
      sessionMap.get(key)!.vals[r.field_key] = r.new_value
    }
    const entries = Array.from(sessionMap.values())
    const originals = entries.filter(e => e.isOriginal)
    const edits = entries.filter(e => !e.isOriginal)
    return [...edits, ...originals]
  }

  toggleHistory(d: any) {

Change to:

  toggleHistory(d: any) {
  • Step 5: Repoint every call site to the imported functions

Using the Edit tool with replace_all: true on make-year-plant-form.component.ts:

  • Replace this.buildHistorySessions( with buildHistorySessions( (4 occurrences: inside saveProject(), toggleHistory(), toggleResearchHistory(), saveResearchProject())

  • Replace this.countEditSessions( with countEditSessions( (4 occurrences: inside the initial project-history batch load, saveProject(), the initial research-history batch load, saveResearchProject())

  • Step 6: Verify no stray references remain

Run: cd /Users/nut.looknut/Project/rmutr/rmutr-web && grep -n "this.buildHistorySessions\|this.countEditSessions" src/app/feature/budget-request/request/request-budget-statistics/presenter/form/make-year-plan-form/make-year-plant-form.component.ts Expected: no output (all call sites repointed).

  • Step 7: Build to confirm no errors

Run: cd /Users/nut.looknut/Project/rmutr/rmutr-web && ng build --configuration=production Expected: build succeeds with no new errors.

  • Step 8: Commit
git add rmutr-web/src/app/core/utils/change-history-session.util.ts rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/form/make-year-plan-form/make-year-plant-form.component.ts
git commit -m "refactor: extract buildHistorySessions/countEditSessions into a shared util"

Task 2: List14ResearchComponent — history fetch state

Files:

  • Modify: /Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list14-research/list14-research.component.ts

Interfaces:

  • Consumes: buildHistorySessions, countEditSessions, HistorySession (Task 1); ChangeProjectResearchDetailService.getHistory(uid): Observable<any[]> (existing, change-project-research-detail.service.ts:14)

  • Produces: getEditCount(uid): number, toggleHistory(uid): void, isHistoryLoading(uid): boolean, getHistorySessions(uid): HistorySession[], expandedHistoryUid: string | null — Task 3's template binds to all of these.

  • Step 1: Replace the component with the history-aware version

Find the full current file content:

import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Input, Output, OnChanges, SimpleChanges } from '@angular/core';
import { PageEvent } from '@angular/material/paginator';
import { BaseList } from 'src/app/core/base/base-list';

@Component({
  selector: 'app-list14-research',
  templateUrl: './list14-research.component.html',
  styleUrls: ['./list14-research.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class List14ResearchComponent extends BaseList implements OnInit, OnChanges {

  @Input() dataSource: any = [];
  @Output() onchange = new EventEmitter();
  @Output() onedit = new EventEmitter();
  @Output() ondelete = new EventEmitter();
  @Output() onexcel = new EventEmitter();
  @Output() onReport = new EventEmitter();
  data;
  details: any[] = [];

  constructor() { super(); }

  ngOnChanges(changes: SimpleChanges): void {
    if ('dataSource' in changes && changes?.dataSource?.currentValue) {
      this.data = this.dataSource;
      this.details = this.dataSource ? [...this.dataSource] : [];
    }
  }

  ngOnInit(): void {}

  edit(val) { this.onedit.emit(val.change_project_research_uid); }
  delete(val) { this.ondelete.emit(val); }

  change(event: PageEvent) {
    let page: number = event.pageIndex + 1;
    let table: any = { page: page, size: event.pageSize };
    this.onchange.emit(table);
  }

  excel(val) { this.onexcel.emit(val); }
  report(el) { this.onReport.emit({ change_project_research_uid: el }); }
}

Change to:

import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, EventEmitter, Input, Output, OnChanges, SimpleChanges } from '@angular/core';
import { PageEvent } from '@angular/material/paginator';
import { forkJoin, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { BaseList } from 'src/app/core/base/base-list';
import { ChangeProjectResearchDetailService } from 'src/app/core/service/request-budget/change-project-research-detail.service';
import { buildHistorySessions, countEditSessions, HistorySession } from 'src/app/core/utils/change-history-session.util';

@Component({
  selector: 'app-list14-research',
  templateUrl: './list14-research.component.html',
  styleUrls: ['./list14-research.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class List14ResearchComponent extends BaseList implements OnInit, OnChanges {

  @Input() dataSource: any = [];
  @Output() onchange = new EventEmitter();
  @Output() onedit = new EventEmitter();
  @Output() ondelete = new EventEmitter();
  @Output() onexcel = new EventEmitter();
  @Output() onReport = new EventEmitter();
  data;
  details: any[] = [];

  editCountMap = new Map<string, number>();
  historySessionsMap = new Map<string, HistorySession[]>();
  historyLoadingUids = new Set<string>();
  expandedHistoryUid: string | null = null;

  constructor(
    private cdRef: ChangeDetectorRef,
    private ChangeProjectResearchDetailSV: ChangeProjectResearchDetailService
  ) { super(); }

  ngOnChanges(changes: SimpleChanges): void {
    if ('dataSource' in changes && changes?.dataSource?.currentValue) {
      this.data = this.dataSource;
      this.details = this.dataSource ? [...this.dataSource] : [];
      this.loadHistoryCounts(this.details);
    }
  }

  ngOnInit(): void {}

  private loadHistoryCounts(details: any[]): void {
    const uids = (details || [])
      .map((d: any) => d.change_project_research_detail_uid)
      .filter((uid: string) => !!uid);
    if (uids.length === 0) return;
    forkJoin(uids.map((uid: string) =>
      this.ChangeProjectResearchDetailSV.getHistory(uid).pipe(catchError(() => of([] as any[])))
    )).subscribe((histories: any[][]) => {
      this.editCountMap = new Map<string, number>();
      uids.forEach((uid: string, i: number) => {
        const count = countEditSessions(histories[i] ?? []);
        if (count > 0) this.editCountMap.set(uid, count);
      });
      this.cdRef.detectChanges();
    });
  }

  getEditCount(uid: string): number {
    return this.editCountMap.get(uid) ?? 0;
  }

  toggleHistory(uid: string): void {
    if (!uid) return;
    if (this.expandedHistoryUid === uid) {
      this.expandedHistoryUid = null;
      this.cdRef.detectChanges();
      return;
    }
    this.expandedHistoryUid = uid;
    if (!this.historySessionsMap.has(uid)) {
      this.historyLoadingUids.add(uid);
      this.ChangeProjectResearchDetailSV.getHistory(uid).pipe(
        catchError(() => of([] as any[]))
      ).subscribe((h: any[]) => {
        this.historySessionsMap.set(uid, buildHistorySessions(h));
        this.historyLoadingUids.delete(uid);
        this.cdRef.detectChanges();
      });
    }
    this.cdRef.detectChanges();
  }

  isHistoryLoading(uid: string): boolean {
    return this.historyLoadingUids.has(uid);
  }

  getHistorySessions(uid: string): HistorySession[] {
    return this.historySessionsMap.get(uid) ?? [];
  }

  edit(val) { this.onedit.emit(val.change_project_research_uid); }
  delete(val) { this.ondelete.emit(val); }

  change(event: PageEvent) {
    let page: number = event.pageIndex + 1;
    let table: any = { page: page, size: event.pageSize };
    this.onchange.emit(table);
  }

  excel(val) { this.onexcel.emit(val); }
  report(el) { this.onReport.emit({ change_project_research_uid: el }); }
}
  • Step 2: Build to confirm no errors

Run: cd /Users/nut.looknut/Project/rmutr/rmutr-web && ng build --configuration=production Expected: build succeeds with no new errors.

  • Step 3: Commit
git add rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list14-research/list14-research.component.ts
git commit -m "feat: fetch and track edit-history counts/sessions in List14ResearchComponent"

Task 3: Template — history icon/badge + expandable session table

Files:

  • Modify: /Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list14-research/list14-research.component.html
  • Modify: /Users/nut.looknut/Project/rmutr/rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list14-research/list14-research.component.scss

Interfaces:

  • Consumes: getEditCount(uid), toggleHistory(uid), isHistoryLoading(uid), getHistorySessions(uid), expandedHistoryUid (Task 2)

  • Step 1: Add the history icon + badge to the "ลำดับ" cell

Find:

                <ng-container *ngFor="let pItem of (detail.change_project_research_p_details || []); let pIdx = index">
                    <tr class="tr_p">
                        <ng-container *ngIf="pIdx === 0">
                            <td class="td_class text-center" [attr.rowspan]="(detail.change_project_research_p_details?.length || 1) + 1">{{i+1}}</td>
                            <td class="td_class text-center" [attr.rowspan]="(detail.change_project_research_p_details?.length || 1) + 1">{{detail.budget_year_name_th}}</td>

Change to:

                <ng-container *ngFor="let pItem of (detail.change_project_research_p_details || []); let pIdx = index">
                    <tr class="tr_p">
                        <ng-container *ngIf="pIdx === 0">
                            <td class="td_class text-center" [attr.rowspan]="(detail.change_project_research_p_details?.length || 1) + 1">
                                <div class="seq-cell">
                                    <span class="seq-number">{{i+1}}</span>
                                    <button *ngIf="getEditCount(detail.change_project_research_detail_uid) > 0"
                                        mat-icon-button (click)="toggleHistory(detail.change_project_research_detail_uid)"
                                        class="seq-btn seq-btn--history"
                                        [style.color]="expandedHistoryUid === detail.change_project_research_detail_uid ? '#1565c0' : null"
                                        title="ดูประวัติการแก้ไข">
                                        <mat-icon>{{ expandedHistoryUid === detail.change_project_research_detail_uid ? 'expand_less' : 'history' }}</mat-icon>
                                    </button>
                                    <span *ngIf="getEditCount(detail.change_project_research_detail_uid) > 0"
                                        class="edit-count-pill edit-count-pill--active">
                                        เปลี่ยนแปลง #{{getEditCount(detail.change_project_research_detail_uid)}}
                                    </span>
                                </div>
                            </td>
                            <td class="td_class text-center" [attr.rowspan]="(detail.change_project_research_p_details?.length || 1) + 1">{{detail.budget_year_name_th}}</td>
  • Step 2: Add the expand row after the "A" row

Find:

                <tr class="tr_a">
                    <td class="td_class text-center"><span class="badge-a">A</span></td>
                    <td class="td_class text-right">{{detail.a_1}}</td>
                    <td class="td_class text-right">{{detail.a_2}}</td>
                    <td class="td_class text-right">{{detail.a_3}}</td>
                    <td class="td_class text-right">{{detail.a_4}}</td>
                    <td class="td_class text-right">{{detail.a_5}}</td>
                    <td class="td_class text-right">{{detail.a_6}}</td>
                    <td class="td_class text-right">{{detail.a_7}}</td>
                    <td class="td_class text-right">{{detail.a_8}}</td>
                    <td class="td_class text-center">{{detail.a_9 | date:'dd/MM/yyyy'}}</td>
                    <td class="td_class text-center">{{detail.a_10 | date:'dd/MM/yyyy'}}</td>
                    <td class="td_class text-right">{{detail.a_11}}</td>
                    <td class="td_class text-right">{{detail.a_12}}</td>
                    <td class="td_class">{{detail.a_13}}</td>
                </tr>
            </ng-container>
        </tbody>
    </table>

Change to:

                <tr class="tr_a">
                    <td class="td_class text-center"><span class="badge-a">A</span></td>
                    <td class="td_class text-right">{{detail.a_1}}</td>
                    <td class="td_class text-right">{{detail.a_2}}</td>
                    <td class="td_class text-right">{{detail.a_3}}</td>
                    <td class="td_class text-right">{{detail.a_4}}</td>
                    <td class="td_class text-right">{{detail.a_5}}</td>
                    <td class="td_class text-right">{{detail.a_6}}</td>
                    <td class="td_class text-right">{{detail.a_7}}</td>
                    <td class="td_class text-right">{{detail.a_8}}</td>
                    <td class="td_class text-center">{{detail.a_9 | date:'dd/MM/yyyy'}}</td>
                    <td class="td_class text-center">{{detail.a_10 | date:'dd/MM/yyyy'}}</td>
                    <td class="td_class text-right">{{detail.a_11}}</td>
                    <td class="td_class text-right">{{detail.a_12}}</td>
                    <td class="td_class">{{detail.a_13}}</td>
                </tr>
                <tr *ngIf="expandedHistoryUid === detail.change_project_research_detail_uid">
                    <td colspan="24" style="padding:0; background:#f0f4ff; border:2px solid #c5cae9;">
                        <div style="padding:10px 16px;">
                            <div style="font-size:12px; font-weight:700; color:#1565c0; margin-bottom:8px;">ประวัติการแก้ไข</div>
                            <div *ngIf="isHistoryLoading(detail.change_project_research_detail_uid)" style="text-align:center; padding:14px; color:#90a4ae;">
                                <mat-spinner [diameter]="20" style="display:inline-block; vertical-align:middle;"></mat-spinner>
                                <span style="margin-left:8px; vertical-align:middle;">กำลังโหลด...</span>
                            </div>
                            <ng-container *ngIf="!isHistoryLoading(detail.change_project_research_detail_uid)">
                                <div style="overflow-x:auto;">
                                    <table style="border-collapse:collapse; width:100%; font-size:11px; min-width:900px;">
                                        <thead>
                                            <tr style="background:#e8eaf6;">
                                                <th style="padding:4px 8px; border:1px solid #c5cae9; width:140px;">วันเวลาที่แก้ไข</th>
                                                <th style="padding:4px 6px; border:1px solid #c5cae9;">แผนงาน</th>
                                                <th style="padding:4px 6px; border:1px solid #c5cae9;">ชื่อโครงการ</th>
                                                <th style="padding:4px 6px; border:1px solid #c5cae9;">ผลผลิต</th>
                                                <th style="padding:4px 6px; border:1px solid #c5cae9;">ประเด็นยุทธศาสตร์</th>
                                                <th style="padding:4px 6px; border:1px solid #c5cae9;">ลักษณะโครงการ</th>
                                                <th style="padding:4px 6px; border:1px solid #c5cae9;">หน่วยงานที่รับผิดชอบ</th>
                                                <th style="padding:4px 6px; border:1px solid #c5cae9;">แหล่งที่มาของเงิน</th>
                                            </tr>
                                        </thead>
                                        <tbody>
                                            <tr *ngFor="let session of getHistorySessions(detail.change_project_research_detail_uid)">
                                                <td style="padding:5px 8px; border:1px solid #c5cae9; white-space:nowrap; font-size:10px; color:#3949ab; font-weight:600;">
                                                    <span *ngIf="session.isOriginal" style="background:#ff8f00; color:#fff; padding:2px 7px; border-radius:4px; font-weight:700; font-size:10px; display:inline-block;">ต้นฉบับ</span>
                                                    <span *ngIf="!session.isOriginal">{{session.datetime | date:'dd/MM/yyyy HH:mm'}}</span>
                                                </td>
                                                <td style="padding:4px 6px; border:1px solid #c5cae9;" [style.background]="!session.isOriginal && session.vals['budget_plan_name_th'] != null ? '#fff8e1' : '#fff'">{{ session.vals['budget_plan_name_th'] }}</td>
                                                <td style="padding:4px 6px; border:1px solid #c5cae9;" [style.background]="!session.isOriginal && session.vals['project_name_th'] != null ? '#fff8e1' : '#fff'">{{ session.vals['project_name_th'] }}</td>
                                                <td style="padding:4px 6px; border:1px solid #c5cae9;" [style.background]="!session.isOriginal && session.vals['budget_project_name_th'] != null ? '#fff8e1' : '#fff'">{{ session.vals['budget_project_name_th'] }}</td>
                                                <td style="padding:4px 6px; border:1px solid #c5cae9;" [style.background]="!session.isOriginal && session.vals['budget_strategy_name_th'] != null ? '#fff8e1' : '#fff'">{{ session.vals['budget_strategy_name_th'] }}</td>
                                                <td style="padding:4px 6px; border:1px solid #c5cae9;" [style.background]="!session.isOriginal && session.vals['budget_topic_name_th'] != null ? '#fff8e1' : '#fff'">{{ session.vals['budget_topic_name_th'] }}</td>
                                                <td style="padding:4px 6px; border:1px solid #c5cae9;" [style.background]="!session.isOriginal && session.vals['responsible_faculty_name_th'] != null ? '#fff8e1' : '#fff'">{{ session.vals['responsible_faculty_name_th'] }}</td>
                                                <td style="padding:4px 6px; border:1px solid #c5cae9;" [style.background]="!session.isOriginal && session.vals['budget_come_from'] != null ? '#fff8e1' : '#fff'">{{ session.vals['budget_come_from'] }}</td>
                                            </tr>
                                            <tr *ngIf="getHistorySessions(detail.change_project_research_detail_uid).length === 0">
                                                <td colspan="8" style="padding:14px; text-align:center; color:#90a4ae; font-style:italic; background:#fafafa;">ไม่มีประวัติการแก้ไข</td>
                                            </tr>
                                        </tbody>
                                    </table>
                                </div>
                            </ng-container>
                        </div>
                    </td>
                </tr>
            </ng-container>
        </tbody>
    </table>
  • Step 3: Append the seq-cell/badge styles to the component's scss

Find, at the end of list14-research.component.scss (after the .text-right { text-align: right; } block):

.text-right {
    text-align: right;
}

Change to:

.text-right {
    text-align: right;
}

// ===== Sequence number cell (history icon + edit-count badge) =====
.seq-cell {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    padding: 6px 4px;
}

.seq-number {
    font-size: 15px;
    font-weight: 700;
    color: #2c3e50;
    line-height: 1;
}

.seq-btn {
    width: 24px !important;
    height: 24px !important;
    line-height: 24px !important;

    mat-icon {
        font-size: 16px !important;
        width: 16px !important;
        height: 16px !important;
        line-height: 16px !important;
    }

    &.seq-btn--history {
        color: #90a4ae !important;

        &:hover {
            color: #0C7469 !important;
        }
    }
}

.edit-count-pill {
    font-size: 9px;
    font-weight: 500;
    border-radius: 10px;
    padding: 2px 6px;
    white-space: nowrap;
    background: #f0f0f0;
    color: #bbb;
    letter-spacing: 0.2px;
    line-height: 1.4;

    &.edit-count-pill--active {
        background: #e8f0fe;
        color: #1a73e8;
        font-weight: 600;
    }
}
  • Step 4: Build to confirm no errors

Run: cd /Users/nut.looknut/Project/rmutr/rmutr-web && ng build --configuration=production Expected: build succeeds with no new errors.

  • Step 5: Commit
git add rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list14-research/list14-research.component.html rmutr-web/src/app/feature/budget-request/request/request-budget-statistics/presenter/list/request-budget-statistics-list/list14-research/list14-research.component.scss
git commit -m "feat: render edit-history icon, badge, and session table on expense-project-research list"

Task 4: End-to-end manual verification

Files: none (verification only)

Interfaces: none

  • Step 1: Start the dev server

Run: cd /Users/nut.looknut/Project/rmutr/rmutr-web && npm start

  • Step 2: Create a history entry

Open "การบริหารและรายงานผล > เปลี่ยนแปลงโครงการ(วิจัย)" (change-project-research-form/list-all) → find a research project row with status allowing edits → change "ชื่อโครงการ" and "แผนงาน" → click "ส่งข้อมูล".

  • Step 3: Confirm the badge appears on expense-project-research

Open the menu that routes to expense-project-research (typeUrl 26). Expected: the row for the project just edited shows a history icon and badge "เปลี่ยนแปลง #1" in the "ลำดับ" column; rows never edited show neither.

  • Step 4: Confirm the expand row content and ordering

Click the history icon. Expected: row expands showing a table with 2 rows — the edit session (newest) first, then "ต้นฉบับ" last. In the edit-session row, "ชื่อโครงการ" and "แผนงาน" cells are highlighted pale yellow and show the new values; other columns (ผลผลิต, ประเด็นยุทธศาสตร์, ลักษณะโครงการ, หน่วยงานที่รับผิดชอบ, แหล่งที่มาของเงิน) are empty/not highlighted. The "ต้นฉบับ" row has no highlighted cells.

  • Step 5: Confirm a second edit appends correctly

Back in change-project-research-form/list-all, edit the same project again, this time changing "ผลผลิต" only → "ส่งข้อมูล". Return to expense-project-research. Expected: badge now reads "เปลี่ยนแปลง #2"; expanding shows 3 rows (2 edit sessions newest-first, then "ต้นฉบับ" still last); only "ผลผลิต" is highlighted in the newest row, only "ชื่อโครงการ"/"แผนงาน" highlighted in the older one.

  • Step 6: Loading state

With browser devtools network throttled (Slow 3G), collapse and re-click the history icon on a row whose sessions aren't cached yet (or hard-refresh the page first). Expected: brief "กำลังโหลด..." spinner shows before the session table renders.

  • Step 7: Regression check — typeUrl 12 history still works

Open "เปลี่ยนแปลงโครงการ(วิจัย)" (change-project-research-form/list-all) again. Expected: the pre-existing history icon/badge/expand-row for research rows still work exactly as before (P/A numeric table, original pinned last) — unaffected by the Task 1 util extraction.

  • Step 8: No-history rows unaffected

On expense-project-research, confirm rows for projects that were never edited via change-project-research-form show no icon, no badge, and the "ลำดับ" cell shows just the row number.