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:
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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user