Commit fe5a6531cf4e2680051a1301a729a226a9436d0a

Authored by zichun
1 parent f1e20216

feat(user): wire user detail + permission categories endpoints to UserDetail page

- Backend: add GET /usr/users/{id} detail endpoint that returns the user row plus its permissionCategoryIds
- Backend: add GET /usr/permission-categories listing for the permission grid (active categories only)
- Frontend: UserDetail consumes both endpoints to populate edit form and the permission grid
backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java
... ... @@ -4,6 +4,8 @@ import com.xly.erp.common.response.Result;
4 4 import com.xly.erp.module.usr.dto.CreateUserDTO;
5 5 import com.xly.erp.module.usr.dto.UpdateUserDTO;
6 6 import com.xly.erp.module.usr.service.UserService;
  7 +import com.xly.erp.module.usr.vo.PermissionCategoryVO;
  8 +import com.xly.erp.module.usr.vo.UserListVO;
7 9 import jakarta.validation.Valid;
8 10 import org.springframework.web.bind.annotation.GetMapping;
9 11 import org.springframework.web.bind.annotation.PathVariable;
... ... @@ -14,6 +16,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
14 16 import org.springframework.web.bind.annotation.RequestParam;
15 17 import org.springframework.web.bind.annotation.RestController;
16 18  
  19 +import java.util.List;
17 20 import java.util.Map;
18 21  
19 22 @RestController
... ... @@ -46,4 +49,14 @@ public class UserController {
46 49 @RequestParam(required = false) Integer pageSize) {
47 50 return Result.ok(userService.list(field, match, value, pageNum, pageSize));
48 51 }
  52 +
  53 + @GetMapping("/users/{id}")
  54 + public Result<UserListVO> detail(@PathVariable Integer id) {
  55 + return Result.ok(userService.detail(id));
  56 + }
  57 +
  58 + @GetMapping("/permission-categories")
  59 + public Result<List<PermissionCategoryVO>> permissionCategories() {
  60 + return Result.ok(userService.listPermissionCategories());
  61 + }
49 62 }
... ...
backend/src/main/java/com/xly/erp/module/usr/mapper/PermissionCategoryMapper.java
1 1 package com.xly.erp.module.usr.mapper;
2 2  
  3 +import com.xly.erp.module.usr.vo.PermissionCategoryVO;
3 4 import org.apache.ibatis.annotations.Mapper;
4 5 import org.apache.ibatis.annotations.Param;
5 6 import org.apache.ibatis.annotations.Select;
... ... @@ -15,4 +16,10 @@ public interface PermissionCategoryMapper {
15 16 + "<foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach>"
16 17 + "</script>")
17 18 int countActiveByIds(@Param("ids") List<Integer> ids);
  19 +
  20 + @Select("SELECT iIncrement, sCategoryCode, sCategoryName, iParentId, iSortOrder "
  21 + + "FROM tPermissionCategory "
  22 + + "WHERE bDeleted = 0 "
  23 + + "ORDER BY iSortOrder ASC, iIncrement ASC")
  24 + List<PermissionCategoryVO> listActive();
18 25 }
... ...
backend/src/main/java/com/xly/erp/module/usr/mapper/UserMapper.java
... ... @@ -31,4 +31,6 @@ public interface UserMapper extends BaseMapper&lt;User&gt; {
31 31 long countWithFilter(@Param("field") String field,
32 32 @Param("matchOp") String matchOp,
33 33 @Param("value") Object value);
  34 +
  35 + UserListVO selectDetailById(@Param("id") Integer id);
34 36 }
... ...
backend/src/main/java/com/xly/erp/module/usr/mapper/UserPermissionMapper.java
... ... @@ -4,9 +4,15 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4 4 import com.xly.erp.module.usr.entity.UserPermission;
5 5 import org.apache.ibatis.annotations.Delete;
6 6 import org.apache.ibatis.annotations.Param;
  7 +import org.apache.ibatis.annotations.Select;
  8 +
  9 +import java.util.List;
