Task #2380
Cập nhật bởi Tiến Lực Phạm cách đây 20 ngày
- Khi 1 khn đã đc duyệt và xếp lịch => bị sai/chỉnh sửa lớp huấn luyện - đưa ra giải pháp khi xoá/điều chỉnh lớp huấn luyện trong KHN - YÊU CẦU: thay đổi 1 lớp trong khn thì sẽ phải xử lý như thế nào để ít thao tác và nhanh chóng nhất Giải pháp của Dev Lực: Thay đổi Kế hoạch năm Đây là một bài toán nghiệp vụ **rất thực tế và điển hình** trong các hệ thống điều hành huấn luyện quân sự/đào tạo đại học. Trong hệ thống, chuỗi liên kết dữ liệu đi theo trục: {Kế hoạch năm (KHN)} -> {TBNVHL (Notification)} -> {Kế hoạch huấn luyện (KHHL)} -> {Thời khóa biểu (TKB Sessions)} --- ### I. ĐÁNH GIÁ NGHẼN NGHIỆP VỤ NẾU LÀM THEO CÁCH THỦ CÔNG HIỆN TẠI Nếu một Kế hoạch năm (KHN) đã duyệt và đã xếp TKB, mà hệ thống áp dụng cơ chế khóa cứng (hiện (như hàm `ValidateTrainingNotificationLockedMonthsAsync` hiện tại đang chặn sửa khi đã có TBNVHL), người dùng sẽ rơi vào **"Cơn ác mộng thao tác"**: 1. Phải hủy duyệt TKB -> $\rightarrow$ Xóa/dỡ lịch các tuần/tháng. 2. Hủy duyệt/Xóa Kế hoạch huấn luyện. 3. Xóa các Thông báo nhiệm vụ huấn luyện (TBNVHL). 4. Hủy duyệt KHN -> $\rightarrow$ Chuyển về Lưu nháp -> $\rightarrow$ Sửa/xóa lớp. 5. Gửi duyệt lại từ đầu toàn bộ các tầng và **phải xếp lại TKB từ đầu** (mất toàn bộ công sức xếp giảng viên, giảng đường, thao trường của cả các lớp không liên quan). 👉 **Yêu cầu đặt ra:** Cần giải pháp **"Khu trú ảnh hưởng (Blast Radius Isolation)"** kết hợp **"Đồng bộ tự động 1 chạm (1-Click Smart Cascade)"** — tức là **sửa tại KHN, hệ thống tự động xử lý các module bên dưới với số lần click chuột ít nhất và không ảnh hưởng đến các lớp khác**. --- ### II. GIẢI PHÁP TỔNG THỂ CHO TỪNG TÌNH HUỐNG Khi quản lý và điều chỉnh thay đổi lớp trong Kế hoạch năm, KHN đã duyệt, thực tế sẽ rơi vào 3 tình huống nghiệp vụ: ``` [ QUẢN LÝ & ĐIỀU CHỈNH KẾ HOẠCH NĂM THAY ĐỔI LỚP TRONG KHN ] │ ┌────────────────────────────┼────────────────────────────┐ ┌──────────────────────────────┐ ▼ ▼ ▼ 【Tình huống 1】 【Tình huống 2】 【Tình huống 3】 XÓA HẲN 1 LỚP ĐIỀU CHỈNH TIẾN ĐỘ NHÂN BẢN KHN CHO LỚP (Lớp hoãn học, gán nhầm) (Lớp học khung riêng) CÙNG CHƯƠNG TRÌNH ĐÀO TẠO │ │ │ ▼ ▼ ▼ Thu hồi lịch cục bộ Tách nhánh (Fork Plan) Sao chép phân bổ bài học (Chỉ xóa session lớp đó) thành KHN riêng cho lớp (Tạo KHN Lưu nháp mới) ``` --- #### 1. TÌNH HUỐNG 1: XÓA HOÀN TOÀN 1 LỚP KHỎI KHN *(Ví dụ: Lớp bị hoãn khóa học, chuyển sang năm sau hoặc chọn nhầm lớp vào KHN này)* * **Mục tiêu:** Xóa lớp khỏi KHN nhưng **không xóa TKB của các lớp khác**, giải phóng tài nguyên giảng viên/phòng học. * **Cách xử lý tối ưu (Ít thao tác nhất):** * **Tại KHN:** Người dùng bấm nút **"Xóa lớp khỏi KHN"** -> $\rightarrow$ Hệ thống bật Popup xác nhận kèm **Ma trận ảnh hưởng** (Hiển thị rõ: *Lớp đang có 3 TBNVHL, 1 KHHL, 45 tiết TKB đã xếp*). * **Hệ thống tự động chạy ngầm trong 1 Transaction:** 1. **Với TKB (`TrainingTimetable_Session`):** * Tự động xóa các tiết của riêng lớp đó (`fixed_class_id = @classId`) từ thời điểm hiện tại trở đi (`date >= TODAY`). * **Giải phóng tài nguyên:** Giảng viên (`lecturer_id`), Phòng học (`room_id`) của các tiết bị xóa lập tức được giải phóng để lớp khác sử dụng. * *Lưu ý lớp ghép:* Nếu là tiết học ghép nhiều lớp, hệ thống chỉ tách lớp bị xóa ra khỏi nhóm ghép, **giữ nguyên tiết học cho các lớp còn lại**. * Các tiết đã diễn ra trong quá khứ (`date < TODAY`) được giữ nguyên trạng thái đóng băng để lưu vết hồ sơ đào tạo. 2. **Với KHHL & TBNVHL:** * Tự động xóa liên kết của lớp trong `TrainingNotification_FixedClass` và `CH_TrainingClass`. * Chạy hàm cập nhật lại tổng số tiết/tổng hợp (Recalculate Summary). 3. **Với KHN:** Xóa bản ghi trong `YearPlan_FixedClass`. * **Thao tác người dùng:** Đúng **1 click** (Chọn lớp -> $\rightarrow$ Bấm Xóa -> $\rightarrow$ Nhập lý do -> $\rightarrow$ Xác nhận). --- #### 2. TÌNH HUỐNG 2: ĐIỀU CHỈNH TIẾN ĐỘ / NỘI DUNG HỌC CỦA RIÊNG LỚP ĐÓ *(Ví dụ: Lớp A cần học thêm một số môn đặc thù hoặc giảm tiết, lệch khung với các lớp còn lại trong KHN)* * **Vấn đề:** Trong hệ thống hiện tại, 1 KHN gồm nhiều lớp dùng chung bảng phân bổ bài học `YearPlan_Lesson`. Nếu sửa trực tiếp bài học trên KHN này sẽ làm lệch luôn tiến độ của các lớp còn lại. * **Cách xử lý tối ưu (Cơ chế "Tách nhánh KHN" - Forking Plan):** * **Tại KHN:** Người dùng chọn Lớp A -> $\rightarrow$ Bấm **"Tách thành Kế hoạch riêng"**. * **Hệ thống tự động:** 1. Rút Lớp A ra khỏi KHN hiện tại (KHN cũ giữ nguyên cho các lớp còn lại). 2. Clone sang một bản ghi KHN mới dành riêng cho Lớp A (kế thừa lại toàn bộ bài học và lịch TKB đã xếp đến thời điểm hiện tại). 3. Mở giao diện lập kế hoạch của KHN mới để người dùng chỉnh sửa bài học/tiết cho Lớp A. 4. Khi duyệt KHN mới, hệ thống chỉ đẩy chênh lệch (Delta) vào TKB để xếp bổ sung các bài mới. --- #### 3. TÌNH HUỐNG 3: NHÂN BẢN KẾ HOẠCH NĂM CHO CÁC LỚP CÙNG CHƯƠNG TRÌNH ĐÀO TẠO (CLONE PLAN) *(Ví dụ: Tiểu đoàn 1 và Tiểu đoàn 2 cùng học CTĐT Sĩ quan Chỉ huy Tham mưu Lục quân Khóa 89; Tiểu đoàn 1 đã lập xong KHN chuẩn, cần nhân bản sang cho các lớp thuộc Tiểu đoàn 2)* * **Mục tiêu:** Tái sử dụng 100% công sức phân bổ bài học theo tháng (`YearPlan_Lesson`), giảm 95% thời gian nhập liệu, đảm bảo tính chuẩn hóa khung chương trình và **cách ly độc lập hoàn toàn** giữa các lớp. * **Các ràng buộc nghiệp vụ bắt buộc (Validation Rules):** 1. **Cùng Chương trình đào tạo (`curriculum_version_id`):** Lớp đích bắt buộc phải thuộc cùng CTĐT với KHN nguồn (`FixedClass -> Cohort -> curriculum_version_id`), đảm bảo cây bài học (`lesson_id`) và môn học (`subject_version_id`) tồn tại và hợp lệ. 2. **Cùng Năm đào tạo (`year_no`):** Lớp đích phải cùng năm thứ đào tạo (ví dụ: cùng học năm thứ 2), tránh vi phạm định mức môn theo năm học (`ValidateSubjectYearPeriodsAsync`). 3. **Lớp đích chưa có KHN trong năm học:** Mỗi lớp trong một năm học chỉ thuộc tối đa 1 KHN (tuân thủ logic `GetAllFixedClassNotInYearPlan`). 4. **Kiểm tra định mức tiết tối đa tháng (`ValidateMonthlyPeriodsAsync`):** Tự động đối soát số tiết sao chép với định mức `ConfigSlot` của lớp đích để cảnh báo nếu tháng nào vượt khung. 5. **Trạng thái Kế hoạch mới:** KHN mới **bắt buộc ở trạng thái "Lưu nháp" (`iYearPlanStatus.LuuNhap`)**, kể cả khi KHN nguồn đã duyệt. 6. **Tuyệt đối KHÔNG tự động nhân bản TBNVHL, KHHL hay TKB:** TKB gắn liền với giảng viên, thao trường, phòng học cụ thể; nhân bản TKB sẽ gây xung đột tài nguyên. KHN mới sau khi được duyệt sẽ sinh TBNVHL và xếp TKB độc lập theo quy trình chuẩn. * **Cách xử lý tối ưu:** * **Tại danh sách KHN hoặc màn lập KHN:** Người dùng chọn KHN gốc -> Bấm nút **"Nhân bản kế hoạch"**. * Bật Modal chọn lớp đích (hệ thống tự động lọc danh sách lớp thỏa mãn 3 điều kiện trên). * Người dùng xác nhận -> Hệ thống sao chép Header `YearPlan`, gán `YearPlan_FixedClass` mới và Bulk Copy toàn bộ `YearPlan_Lesson` trong 1 Transaction -> Điều hướng ngay sang màn hình Lập kế hoạch của bản ghi mới để rà soát/tinh chỉnh. --- ### III. THIẾT KẾ GIAO DIỆN & TRẢI NGHIỆM NGƯỜI DÙNG (UX/UI) Để đảm bảo tính pháp lý quân sự (không sửa ngầm) nhưng vẫn nhanh chóng: 1. **Vị trí đặt tính năng:** * Tại màn hình danh sách Kế hoạch năm (`LapKeHoachNamListComponent`): Bổ sung các nút hành động: * **"Nhân bản kế hoạch"** (Clone Plan). * **"Điều chỉnh lớp"** (Adjust Class) trên từng dòng hoặc trong xem chi tiết. * Tại màn xem chi tiết (`KeHoachNamViewDetailComponent`): Menu (`KeHoachNamViewDetailComponent`), thêm menu thao tác nhanh. tác: **"Điều chỉnh lớp"**. 2. **Modal Hướng dẫn Thông minh (Impact Wizard - Dùng cho Điều chỉnh/Xóa/Tách lớp):** 2 bước):** * **Bước 1 (Chọn hành động):** * Chọn lớp cần thao tác. * Chọn loại điều chỉnh: `[Xóa bỏ]` hoặc `[Tách lớp ra kế hoạch riêng]`. * Nhập *Lý do điều chỉnh* (Bắt buộc theo nghiệp vụ quân sự: ví dụ *"Theo Quyết định số... của Hiệu trưởng"*). * **Bước 2 (Xem trước ảnh hưởng & Xác nhận):** * Hệ thống hiển thị bảng tóm tắt: * *Số đợt TBNVHL bị ảnh hưởng*: X đợt. * *Số tiết TKB sẽ tự động hủy/chuyển đổi*: Y tiết (từ ngày DD/MM/YYYY). * *Cảnh báo tiết học ghép* (nếu có). * Bấm nút: **`[Xác nhận & Tự động đồng bộ]`**. 3. **Modal Nhân bản Kế hoạch năm (Clone Plan Modal):** * **Thông tin KHN gốc:** Hiển thị rõ Mã, Tên, Năm học, CTĐT, Năm thứ. * **Thông tin KHN mới:** * Mã KHN mới (Hệ thống tự sinh gợi ý theo mã lớp đích). * Tên KHN mới. * **Chọn Lớp đích:** Danh sách checkbox chỉ hiển thị các lớp thỏa mãn (Cùng CTĐT, Cùng năm thứ, Chưa có KHN trong năm học). * **Tóm tắt dữ liệu sẽ nhân bản:** * *Số môn học sao chép:* A môn. * *Số bài học sao chép:* B bài học (Tổng C tiết phân bổ trên 10-12 tháng). * Bấm nút: **`[Nhân bản sang kế hoạch mới]`** -> Mở ngay màn Lập kế hoạch để người dùng kiểm tra. --- ### IV. KIẾN TRÚC KỸ THUẬT BACKEND (ĐỂ ĐẢM BẢO CHẠY NHANH & TOÀN VẸN) Để hiện thực hóa luồng này mà không bị lỗi timeout hoặc deadlock: 1. **Tạo API Endpoints Endpoint chuyên biệt:** * **API Điều chỉnh/Tách lớp:** * `POST api/tenant/YearPlans/adjust-fixed-class` * DTO truyền lên: * DTO: ```typescript { { year_plan_id: string; action_type: 'DELETE' | 'REPLACE' | 'FORK'; source_fixed_class_id: string; target_fixed_class_id?: string; // Dùng khi REPLACE reason: string; effective_from_date: Date; // Mặc định là ngày hôm nay } ``` * **API Nhân bản Kế hoạch năm:** * `POST api/tenant/YearPlans/clone` * DTO: ```typescript { source_year_plan_id: string; target_code: string; target_name: string; target_fixed_class_ids: string[]; } ``` 2. **Cơ chế xử lý trong Backend:** * **Bọc toàn bộ trong 1 `DbTransaction`:** Đảm bảo hoặc thành công toàn bộ từ KHN đến TKB, hoặc rollback sạch sẽ nếu có lỗi. * **Cơ chế xử lý khi Clone KHN:** 1. Validate các lớp đích: đảm bảo cùng `curriculum_version_id`, cùng `year_no` và chưa tồn tại trong KHN nào của năm học đó. 2. Insert bản ghi cha `YearPlan` mới với `status = iYearPlanStatus.LuuNhap`. 3. Insert danh sách `YearPlan_FixedClass` cho các lớp đích. 4. Query toàn bộ `YearPlan_Lesson` của KHN nguồn và Bulk Insert sang KHN mới (giữ nguyên `subject_version_id`, `lesson_id`, `calendar_month`, `calendar_year`, `total_periods`, `details_json`, `all_periods_selected`). 5. Tái sử dụng hàm validation: `ValidateMonthlyPeriodsAsync` và `ValidateSubjectYearPeriodsAsync` để đảm bảo dữ liệu mới không vi phạm quota của lớp đích. * **Tái sử dụng Engine TKB đã tối ưu (khi xóa/tách lớp):** ưu:** * Khi xóa/sửa session hàng loạt của lớp đó, tái sử dụng service `TrainingTimetable_SessionService` với cơ chế **Warm Cache** + **Defer Side-Effects** + **Flush gom nhóm key** (theo tài liệu [Toi_uu_luong_luu_xoa_lich.md](file:///d:/Projects/SQLQ_LichHuanLuyen/source/TaiLieu/Notes/Toi_uu_luong_luu_xoa_lich.md)). Điều này giúp việc xóa 50-100 tiết của lớp diễn ra trong **dưới 1 giây**. * **Ghi vết lịch sử tự động (`TrainingTimetable_ChangeLog`):** * Ghi nhận lịch sử điều chỉnh với lý do của người dùng để phục vụ công tác thanh tra/kiểm tra sau này. * **Bypass Validation chặn thông thường:** Trong luồng này, thêm cờ `Context.IsAdminAdjustment = true` để bỏ qua việc throw exception tại `ValidateTrainingNotificationLockedMonthsAsync`. --- ### BẢNG SO SÁNH HIỆU QUẢ | Tiêu chí | Quy trình thủ công hiện tại | Giải pháp đề xuất | | :--- | :--- | :--- | | **Xóa / Điều chỉnh lớp trong KHN đã duyệt** **Số bước thao tác** | 8 – 10 bước qua 4 màn hình (30 khác nhau | **1 bước** tại màn hình Kế hoạch năm | | **Thời gian thực hiện** | 30 phút – vài tiếng nếu (nếu phải xếp lại TKB) | **1 click** tại KHN, xử lý ngầm **3 – 5 giây**, cách ly 100% các lớp khác giây** (xử lý ngầm tự động) | | **Lập KHN cho lớp mới cùng CTĐT** | Nhập thủ công lại từ đầu hàng trăm bài học (**1 – 2 ngày**) | **Nhân bản 1 chạm** kế thừa toàn bộ khung bài học (**1 – 2 giây**), chuyển về Lưu nháp để rà soát | | **Tác động đến lớp khác** | Nguy cơ làm hỏng hoặc phải xếp lại lịch cả khóa | **0%** (Cách ly hoàn toàn giữa các lớp) lớp khác) | | **Tính toàn vẹn dữ liệu** | Dễ sót dữ liệu giữa các module | 100% đồng bộ qua Database Transaction | | **Tính pháp lý quân đội** | Mất dấu vết cũ do xóa đi làm lại | Lưu vết đầy đủ qua `ChangeLog` và `RevisionRequest` | Giải pháp này giải quyết triệt để cả hai bài toán cốt lõi: 1. **Tiền kỳ (Lập kế hoạch):** Nhân bản nhanh chóng, chuẩn hóa theo Chương trình đào tạo. 2. **Hậu kỳ (Điều hành thực tế):** Người toán: **Người dùng chỉ thao tác tại điểm phát sinh nhu cầu (Kế hoạch năm), toàn bộ các mắt xích phía sau (TBNVHL, KHHL, TKB) tự động thích ứng chính xác theo quan hệ dữ liệu đã được thiết lập sẵn. sẵn.**