docs: revise review-stage spec — ส่งแก้ไข reopens the chain's last approver

Per feedback: "ส่งแก้ไข" from central review must route back through the
existing income-report chain to its last approver (who can cascade the
rejection further back to the creator via the existing mechanism), not
create a disconnected new status. income_budget_review is now log/history
only — actual in-progress-editing state still comes from
request_expense.is_draft/is_approve directly, and re-entering "รอตรวจสอบ"
happens automatically via the existing is_approve-completion trigger, so
no separate resubmit endpoint or button is needed on income-budget-plan-01.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153wBt23EjW125ZsADwWVAs
This commit is contained in:
Nut.ไปเรื่อย
2026-09-07 17:36:22 +07:00
co-authored by Claude Sonnet 5
parent 5f634c1bf8
commit f834bbefd8
@@ -19,6 +19,11 @@
**การค้นหา/กรอง:** ที่ `income-budget-plan-02` ต้องกรองตาม **ปีงบประมาณ + หน่วยงาน/คณะ + พื้นที่** **การค้นหา/กรอง:** ที่ `income-budget-plan-02` ต้องกรองตาม **ปีงบประมาณ + หน่วยงาน/คณะ + พื้นที่**
เหมือนหน้า income-report เดิม (ยืนยันจากผู้ใช้) เหมือนหน้า income-report เดิม (ยืนยันจากผู้ใช้)
**เมื่อคนกลางกด "ส่งแก้ไข":** ต้องส่งกลับไปหา**ผู้อนุมัติคนสุดท้าย**ของเชนเดิม เพื่อเข้าสู่ขั้นตอน
"ส่งกลับให้ต้นทาง" แบบเดียวกับที่เชน income-report มีอยู่แล้ว (ผู้อนุมัติคนสุดท้ายเห็นว่าเรื่องถูกตีกลับ
แก้ไขได้ตามปกติ หรือจะกดปฏิเสธต่อไปยังลำดับก่อนหน้าจนถึงผู้สร้างก็ได้ตามกลไกเดิมทุกประการ) — ไม่ใช่การ
สร้างสถานะ "ส่งแก้ไข" แยกลอยๆ ที่ไม่เชื่อมกับเชนเดิม (ยืนยันจากผู้ใช้)
## 2. ทำไมไม่ใช้กลไก `round_no` (resubmit) ที่มีอยู่แล้ว ## 2. ทำไมไม่ใช้กลไก `round_no` (resubmit) ที่มีอยู่แล้ว
ระบบมี "ส่งอนุมัติเพิ่มเติม" (`ResubmitGetEntity`, เพิ่ม `round_no`) อยู่แล้ว แต่ **ผูกตายตัวกับ ระบบมี "ส่งอนุมัติเพิ่มเติม" (`ResubmitGetEntity`, เพิ่ม `round_no`) อยู่แล้ว แต่ **ผูกตายตัวกับ
@@ -29,8 +34,9 @@
## 3. โมเดลข้อมูลใหม่ ## 3. โมเดลข้อมูลใหม่
ตารางใหม่ `income_budget_review` (1 แถวต่อ "รอบการตรวจสอบ" — มีได้หลายแถวถ้าโดนส่งแก้ไขแล้วส่งใหม่หลายรอบ ตารางใหม่ `income_budget_review` — **ใช้เป็น log/ประวัติเท่านั้น** ไม่ใช่ตัวเก็บ "สถานะปัจจุบัน" — สถานะ
เพื่อเก็บประวัติ): จริงที่ว่าเรื่องกำลังอยู่ระหว่างแก้ไขหรือไม่ยังคงมาจาก `request_expense.is_draft`/`is_approve` เดิมเป๊ะ
(ดูเหตุผลในข้อ 4):
```csharp ```csharp
public class income_budget_review : base_table public class income_budget_review : base_table
@@ -48,29 +54,84 @@ public class income_budget_review : base_table
ในจุดเดียวกับที่ `ApproveGetEntity` (ApproveState.cs) ตั้ง `request_expense.is_approve = true` (เคสที่ไม่มี ในจุดเดียวกับที่ `ApproveGetEntity` (ApproveState.cs) ตั้ง `request_expense.is_approve = true` (เคสที่ไม่มี
ลำดับถัดไปแล้ว — ดู [[2026-09-07-approve-state-workflow-reference]] ข้อ 5) ให้เพิ่ม insert ลำดับถัดไปแล้ว — ดู [[2026-09-07-approve-state-workflow-reference]] ข้อ 5) ให้เพิ่ม insert
`income_budget_review` แถวใหม่ `status_id=1` **เฉพาะตอน `request_approve_type == "04"`** (ไม่กระทบ `income_budget_review` แถวใหม่ `status_id=1` **เฉพาะตอน `request_approve_type == "04"`** (ไม่กระทบ
`invest_asset`/`invest_construct`/`request_expense` code อื่นๆ ที่ใช้ engine เดียวกัน) `invest_asset`/`invest_construct`/`request_expense` code อื่นๆ ที่ใช้ engine เดียวกัน) — จุด trigger นี้จะยิง
ซ้ำทุกครั้งที่เชนอนุมัติผ่านครบ **ไม่ว่าจะเป็นครั้งแรกหรือหลังถูกคนกลางตีกลับแล้วแก้ไขจนอนุมัติผ่านใหม่**
เพราะเป็น branch เดียวกันของโค้ดเดิม (ดูข้อ 4) — ระบบจึงวนกลับเข้า "รอตรวจสอบ" ให้อัตโนมัติโดยไม่ต้องมีปุ่ม
"ส่งตรวจสอบใหม่" แยกต่างหาก
**สถานะปัจจุบันของเรื่องหนึ่งๆ** = แถว `income_budget_review` ที่มี `created_datetime` ล่าสุดของ **สถานะปัจจุบันของเรื่องหนึ่งๆ (สำหรับ dashboard/badge):**
`request_expense_uid` นั้น (query `OrderByDescending(created_datetime).First()`) - ถ้า `request_expense.is_draft == true && is_approve == false` → **"ถูกส่งแก้ไข"** (กำลังอยู่ในเชน
income-report รอฝั่งคณะแก้ไข — เช็คจาก `request_expense` โดยตรง ไม่ต้องพึ่ง log)
- ไม่งั้น ดูแถว `income_budget_review` ล่าสุด (`OrderByDescending(created_datetime).First()`) ของ
`request_expense_uid` นั้น: `status_id=1` → "รอตรวจสอบ", `status_id=3` → "ตรวจสอบแล้ว"
## 4. Flow ## 4. Flow — "ส่งแก้ไข" จากส่วนกลาง = เปิดคิวผู้อนุมัติคนสุดท้ายกลับมาใหม่
หัวใจของดีไซน์นี้: การกด "ส่งแก้ไข" ที่ `income-budget-plan-02` **ไม่ใช่สถานะแยกลอย** แต่เป็นการจำลอง
สิ่งที่จะเกิดขึ้นถ้า "ผู้อนุมัติคนสุดท้าย" ของเชนกด "ส่งแก้ไข" เอง — เปิดแถว `approve_state` ที่
`row_order` สูงสุดของรอบล่าสุดกลับมา `is_state=true` อีกครั้ง แล้วปล่อยให้กลไก "ส่งแก้ไข"/"อนุมัติ" เดิมของ
income-report ([[2026-09-07-approve-state-workflow-reference]] ข้อ 5-6) ทำงานต่อตามปกติทุกประการ —
รวมถึงถ้าผู้อนุมัติคนสุดท้ายกดปฏิเสธต่อ ก็จะไหลกลับไปลำดับก่อนหน้าจนถึงผู้สร้างได้เหมือนเดิม
```csharp
// สรุปพฤติกรรม ReturnForEdit(requestExpenseUid, remark, claimsIdentity) — รายละเอียด field เป๊ะๆ
// จะสรุปอีกครั้งตอนทำ implementation plan
var entity = await _context.t_request_expense.FirstOrDefaultAsync(c => c.request_expense_uid == requestExpenseUid);
if (entity.is_approve != true)
return (false, "รายการนี้ยังไม่ผ่านการอนุมัติครบทุกลำดับ ไม่สามารถส่งแก้ไขจากการตรวจสอบได้");
var currentRound = await _context.t_approve_state.Where(c => c.request_expense_uid == requestExpenseUid)
.Select(c => (int?)c.round_no).MaxAsync() ?? 1;
var lastRow = await _context.t_approve_state
.Where(c => c.request_expense_uid == requestExpenseUid && c.round_no == currentRound)
.OrderByDescending(c => c.row_order).FirstOrDefaultAsync(); // ผู้อนุมัติคนสุดท้าย
lastRow.is_state = true;
lastRow.is_approve = null; // เปิดคิวนี้กลับมาใหม่ เหมือนยังไม่เคยอนุมัติ
entity.is_draft = true;
entity.is_approve = false;
entity.current_approve_name = lastRow.display_name_th;
if (lastRow.row_order == 1) entity.is_sent = false; // เชนมีคนเดียว = เท่ากับตีกลับไปหาผู้สร้างเลย (เหมือน RejectGetEntity index_current==0)
// บันทึก remark ผ่านกลไกเดิมที่ income-report ใช้แสดงในตาราง "รายละเอียดสำหรับการส่งแก้ไข" (remark_history)
await _context.t_remark_history.AddAsync(new t_remark_history {
request_expense_uid = requestExpenseUid,
name = "ส่งแก้ไขจากการตรวจสอบส่วนกลาง",
remark = remark,
});
// log ไว้สำหรับ dashboard/ประวัติที่ plan-02 เท่านั้น — ไม่ใช่ตัวกำหนด state จริง
await _context.income_budget_review.AddAsync(new t_income_budget_review {
request_expense_uid = requestExpenseUid, status_id = 2, remark = remark,
reviewed_by = claimsIdentity?.FindFirst("login_name")?.Value, reviewed_datetime = DateTime.Now,
});
```
ผลลัพธ์: ผู้อนุมัติคนสุดท้ายเห็นเรื่องนี้ที่หน้า income-report **เหมือนถูกตีกลับตามปกติทุกประการ**
(remark จากส่วนกลางโชว์ในตารางเดิมที่มีอยู่แล้ว) แก้ไข/ส่งอนุมัติใหม่ตามกลไกเดิมจนอนุมัติผ่านครบอีกครั้ง
→ trigger เดิมในข้อ 3 จะ log "รอตรวจสอบ" ให้อัตโนมัติ วนกลับไป plan-02
``` ```
income-report เชนอนุมัติผ่านครบ (is_approve=true) income-report เชนอนุมัติผ่านครบ (is_approve=true)
│ (auto insert income_budget_review status_id=1) │ (auto insert income_budget_review status_id=1)
▼ ▼
income-budget-plan-01 (ฝั่งคณะ) income-budget-plan-02 (ฝั่งส่วนกลาง) income-budget-plan-02 (ฝั่งส่วนกลาง) เห็นเรื่องนี้ใน dashboard "รอตรวจสอบ"
เห็นสถานะ "รอตรวจสอบ" เห็นเรื่องนี้ใน dashboard "รอตรวจสอบ"
│ │
│ ผู้ตรวจสอบกด "ส่งแก้ไข" + พิมพ์ remark
│◄───────────────────────────────────────┘
เห็นสถานะ "ถูกส่งแก้ไข" + remark
กด "ส่งตรวจสอบใหม่" (แก้ไขข้อมูลผ่าน income-report/plan-01 ตามปกติ)
│ (insert income_budget_review row ใหม่ status_id=1)
▼
income-budget-plan-02 เห็นเรื่องนี้กลับมาที่ "รอตรวจสอบ" อีกครั้ง
│ │
ผู้ตรวจสอบกด "ตรวจสอบแล้ว" (status_id=3, จบกระบวนการ) ┌────┴─────────────────────┐
│ │
กด "ตรวจสอบแล้ว" กด "ส่งแก้ไข" + remark
(status_id=3, จบ) │
เปิดแถว approve_state ล่าสุด (row_order สูงสุด) กลับเป็น is_state=true
request_expense.is_draft=true, is_approve=false
บันทึก remark ผ่าน remark_history เดิม + log status_id=2
│
เรื่องนี้กลับเข้าเชน income-report ปกติ (ผู้อนุมัติคนสุดท้ายเห็น "ถูกส่งแก้ไข"
แก้ไข/ปฏิเสธต่อไปยังลำดับก่อนหน้า/ส่งอนุมัติใหม่ได้ตามกลไกเดิมทุกประการ)
│
เชนอนุมัติผ่านครบอีกครั้ง (is_approve=true)
│
(auto insert income_budget_review status_id=1 — วนกลับไป "รอตรวจสอบ")
``` ```
## 5. Backend API (ใหม่) ## 5. Backend API (ใหม่)
@@ -79,31 +140,35 @@ income-budget-plan-02 เห็นเรื่องนี้กลับมา
| Endpoint | Method | หน้าที่ | | Endpoint | Method | หน้าที่ |
|---|---|---| |---|---|---|
| `/api/income_budget_review/search` | GET | query `budget_year_uid`, `faculty_uid`, `budget_location_uid` → คืน `request_expense` 1 เรื่อง (ถ้ามี) + สถานะรีวิวล่าสุด + ประวัติ remark ทั้งหมด | | `/api/income_budget_review/search` | GET | query `budget_year_uid`, `faculty_uid`, `budget_location_uid` → คืน `request_expense` 1 เรื่อง (ถ้ามี) + สถานะปัจจุบัน (คำนวณตามข้อ 3) + ประวัติ `income_budget_review`/`remark_history` ทั้งหมด |
| `/api/income_budget_review/dashboard` | GET | นับจำนวนตามสถานะทั่วทั้งระบบ (ไม่กรองคณะ — ผู้ตรวจสอบเป็นส่วนกลาง): รอตรวจสอบ, ส่งแก้ไข(ที่ยังไม่ถูกแก้), ตรวจสอบแล้ว — พร้อม list รายละเอียด (คณะ/พื้นที่/ปีงบ) ต่อหมวด เหมือน `request_expense/type_2/my_dashboard` | | `/api/income_budget_review/dashboard` | GET | นับจำนวนตามสถานะทั่วทั้งระบบ (ไม่กรองคณะ — ผู้ตรวจสอบเป็นส่วนกลาง): รอตรวจสอบ, ถูกส่งแก้ไข(ที่ยังไม่ผ่านการอนุมัติใหม่), ตรวจสอบแล้ว — พร้อม list รายละเอียด (คณะ/พื้นที่/ปีงบ) ต่อหมวด เหมือน `request_expense/type_2/my_dashboard` |
| `/api/income_budget_review/return_for_edit/{request_expense_uid}` | POST | body `{ remark }` → insert แถวใหม่ `status_id=2` | | `/api/income_budget_review/return_for_edit/{request_expense_uid}` | POST | body `{ remark }` → เปิดแถวผู้อนุมัติคนสุดท้ายกลับมา + ตั้ง `is_draft`/`is_approve` ตามข้อ 4 + insert `remark_history` + log `status_id=2` |
| `/api/income_budget_review/mark_reviewed/{request_expense_uid}` | POST | insert แถวใหม่ `status_id=3` — เงื่อนไข: แถวล่าสุดต้องเป็น `status_id=1` เท่านั้น (กันกดซ้ำ/กดข้ามสถานะ) | | `/api/income_budget_review/mark_reviewed/{request_expense_uid}` | POST | insert แถวใหม่ `status_id=3` — เงื่อนไข: `request_expense.is_approve==true` และแถว log ล่าสุดต้องเป็น `status_id=1` เท่านั้น (กันกดซ้ำ/กดข้ามสถานะ) |
| `/api/income_budget_review/resubmit/{request_expense_uid}` | POST | ใช้ที่ฝั่งคณะ (plan-01) กด "ส่งตรวจสอบใหม่" หลังแก้ไข — เงื่อนไข: แถวล่าสุดต้องเป็น `status_id=2` → insert แถวใหม่ `status_id=1` |
ทุก endpoint ตรวจสอบเงื่อนไข "แถวล่าสุดของเรื่องนี้ต้องเป็นสถานะอะไร" ก่อน insert เสมอ กัน race condition/ ไม่มี endpoint "resubmit" แยกต่างหากฝั่งคณะ — การกลับเข้า "รอตรวจสอบ" เกิดขึ้นอัตโนมัติผ่าน trigger เดิม
กดปุ่มผิดจังหวะ (เช่น กด "ตรวจสอบแล้ว" ซ้ำ หรือกด "ส่งตรวจสอบใหม่" ทั้งที่ยังไม่เคยถูกส่งแก้ไข) (ข้อ 3) เมื่อเชน income-report อนุมัติผ่านครบอีกครั้ง
ทุก endpoint ตรวจสอบเงื่อนไข "สถานะปัจจุบันของเรื่องนี้ต้องเป็นอะไร" ก่อนทำงานเสมอ กัน race condition/
กดปุ่มผิดจังหวะ (เช่น กด "ตรวจสอบแล้ว"/"ส่งแก้ไข" ซ้ำ หรือกดตอนเชนยังไม่อนุมัติผ่านครบ)
## 6. Frontend ## 6. Frontend
**`income-budget-plan-02`** (เพิ่มเข้าไปในหน้าเดิม เหนือฟอร์มค้นหา — โครงเดียวกับ dashboard ที่ทำให้ **`income-budget-plan-02`** (เพิ่มเข้าไปในหน้าเดิม เหนือฟอร์มค้นหา — โครงเดียวกับ dashboard ที่ทำให้
income-report): income-report):
- การ์ด dashboard: chip กดได้ 3 อัน — "รอตรวจสอบ N", "ส่งแก้ไขไปแล้ว N" (ที่ยังไม่ถูกแก้กลับมา), - การ์ด dashboard: chip กดได้ 3 อัน — "รอตรวจสอบ N", "ถูกส่งแก้ไข N" (ที่ยังไม่ผ่านการอนุมัติใหม่),
"ตรวจสอบแล้ว N" — กด chip กางลิสต์ (คณะ/พื้นที่/ปีงบ), กดแถวในลิสต์ = pre-fill ช่องค้นหาแล้วค้นหาทันที "ตรวจสอบแล้ว N" — กด chip กางลิสต์ (คณะ/พื้นที่/ปีงบ), กดแถวในลิสต์ = pre-fill ช่องค้นหาแล้วค้นหาทันที
(รูปแบบเดียวกับ `income-report-approve-dashboard.component.ts` ที่มีอยู่แล้ว — reuse โครงสร้างเดิม) (รูปแบบเดียวกับ `income-report-approve-dashboard.component.ts` ที่มีอยู่แล้ว — reuse โครงสร้างเดิม)
- หลังค้นหาแล้วเจอเรื่อง: แสดง **สถานะปัจจุบันชัดเจน** (badge: "รอตรวจสอบ" / "ส่งแก้ไขแล้ว รอคณะแก้ไข" / - หลังค้นหาแล้วเจอเรื่อง: แสดง **สถานะปัจจุบันชัดเจน** (badge: "รอตรวจสอบ" / "ถูกส่งแก้ไข รอคณะแก้ไข" /
"ตรวจสอบแล้ว") + ตาราง**ประวัติการส่งแก้ไข**ทั้งหมดของเรื่องนี้ (วันที่, remark, ผู้ตรวจ) — โครงตาราง "ตรวจสอบแล้ว") + ตาราง**ประวัติการส่งแก้ไข**ทั้งหมดของเรื่องนี้ (วันที่, remark, ผู้ตรวจ) — โครงตาราง
เดียวกับ "รายละเอียดสำหรับการส่งแก้ไข" ที่มีอยู่แล้วในหน้า income-report (`remark_history`) เดียวกับ "รายละเอียดสำหรับการส่งแก้ไข" ที่มีอยู่แล้วในหน้า income-report (`remark_history` — ตารางเดียวกัน
ที่ผู้อนุมัติคนสุดท้ายจะเห็น remark จากส่วนกลางด้วย)
- ปุ่ม "ส่งแก้ไข" (เปิด dialog ให้พิมพ์ remark ก่อนยืนยัน) และ "ตรวจสอบแล้ว" — โชว์เฉพาะตอนสถานะปัจจุบัน - ปุ่ม "ส่งแก้ไข" (เปิด dialog ให้พิมพ์ remark ก่อนยืนยัน) และ "ตรวจสอบแล้ว" — โชว์เฉพาะตอนสถานะปัจจุบัน
เป็น "รอตรวจสอบ" เท่านั้น เป็น "รอตรวจสอบ" เท่านั้น
**`income-budget-plan-01`** (เพิ่มเข้าไปในหน้าเดิม): **`income-budget-plan-01`** (เพิ่มเข้าไปในหน้าเดิม — ฝั่งคณะดูอย่างเดียว ไม่มีปุ่มใหม่):
- Badge สถานะเดียวกัน ("รอตรวจสอบ" / "ถูกส่งแก้ไข" + remark ล่าสุด / "ตรวจสอบแล้ว") ต่อเรื่องที่ค้นหาอยู่ - Badge สถานะเดียวกัน ("รอตรวจสอบ" / "ถูกส่งแก้ไข" + remark ล่าสุด / "ตรวจสอบแล้ว") ต่อเรื่องที่ค้นหาอยู่ —
- ปุ่ม "ส่งตรวจสอบใหม่" — โชว์เฉพาะตอนสถานะเป็น "ถูกส่งแก้ไข" เท่านั้น ตอน "ถูกส่งแก้ไข" ฝั่งคณะแก้ไข/ส่งอนุมัติใหม่ผ่านหน้า income-report ตามกลไกเดิมที่มีอยู่แล้ว ไม่ต้องมี
ปุ่มใหม่ที่หน้านี้
## 7. นอกขอบเขตรอบนี้ (YAGNI) ## 7. นอกขอบเขตรอบนี้ (YAGNI)