Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01613NaY9LAvy2wFXEeyE5eR
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-apichanges — 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 vianpm start. - Extracting
buildHistorySessions/countEditSessionsout ofmake-year-plant-form.component.tsmust not change behavior for the existingtypeUrl 12(change-project-research-form/list-all) history view — same output for the same input, verified by a regression check in the final task. List14ResearchComponentusesChangeDetectionStrategy.OnPush— every async state update must be followed bythis.cdRef.detectChanges(), matching the pattern already used inmake-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 adda_1-a_13/p_1-p_13orbudget_location_name_th— out of scope per the approved design spec. - Failed
getHistory()calls degrade silently (catchError(() => of([]))) — noSwalerror 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[]andcountEditSessions(history: any[]): number, plus theHistorySessioninterface ({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
countEditSessionsmethod
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
buildHistorySessionsmethod
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(withbuildHistorySessions((4 occurrences: insidesaveProject(),toggleHistory(),toggleResearchHistory(),saveResearchProject()) -
Replace
this.countEditSessions(withcountEditSessions((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.