diff --git a/contracts/admin-openapi.json b/contracts/admin-openapi.json index 097f534..dc41c8b 100644 --- a/contracts/admin-openapi.json +++ b/contracts/admin-openapi.json @@ -8875,8 +8875,11 @@ }, "ApiPageManager": { "type": "object", - "required": ["items", "page", "pageSize", "total"], + "required": ["items", "page", "pageSize", "total", "bdLeaderInviteScopePolicy"], "properties": { + "bdLeaderInviteScopePolicy": { + "$ref": "#/components/schemas/ManagerRoleScopePolicy" + }, "items": { "type": "array", "items": { @@ -10112,6 +10115,19 @@ } } }, + "ManagerRoleScopePolicy": { + "type": "object", + "required": ["baseScope", "regionExpansionConfigurable"], + "properties": { + "baseScope": { + "type": "string", + "enum": ["country", "region", "global"] + }, + "regionExpansionConfigurable": { + "type": "boolean" + } + } + }, "Manager": { "type": "object", "required": ["userId"], @@ -10128,6 +10144,20 @@ "canAddBdLeader": { "type": "boolean" }, + "canAddBdLeaderInRegion": { + "type": "boolean" + }, + "bdLeaderInviteBaseScope": { + "type": "string", + "enum": ["country", "region", "global"] + }, + "bdLeaderInviteEffectiveScope": { + "type": "string", + "enum": ["country", "region", "global"] + }, + "bdLeaderRegionExpansionConfigurable": { + "type": "boolean" + }, "canAddSuperadmin": { "type": "boolean" }, @@ -10638,6 +10668,9 @@ "canAddBdLeader": { "type": "boolean" }, + "canAddBdLeaderInRegion": { + "type": "boolean" + }, "canAddSuperadmin": { "type": "boolean" }, @@ -10680,6 +10713,9 @@ "canAddBdLeader": { "type": "boolean" }, + "canAddBdLeaderInRegion": { + "type": "boolean" + }, "canAddSuperadmin": { "type": "boolean" }, diff --git a/src/features/host-org/api.test.ts b/src/features/host-org/api.test.ts index dd49640..766b13b 100644 --- a/src/features/host-org/api.test.ts +++ b/src/features/host-org/api.test.ts @@ -69,6 +69,7 @@ test("host org list APIs use generated admin paths and filters", async () => { status: "active", }); await createManager({ + canAddBdLeaderInRegion: false, canBlockUser: false, canGrantBadge: false, canGrantVip: true, @@ -76,6 +77,7 @@ test("host org list APIs use generated admin paths and filters", async () => { targetUserId: "1003", }); await updateManager("1003", { + canAddBdLeaderInRegion: true, canBlockUser: false, canGrantVehicle: true, canGrantVip: false, @@ -158,6 +160,7 @@ test("host org list APIs use generated admin paths and filters", async () => { expect(String(managerCreateUrl)).toContain("/api/v1/admin/managers"); expect(managerCreateInit?.method).toBe("POST"); expect(JSON.parse(String(managerCreateInit?.body))).toEqual({ + canAddBdLeaderInRegion: false, canBlockUser: false, canGrantBadge: false, canGrantVip: true, @@ -167,6 +170,7 @@ test("host org list APIs use generated admin paths and filters", async () => { expect(String(managerUpdateUrl)).toContain("/api/v1/admin/managers/1003"); expect(managerUpdateInit?.method).toBe("PUT"); expect(JSON.parse(String(managerUpdateInit?.body))).toEqual({ + canAddBdLeaderInRegion: true, canBlockUser: false, canGrantVehicle: true, canGrantVip: false, diff --git a/src/features/host-org/hooks/useHostManagersPage.js b/src/features/host-org/hooks/useHostManagersPage.js index f536c94..29ed8e7 100644 --- a/src/features/host-org/hooks/useHostManagersPage.js +++ b/src/features/host-org/hooks/useHostManagersPage.js @@ -17,29 +17,44 @@ export const managerPermissionFields = [ "canGrantVip", "canUpdateUserLevel", "canAddBdLeader", + "canAddBdLeaderInRegion", "canAddAdmin", "canAddSuperadmin", "canBlockUser", "canTransferUserCountry", ]; -const emptyPermissions = () => Object.fromEntries(managerPermissionFields.map((field) => [field, true])); +const managerPermissionDefault = (field) => field !== "canAddBdLeaderInRegion"; -const emptyManagerForm = () => ({ contact: "", targetUserId: "", ...emptyPermissions() }); +const managerPermissionEnabled = (source, field) => { + const value = source?.[field]; + return value === undefined || value === null ? managerPermissionDefault(field) : value !== false; +}; + +const emptyPermissions = () => + Object.fromEntries(managerPermissionFields.map((field) => [field, managerPermissionDefault(field)])); + +const emptyManagerForm = (regionExpansionConfigurable = false) => ({ + bdLeaderRegionExpansionConfigurable: regionExpansionConfigurable, + contact: "", + targetUserId: "", + ...emptyPermissions(), +}); const managerPermissionPayload = (form) => - Object.fromEntries(managerPermissionFields.map((field) => [field, form[field] !== false])); + Object.fromEntries(managerPermissionFields.map((field) => [field, managerPermissionEnabled(form, field)])); const managerStatusPayload = (item, status) => ({ contact: item?.contact || "", status, - ...Object.fromEntries(managerPermissionFields.map((field) => [field, item?.[field] !== false])), + ...Object.fromEntries(managerPermissionFields.map((field) => [field, managerPermissionEnabled(item, field)])), }); const managerFormFromItem = (item) => ({ + bdLeaderRegionExpansionConfigurable: item?.bdLeaderRegionExpansionConfigurable === true, contact: item?.contact || "", targetUserId: String(item?.userId || ""), - ...Object.fromEntries(managerPermissionFields.map((field) => [field, item?.[field] !== false])), + ...Object.fromEntries(managerPermissionFields.map((field) => [field, managerPermissionEnabled(item, field)])), }); export function useHostManagersPage() { @@ -118,7 +133,7 @@ export function useHostManagersPage() { }); showToast("经理已添加", "success"); } - setManagerForm(emptyManagerForm()); + setManagerForm(emptyManagerForm(data.bdLeaderInviteScopePolicy?.regionExpansionConfigurable === true)); setActiveAction(""); setEditingManager(null); await reload(); @@ -132,12 +147,12 @@ export function useHostManagersPage() { const closeAction = () => { setActiveAction(""); setEditingManager(null); - setManagerForm(emptyManagerForm()); + setManagerForm(emptyManagerForm(data.bdLeaderInviteScopePolicy?.regionExpansionConfigurable === true)); }; const openManagerForm = () => { setEditingManager(null); - setManagerForm(emptyManagerForm()); + setManagerForm(emptyManagerForm(data.bdLeaderInviteScopePolicy?.regionExpansionConfigurable === true)); setActiveAction("manager"); }; diff --git a/src/features/host-org/pages/HostManagersPage.jsx b/src/features/host-org/pages/HostManagersPage.jsx index c2005d0..d7a6b29 100644 --- a/src/features/host-org/pages/HostManagersPage.jsx +++ b/src/features/host-org/pages/HostManagersPage.jsx @@ -22,6 +22,7 @@ const managerPermissionOptions = [ { key: "canGrantVip", label: "VIP" }, { key: "canUpdateUserLevel", label: "升级等级" }, { key: "canAddBdLeader", label: "BD Leader" }, + { key: "canAddBdLeaderInRegion", label: "区域邀请 BD Leader", regionExpansionOnly: true }, { key: "canAddAdmin", label: "Admin" }, { key: "canAddSuperadmin", label: "Superadmin" }, { key: "canBlockUser", label: "Block User" }, @@ -201,20 +202,28 @@ function ManagerStatusSwitch({ item, page }) { function ManagerPermissions({ item }) { return (
- {managerPermissionOptions.map((permission) => { - const enabled = item?.[permission.key] !== false; - return ( - - {permission.label} - {enabled ? "开" : "关"} - - ); - })} + {managerPermissionOptions + .filter( + (permission) => + !permission.regionExpansionOnly || item?.bdLeaderRegionExpansionConfigurable === true, + ) + .map((permission) => { + const enabled = + permission.key === "canAddBdLeaderInRegion" + ? item?.[permission.key] === true + : item?.[permission.key] !== false; + return ( + + {permission.label} + {enabled ? "开" : "关"} + + ); + })}
); } @@ -237,19 +246,28 @@ function ManagerActions({ item, page }) { function PermissionSwitchGrid({ disabled, form, onChange }) { return (
- {managerPermissionOptions.map((permission) => ( - - ))} + {managerPermissionOptions + .filter( + (permission) => + !permission.regionExpansionOnly || form?.bdLeaderRegionExpansionConfigurable === true, + ) + .map((permission) => ( + + ))}
); } diff --git a/src/features/host-org/pages/HostManagersPage.test.jsx b/src/features/host-org/pages/HostManagersPage.test.jsx index 52aad46..a5cd6fc 100644 --- a/src/features/host-org/pages/HostManagersPage.test.jsx +++ b/src/features/host-org/pages/HostManagersPage.test.jsx @@ -17,7 +17,9 @@ afterEach(() => { test("manager status column uses a switch and toggles through the page hook", () => { const manager = managerFixture(); - vi.mocked(useHostManagersPage).mockReturnValue(pageFixture({ data: { items: [manager], page: 1, pageSize: 50, total: 1 } })); + vi.mocked(useHostManagersPage).mockReturnValue( + pageFixture({ data: { items: [manager], page: 1, pageSize: 50, total: 1 } }), + ); render(); @@ -31,6 +33,35 @@ test("manager status column uses a switch and toggles through the page hook", () expect(mocks.toggleManager).toHaveBeenCalledWith(manager, false); }); +test("manager form only shows the region invitation switch when backend policy allows configuration", () => { + vi.mocked(useHostManagersPage).mockReturnValue( + pageFixture({ + activeAction: "manager", + managerForm: { + ...pageFixture().managerForm, + bdLeaderRegionExpansionConfigurable: true, + canAddBdLeaderInRegion: false, + }, + }), + ); + + const { rerender } = render(); + expect(screen.getByRole("switch", { name: "区域邀请 BD Leader" })).not.toBeChecked(); + + vi.mocked(useHostManagersPage).mockReturnValue( + pageFixture({ + activeAction: "manager", + managerForm: { + ...pageFixture().managerForm, + bdLeaderRegionExpansionConfigurable: false, + canAddBdLeaderInRegion: false, + }, + }), + ); + rerender(); + expect(screen.queryByRole("switch", { name: "区域邀请 BD Leader" })).not.toBeInTheDocument(); +}); + function pageFixture(patch = {}) { return { abilities: { @@ -50,8 +81,10 @@ function pageFixture(patch = {}) { loadingAction: "", loadingRegions: false, managerForm: { + bdLeaderRegionExpansionConfigurable: true, canAddAdmin: true, canAddBdLeader: true, + canAddBdLeaderInRegion: false, canAddSuperadmin: true, canBlockUser: true, canGrantAvatarFrame: true, @@ -86,6 +119,8 @@ function managerFixture() { bdLeaderCount: 3, canAddAdmin: true, canAddBdLeader: false, + canAddBdLeaderInRegion: false, + bdLeaderRegionExpansionConfigurable: true, canAddSuperadmin: true, canBlockUser: true, canGrantAvatarFrame: true, diff --git a/src/features/host-org/schema.test.ts b/src/features/host-org/schema.test.ts index 279c900..99c4ec5 100644 --- a/src/features/host-org/schema.test.ts +++ b/src/features/host-org/schema.test.ts @@ -132,6 +132,7 @@ test("manager schema defaults vip grant permission on and preserves explicit fal expect(disabledResult.success).toBe(true); if (defaultResult.success) { expect(defaultResult.data.canGrantVip).toBe(true); + expect(defaultResult.data.canAddBdLeaderInRegion).toBe(false); } if (disabledResult.success) { expect(disabledResult.data.canGrantVip).toBe(false); diff --git a/src/features/host-org/schema.ts b/src/features/host-org/schema.ts index 0b587d0..7e30b36 100644 --- a/src/features/host-org/schema.ts +++ b/src/features/host-org/schema.ts @@ -112,6 +112,7 @@ export const createCoinSellerSchema = commandContactSchema.extend({ export const createManagerSchema = z.object({ canAddAdmin: z.boolean().optional().default(true), canAddBdLeader: z.boolean().optional().default(true), + canAddBdLeaderInRegion: z.boolean().optional().default(false), canAddSuperadmin: z.boolean().optional().default(true), canBlockUser: z.boolean().optional().default(true), canGrantAvatarFrame: z.boolean().optional().default(true), diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts index 544ac60..d6915d1 100644 --- a/src/shared/api/generated/schema.d.ts +++ b/src/shared/api/generated/schema.d.ts @@ -4677,6 +4677,7 @@ export interface components { total: number; }; ApiPageManager: { + bdLeaderInviteScopePolicy: components["schemas"]["ManagerRoleScopePolicy"]; items: components["schemas"]["Manager"][]; page: number; pageSize: number; @@ -5056,11 +5057,22 @@ export interface components { status?: string; updatedAtMs?: number; }; + ManagerRoleScopePolicy: { + /** @enum {string} */ + baseScope: "country" | "region" | "global"; + regionExpansionConfigurable: boolean; + }; Manager: { avatar?: string; bdLeaderCount?: number; canAddAdmin?: boolean; canAddBdLeader?: boolean; + canAddBdLeaderInRegion?: boolean; + /** @enum {string} */ + bdLeaderInviteBaseScope?: "country" | "region" | "global"; + /** @enum {string} */ + bdLeaderInviteEffectiveScope?: "country" | "region" | "global"; + bdLeaderRegionExpansionConfigurable?: boolean; canAddSuperadmin?: boolean; canBlockUser?: boolean; canGrantAvatarFrame?: boolean; @@ -5232,6 +5244,7 @@ export interface components { CreateManagerPayload: { canAddAdmin?: boolean; canAddBdLeader?: boolean; + canAddBdLeaderInRegion?: boolean; canAddSuperadmin?: boolean; canBlockUser?: boolean; canGrantAvatarFrame?: boolean; @@ -5246,6 +5259,7 @@ export interface components { UpdateManagerPayload: { canAddAdmin?: boolean; canAddBdLeader?: boolean; + canAddBdLeaderInRegion?: boolean; canAddSuperadmin?: boolean; canBlockUser?: boolean; canGrantAvatarFrame?: boolean; diff --git a/src/shared/api/types.ts b/src/shared/api/types.ts index c858f8a..30de140 100644 --- a/src/shared/api/types.ts +++ b/src/shared/api/types.ts @@ -1,10 +1,16 @@ export interface ApiPage { + bdLeaderInviteScopePolicy?: ManagerRoleScopePolicy; items: TItem[]; page: number; pageSize: number; total: number; } +export interface ManagerRoleScopePolicy { + baseScope: "country" | "region" | "global"; + regionExpansionConfigurable: boolean; +} + export interface ApiList { items: TItem[]; total: number; @@ -756,6 +762,10 @@ export interface ManagerDto { bdLeaderCount?: number; canAddAdmin?: boolean; canAddBdLeader?: boolean; + canAddBdLeaderInRegion?: boolean; + bdLeaderInviteBaseScope?: "country" | "region" | "global"; + bdLeaderInviteEffectiveScope?: "country" | "region" | "global"; + bdLeaderRegionExpansionConfigurable?: boolean; canAddSuperadmin?: boolean; canBlockUser?: boolean; canGrantAvatarFrame?: boolean; @@ -779,6 +789,7 @@ export interface ManagerDto { export interface CreateManagerPayload { canAddAdmin?: boolean; canAddBdLeader?: boolean; + canAddBdLeaderInRegion?: boolean; canAddSuperadmin?: boolean; canBlockUser?: boolean; canGrantAvatarFrame?: boolean; @@ -794,6 +805,7 @@ export interface CreateManagerPayload { export interface UpdateManagerPayload { canAddAdmin?: boolean; canAddBdLeader?: boolean; + canAddBdLeaderInRegion?: boolean; canAddSuperadmin?: boolean; canBlockUser?: boolean; canGrantAvatarFrame?: boolean;