docs: add implementation plan for expense-project-research edit-history view
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01613NaY9LAvy2wFXEeyE5eR
This commit is contained in:
@@ -0,0 +1,598 @@
|
|||||||
|
# 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**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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:
|
||||||
|
```ts
|
||||||
|
import { ChangeProjectService } from 'src/app/core/service/request-budget/change-project.service';
|
||||||
|
```
|
||||||
|
Change to:
|
||||||
|
```ts
|
||||||
|
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:
|
||||||
|
```ts
|
||||||
|
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:
|
||||||
|
```ts
|
||||||
|
getEditCount(uid: string): number {
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Remove the private `buildHistorySessions` method**
|
||||||
|
|
||||||
|
Find:
|
||||||
|
```ts
|
||||||
|
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:
|
||||||
|
```ts
|
||||||
|
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**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
```ts
|
||||||
|
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:
|
||||||
|
```ts
|
||||||
|
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**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
```html
|
||||||
|
<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:
|
||||||
|
```html
|
||||||
|
<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:
|
||||||
|
```html
|
||||||
|
<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:
|
||||||
|
```html
|
||||||
|
<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):
|
||||||
|
```scss
|
||||||
|
.text-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Change to:
|
||||||
|
```scss
|
||||||
|
.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**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.
|
||||||
Reference in New Issue
Block a user