Files
solution-erp/.claude/workflows/runs/2026-07-27-S155-pe-delete-approver/sub-invest-menu-3.md
2026-07-27 09:39:31 +07:00

21 KiB
Raw Blame History

sub-invest-menu-3 — lát cắt 3: MENU restructure + màn "Đã xóa" + "người đứng đầu phòng"

run: 2026-07-27-S155-pe-delete-approver · vai: investigator-codebase · ghi-đĩa-trong-lúc-làm (chống #53) Nhãn: [CODE] = đọc thẳng file có file:line · [SUY LUẬN] = diễn giải · [CHƯA XÁC MINH] = chưa chứng được


Q-A — Menu định nghĩa ở ĐÂU, thêm mục mới chạm những gì

A-1 [CODE] 🔴 MENU LÀ SEED DB, KHÔNG phải hard-code FE — dứt khoát

Bằng chứng 3 tầng:

  1. Entity + bảng thật: src/Backend/SolutionErp.Domain/Identity/MenuItem.cs — bảng MenuItems (Key PK, Label, ParentKey, Order, Icon, +IsVisible/DisplayLabel từ Mig 27).
  2. Nơi khai 4 mục con của Pe_<Code>: src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs:1867-1878
foreach (var code in MenuKeys.PurchaseEvaluationTypeCodes)
{
    tree.Add((MenuKeys.PurchaseEvaluationGroup(code),         label,         MenuKeys.PurchaseEvaluations, peOrder++, "FileCheck"));  // :1870
    tree.Add((MenuKeys.PurchaseEvaluationWorkflowView(code),  "Luồng duyệt", ...group..., peOrder++, "Network"));                    // :1874
    tree.Add((MenuKeys.PurchaseEvaluationList(code),          "Danh sách",   ...group..., peOrder++, "List"));                        // :1875
    tree.Add((MenuKeys.PurchaseEvaluationCreate(code),        "Thao tác",    ...group..., peOrder++, "Plus"));                        // :1876
    tree.Add((MenuKeys.PurchaseEvaluationPending(code),       "Duyệt",       ...group..., peOrder++, "CheckCircle2"));                // :1877
}

Đúng 4 mục owner thấy trên prod = 4 dòng :1874-1877. Nhãn "Duyệt" nằm ở :1877, key = Pe_<Code>_Pending.

  1. FE render từ API, KHÔNG hard-code danh sách: fe-user/src/components/Layout.tsx render AuthContext.menu (fetch /api/menus/meGetMyMenuTreeQuery). FE chỉ có map key → route (resolvePath, Layout.tsx:55-137), không có danh sách mục.

A-2 [CODE] KHÔNG cần migration — nhưng CẦN chạy seed (restart API)

DbInitializer.cs:1888-1914 = upsert idempotent: existingItems theo Key → thiếu thì INSERT, có rồi thì chỉ sửa Order (:1899-1903), KHÔNG đụng Label/ParentKey/Icon (:1897-1898 comment nói rõ). ⇒ Thêm 2 mục mới Đã duyệt + Đã xóa = thêm 2 dòng tree.Add(...) + 2 const trong MenuKeys.csAPI restart là có (DbInitializer chạy lúc startup). KHÔNG cần EF migration (không đổi schema). [SUY LUẬN] nhưng dựa thẳng vào cơ chế upsert ở :1891-1908.

A-3 [CODE] Không phải 4-place mirror — là 6 chỗ cho mục PE mới

# Chỗ File:line Ghi chú
1 Hằng key BE MenuKeys.cs:134-141 (thêm PurchaseEvaluationApproved/Deleted(typeCode)) factory $"Pe_{typeCode}_<X>"
2 Seed tree DB DbInitializer.cs:1874-1877 (+2 dòng) 🔴 chỗ FE-only-mindset hay quên
3 Seed permission admin DbInitializer.cs:2092-2096 + :2492-2496 2 nơi, xem A-3bis
4 FE resolvePath fe-user/Layout.tsx:120 regex + :128-134 branch · fe-admin/Layout.tsx:107 regex + :110-118 regex (List|Create|Pending|WfView) phải nới
5 FE route App.tsx (chỉ nếu dùng URL MỚI; nếu tái dùng /purchase-evaluations?... thì 0 chỗ) xem Q-B
6 FE menuKeys.ts fe-{admin,user}/src/lib/menuKeys.ts 🔴 xem A-3ter: hiện KHÔNG có Pe_* ⇒ có thể là 0 chỗ

A-3bis [CODE] Chỗ thứ 5 & 6 THẬT: 2 seed permission

  • DbInitializer.cs:2092-2096 — gom peKeys (Group/WfView/List/Create/Pending)
  • DbInitializer.cs:2492-2496 — gom menuKeys (cùng 5 key)

2 danh sách song song liệt kê ĐÍCH DANH 5 key PE. Thêm key mới mà quên 1 trong 2 = permission seed lệch. (Đã đọc ngữ cảnh 2 hàm ở A-6.)

A-3ter [CODE] menuKeys.ts FE không chứa Pe_* — lệnh đã chạy

grep -rn "Pe_DuyetNcc|Pe_\$\{" fe-user/src/lib/menuKeys.ts fe-admin/src/lib/menuKeys.ts   → 0 dòng output

⇒ FE mirror const chỉ có key top-level; Pe_* sinh động qua regex trong Layout.tsx. Không phải chỗ phải sửa.

A-4 [CODE] Đổi tên DuyệtĐang duyệt: 2 đường, 1 rủi ro thật

Đường rẻ nhất (0 code): admin sửa DisplayLabel qua /system/menu-visibility (fe-admin/src/pages/system/MenuVisibilityPage.tsxPATCH MenusController.cs:26-32). ⚠️ CHỈ ăn ở fe-userMenuDtos.cs:14 ghi rõ "override label cho fe-user; fe-admin luôn render Label gốc"; fe-user đọc effectiveLabel() (Layout.tsx:161-163), fe-admin không có hàm này. Đường code: thêm [Pe_<code>_Pending] = "Đang duyệt" vào labelBackfill (DbInitializer.cs:1918-1924) — vì vòng upsert :1893-1904 KHÔNG update Label row đã tồn tại. Sửa mỗi :1877KHÔNG ăn trên DB đã seed.

🔴 Rủi ro nếu ĐỔI KEY (không chỉ label): đo trên Dev DB —

sqlcmd -d SolutionErp_Dev -Q "SELECT MenuKey,COUNT(*) FROM Permissions WHERE MenuKey LIKE 'Pe[_]%' GROUP BY MenuKey"
→ 10 key × 13 row (13 role) mỗi key, gồm Pe_DuyetNcc_Pending = 13 row

⇒ Permission row là explicit, không phải chỉ dựa inherit. Đổi key = 13 row/key thành mồ côi ⇒ role mất quyền (rơi về inherit từ root PurchaseEvaluations — có cứu, nhưng cờ CRUD thành cờ của ROOT chứ không phải cờ đã tinh chỉnh). Khuyến nghị: GIỮ key Pe_*_Pending, chỉ đổi label.

A-5 [CODE] Cả 2 app đều có menu này — nhưng lệch 1 nhánh

fe-user Layout.tsx:120 regex (List|Create|Pending|WfView) · fe-admin Layout.tsx:107 regex (List|Create|Pending)fe-admin THIẾU WfView ⇒ leaf "Luồng duyệt" resolve nullMenuLeaf drop im lặng bên admin. Thêm leaf mới phải sửa cả 2, và biết trước là 2 file này đã lệch sẵn (không byte-identical như các file PE khác).


Q-B — Dựng 2 màn Đã duyệt + Đã xóa

B-1 [CODE] Màn Duyệt KHÔNG lọc phase ở BE — nó gọi endpoint khác rồi lọc ở FE

pendingMe=1 không phải tham số BE. FE rẽ nhánh sang endpoint inbox:

// fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx:61-68
if (pendingMe) {
  const res = await api.get<PeListItem[]>('/purchase-evaluations/inbox', {
    params: { type: typeFilter ?? undefined, approvalWorkflowId: approvalWorkflowId || undefined },
  })
  return { items: res.data, total: res.data.length, page: 1, pageSize: res.data.length }
}

BE = GetMyPurchaseEvaluationInboxQuery (PurchaseEvaluationFeatures.cs:715-771), controller PurchaseEvaluationsController.cs:30-35. Không có Phase == ChoDuyet đơn lẻ — mà là 2 nhánh OR (:762):

where eligiblePhases.Contains(e.Phase) || v2InboxIds.Contains(e.Id)

v2InboxIds = ResolveV2InboxIdsAsync(userId) (:748) — phiếu V2 mà actor là approver Cấp hiện tại. Lọc "Đã gửi duyệt" cứng nằm ở FE, sau khi nhận data:

// :182-183
const filtered = pendingMe
  ? allRows.filter(p => getPeDisplayStatus(p.phase) === PeDisplayStatus.DaGuiDuyet)

[SUY LUẬN] ⇒ màn Đang duyệt giữ nguyên 100%, chỉ đổi nhãn menu. Không đụng BE.

B-2 [CODE] Đã duyệt = ĐÃ CHẠY ĐƯỢC HÔM NAY, 0 dòng BE, 0 route mới

Tham số phase= đã tồn tại đủ 3 tầng:

Tầng file:line
Controller PurchaseEvaluationsController.cs:23 [FromQuery] PurchaseEvaluationPhase? phase = null
Query record PurchaseEvaluationFeatures.cs:565 PurchaseEvaluationPhase? Phase = null
Áp filter PurchaseEvaluationFeatures.cs:620 if (request.Phase is not null) q = q.Where(x => x.e.Phase == request.Phase)
FE đọc URL PurchaseEvaluationsListPage.tsx:39 sp.get('phase'):75 truyền xuống API
FE đã có dropdown :389-403 — option "Đã duyệt" map String(PurchaseEvaluationPhase.DaDuyet) (:395)

DaDuyet = 7 (PurchaseEvaluationPhase.cs:23). ⇒ URL /purchase-evaluations?type=1&phase=7 chạy ngay. Việc phải làm = 1 key menu + 1 nhánh resolvePath (×2 app) + 1 dòng seed. Ô #5 bảng A-3 = 0 chỗ (không route mới, không page mới). ⚠️ 1 khác biệt UX: nhánh phase đi đường list (có phân trang pageSize:50, readOnly={!pendingMe} :590/:676detail read-only) — khác nhánh inbox (không phân trang).

B-3 [CODE] Xác minh 2 con số bằng lệnh của chính tôi

grep -rn "IgnoreQueryFilters" src/Backend --include=*.cs | wc -l   → 0
grep -rn "b\.HasQueryFilter(" src/Backend --include=*.cs | wc -l   → 12   (20 hit thô, 8 là comment)
grep -rn "IgnoreQueryFilters" tests --include=*.cs | wc -l         → 15   ← BẤT NGỜ, xem B-4

Cả 2 con số sub BE báo đều ĐÚNG: IgnoreQueryFilters = 0 hit toàn src/Backend; PE global filter ở PurchaseEvaluationConfiguration.cs:84.

B-4 [CODE] 🎁 Đã có 15 tiền lệ IgnoreQueryFilters — nhưng ở tests, không phải production

tests/.../Application/MasterCatalogFilteredUniqueTests.cs:71,102,134 · PeWorkItemBudgetTests.cs:260 · Services/SupplierExcelImportServiceTests.cs:78,174 · Services/SupplierImportV2DedupTests.cs (×9). Khuôn dùng: await db.Suppliers.IgnoreQueryFilters().CountAsync()"không có bản ghi phantom nào kể cả đã soft-delete" (SupplierExcelImportServiceTests.cs:78). ⇒ [SUY LUẬN] Đội đã quen cú pháp này; cái mới chỉ là lần đầu ở tầng production, không phải kỹ thuật lạ. Test cho màn "Đã xóa" có khuôn assert sẵn để bắt chước.

B-5 [CODE] 🔴 Chỗ HẸP NHẤT + ranh giới an toàn (câu quan trọng nhất của Q-B)

Vì sao an toàn về mặt cấu trúc: IgnoreQueryFilters() là toán tử trên MỘT IQueryable, không phải cờ trên DbContext. Cả 2 hàm accumulator đều dựng peers từ db.PurchaseEvaluations.AsNoTracking() mới tinh mỗi lần gọi:

  • PeBudgetAccumulator.cs:42-44 (ComputeAsync) — var peers = db.PurchaseEvaluations.AsNoTracking().Where(...)
  • PeBudgetAccumulator.cs:100-102 (ComputePendingAsync) — y hệt, var peers = db.PurchaseEvaluations.AsNoTracking().Where(...)

Không có đường rò từ 1 query list sang accumulator, trừ khi vi phạm 1 trong 3 điều sau. Đây là ranh giới cần ghi vào checklist:

🔴 CẤM Vì sao
Đặt IgnoreQueryFilters() trong helper dùng chung trả IQueryable<PurchaseEvaluation> mà accumulator/list cũng gọi rò gián tiếp — biến 1 dòng thành N call-site
Gỡ / sửa HasQueryFilterPurchaseEvaluationConfiguration.cs:84 phá toàn hệ, đúng thứ đang cho owner "xóa là hết ăn lũy kế"
Bật IgnoreQueryFilters ở query detail rồi tái dùng cho snapshot/finalize PeBudgetAccumulator được gọi từ cả display lẫn ApplyBudgetSnapshotOnFinalizeAsync (comment PeBudgetAccumulator.cs:10-12) — chung đường

Đề xuất hẹp nhất [SUY LUẬN]: query/handler RIÊNG (vd ListDeletedPurchaseEvaluationsQuery) + endpoint riêng GET /api/purchase-evaluations/deleted, IgnoreQueryFilters() đặt đúng 1 lần trong thân handler đó. KHÔNG thêm cờ includeDeleted vào ListPurchaseEvaluationsQuery — vì handler đó đã có 4 filter + IDOR block (:596-617) + subquery projection (:656-685); nhét cờ vào = mọi call-site cũ đều thành đường có-thể-rò và phải review lại.

B-6 [CODE] KHÔNG có tiền lệ "thùng rác" nào trong repo — nói thẳng

Lệnh đã chạy:

grep -rniE "Thùng rác|thung-rac|deleted=|showDeleted|includeDeleted" fe-user/src fe-admin/src   → 0 màn thùng rác
grep -rniE "IsDeleted\s*=\s*false" src/Backend --include=*.cs | grep -v Migrations              → 0 dòng
grep -rniE "Restore|Undelete|Khôi phục" src/Backend --include=*.cs                              → 1 hit DUY NHẤT, là chuỗi thông báo

Hit duy nhất: EmployeeFeatures.cs:162$"User {user.UserName} đã có hồ sơ NV (đã xoá mềm). Cần khôi phục thay vì tạo mới."câu chữ có, chức năng KHÔNG có (ghost-wire loại "message nói về thứ chưa tồn tại"). Màn "Đã xóa" sẽ là màn đầu tiên loại này trong toàn hệ.

B-7 [CODE] Restore: 0 cơ chế sẵn — chỉ khảo sát, không chốt

Không endpoint nào set IsDeleted=false (lệnh B-6). Chiều ngược lại — AuditingInterceptor.cs:56-62 chỉ chuyển Deleted→Modified, không có nhánh un-delete. Dữ kiện để owner quyết [SUY LUẬN]:

  • Restore = 1 PATCH set 3 field (IsDeleted=false, DeletedAt=null, DeletedBy=null) — rẻ về code;
  • nhưng phiếu restore quay lại ăn lũy kế ngay (accumulator không lọc gì thêm ngoài query filter) ⇒ đúng thứ owner muốn tránh sẽ quay lại. Nếu owner muốn restore thì phải nói rõ phiếu restore về phase nào (giữ ChoDuyet = ăn lũy kế lại · ép về DangSoanThao = không ăn theo ComputePendingAsync:105 loại DangSoanThao).
  • ⚠️ Suppliers/Projects/Departments có UNIQUE filtered [IsDeleted]=0 (Mig 47) — restore ở các bảng đó có thể đụng key trùng. PE thì KHÔNG (không có unique filtered trên PurchaseEvaluations) ⇒ riêng PE restore không vướng index.

B-8 [CODE] 🎁 Vết xóa KHÔNG bị chôn — changelog sống sót, không cần IgnoreQueryFilters

PurchaseEvaluationChangelog : BaseEntity (PurchaseEvaluationChangelog.cs:9) — BaseEntity, KHÔNG phải AuditableEntitykhông có IsDeletedkhông có query filter (12 HasQueryFilter không có cái nào cho bảng này). Query lịch sử lọc chỉ theo Id, không join PE:

// PurchaseEvaluationFeatures.cs:1424-1425
return await db.PurchaseEvaluationChangelogs.AsNoTracking()
    .Where(c => c.PurchaseEvaluationId == request.PurchaseEvaluationId)

ListPurchaseEvaluationChangelogsQuery chạy được nguyên vẹn cho phiếu đã xóa, 0 sửa đổi. 🔴 Cái BỊ chôn là chỗ khác: GetPurchaseEvaluationQuery (detail bundle) load PE root ⇒ dính filter ⇒ 404. Nên: mở lại lịch sử = miễn phí; mở lại DETAIL = phải IgnoreQueryFilters chỗ thứ 2. Owner cần chốt: màn "Đã xóa" chỉ LIST (rẻ, 1 chỗ) hay LIST + mở được phiếu (2 chỗ).


Q-C — "NGƯỜI ĐỨNG ĐẦU PHÒNG" map vào đâu?

C-1 [CODE] 🎯 CÓ TỒN TẠIDepartment.ManagerUserId, chú thích đúng chữ "Trưởng Phòng ban"

// src/Backend/SolutionErp.Domain/Master/Department.cs:9
public Guid? ManagerUserId { get; set; }                 // TPB — Trưởng Phòng ban

⇒ Giả thuyết (b) ĐÚNG về schema. Không cần migration để có field.

C-2 [CODE] 🔴 NHƯNG field đó RỖNG + bị FE chủ động ghi null — đây là cái bẫy

sqlcmd -d SolutionErp_Dev -Q "SELECT COUNT(*), SUM(CASE WHEN ManagerUserId IS NOT NULL THEN 1 ELSE 0 END) FROM Departments WHERE IsDeleted=0"
→ TotalDept=10 · HasManager=0        (0/10)

Tệ hơn: cả 2 app hardcode managerUserId: null trong payload SỬA, nên mỗi lần ai đó sửa phòng ban là xóa sạch giá trị vừa nhập:

  • fe-user/src/pages/master/DepartmentsPage.tsx:43const payload = { id, code, name, managerUserId: null, note }
  • fe-admin/src/pages/master/DepartmentsPage.tsx:57managerUserId: null, BE thì ghi thẳng, không bảo vệ: DepartmentFeatures.cs:217 entity.ManagerUserId = request.ManagerUserId; ⇒ Dùng đường (b) thì bắt buộc vá 2 file FE trước, nếu không tính năng sẽ "tự tắt" sau lần sửa phòng ban đầu tiên. [CHƯA XÁC MINH] prod (SSH chết khi nạp SQL-client, S134/S148) — nhưng vì FE null-hoá nên khả năng prod cũng rỗng là cao. 📌 0 read-site nào dùng ManagerUserId để phân quyền — grep toàn BE chỉ ra 5 hit, tất cả là DTO passthrough (DepartmentFeatures.cs:15,126,140,144,164,173,217).

C-3 [CODE] Giả thuyết (a) SAILevel.Ordertrình tự, không phải thứ bậc

// ApprovalWorkflow.cs:81-88
// Cấp = 1 NV cụ thể. 1 bước có nhiều cấp theo Order. ... Sequential trong cùng bước: cấp 1 → cấp 2 → ...
public int Order { get; set; }        // 1-based trong cùng step

Order 1-based tăng dần = ai duyệt trước/sau, không mang nghĩa chức vụ. Nhiều row cùng Order = cùng 1 Cấp (đúng ảnh: Cấp 2 có 3 người). ⇒ "Cấp cao nhất trong Bước" không phải khái niệm có sẵn; muốn dùng phải TỰ định nghĩa MAX(Order) = trưởng phòng — [SUY LUẬN] đây là suy diễn, schema không hứa điều đó (một Bước có thể cấu hình ngược). Còn "Kết thúc tại Cấp 2" trong ảnh = cờ AllowApproverFinalize (ApprovalWorkflow.cs:142, F5 Mig 58) — là quyền per-slot do admin tick, KHÔNG phải dấu hiệu chức vụ.

C-4 [CODE] Giả thuyết (c): 2 ứng viên, 1 có data 1 không

Ứng viên file:line Data Dev
Role DeptManager AppRoles.cs:8 8 user
PositionLevel.TruongPhong = 3 PositionLevel.cs:14 (chữ "TruongPhong" đúng nghĩa nhất) 0 user — 33/33 NULL
sqlcmd -d SolutionErp_Dev -Q "SELECT PositionLevel, COUNT(*) FROM Users GROUP BY PositionLevel"  → NULL|33
sqlcmd -d SolutionErp_Dev -Q "SELECT r.Name, COUNT(ur.UserId) FROM Roles r LEFT JOIN UserRoles ur ... GROUP BY r.Name"
→ Drafter|10 · CostControl|8 · DeptManager|8 · Procurement|6 · ProjectManager|2 · Director|2 · ...

⚠️ PositionLevel là di sản V1 (Mig 18/21): mọi read-site còn lại đều thuộc workflow V1 (PeWorkflowAdminFeatures.cs:187,223,264 · WorkflowAdminFeatures.cs:194,223,271) + CRUD user (UsersController.cs:81). ApproveV2Async không đọc nó. ⚠️ Role DeptManager không nói PHÒNG NÀO — phòng của user nằm ở User.DepartmentId (Dev: 31/33 có). Nên (c) chỉ dùng được khi ghép role DeptManager ∧ User.DepartmentId == Step.DepartmentId. Đó là "một trưởng phòng nào đó của phòng này", không phải "ĐÚNG người đứng đầu".

C-5 [CODE] Mảnh ghép "đến lượt họ" thì đã có sẵn và ĐÁNG TIN

  • Step.DepartmentId (ApprovalWorkflow.cs:75) — Dev 6/6 step có DepartmentId (SELECT COUNT(*), SUM(CASE WHEN DepartmentId IS NOT NULL...) FROM ApprovalWorkflowSteps6|6).
  • Con trỏ hiện tại = PE.CurrentWorkflowStepIndex + PE.CurrentApprovalLevelOrder; sub FE đã đo blockedByV2Level (PeWorkflowPanel.tsx:99-103) = isAdmin ∥ actor ∈ currentApproval.approvers. ⇒ Vế "đến lượt" không phải làm mới. Chỉ vế "người đứng đầu phòng" là thiếu.

C-6 [SUY LUẬN] KẾT LUẬN Q-C + 3 chỗ thêm rẻ nhất (owner chọn, tôi không chốt)

Kết luận: khái niệm "người đứng đầu phòng" có tên trong schema nhưng chưa có ĐỜI SỐNG. Department.ManagerUserId là chỗ duy nhất mang đúng nghĩa — 0/10 dữ liệu, 0 read-site, và bị 2 file FE ghi đè null. Không có cờ nào trên ApprovalWorkflowLevel nói "người này là trưởng phòng". ⇒ Viết spec được, nhưng phải chọn 1 trong 3 và trả giá tương ứng:

# Cách Migration? Giá phải trả
1 Dùng Department.ManagerUserId, so với Step.DepartmentId của bước đang chờ KHÔNG phải vá 2 FE null-hoá (DepartmentsPage.tsx:43 / :57) + thêm ô chọn TP + nhập tay 10 phòng trên prod; chưa nhập = nút không hiện với ai (fail-closed, an toàn)
2 Thêm cờ per-slot AllowApproverDelete trên ApprovalWorkflowLevel (1 AddColumn, no new table) đắt hơn 1 migration nhưng khớp khuôn đã lặp 4× ở đúng bảng này (F2 Mig 31 · F3 Mig 29 · F4 Mig 30 · F5 Mig 58 — ApprovalWorkflow.cs:104-142); admin tick trong Designer, 0 phụ thuộc data master, đúng chỗ "đến lượt" đã có sẵn
3 Role DeptManagerUser.DepartmentId == Step.DepartmentId KHÔNG có data ngay (8 user) nhưng ngữ nghĩa lỏng: 8 người mang role trên 10 phòng ⇒ có phòng nhiều "trưởng", có phòng không ai. Không phải "người đứng đầu" theo nghĩa owner nói

[CHƯA XÁC MINH] — số liệu C-2/C-4 là Dev DB. Prod chưa đo được (SSH vietreport-vps đứt phiên mỗi khi nạp SQL-client, tái xác nhận S134/S148). Lệnh để implementer chạy trên prod:

SELECT COUNT(*) Total, SUM(CASE WHEN ManagerUserId IS NOT NULL THEN 1 ELSE 0 END) HasManager FROM Departments WHERE IsDeleted=0;
SELECT PositionLevel, COUNT(*) FROM Users GROUP BY PositionLevel;