diff --git a/docs/superpowers/specs/2026-09-07-income-budget-review-stage-design.md b/docs/superpowers/specs/2026-09-07-income-budget-review-stage-design.md index 8d63856..97e57c2 100644 --- a/docs/superpowers/specs/2026-09-07-income-budget-review-stage-design.md +++ b/docs/superpowers/specs/2026-09-07-income-budget-review-stage-design.md @@ -19,6 +19,11 @@ **การค้นหา/กรอง:** ที่ `income-budget-plan-02` ต้องกรองตาม **ปีงบประมาณ + หน่วยงาน/คณะ + พื้นที่** เหมือนหน้า income-report เดิม (ยืนยันจากผู้ใช้) +**เมื่อคนกลางกด "ส่งแก้ไข":** ต้องส่งกลับไปหา**ผู้อนุมัติคนสุดท้าย**ของเชนเดิม เพื่อเข้าสู่ขั้นตอน +"ส่งกลับให้ต้นทาง" แบบเดียวกับที่เชน income-report มีอยู่แล้ว (ผู้อนุมัติคนสุดท้ายเห็นว่าเรื่องถูกตีกลับ +แก้ไขได้ตามปกติ หรือจะกดปฏิเสธต่อไปยังลำดับก่อนหน้าจนถึงผู้สร้างก็ได้ตามกลไกเดิมทุกประการ) — ไม่ใช่การ +สร้างสถานะ "ส่งแก้ไข" แยกลอยๆ ที่ไม่เชื่อมกับเชนเดิม (ยืนยันจากผู้ใช้) + ## 2. ทำไมไม่ใช้กลไก `round_no` (resubmit) ที่มีอยู่แล้ว ระบบมี "ส่งอนุมัติเพิ่มเติม" (`ResubmitGetEntity`, เพิ่ม `round_no`) อยู่แล้ว แต่ **ผูกตายตัวกับ @@ -29,8 +34,9 @@ ## 3. โมเดลข้อมูลใหม่ -ตารางใหม่ `income_budget_review` (1 แถวต่อ "รอบการตรวจสอบ" — มีได้หลายแถวถ้าโดนส่งแก้ไขแล้วส่งใหม่หลายรอบ -เพื่อเก็บประวัติ): +ตารางใหม่ `income_budget_review` — **ใช้เป็น log/ประวัติเท่านั้น** ไม่ใช่ตัวเก็บ "สถานะปัจจุบัน" — สถานะ +จริงที่ว่าเรื่องกำลังอยู่ระหว่างแก้ไขหรือไม่ยังคงมาจาก `request_expense.is_draft`/`is_approve` เดิมเป๊ะ +(ดูเหตุผลในข้อ 4): ```csharp 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` (เคสที่ไม่มี ลำดับถัดไปแล้ว — ดู [[2026-09-07-approve-state-workflow-reference]] ข้อ 5) ให้เพิ่ม insert `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` ล่าสุดของ -`request_expense_uid` นั้น (query `OrderByDescending(created_datetime).First()`) +**สถานะปัจจุบันของเรื่องหนึ่งๆ (สำหรับ dashboard/badge):** +- ถ้า `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) │ (auto insert income_budget_review status_id=1) ▼ -income-budget-plan-01 (ฝั่งคณะ) income-budget-plan-02 (ฝั่งส่วนกลาง) - เห็นสถานะ "รอตรวจสอบ" เห็นเรื่องนี้ใน dashboard "รอตรวจสอบ" - │ │ - │ ผู้ตรวจสอบกด "ส่งแก้ไข" + พิมพ์ remark - │◄───────────────────────────────────────┘ - เห็นสถานะ "ถูกส่งแก้ไข" + remark - กด "ส่งตรวจสอบใหม่" (แก้ไขข้อมูลผ่าน income-report/plan-01 ตามปกติ) - │ (insert income_budget_review row ใหม่ status_id=1) - ▼ -income-budget-plan-02 เห็นเรื่องนี้กลับมาที่ "รอตรวจสอบ" อีกครั้ง +income-budget-plan-02 (ฝั่งส่วนกลาง) เห็นเรื่องนี้ใน dashboard "รอตรวจสอบ" │ - ผู้ตรวจสอบกด "ตรวจสอบแล้ว" (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 (ใหม่) @@ -79,31 +140,35 @@ income-budget-plan-02 เห็นเรื่องนี้กลับมา | 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/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/mark_reviewed/{request_expense_uid}` | POST | insert แถวใหม่ `status_id=3` — เงื่อนไข: แถวล่าสุดต้องเป็น `status_id=1` เท่านั้น (กันกดซ้ำ/กดข้ามสถานะ) | -| `/api/income_budget_review/resubmit/{request_expense_uid}` | POST | ใช้ที่ฝั่งคณะ (plan-01) กด "ส่งตรวจสอบใหม่" หลังแก้ไข — เงื่อนไข: แถวล่าสุดต้องเป็น `status_id=2` → insert แถวใหม่ `status_id=1` | +| `/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/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` — เงื่อนไข: `request_expense.is_approve==true` และแถว log ล่าสุดต้องเป็น `status_id=1` เท่านั้น (กันกดซ้ำ/กดข้ามสถานะ) | -ทุก endpoint ตรวจสอบเงื่อนไข "แถวล่าสุดของเรื่องนี้ต้องเป็นสถานะอะไร" ก่อน insert เสมอ กัน race condition/ -กดปุ่มผิดจังหวะ (เช่น กด "ตรวจสอบแล้ว" ซ้ำ หรือกด "ส่งตรวจสอบใหม่" ทั้งที่ยังไม่เคยถูกส่งแก้ไข) +ไม่มี endpoint "resubmit" แยกต่างหากฝั่งคณะ — การกลับเข้า "รอตรวจสอบ" เกิดขึ้นอัตโนมัติผ่าน trigger เดิม +(ข้อ 3) เมื่อเชน income-report อนุมัติผ่านครบอีกครั้ง + +ทุก endpoint ตรวจสอบเงื่อนไข "สถานะปัจจุบันของเรื่องนี้ต้องเป็นอะไร" ก่อนทำงานเสมอ กัน race condition/ +กดปุ่มผิดจังหวะ (เช่น กด "ตรวจสอบแล้ว"/"ส่งแก้ไข" ซ้ำ หรือกดตอนเชนยังไม่อนุมัติผ่านครบ) ## 6. Frontend **`income-budget-plan-02`** (เพิ่มเข้าไปในหน้าเดิม เหนือฟอร์มค้นหา — โครงเดียวกับ dashboard ที่ทำให้ income-report): -- การ์ด dashboard: chip กดได้ 3 อัน — "รอตรวจสอบ N", "ส่งแก้ไขไปแล้ว N" (ที่ยังไม่ถูกแก้กลับมา), +- การ์ด dashboard: chip กดได้ 3 อัน — "รอตรวจสอบ N", "ถูกส่งแก้ไข N" (ที่ยังไม่ผ่านการอนุมัติใหม่), "ตรวจสอบแล้ว N" — กด chip กางลิสต์ (คณะ/พื้นที่/ปีงบ), กดแถวในลิสต์ = pre-fill ช่องค้นหาแล้วค้นหาทันที (รูปแบบเดียวกับ `income-report-approve-dashboard.component.ts` ที่มีอยู่แล้ว — reuse โครงสร้างเดิม) -- หลังค้นหาแล้วเจอเรื่อง: แสดง **สถานะปัจจุบันชัดเจน** (badge: "รอตรวจสอบ" / "ส่งแก้ไขแล้ว รอคณะแก้ไข" / +- หลังค้นหาแล้วเจอเรื่อง: แสดง **สถานะปัจจุบันชัดเจน** (badge: "รอตรวจสอบ" / "ถูกส่งแก้ไข รอคณะแก้ไข" / "ตรวจสอบแล้ว") + ตาราง**ประวัติการส่งแก้ไข**ทั้งหมดของเรื่องนี้ (วันที่, remark, ผู้ตรวจ) — โครงตาราง - เดียวกับ "รายละเอียดสำหรับการส่งแก้ไข" ที่มีอยู่แล้วในหน้า income-report (`remark_history`) + เดียวกับ "รายละเอียดสำหรับการส่งแก้ไข" ที่มีอยู่แล้วในหน้า income-report (`remark_history` — ตารางเดียวกัน + ที่ผู้อนุมัติคนสุดท้ายจะเห็น remark จากส่วนกลางด้วย) - ปุ่ม "ส่งแก้ไข" (เปิด dialog ให้พิมพ์ remark ก่อนยืนยัน) และ "ตรวจสอบแล้ว" — โชว์เฉพาะตอนสถานะปัจจุบัน เป็น "รอตรวจสอบ" เท่านั้น -**`income-budget-plan-01`** (เพิ่มเข้าไปในหน้าเดิม): -- Badge สถานะเดียวกัน ("รอตรวจสอบ" / "ถูกส่งแก้ไข" + remark ล่าสุด / "ตรวจสอบแล้ว") ต่อเรื่องที่ค้นหาอยู่ -- ปุ่ม "ส่งตรวจสอบใหม่" — โชว์เฉพาะตอนสถานะเป็น "ถูกส่งแก้ไข" เท่านั้น +**`income-budget-plan-01`** (เพิ่มเข้าไปในหน้าเดิม — ฝั่งคณะดูอย่างเดียว ไม่มีปุ่มใหม่): +- Badge สถานะเดียวกัน ("รอตรวจสอบ" / "ถูกส่งแก้ไข" + remark ล่าสุด / "ตรวจสอบแล้ว") ต่อเรื่องที่ค้นหาอยู่ — + ตอน "ถูกส่งแก้ไข" ฝั่งคณะแก้ไข/ส่งอนุมัติใหม่ผ่านหน้า income-report ตามกลไกเดิมที่มีอยู่แล้ว ไม่ต้องมี + ปุ่มใหม่ที่หน้านี้ ## 7. นอกขอบเขตรอบนี้ (YAGNI)