7 10  
8 11 public interface UserPermissionMapper extends BaseMapper<UserPermission> {
9 12  
10 13 @Delete("DELETE FROM tUserPermission WHERE iUserId = #{userId}")
11 14 int deleteByUserId(@Param("userId") Integer userId);
  15 +
  16 + @Select("SELECT iCategoryId FROM tUserPermission WHERE iUserId = #{userId} ORDER BY iIncrement ASC")
  17 + List<Integer> selectCategoryIdsByUserId(@Param("userId") Integer userId);
12 18 }
... ...
backend/src/main/java/com/xly/erp/module/usr/service/UserService.java
... ... @@ -4,7 +4,10 @@ import com.xly.erp.module.usr.dto.CreateUserDTO;
4 4 import com.xly.erp.module.usr.dto.LoginDTO;
5 5 import com.xly.erp.module.usr.dto.UpdateUserDTO;
6 6 import com.xly.erp.module.usr.vo.LoginVO;
  7 +import com.xly.erp.module.usr.vo.PermissionCategoryVO;
  8 +import com.xly.erp.module.usr.vo.UserListVO;
7 9  
  10 +import java.util.List;
8 11 import java.util.Map;
9 12  
10 13 public interface UserService {
... ... @@ -14,5 +17,9 @@ public interface UserService {
14 17  
15 18 Map<String, Object> list(String field, String match, String value, Integer pageNum, Integer pageSize);
16 19  
  20 + UserListVO detail(Integer id);
  21 +
  22 + List<PermissionCategoryVO> listPermissionCategories();
  23 +
17 24 LoginVO login(LoginDTO dto);
18 25 }
... ...
backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java
... ... @@ -11,6 +11,7 @@ import com.xly.erp.module.usr.dto.UpdateUserDTO;
11 11 import com.xly.erp.module.usr.entity.User;
12 12 import com.xly.erp.module.usr.security.LoginAttemptStore;
13 13 import com.xly.erp.module.usr.vo.LoginVO;
  14 +import com.xly.erp.module.usr.vo.PermissionCategoryVO;
14 15 import com.xly.erp.module.usr.vo.UserBriefVO;
15 16 import com.xly.erp.module.usr.vo.UserListVO;
16 17 import com.xly.erp.module.usr.entity.UserPermission;
... ... @@ -259,6 +260,23 @@ public class UserServiceImpl implements UserService {
259 260 return result;
260 261 }
261 262  
  263 + @Override
  264 + @Transactional(readOnly = true)
  265 + public UserListVO detail(Integer id) {
  266 + UserListVO vo = userMapper.selectDetailById(id);
  267 + if (vo == null || Boolean.TRUE.equals(vo.getBDeleted())) {
  268 + throw new BizException(40400, "用户不存在或已删除");
  269 + }
  270 + vo.setPermissionCategoryIds(userPermissionMapper.selectCategoryIdsByUserId(id));
  271 + return vo;
  272 + }
  273 +
  274 + @Override
  275 + @Transactional(readOnly = true)
  276 + public List<PermissionCategoryVO> listPermissionCategories() {
  277 + return permissionCategoryMapper.listActive();
  278 + }
  279 +
262 280 private Integer parseBoolean(String v) {
263 281 return switch (v.toLowerCase()) {
264 282 case "true", "1" -> 1;
... ...
backend/src/main/java/com/xly/erp/module/usr/vo/PermissionCategoryVO.java 0 → 100644
  1 +package com.xly.erp.module.usr.vo;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonProperty;
  4 +
  5 +public class PermissionCategoryVO {
  6 +
  7 + @JsonProperty("iIncrement")
  8 + private Integer iIncrement;
  9 +
  10 + @JsonProperty("sCategoryCode")
  11 + private String sCategoryCode;
  12 +
  13 + @JsonProperty("sCategoryName")
  14 + private String sCategoryName;
  15 +
  16 + @JsonProperty("iParentId")
  17 + private Integer iParentId;
  18 +
  19 + @JsonProperty("iSortOrder")
  20 + private Integer iSortOrder;
  21 +
  22 + public Integer getIIncrement() { return iIncrement; }
  23 + public void setIIncrement(Integer iIncrement) { this.iIncrement = iIncrement; }
  24 + public String getSCategoryCode() { return sCategoryCode; }
  25 + public void setSCategoryCode(String sCategoryCode) { this.sCategoryCode = sCategoryCode; }
  26 + public String getSCategoryName() { return sCategoryName; }
  27 + public void setSCategoryName(String sCategoryName) { this.sCategoryName = sCategoryName; }
  28 + public Integer getIParentId() { return iParentId; }
  29 + public void setIParentId(Integer iParentId) { this.iParentId = iParentId; }
  30 + public Integer getISortOrder() { return iSortOrder; }
  31 + public void setISortOrder(Integer iSortOrder) { this.iSortOrder = iSortOrder; }
  32 +}
... ...
backend/src/main/java/com/xly/erp/module/usr/vo/UserListVO.java
... ... @@ -3,6 +3,7 @@ package com.xly.erp.module.usr.vo;
3 3 import com.fasterxml.jackson.annotation.JsonProperty;
4 4  
5 5 import java.time.LocalDateTime;
  6 +import java.util.List;
6 7  
7 8 public class UserListVO {
8 9  
... ... @@ -30,6 +31,12 @@ public class UserListVO {
30 31 @JsonProperty("sLanguage")
31 32 private String sLanguage;
32 33  
  34 + @JsonProperty("bCanModifyDocs")
  35 + private Boolean bCanModifyDocs;
  36 +
  37 + @JsonProperty("permissionCategoryIds")
  38 + private List<Integer> permissionCategoryIds;
  39 +
33 40 @JsonProperty("bDeleted")
34 41 private Boolean bDeleted;
35 42  
... ... @@ -58,6 +65,10 @@ public class UserListVO {
58 65 public void setSUserType(String sUserType) { this.sUserType = sUserType; }
59 66 public String getSLanguage() { return sLanguage; }
60 67 public void setSLanguage(String sLanguage) { this.sLanguage = sLanguage; }
  68 + public Boolean getBCanModifyDocs() { return bCanModifyDocs; }
  69 + public void setBCanModifyDocs(Boolean bCanModifyDocs) { this.bCanModifyDocs = bCanModifyDocs; }
  70 + public List<Integer> getPermissionCategoryIds() { return permissionCategoryIds; }
  71 + public void setPermissionCategoryIds(List<Integer> permissionCategoryIds) { this.permissionCategoryIds = permissionCategoryIds; }
61 72 public Boolean getBDeleted() { return bDeleted; }
62 73 public void setBDeleted(Boolean bDeleted) { this.bDeleted = bDeleted; }
63 74 public LocalDateTime getTLastLoginDate() { return tLastLoginDate; }
... ...
backend/src/main/resources/mapper/usr/UserMapper.xml
... ... @@ -12,6 +12,7 @@
12 12 s.sDepartment AS department,
13 13 u.sUserType AS sUserType,
14 14 u.sLanguage AS sLanguage,
  15 + u.bCanModifyDocs AS bCanModifyDocs,
15 16 u.bDeleted AS bDeleted,
16 17 u.tLastLoginDate AS tLastLoginDate,
17 18 u.sCreatedBy AS sCreatedBy,
... ... @@ -46,4 +47,12 @@
46 47 <include refid="filterClause"/>
47 48 </select>
48 49  
  50 + <select id="selectDetailById" resultType="com.xly.erp.module.usr.vo.UserListVO">
  51 + SELECT <include refid="baseSelectColumns"/>
  52 + FROM tUser u
  53 + LEFT JOIN tStaff s ON s.iIncrement = u.iStaffId AND s.bDeleted = 0
  54 + WHERE u.iIncrement = #{id}
  55 + LIMIT 1
  56 + </select>
  57 +
49 58 </mapper>
... ...
frontend/src/api/user.ts
... ... @@ -9,12 +9,22 @@ export interface UserListVO {
9 9 department: string | null;
10 10 sUserType: string;
11 11 sLanguage: string;
  12 + bCanModifyDocs?: boolean;
  13 + permissionCategoryIds?: number[];
12 14 bDeleted: boolean;
13 15 tLastLoginDate: string | null;
14 16 sCreatedBy: string | null;
15 17 tCreateDate: string;
16 18 }
17 19  
  20 +export interface PermissionCategoryVO {
  21 + iIncrement: number;
  22 + sCategoryCode: string;
  23 + sCategoryName: string;
  24 + iParentId: number | null;
  25 + iSortOrder: number;
  26 +}
  27 +
18 28 export interface UserListPage {
19 29 records: UserListVO[];
20 30 total: number;
... ... @@ -37,6 +47,7 @@ export interface UserDTO {
37 47 sUserType: string;
38 48 sLanguage: string;
39 49 bCanModifyDocs?: boolean;
  50 + permissionCategoryIds?: number[];
40 51 }
41 52  
42 53 export function listUsers(params: UserListParams = {}): Promise<UserListPage> {
... ... @@ -47,6 +58,17 @@ export function listUsers(params: UserListParams = {}): Promise&lt;UserListPage&gt; {
47 58 });
48 59 }
49 60  
  61 +export function getUser(id: number): Promise<UserListVO> {
  62 + return request<UserListVO>({ url: `/usr/users/${id}`, method: "GET" });
  63 +}
  64 +
  65 +export function listPermissionCategories(): Promise<PermissionCategoryVO[]> {
  66 + return request<PermissionCategoryVO[]>({
  67 + url: "/usr/permission-categories",
  68 + method: "GET",
  69 + });
  70 +}
  71 +
50 72 export function createUser(dto: UserDTO): Promise<{ iIncrement: number; sUserNo: string }> {
51 73 return request({ url: "/usr/users", method: "POST", data: dto });
52 74 }
... ...
frontend/src/pages/usr/UserDetail.tsx
... ... @@ -20,11 +20,18 @@ import {
20 20 ToolbarBtnDark,
21 21 } from "@/components/Primitives";
22 22 import StaffPicker from "@/components/StaffPicker";
23   -import { createUser, updateUser, type UserDTO } from "@/api/user";
  23 +import {
  24 + createUser,
  25 + getUser,
  26 + listPermissionCategories,
  27 + updateUser,
  28 + type PermissionCategoryVO,
  29 + type UserDTO,
  30 + type UserListVO,
  31 +} from "@/api/user";
24 32 import {
25 33 USER_TYPES,
26 34 LANGUAGE_OPTIONS,
27   - PERMISSION_GROUPS,
28 35 SCOPE_ITEMS,
29 36 } from "@/utils/data";
30 37 import { useAppDispatch, useAppSelector } from "@/store";
... ... @@ -65,22 +72,14 @@ export default function UserDetail({ userId, mode: initialMode }: Props) {
65 72 const tabs = useAppSelector((s) => s.tabs.tabs);
66 73 const activeTabId = useAppSelector((s) => s.tabs.activeTabId);
67 74 const tab = tabs.find((t) => t.id === activeTabId);
68   - const snapshot = tab?.meta?.snapshot as
69   - | {
70   - sUserNo: string;
71   - sUserName: string;
72   - iStaffId: number | null;
73   - staffName: string | null;
74   - sUserType: string;
75   - sLanguage: string;
76   - bCanModifyDocs?: boolean;
77   - tCreateDate?: string;
78   - sCreatedBy?: string;
79   - }
80   - | undefined;
  75 + const snapshot = tab?.meta?.snapshot as UserListVO | undefined;
  76 + const [detail, setDetail] = useState<UserListVO | undefined>(snapshot);
  77 + const currentSnapshot = detail ?? snapshot;
  78 + const [permissionCategories, setPermissionCategories] = useState<PermissionCategoryVO[]>([]);
  79 + const [permissionCategoryIds, setPermissionCategoryIds] = useState<number[]>([]);
81 80  
82   - const initialForm: FormState =
83   - mode === "new" || !snapshot
  81 + const buildForm = (snap?: UserListVO): FormState =>
  82 + mode === "new" || !snap
84 83 ? {
85 84 sUserNo: "",
86 85 sUserName: "",
... ... @@ -91,33 +90,54 @@ export default function UserDetail({ userId, mode: initialMode }: Props) {
91 90 bCanModifyDocs: false,
92 91 }
93 92 : {
94   - sUserNo: snapshot.sUserNo,
95   - sUserName: snapshot.sUserName,
96   - iStaffId: snapshot.iStaffId,
97   - staffName: snapshot.staffName ?? "",
98   - sUserType: snapshot.sUserType,
99   - sLanguage: snapshot.sLanguage,
100   - bCanModifyDocs: !!snapshot.bCanModifyDocs,
  93 + sUserNo: snap.sUserNo,
  94 + sUserName: snap.sUserName,
  95 + iStaffId: snap.iStaffId,
  96 + staffName: snap.staffName ?? "",
  97 + sUserType: snap.sUserType,
  98 + sLanguage: snap.sLanguage,
  99 + bCanModifyDocs: !!snap.bCanModifyDocs,
101 100 };
102 101  
103   - const [form, setForm] = useState<FormState>(initialForm);
  102 + const [form, setForm] = useState<FormState>(() => buildForm(currentSnapshot));
104 103 const [permTab, setPermTab] = useState("groups");
105   - // Visual-only — not persisted to backend.
106   - const [permissions, setPermissions] = useState<Record<string, boolean>>(() =>
107   - Object.fromEntries(PERMISSION_GROUPS.map((g) => [g, false]))
108   - );
109 104 const [tabPerms, setTabPerms] = useState<Record<string, boolean>>({});
110 105  
111 106 useEffect(() => {
112   - setForm(initialForm);
  107 + setDetail(snapshot);
  108 + }, [snapshot]);
  109 +
  110 + useEffect(() => {
  111 + void listPermissionCategories()
  112 + .then(setPermissionCategories)
  113 + .catch(() => {
  114 + // interceptor already showed message
  115 + });
  116 + }, []);
  117 +
  118 + useEffect(() => {
  119 + if (mode === "new" || !userId) return;
  120 + void getUser(userId)
  121 + .then((u) => {
  122 + setDetail(u);
  123 + setPermissionCategoryIds(u.permissionCategoryIds ?? []);
  124 + })
  125 + .catch(() => {
  126 + // interceptor already showed message
  127 + });
  128 + }, [mode, userId]);
  129 +
  130 + useEffect(() => {
  131 + setForm(buildForm(currentSnapshot));
  132 + setPermissionCategoryIds(mode === "new" ? [] : currentSnapshot?.permissionCategoryIds ?? []);
113 133 // eslint-disable-next-line react-hooks/exhaustive-deps
114   - }, [snapshot, mode]);
  134 + }, [currentSnapshot, mode]);
115 135  
116 136 const set = <K extends keyof FormState>(k: K, v: FormState[K]) =>
117 137 setForm((s) => ({ ...s, [k]: v }));
118 138  
119 139 const disabled = mode === "view";
120   - const checkedCount = Object.values(permissions).filter(Boolean).length;
  140 + const checkedCount = permissionCategoryIds.length;
121 141  
122 142 const startEdit = () => setMode("edit");
123 143 const startNew = () => setMode("new");
... ... @@ -126,7 +146,8 @@ export default function UserDetail({ userId, mode: initialMode }: Props) {
126 146 dispatch(closeTab(activeTabId));
127 147 dispatch(setActiveTab("userlist"));
128 148 } else {
129   - setForm(initialForm);
  149 + setForm(buildForm(currentSnapshot));
  150 + setPermissionCategoryIds(currentSnapshot?.permissionCategoryIds ?? []);
130 151 setMode("view");
131 152 }
132 153 };
... ... @@ -143,7 +164,7 @@ export default function UserDetail({ userId, mode: initialMode }: Props) {
143 164 sUserType: form.sUserType,
144 165 sLanguage: form.sLanguage,
145 166 bCanModifyDocs: form.bCanModifyDocs,
146   - // permissionCategoryIds omitted: prototype permissions are by name; backend wants IDs.
  167 + permissionCategoryIds,
147 168 };
148 169 setSubmitting(true);
149 170 try {
... ... @@ -154,6 +175,9 @@ export default function UserDetail({ userId, mode: initialMode }: Props) {
154 175 dispatch(setActiveTab("userlist"));
155 176 } else if (userId) {
156 177 await updateUser(userId, dto);
  178 + const refreshed = await getUser(userId);
  179 + setDetail(refreshed);
  180 + setPermissionCategoryIds(refreshed.permissionCategoryIds ?? []);
157 181 message.success("保存成功");
158 182 setMode("view");
159 183 }
... ... @@ -222,7 +246,7 @@ export default function UserDetail({ userId, mode: initialMode }: Props) {
222 246 <span style={{ fontSize: 11, color: "var(--text-on-dark-muted)", marginRight: 8 }}>
223 247 已选权限:
224 248 <span style={{ color: "#fff", fontWeight: 500 }}>{checkedCount}</span> /{" "}
225   - {PERMISSION_GROUPS.length - 1}
  249 + {permissionCategories.length}
226 250 </span>
227 251 <span
228 252 style={{
... ... @@ -283,7 +307,7 @@ export default function UserDetail({ userId, mode: initialMode }: Props) {
283 307 value={
284 308 mode === "new"
285 309 ? "保存后自动生成"
286   - : fmtDateTime(snapshot?.tCreateDate)
  310 + : fmtDateTime(currentSnapshot?.tCreateDate)
287 311 }
288 312 disabled
289 313 mono
... ... @@ -291,7 +315,7 @@ export default function UserDetail({ userId, mode: initialMode }: Props) {
291 315 </Field>
292 316 <Field label="制单人" required>
293 317 <PrimInput
294   - value={mode === "new" ? "保存后自动生成" : snapshot?.sCreatedBy ?? ""}
  318 + value={mode === "new" ? "保存后自动生成" : currentSnapshot?.sCreatedBy ?? ""}
295 319 disabled
296 320 required
297 321 />
... ... @@ -415,9 +439,12 @@ export default function UserDetail({ userId, mode: initialMode }: Props) {
415 439 <div style={{ flex: 1, overflow: "auto", background: "#fff" }}>
416 440 {permTab === "groups" ? (
417 441 <PermissionGrid
418   - permissions={permissions}
419   - setPermission={(g, v) =>
420   - setPermissions((s) => ({ ...s, [g]: v }))
  442 + categories={permissionCategories}
  443 + selectedIds={permissionCategoryIds}
  444 + setPermission={(id, v) =>
  445 + setPermissionCategoryIds((s) =>
  446 + v ? Array.from(new Set([...s, id])) : s.filter((x) => x !== id)
  447 + )
421 448 }
422 449 disabled={disabled}
423 450 />
... ... @@ -454,15 +481,20 @@ export default function UserDetail({ userId, mode: initialMode }: Props) {
454 481 }
455 482  
456 483 interface PermissionGridProps {
457   - permissions: Record<string, boolean>;
458   - setPermission: (g: string, v: boolean) => void;
  484 + categories: PermissionCategoryVO[];
  485 + selectedIds: number[];
  486 + setPermission: (id: number, v: boolean) => void;
459 487 disabled?: boolean;
460 488 }
461 489  
462   -function PermissionGrid({ permissions, setPermission, disabled }: PermissionGridProps) {
  490 +function PermissionGrid({ categories, selectedIds, setPermission, disabled }: PermissionGridProps) {
463 491 const [filter, setFilter] = useState("");
464 492 const [hovered, setHovered] = useState<string | null>(null);
465   - const allChecked = PERMISSION_GROUPS.every((g) => permissions[g]);
  493 + const selected = new Set(selectedIds);
  494 + const visibleCategories = categories.filter(
  495 + (c) => !filter || c.sCategoryName.includes(filter) || c.sCategoryCode.includes(filter)
  496 + );
  497 + const allChecked = categories.length > 0 && categories.every((c) => selected.has(c.iIncrement));
466 498  
467 499 return (
468 500 <table
... ... @@ -489,7 +521,7 @@ function PermissionGrid({ permissions, setPermission, disabled }: PermissionGrid
489 521 <PrimCheckbox
490 522 checked={allChecked}
491 523 onChange={(v) => {
492   - PERMISSION_GROUPS.forEach((g) => setPermission(g, v));
  524 + categories.forEach((c) => setPermission(c.iIncrement, v));
493 525 }}
494 526 disabled={disabled}
495 527 />
... ... @@ -517,15 +549,15 @@ function PermissionGrid({ permissions, setPermission, disabled }: PermissionGrid
517 549 </tr>
518 550 </thead>
519 551 <tbody>
520   - {PERMISSION_GROUPS.filter((g) => !filter || g.includes(filter)).map((g, i) => {
521   - const isHover = hovered === g;
522   - const checked = !!permissions[g];
  552 + {visibleCategories.map((c, i) => {
  553 + const isHover = hovered === c.sCategoryCode;
  554 + const checked = selected.has(c.iIncrement);
523 555 return (
524 556 <tr
525   - key={g}
526   - onMouseEnter={() => setHovered(g)}
  557 + key={c.iIncrement}
  558 + onMouseEnter={() => setHovered(c.sCategoryCode)}
527 559 onMouseLeave={() => setHovered(null)}
528   - onClick={disabled ? undefined : () => setPermission(g, !checked)}
  560 + onClick={disabled ? undefined : () => setPermission(c.iIncrement, !checked)}
529 561 style={{
530 562 background:
531 563 isHover && !disabled
... ... @@ -549,7 +581,7 @@ function PermissionGrid({ permissions, setPermission, disabled }: PermissionGrid
549 581 >
550 582 <PrimCheckbox
551 583 checked={checked}
552   - onChange={(v) => setPermission(g, v)}
  584 + onChange={(v) => setPermission(c.iIncrement, v)}
553 585 disabled={disabled}
554 586 />
555 587 </td>
... ... @@ -561,7 +593,7 @@ function PermissionGrid({ permissions, setPermission, disabled }: PermissionGrid
561 593 fontWeight: checked ? 500 : 400,
562 594 }}
563 595 >
564   - {g}
  596 + {c.sCategoryName}
565 597 </td>
566 598 </tr>
567 599 );
... ...