diff --git a/contracts/admin-openapi.json b/contracts/admin-openapi.json
index 27bbddd..48cb2b0 100644
--- a/contracts/admin-openapi.json
+++ b/contracts/admin-openapi.json
@@ -3117,6 +3117,109 @@
"x-permissions": ["payment-product:delete"]
}
},
+ "/admin/databi/social/master": {
+ "get": {
+ "operationId": "getSocialBiMaster",
+ "x-permission": "overview:view",
+ "responses": {
+ "200": {
+ "$ref": "#/components/responses/StatisticsObjectResponse"
+ }
+ },
+ "x-permissions": ["overview:view"]
+ }
+ },
+ "/admin/databi/social/overview": {
+ "get": {
+ "operationId": "getSocialBiOverview",
+ "x-permission": "overview:view",
+ "parameters": [
+ { "in": "query", "name": "stat_tz", "schema": { "type": "string" } },
+ { "in": "query", "name": "start_ms", "schema": { "type": "integer", "format": "int64" } },
+ { "in": "query", "name": "end_ms", "schema": { "type": "integer", "format": "int64" } },
+ { "in": "query", "name": "app_codes", "schema": { "type": "string" } },
+ { "in": "query", "name": "region_id", "schema": { "type": "integer", "format": "int64" } }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/components/responses/StatisticsObjectResponse"
+ }
+ },
+ "x-permissions": ["overview:view"]
+ }
+ },
+ "/admin/databi/social/kpi": {
+ "get": {
+ "operationId": "getSocialBiKpi",
+ "x-permission": "overview:view",
+ "parameters": [
+ { "in": "query", "name": "stat_tz", "schema": { "type": "string" } },
+ { "in": "query", "name": "start_ms", "schema": { "type": "integer", "format": "int64" } },
+ { "in": "query", "name": "end_ms", "schema": { "type": "integer", "format": "int64" } },
+ { "in": "query", "name": "period_month", "schema": { "type": "string" } },
+ { "in": "query", "name": "app_codes", "schema": { "type": "string" } },
+ { "in": "query", "name": "operator_user_id", "schema": { "type": "integer", "format": "int64" } }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/components/responses/StatisticsObjectResponse"
+ }
+ },
+ "x-permissions": ["overview:view"]
+ }
+ },
+ "/admin/databi/social/kpi-targets": {
+ "get": {
+ "operationId": "listSocialBiKpiTargets",
+ "x-permission": "overview:view",
+ "parameters": [
+ { "in": "query", "name": "period_month", "schema": { "type": "string" } },
+ { "in": "query", "name": "user_id", "schema": { "type": "integer", "format": "int64" } }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/components/responses/StatisticsObjectResponse"
+ }
+ },
+ "x-permissions": ["overview:view"]
+ },
+ "put": {
+ "operationId": "replaceSocialBiKpiTargets",
+ "x-permission": "databi-kpi:manage",
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "userId": { "type": "integer", "format": "int64" },
+ "appCode": { "type": "string" },
+ "regionId": { "type": "integer", "format": "int64" },
+ "periodMonth": { "type": "string" },
+ "targetUsdMinor": { "type": "integer", "format": "int64" },
+ "dailyTargetUsdMinor": { "type": "integer", "format": "int64" }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "$ref": "#/components/responses/StatisticsObjectResponse"
+ }
+ },
+ "x-permissions": ["databi-kpi:manage"]
+ }
+ },
"/admin/finance/scope": {
"get": {
"operationId": "getFinanceScope",
diff --git a/databi/src/DatabiApp.test.jsx b/databi/src/DatabiApp.test.jsx
index 42ee7e2..d0dbf34 100644
--- a/databi/src/DatabiApp.test.jsx
+++ b/databi/src/DatabiApp.test.jsx
@@ -1,11 +1,23 @@
import { act, render, screen, within } from "@testing-library/react";
import { afterEach, beforeEach, expect, test, vi } from "vitest";
import { DatabiApp } from "./DatabiApp.jsx";
-import { fetchFilterOptions, fetchSelfGameStatisticsOverview, fetchSocialBiFilterOptions, fetchStatisticsOverview } from "./api.js";
+import {
+ fetchFilterOptions,
+ fetchSelfGameStatisticsOverview,
+ fetchSocialBiKpi,
+ fetchSocialBiMaster,
+ fetchSocialBiOverview,
+ fetchStatisticsOverview
+} from "./api.js";
vi.mock("./api.js", () => ({
fetchFilterOptions: vi.fn(),
fetchSocialBiFilterOptions: vi.fn(),
+ fetchSocialBiKpi: vi.fn(),
+ fetchSocialBiKpiTargets: vi.fn(),
+ fetchSocialBiMaster: vi.fn(),
+ fetchSocialBiOverview: vi.fn(),
+ replaceSocialBiKpiTargets: vi.fn(),
fetchSelfGameStatisticsOverview: vi.fn(),
fetchStatisticsOverview: vi.fn(),
getCurrentAppCode: vi.fn(() => "lalu"),
@@ -36,15 +48,15 @@ beforeEach(() => {
regionOptions: [{ countryCodes: [], id: "all", label: "全部区域", value: "all" }]
});
fetchSelfGameStatisticsOverview.mockResolvedValue({});
- fetchSocialBiFilterOptions.mockResolvedValue({
- appOptions: [
- { label: "全部 App", value: "all" },
- { label: "Lalu", value: "lalu" },
- { label: "Aslan", value: "aslan" },
- { label: "Yumi", value: "yumi" }
- ],
- operatorOptions: [{ label: "全部人员", searchText: "all 全部人员", value: "all" }]
+ fetchSocialBiMaster.mockResolvedValue({
+ access: { all: true, scopes: [] },
+ apps: [],
+ operators: [],
+ permissions: { kpi_manage: false, kpi_view_all: true },
+ regions: []
});
+ fetchSocialBiOverview.mockResolvedValue({ access: { all: true, scopes: [] }, apps: [] });
+ fetchSocialBiKpi.mockResolvedValue({ items: [], period_month: "2026-06", summary: {} });
});
afterEach(() => {
@@ -212,55 +224,108 @@ test("renders real-room robot gift cards in a separate row", async () => {
test("routes databi social page to Social BI and loads real overview rows", async () => {
window.history.pushState(null, "", "/databi/social/");
- fetchStatisticsOverview.mockImplementation(async ({ appCode }) => ({
- active_users: appCode === "lalu" ? 20 : 10,
- app_name: appCode === "lalu" ? "Lalu" : appCode === "aslan" ? "Aslan" : "Yumi",
+ const laluTotal = {
+ active_users: 20,
arpu_usd_minor: 410,
- arppu_usd_minor: appCode === "aslan" ? null : 615,
+ arppu_usd_minor: 615,
avg_mic_online_ms: 120_000,
- coin_seller_recharge_usd_minor: 3400,
- coin_seller_stock_coin: 400_000,
- coin_seller_transfer_coin: 160_000,
- coin_total: appCode === "aslan" ? 5000 : null,
consumed_coin: 900_000,
consume_output_delta: 450_000,
consume_output_ratio: 0.5,
- country_breakdown: [],
+ d1_retention_rate: 0.25,
game_profit_rate: 0.17,
game_turnover: 300_000,
gift_coin_spent: 123_456,
google_recharge_usd_minor: 700,
- lucky_gift_profit_rate: 0.25,
- lucky_gift_turnover: 200_000,
- manual_grant_coin: 222,
- mic_online_ms: 3_600_000,
mifapay_recharge_usd_minor: 800,
- new_user_recharge_usd_minor: 1200,
- new_users: appCode === "lalu" ? 3 : 1,
+ new_users: 3,
output_coin: 450_000,
paid_conversion_rate: 0.2,
paid_users: 4,
- platform_grant_coin: 111,
- recharge_usd_minor: appCode === "lalu" ? 12300 : 5000,
- recharge_conversion_rate: 0.2,
- recharge_users: 4,
- retention: { day1_rate: 0.25, day7_rate: 0.1, day30_rate: 0.05 },
- salary_transfer_coin: 88_000,
- salary_usd_minor: appCode === "aslan" ? 2500 : null,
- super_lucky_gift_profit_rate: 0.08,
- super_lucky_gift_turnover: 50_000
- }));
+ recharge_usd_minor: 12_300,
+ recharge_users: 4
+ };
+ const aslanRegionRow = {
+ active_users: 10,
+ new_users: 1,
+ paid_users: 2,
+ recharge_usd_minor: 5000,
+ recharge_users: 2,
+ region_code: "MENA",
+ region_id: 11,
+ region_name: "中东大区"
+ };
+ fetchSocialBiMaster.mockResolvedValue({
+ access: { all: true, scopes: [] },
+ apps: [
+ { app_code: "lalu", app_name: "Lalu", kind: "hyapp" },
+ { app_code: "aslan", app_name: "Aslan", kind: "legacy" }
+ ],
+ operators: [
+ { account: "omar", name: "Omar", scopes: [{ app_code: "aslan", region_id: 11 }], team: "中东运营部", team_id: 2, user_id: 9 }
+ ],
+ permissions: { kpi_manage: true, kpi_view_all: true },
+ regions: [{ app_code: "aslan", countries: ["SA"], region_code: "MENA", region_id: 11, region_name: "中东大区" }]
+ });
+ fetchSocialBiOverview.mockResolvedValue({
+ access: { all: true, scopes: [] },
+ apps: [
+ {
+ app_code: "lalu",
+ app_name: "Lalu",
+ country_breakdown: [],
+ daily_country_breakdown: [],
+ daily_region_breakdown: [],
+ daily_series: [],
+ kind: "hyapp",
+ region_breakdown: [],
+ total: laluTotal
+ },
+ {
+ app_code: "aslan",
+ app_name: "Aslan",
+ country_breakdown: [],
+ daily_country_breakdown: [],
+ daily_region_breakdown: [],
+ daily_series: [],
+ kind: "legacy",
+ region_breakdown: [aslanRegionRow],
+ total: { active_users: 10, new_users: 1, paid_users: 2, recharge_usd_minor: 5000, recharge_users: 2 }
+ }
+ ]
+ });
+ fetchSocialBiKpi.mockResolvedValue({
+ items: [
+ {
+ app_code: "aslan",
+ app_name: "Aslan",
+ daily_target_usd_minor: 0,
+ month_attainment_rate: 0.9,
+ month_recharge_usd_minor: 90_000,
+ month_target_usd_minor: 100_000,
+ operator_account: "omar",
+ operator_name: "Omar",
+ operator_user_id: 9,
+ range_new_users: 1,
+ range_recharge_usd_minor: 5000,
+ region_id: 11,
+ region_name: "中东大区",
+ team: "中东运营部",
+ team_id: 2
+ }
+ ],
+ period_month: "2026-06",
+ summary: { month_recharge_usd_minor: 90_000, month_target_usd_minor: 100_000, operator_count: 1, range_recharge_usd_minor: 5000 }
+ });
render();
await flushEffects();
expect(screen.getByLabelText("Social BI 数据中心")).toBeTruthy();
- expect(screen.queryByText("App 数据明细")).toBeNull();
- expect(fetchSocialBiFilterOptions).toHaveBeenCalledTimes(1);
- expect(fetchStatisticsOverview).toHaveBeenCalledWith(expect.objectContaining({ appCode: "lalu", statTz: "Asia/Shanghai" }));
- expect(fetchStatisticsOverview).toHaveBeenCalledWith(expect.objectContaining({ appCode: "aslan", statTz: "Asia/Shanghai" }));
- expect(fetchStatisticsOverview).toHaveBeenCalledWith(expect.objectContaining({ appCode: "yumi", statTz: "Asia/Shanghai" }));
+ expect(fetchSocialBiMaster).toHaveBeenCalledTimes(1);
+ expect(fetchSocialBiOverview).toHaveBeenCalledWith(expect.objectContaining({ statTz: "Asia/Shanghai" }));
+ expect(fetchSocialBiKpi).toHaveBeenCalledWith(expect.objectContaining({ statTz: "Asia/Shanghai" }));
expect(screen.getAllByText("Lalu").length).toBeGreaterThan(0);
expect(screen.getByText("付费用户")).toBeTruthy();
expect(screen.getByText("游戏流水/利润率")).toBeTruthy();
@@ -270,7 +335,18 @@ test("routes databi social page to Social BI and loads real overview rows", asyn
expect(screen.getAllByText("300,000").length).toBeGreaterThan(0);
expect(screen.getAllByText("17.0%").length).toBeGreaterThan(0);
expect(screen.getAllByText("2分").length).toBeGreaterThan(0);
- expect(screen.queryByText("$284,300")).toBeNull();
+
+ await act(async () => {
+ screen.getByRole("tab", { name: "地区分析" }).click();
+ });
+ expect(screen.getAllByText("中东大区").length).toBeGreaterThan(0);
+
+ await act(async () => {
+ screen.getByRole("tab", { name: "人员绩效" }).click();
+ });
+ expect(screen.getAllByText("Omar").length).toBeGreaterThan(0);
+ expect(screen.getByRole("button", { name: "配置 KPI 目标" })).toBeTruthy();
+ expect(screen.getAllByText(/90.0%|90,000/).length).toBeGreaterThan(0);
});
async function flushEffects() {
diff --git a/databi/src/api.js b/databi/src/api.js
index 67328ce..a933aa6 100644
--- a/databi/src/api.js
+++ b/databi/src/api.js
@@ -155,11 +155,84 @@ export async function fetchSocialBiFilterOptions({ appCode } = {}) {
};
}
-async function fetchDatabiData(path, { appCode, query } = {}) {
- const response = await fetch(buildURL(path, query), {
- credentials: "include",
- headers: requestHeaders(appCode)
+// 社交 BI 主数据:按当前登录用户的数据范围返回可见 App、区域目录、运营人员及权限开关。
+export async function fetchSocialBiMaster() {
+ return fetchDatabiData(apiEndpointPath(API_OPERATIONS.getSocialBiMaster));
+}
+
+export async function fetchSocialBiOverview({ appCodes, endMs, regionId, startMs, statTz }) {
+ const query = {};
+ if (statTz) {
+ query.stat_tz = statTz;
+ }
+ if (startMs) {
+ query.start_ms = String(startMs);
+ }
+ if (endMs) {
+ query.end_ms = String(endMs);
+ }
+ if (appCodes?.length) {
+ query.app_codes = appCodes.join(",");
+ }
+ if (regionId) {
+ query.region_id = String(regionId);
+ }
+ return fetchDatabiData(apiEndpointPath(API_OPERATIONS.getSocialBiOverview), { query });
+}
+
+export async function fetchSocialBiKpi({ appCodes, endMs, operatorUserId, periodMonth, startMs, statTz }) {
+ const query = {};
+ if (statTz) {
+ query.stat_tz = statTz;
+ }
+ if (startMs) {
+ query.start_ms = String(startMs);
+ }
+ if (endMs) {
+ query.end_ms = String(endMs);
+ }
+ if (periodMonth) {
+ query.period_month = periodMonth;
+ }
+ if (appCodes?.length) {
+ query.app_codes = appCodes.join(",");
+ }
+ if (operatorUserId) {
+ query.operator_user_id = String(operatorUserId);
+ }
+ return fetchDatabiData(apiEndpointPath(API_OPERATIONS.getSocialBiKpi), { query });
+}
+
+export async function fetchSocialBiKpiTargets({ periodMonth, userId } = {}) {
+ const query = {};
+ if (periodMonth) {
+ query.period_month = periodMonth;
+ }
+ if (userId) {
+ query.user_id = String(userId);
+ }
+ return fetchDatabiData(apiEndpointPath(API_OPERATIONS.listSocialBiKpiTargets), { query });
+}
+
+export async function replaceSocialBiKpiTargets(items) {
+ return fetchDatabiData(apiEndpointPath(API_OPERATIONS.replaceSocialBiKpiTargets), {
+ body: { items },
+ method: "PUT"
});
+}
+
+async function fetchDatabiData(path, { appCode, body, method, query } = {}) {
+ const headers = requestHeaders(appCode);
+ const init = {
+ credentials: "include",
+ headers,
+ method: method || "GET"
+ };
+ if (body !== undefined) {
+ headers["Content-Type"] = "application/json";
+ init.body = JSON.stringify(body);
+ }
+ const response = await fetch(buildURL(path, query), init);
const payload = await readJSON(response);
if (isAuthExpired(response, payload)) {
redirectToLogin(window);
diff --git a/databi/src/components/SocialBiDashboard.jsx b/databi/src/components/SocialBiDashboard.jsx
index a74d619..ac7ec85 100644
--- a/databi/src/components/SocialBiDashboard.jsx
+++ b/databi/src/components/SocialBiDashboard.jsx
@@ -7,14 +7,12 @@ import QueryStatsOutlined from "@mui/icons-material/QueryStatsOutlined";
import RepeatOutlined from "@mui/icons-material/RepeatOutlined";
import Button from "@mui/material/Button";
import Checkbox from "@mui/material/Checkbox";
-import { fetchSocialBiFilterOptions, fetchStatisticsOverview } from "../api.js";
+import { fetchSocialBiKpi, fetchSocialBiMaster, fetchSocialBiOverview, replaceSocialBiKpiTargets } from "../api.js";
import {
ALL_VALUE,
socialBiAppOptions,
- socialBiDepartmentOptions,
socialBiFilterDefinitions,
socialBiOperatorOptions,
- socialBiRegionOptions,
socialBiSections
} from "../data/socialBiDashboard.js";
import { DEFAULT_TIME_ZONE, rangeEndMs, rangeStartMs, thisMonthRange, todayRange } from "../utils/time.js";
@@ -39,17 +37,17 @@ const sectionIcons = {
export function SocialBiDashboard() {
const [activeSectionKey, setActiveSectionKey] = useState(socialBiSections[0].key);
const [filters, setFilters] = useState(() => createInitialFilters());
- const [filterOptions, setFilterOptions] = useState(() => ({
- apps: socialBiAppOptions,
- operators: socialBiOperatorOptions
- }));
+ const [master, setMaster] = useState(null);
const [loadingToken, setLoadingToken] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [loadError, setLoadError] = useState("");
const [realRows, setRealRows] = useState(() => createEmptyRealRows());
+ const [kpiData, setKpiData] = useState(null);
+ const [isTargetDialogOpen, setIsTargetDialogOpen] = useState(false);
const [openFilterKey, setOpenFilterKey] = useState(null);
const [sortBySection, setSortBySection] = useState(() => createInitialSortState());
const filterSectionRef = useRef(null);
+ const filterOptions = useMemo(() => filterOptionsFromMaster(master), [master]);
const filterDefinitions = useMemo(() => createRuntimeFilterDefinitions(filterOptions), [filterOptions]);
const runtimeSections = useMemo(() => applyRealRowsToSections(realRows), [realRows]);
const activeSection = useMemo(
@@ -61,24 +59,36 @@ export function SocialBiDashboard() {
const sortedRows = useMemo(() => sortRows(filteredRows, activeSection.columns, sortState), [activeSection.columns, filteredRows, sortState]);
const totalRow = useMemo(() => createTotalRow(activeSection, filteredRows), [activeSection, filteredRows]);
const summaryCards = useMemo(() => createSummaryCards(activeSection, filteredRows, totalRow), [activeSection, filteredRows, totalRow]);
+ const canManageKpi = Boolean(master?.permissions?.kpi_manage);
+ const canViewAllKpi = Boolean(master?.permissions?.kpi_view_all);
useEffect(() => {
let ignore = false;
const range = dateRangeToCalendarRange(filters.dateRange);
const appCodes = selectedSocialBiAppCodes(filters.apps, filterOptions.apps);
+ const startMs = rangeStartMs(range, SOCIAL_BI_STAT_TZ);
+ const endMs = rangeEndMs(range, SOCIAL_BI_STAT_TZ);
setIsLoading(true);
setLoadError("");
- fetchSocialBiOverviewRows({ appCodes, range })
- .then((rows) => {
- if (!ignore) {
- setRealRows(rows);
+ Promise.allSettled([
+ fetchSocialBiOverview({ appCodes, endMs, startMs, statTz: SOCIAL_BI_STAT_TZ }),
+ fetchSocialBiKpi({ appCodes, endMs, startMs, statTz: SOCIAL_BI_STAT_TZ })
+ ])
+ .then(([overviewResult, kpiResult]) => {
+ if (ignore) {
+ return;
}
- })
- .catch((err) => {
- if (!ignore) {
+ const overview = overviewResult.status === "fulfilled" ? overviewResult.value : null;
+ const kpi = kpiResult.status === "fulfilled" ? kpiResult.value : null;
+ if (!overview && !kpi) {
setRealRows(createEmptyRealRows());
- setLoadError(err.message || "数据加载失败");
+ setKpiData(null);
+ setLoadError(overviewResult.reason?.message || "数据加载失败");
+ return;
}
+ setKpiData(kpi);
+ setRealRows(buildSocialBiRows({ kpi, overview, range }));
+ setLoadError(collectSocialBiErrors(overview, kpi, overviewResult, kpiResult));
})
.finally(() => {
if (!ignore) {
@@ -107,25 +117,15 @@ export function SocialBiDashboard() {
useEffect(() => {
let ignore = false;
- fetchSocialBiFilterOptions()
- .then((options) => {
- if (ignore) {
- return;
+ fetchSocialBiMaster()
+ .then((data) => {
+ if (!ignore && data) {
+ setMaster(data);
}
- const nextOptions = {
- apps: options.appOptions?.length ? options.appOptions : socialBiAppOptions,
- operators: options.operatorOptions?.length ? options.operatorOptions : socialBiOperatorOptions
- };
- setFilterOptions((current) => {
- if (sameOptionValues(current.apps, nextOptions.apps) && sameOptionValues(current.operators, nextOptions.operators)) {
- return current;
- }
- return nextOptions;
- });
})
.catch(() => {
if (!ignore) {
- setFilterOptions({ apps: socialBiAppOptions, operators: socialBiOperatorOptions });
+ setMaster(null);
}
});
return () => {
@@ -142,6 +142,11 @@ export function SocialBiDashboard() {
setLoadingToken((current) => current + 1);
}, []);
+ const handleTargetsSaved = useCallback(() => {
+ setIsTargetDialogOpen(false);
+ setLoadingToken((current) => current + 1);
+ }, []);
+
const toggleFilterPopup = useCallback((key) => {
setOpenFilterKey((current) => (current === key ? null : key));
}, []);
@@ -173,8 +178,25 @@ export function SocialBiDashboard() {
onQuery={handleQuery}
onToggleDropdown={toggleFilterPopup}
openFilterKey={openFilterKey}
+ actions={
+ activeSection.key === "personnel" && canManageKpi ? (
+
+ ) : null
+ }
/>
+ {(activeSection.key === "personnel" || activeSection.key === "department") && master && !canViewAllKpi ? (
+
当前仅展示你负责的 App / 区域数据(我的 KPI)。
+ ) : null}
+
+ {isTargetDialogOpen ? (
+ setIsTargetDialogOpen(false)}
+ onSaved={handleTargetsSaved}
+ />
+ ) : null}
);
}
@@ -223,6 +252,7 @@ function SocialBiSidebar({ activeKey, onSectionChange }) {
}
function FilterSection({
+ actions = null,
activeSection,
filterDefinitions,
filterRef,
@@ -303,6 +333,7 @@ function FilterSection({
>
{isLoading ? "查询中" : "查询"}
+ {actions}
{loadError ? {loadError} : null}
);
@@ -631,61 +662,181 @@ function applyRealRowsToSections(realRows) {
}));
}
-async function fetchSocialBiOverviewRows({ appCodes, range }) {
- if (!appCodes.length) {
- return createEmptyRealRows();
- }
- const startMs = rangeStartMs(range, SOCIAL_BI_STAT_TZ);
- const endMs = rangeEndMs(range, SOCIAL_BI_STAT_TZ);
- const results = await Promise.allSettled(
- appCodes.map(async (appCode) => {
- const overview = await fetchStatisticsOverview({
- appCode,
- countryId: 0,
- endMs,
- regionId: "all",
- seriesEndMs: endMs,
- seriesStartMs: startMs,
- startMs,
- statTz: SOCIAL_BI_STAT_TZ
- });
- return { appCode, overview: overview || {} };
- })
- );
- const fulfilled = results.filter((item) => item.status === "fulfilled").map((item) => item.value);
- if (!fulfilled.length && results.length) {
- throw results[0].reason || new Error("数据加载失败");
- }
+// buildSocialBiRows 把后端 databi 聚合结果映射成五个板块的表格行:
+// App 数据/地区/留存来自 overview(服务端已按数据范围裁剪),人员绩效与部门看板来自 KPI 接口。
+function buildSocialBiRows({ kpi, overview, range }) {
const rows = createEmptyRealRows();
- fulfilled.forEach(({ appCode, overview }) => {
- rows.appData.push(createAppDataRow(appCode, overview, range));
- const countryRows = normalizeSocialCountries(appCode, overview, range);
- rows.regional.push(...countryRows.map(createRegionalRow));
- rows.retention.push(...countryRows.map(createRetentionRow));
+ (overview?.apps || []).forEach((app) => {
+ if (app.error) {
+ return;
+ }
+ rows.appData.push(...createAppDailyRows(app, range));
+ const regionRows = createRegionRows(app, range);
+ rows.regional.push(...regionRows.map(createRegionalRow));
+ rows.retention.push(...regionRows.map(createRetentionRow));
});
+ const kpiRows = createKpiRows(kpi, range);
+ rows.personnel = kpiRows;
+ rows.department = createDepartmentRows(kpiRows, range);
return rows;
}
-function createAppDataRow(appCode, overview, range) {
- return {
- appName: appLabel(appCode, overview),
- date: rangeLabel(range),
- id: `app-${appCode}`,
- ...createSocialMetricFields(overview)
- };
+function collectSocialBiErrors(overview, kpi, overviewResult, kpiResult) {
+ const errors = [];
+ if (overviewResult?.status === "rejected") {
+ errors.push(`统计数据加载失败: ${overviewResult.reason?.message || "未知错误"}`);
+ }
+ if (kpiResult?.status === "rejected") {
+ errors.push(`KPI 数据加载失败: ${kpiResult.reason?.message || "未知错误"}`);
+ }
+ (overview?.apps || []).forEach((app) => {
+ if (app.error) {
+ errors.push(`${app.app_name || app.app_code}: ${app.error}`);
+ }
+ });
+ Object.entries(kpi?.app_errors || {}).forEach(([appCode, message]) => {
+ errors.push(`${appCode}: ${message}`);
+ });
+ return errors.join(";");
}
-function normalizeSocialCountries(appCode, overview, range) {
- const countries = Array.isArray(overview?.country_breakdown) ? overview.country_breakdown : [];
- return countries.map((country, index) => ({
- appCode,
- appName: appLabel(appCode, overview),
- country,
- date: rangeLabel(range),
- id: `${appCode}-country-${country.country_code || country.countryCode || country.country_id || country.countryId || index}`,
- region: countryLabel(country),
- ...createSocialMetricFields(country)
- }));
+function createAppDailyRows(app, range) {
+ const appName = app.app_name || appCodeLabel(app.app_code);
+ const series = Array.isArray(app.daily_series) ? app.daily_series : [];
+ // 单日区间直接用整段合计(保留留存等区间口径字段),多日区间展开成 天×App 行,与截图口径一致。
+ if (series.length > 1) {
+ return series.map((dayRow) => ({
+ appCode: app.app_code,
+ appName,
+ date: dayRow.stat_day || dayRow.label || rangeLabel(range),
+ id: `app-${app.app_code}-${dayRow.stat_day || dayRow.label}`,
+ ...createSocialMetricFields(dayRow)
+ }));
+ }
+ return [
+ {
+ appCode: app.app_code,
+ appName,
+ date: rangeLabel(range),
+ id: `app-${app.app_code}`,
+ ...createSocialMetricFields(app.total || {})
+ }
+ ];
+}
+
+function createRegionRows(app, range) {
+ const appName = app.app_name || appCodeLabel(app.app_code);
+ const daily = Array.isArray(app.daily_region_breakdown) ? app.daily_region_breakdown : [];
+ const totals = Array.isArray(app.region_breakdown) ? app.region_breakdown : [];
+ const useDaily = daily.length > 0 && new Set(daily.map((row) => row.stat_day || row.label)).size > 1;
+ const source = useDaily ? daily : totals;
+ return source.map((row, index) => {
+ const regionId = Number(row.region_id ?? 0);
+ const date = useDaily ? row.stat_day || row.label || rangeLabel(range) : rangeLabel(range);
+ return {
+ appCode: app.app_code,
+ appName,
+ date,
+ id: `${app.app_code}-region-${regionId || index}-${date}`,
+ region: row.region_name || row.region_code || `区域 ${regionId}`,
+ regionId,
+ regionKey: `${app.app_code}:${regionId}`,
+ ...createSocialMetricFields(row)
+ };
+ });
+}
+
+function createKpiRows(kpi, range) {
+ const days = rangeDayCount(range);
+ return (kpi?.items || []).map((item) => {
+ const rangeRecharge = moneyMajor(readOptionalNumber(item, "range_recharge_usd_minor"));
+ const monthRecharge = moneyMajor(readOptionalNumber(item, "month_recharge_usd_minor"));
+ const monthTarget = moneyMajor(Number(item.month_target_usd_minor || 0)) || 0;
+ const dailyTarget = moneyMajor(Number(item.daily_target_usd_minor || 0)) || 0;
+ const dayTarget = dailyTarget > 0 ? dailyTarget * days : monthTarget > 0 ? (monthTarget / 30) * days : 0;
+ return {
+ appCode: item.app_code,
+ appName: item.app_name || appCodeLabel(item.app_code),
+ dataError: item.data_error || "",
+ date: rangeLabel(range),
+ dayProgress: dayTarget > 0 ? { actual: rangeRecharge || 0, target: dayTarget } : { actual: rangeRecharge || 0, noGoal: true, target: 0 },
+ id: `kpi-${item.operator_user_id}-${item.app_code}-${item.region_id}`,
+ monthKpiRate: item.month_attainment_rate === null || item.month_attainment_rate === undefined ? null : Number(item.month_attainment_rate) * 100,
+ monthProgress: monthTarget > 0 ? { actual: monthRecharge || 0, target: monthTarget } : { actual: monthRecharge || 0, noGoal: true, target: 0 },
+ monthRecharge,
+ monthTarget: monthTarget > 0 ? monthTarget : null,
+ newUsers: readOptionalNumber(item, "range_new_users"),
+ operator: item.operator_name || item.operator_account || `#${item.operator_user_id}`,
+ operatorUserId: item.operator_user_id,
+ recharge: rangeRecharge,
+ region: item.region_name || "全部区域",
+ regionId: Number(item.region_id ?? 0),
+ regionKey: `${item.app_code}:${Number(item.region_id ?? 0)}`,
+ team: item.team || "",
+ teamId: item.team_id ?? null
+ };
+ });
+}
+
+function createDepartmentRows(kpiRows, range) {
+ const groups = new Map();
+ kpiRows.forEach((row) => {
+ const team = row.team || "未分组";
+ const key = `${team}|${row.appCode}`;
+ const group = groups.get(key) || {
+ appCode: row.appCode,
+ appName: row.appName,
+ date: rangeLabel(range),
+ dayActual: 0,
+ dayTarget: 0,
+ department: team,
+ id: `dept-${key}`,
+ monthActual: 0,
+ monthRecharge: 0,
+ monthTarget: 0,
+ newUsers: null,
+ recharge: null,
+ regions: new Set(),
+ team
+ };
+ group.recharge = sumOptionalNumbers(group.recharge, row.recharge);
+ group.newUsers = sumOptionalNumbers(group.newUsers, row.newUsers);
+ group.monthRecharge = sumOptionalNumbers(group.monthRecharge, row.monthRecharge) || 0;
+ group.monthTarget += row.monthTarget || 0;
+ group.dayActual += Number(row.dayProgress?.actual || 0);
+ group.dayTarget += row.dayProgress?.noGoal ? 0 : Number(row.dayProgress?.target || 0);
+ group.monthActual += Number(row.monthProgress?.actual || 0);
+ group.regions.add(row.region);
+ groups.set(key, group);
+ });
+ return [...groups.values()].map((group) => {
+ const regionNames = [...group.regions];
+ return {
+ appCode: group.appCode,
+ appName: group.appName,
+ date: group.date,
+ dayProgress: group.dayTarget > 0 ? { actual: group.dayActual, target: group.dayTarget } : { actual: group.dayActual, noGoal: true, target: 0 },
+ department: group.department,
+ id: group.id,
+ monthKpiRate: group.monthTarget > 0 ? (group.monthActual / group.monthTarget) * 100 : null,
+ monthProgress: group.monthTarget > 0 ? { actual: group.monthActual, target: group.monthTarget } : { actual: group.monthActual, noGoal: true, target: 0 },
+ monthRecharge: group.monthRecharge,
+ monthTarget: group.monthTarget > 0 ? group.monthTarget : null,
+ newUsers: group.newUsers,
+ recharge: group.recharge,
+ region: regionNames.length > 2 ? `${regionNames.slice(0, 2).join(" / ")} 等${regionNames.length}个` : regionNames.join(" / "),
+ team: group.team
+ };
+ });
+}
+
+function rangeDayCount(range) {
+ const start = new Date(`${range.start}T00:00:00Z`).getTime();
+ const end = new Date(`${range.end}T00:00:00Z`).getTime();
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) {
+ return 1;
+ }
+ return Math.round((end - start) / 86400000) + 1;
}
function createSocialMetricFields(source) {
@@ -737,25 +888,11 @@ function createSocialMetricFields(source) {
}
function createRegionalRow(row) {
- return {
- ...row,
- agencyCount: null,
- bdCount: null,
- coinSellerCoins: readOptionalNumber(row.country, "coin_seller_stock_coin", "coinSellerStockCoin"),
- coinSellerCount: null,
- hostCount: null
- };
+ return { ...row, id: `regional-${row.id}` };
}
function createRetentionRow(row) {
- return {
- ...row,
- agencyActive: null,
- bdActive: null,
- coinSellerActive: null,
- ecosystemActive: null,
- hostActive: null
- };
+ return { ...row, id: `retention-${row.id}` };
}
function selectedSocialBiAppCodes(selectedValues, appOptions) {
@@ -802,19 +939,11 @@ function rangeLabel(range) {
return range?.start === range?.end ? range.start : `${range.start} ~ ${range.end}`;
}
-function appLabel(appCode, overview) {
- return overview?.app_name || overview?.appName || appCodeLabel(appCode);
-}
-
function appCodeLabel(appCode) {
const normalized = String(appCode || "").toLowerCase();
return socialBiAppOptions.find((option) => option.value === normalized)?.label || normalized || "-";
}
-function countryLabel(country) {
- return country?.country || country?.country_name || country?.countryName || country?.country_code || country?.countryCode || "未知地区";
-}
-
function retentionPercent(source, topLevelKey, retentionKey) {
const value = readOptionalNumber(source, topLevelKey, retentionKey) ?? readOptionalNumber(source?.retention, retentionKey, topLevelKey);
return value === null ? null : value * 100;
@@ -905,72 +1034,158 @@ function defaultSortState(section) {
return { direction: "desc", key: revenueColumn?.key || section.columns[0].key };
}
+// filterOptionsFromMaster 把 master 主数据(已按登录用户数据范围裁剪)转成筛选器选项:
+// App 多选、按 App 分组的区域级联、带负责范围的运营人员、按 team 聚合的部门。
+function filterOptionsFromMaster(master) {
+ if (!master) {
+ return {
+ apps: socialBiAppOptions,
+ departments: [{ label: "全部部门", value: ALL_VALUE }],
+ operators: socialBiOperatorOptions,
+ regions: []
+ };
+ }
+ const apps = [
+ { label: "全部 App", value: ALL_VALUE },
+ ...(master.apps || []).map((app) => ({ label: app.app_name || app.app_code, value: app.app_code }))
+ ];
+ const operators = [
+ { label: "全部人员", scopes: [], searchText: "all 全部人员", value: ALL_VALUE },
+ ...(master.operators || []).map((operator) => ({
+ label: operator.name || operator.account || `#${operator.user_id}`,
+ scopes: operator.scopes || [],
+ searchText: [operator.name, operator.account, operator.team].filter(Boolean).join(" ").toLowerCase(),
+ value: String(operator.user_id)
+ }))
+ ];
+ const teamNames = [...new Set((master.operators || []).map((operator) => operator.team).filter(Boolean))];
+ const departments = [
+ { label: "全部部门", value: ALL_VALUE },
+ ...teamNames.map((name) => ({ label: name, value: name })),
+ ...((master.operators || []).some((operator) => !operator.team) ? [{ label: "未分组", value: "未分组" }] : [])
+ ];
+ const regionsByApp = new Map();
+ (master.regions || []).forEach((region) => {
+ const list = regionsByApp.get(region.app_code) || [];
+ list.push(region);
+ regionsByApp.set(region.app_code, list);
+ });
+ const regions = (master.apps || [])
+ .map((app) => ({
+ children: (regionsByApp.get(app.app_code) || []).map((region) => ({
+ label: region.region_name || region.region_code || `区域 ${region.region_id}`,
+ value: `${app.app_code}:${region.region_id}`
+ })),
+ label: app.app_name || app.app_code,
+ value: `app:${app.app_code}`
+ }))
+ .filter((group) => group.children.length);
+ return { apps, departments, operators, regions };
+}
+
function createRuntimeFilterDefinitions(filterOptions) {
return {
...socialBiFilterDefinitions,
apps: { ...socialBiFilterDefinitions.apps, options: filterOptions.apps },
- operators: { ...socialBiFilterDefinitions.operators, options: filterOptions.operators }
+ departments: { ...socialBiFilterDefinitions.departments, options: filterOptions.departments },
+ operators: { ...socialBiFilterDefinitions.operators, options: filterOptions.operators },
+ regions: { ...socialBiFilterDefinitions.regions, options: filterOptions.regions }
};
}
function applySectionFilters(section, filters, filterDefinitions) {
- // 前端布局阶段只做可验证的本地过滤:按当前行里已有的 App、部门、人员、地区字段筛选,不推断后端不存在的维度。
+ const operatorScopes = selectedOperatorScopes(filters.operators, filterDefinitions.operators.options);
return section.rows.filter((row) => {
- if (!matchesFlatSelection(row.appName, filters.apps, filterDefinitions.apps.options)) {
+ if (!matchesAppSelection(row.appCode, filters.apps)) {
return false;
}
- if (section.filters.includes("departments") && !matchesFlatSelection(row.department, filters.departments, socialBiDepartmentOptions)) {
+ if (section.filters.includes("departments") && !matchesDepartmentSelection(row, filters.departments)) {
return false;
}
- if (section.filters.includes("operators") && row.operator && !matchesFlatSelection(row.operator, filters.operators, filterDefinitions.operators.options)) {
+ if (section.filters.includes("operators") && !matchesOperatorSelection(row, filters.operators, operatorScopes)) {
return false;
}
- if (section.filters.includes("regions") && !matchesRegionSelection(row.region, filters.regions)) {
+ if (section.filters.includes("regions") && !matchesRegionSelection(row, filters.regions)) {
return false;
}
return true;
});
}
-function matchesFlatSelection(rowLabel, values, options) {
- if (!values?.length || values.includes(ALL_VALUE) || !rowLabel) {
+function matchesAppSelection(appCode, values) {
+ if (!values?.length || values.includes(ALL_VALUE) || !appCode) {
return true;
}
- const selectedLabels = options.filter((option) => values.includes(option.value)).map((option) => normalizeLabel(option.label));
- const normalizedRowLabel = normalizeLabel(rowLabel);
- return selectedLabels.some((label) => normalizedRowLabel.includes(label) || label.includes(normalizedRowLabel));
+ return values.includes(String(appCode).toLowerCase());
}
-function matchesRegionSelection(rowLabel, values) {
- if (!values?.length || values.includes(ALL_VALUE) || !rowLabel) {
+function matchesDepartmentSelection(row, values) {
+ if (!values?.length || values.includes(ALL_VALUE)) {
return true;
}
- const selectedLabels = selectedRegionLabels(values).map(normalizeLabel);
- const normalizedRowLabel = normalizeLabel(rowLabel);
- return selectedLabels.some((label) => normalizedRowLabel.includes(label) || label.includes(normalizedRowLabel));
+ return values.includes(row.team || row.department || "未分组");
}
-function selectedRegionLabels(values) {
- return socialBiRegionOptions.flatMap((group) => {
- const labels = [];
- if (values.includes(group.value)) {
- labels.push(group.label, ...group.children.map((child) => child.label));
+function selectedOperatorScopes(values, options) {
+ if (!values?.length || values.includes(ALL_VALUE)) {
+ return null;
+ }
+ const scopes = [];
+ options.forEach((option) => {
+ if (values.includes(option.value)) {
+ scopes.push(...(option.scopes || []));
}
- group.children.forEach((child) => {
- if (values.includes(child.value)) {
- labels.push(child.label);
- }
- });
- return labels;
});
+ return scopes;
+}
+
+// 人员绩效行按 operatorUserId 精确过滤;其它板块按所选人员的负责范围(App×区域)过滤行。
+function matchesOperatorSelection(row, values, operatorScopes) {
+ if (!values?.length || values.includes(ALL_VALUE)) {
+ return true;
+ }
+ if (row.operatorUserId !== undefined) {
+ return values.includes(String(row.operatorUserId));
+ }
+ if (!operatorScopes) {
+ return true;
+ }
+ if (!operatorScopes.length) {
+ return false;
+ }
+ const appCode = String(row.appCode || "").toLowerCase();
+ return operatorScopes.some((scope) => {
+ if (String(scope.app_code || "").toLowerCase() !== appCode) {
+ return false;
+ }
+ if (row.regionId === undefined || scope.region_id === 0) {
+ return true;
+ }
+ return Number(scope.region_id) === Number(row.regionId);
+ });
+}
+
+function matchesRegionSelection(row, values) {
+ if (!values?.length || values.includes(ALL_VALUE)) {
+ return true;
+ }
+ return values.includes(`app:${row.appCode}`) || values.includes(row.regionKey);
}
function createSummaryCards(section, rows, totalRow) {
- return section.cards.map((card) => ({
- ...card,
- delta: null,
- value: totalRow[card.key] ?? aggregateLooseValue(rows, card)
- }));
+ return section.cards.map((card) => {
+ // KPI 达成率是比值,用充值合计/目标合计的加权口径,而不是逐行平均。
+ if (card.key === "monthKpiRate") {
+ const monthRecharge = sumOptionalNumbers(...rows.map((row) => row.monthRecharge));
+ const monthTarget = sumOptionalNumbers(...rows.map((row) => row.monthTarget));
+ return { ...card, delta: null, value: monthTarget ? ((monthRecharge || 0) / monthTarget) * 100 : null };
+ }
+ return {
+ ...card,
+ delta: null,
+ value: totalRow[card.key] ?? aggregateLooseValue(rows, card)
+ };
+ });
}
function createTotalRow(section, rows) {
@@ -1113,13 +1328,6 @@ function selectedOptionsLabel(options, values) {
return `${selected[0].label} +${selected.length - 1}`;
}
-function sameOptionValues(left = [], right = []) {
- if (left.length !== right.length) {
- return false;
- }
- return left.every((item, index) => item.value === right[index]?.value && item.label === right[index]?.label);
-}
-
function selectedCascadeLabel(options, values) {
if (!values?.length || values.includes(ALL_VALUE)) {
return "全部地区";
@@ -1159,10 +1367,6 @@ function toggleCascadeChild(currentValues, childValue) {
return next.length ? next : [ALL_VALUE];
}
-function normalizeLabel(value) {
- return String(value || "").trim().toLowerCase();
-}
-
function formatTypedValue(value, type) {
if (value === null || value === undefined || value === "" || (typeof value === "number" && !Number.isFinite(value))) {
return "--";
@@ -1228,3 +1432,144 @@ function formatDecimal(value) {
}
return numeric.toLocaleString("en-US", { maximumFractionDigits: 1, minimumFractionDigits: 1 });
}
+
+// KpiTargetDialog 按 运营人员×App×区域 编辑当月充值 KPI 目标(美元),保存走 PUT /admin/databi/social/kpi-targets。
+// 月/日目标都填 0 表示清除该条目标。
+function KpiTargetDialog({ kpiData, onClose, onSaved }) {
+ const periodMonth = kpiData?.period_month || defaultPeriodMonth();
+ const [drafts, setDrafts] = useState(() =>
+ (kpiData?.items || []).map((item) => ({
+ appCode: item.app_code,
+ appName: item.app_name || item.app_code,
+ dailyTargetUsd: minorToUsdInput(item.daily_target_usd_minor),
+ monthTargetUsd: minorToUsdInput(item.month_target_usd_minor),
+ operator: item.operator_name || item.operator_account || `#${item.operator_user_id}`,
+ operatorUserId: item.operator_user_id,
+ regionId: Number(item.region_id ?? 0),
+ regionName: item.region_name || "全部区域"
+ }))
+ );
+ const [isSaving, setIsSaving] = useState(false);
+ const [saveError, setSaveError] = useState("");
+
+ const updateDraft = (index, key, value) => {
+ setDrafts((current) => current.map((draft, draftIndex) => (draftIndex === index ? { ...draft, [key]: value } : draft)));
+ };
+
+ const handleSave = async () => {
+ const items = drafts.map((draft) => ({
+ appCode: draft.appCode,
+ dailyTargetUsdMinor: usdInputToMinor(draft.dailyTargetUsd),
+ periodMonth,
+ regionId: draft.regionId,
+ targetUsdMinor: usdInputToMinor(draft.monthTargetUsd),
+ userId: draft.operatorUserId
+ }));
+ if (items.some((item) => item.targetUsdMinor === null || item.dailyTargetUsdMinor === null)) {
+ setSaveError("目标金额格式不正确");
+ return;
+ }
+ setIsSaving(true);
+ setSaveError("");
+ try {
+ await replaceSocialBiKpiTargets(items);
+ onSaved();
+ } catch (error) {
+ setSaveError(error.message || "保存失败");
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ return (
+
+
event.stopPropagation()}
+ role="dialog"
+ >
+
+ 配置充值 KPI 目标
+ {periodMonth} · 按 运营人员 × App × 区域
+
+
+ {drafts.length ? (
+
+ ) : (
+
当前没有可配置的负责范围;请先在「用户管理」里为运营人员分配 App/区域数据范围。
+ )}
+
+
+
+
+ );
+}
+
+function defaultPeriodMonth() {
+ const now = new Date();
+ return `${now.getFullYear()}-${pad2(now.getMonth() + 1)}`;
+}
+
+function minorToUsdInput(minor) {
+ const numeric = Number(minor || 0);
+ if (!Number.isFinite(numeric) || numeric <= 0) {
+ return "0";
+ }
+ return String(numeric / 100);
+}
+
+function usdInputToMinor(value) {
+ const trimmed = String(value ?? "").trim();
+ if (trimmed === "") {
+ return 0;
+ }
+ const numeric = Number(trimmed);
+ if (!Number.isFinite(numeric) || numeric < 0) {
+ return null;
+ }
+ return Math.round(numeric * 100);
+}
diff --git a/databi/src/data/socialBiDashboard.js b/databi/src/data/socialBiDashboard.js
index 28a131e..71cc126 100644
--- a/databi/src/data/socialBiDashboard.js
+++ b/databi/src/data/socialBiDashboard.js
@@ -1,5 +1,7 @@
export const ALL_VALUE = "all";
+// 以下 options 只是接口不可用时的兜底;真实的 App/人员/部门/地区目录来自
+// GET /admin/databi/social/master(按登录用户数据范围裁剪)。
export const socialBiAppOptions = [
{ label: "全部 App", value: ALL_VALUE },
{ label: "Lalu", value: "lalu" },
@@ -7,49 +9,11 @@ export const socialBiAppOptions = [
{ label: "Yumi", value: "yumi" }
];
-export const socialBiOperatorOptions = [
- { label: "全部人员", searchText: "all", value: ALL_VALUE },
- { label: "Ava Chen", searchText: "ava chen 陈", value: "ava" },
- { label: "Nora Li", searchText: "nora li 李", value: "nora" },
- { label: "Omar Saleh", searchText: "omar saleh", value: "omar" },
- { label: "Lina Gomez", searchText: "lina gomez", value: "lina" }
-];
+export const socialBiOperatorOptions = [{ label: "全部人员", searchText: "all", value: ALL_VALUE }];
-export const socialBiDepartmentOptions = [
- { label: "全部部门", value: ALL_VALUE },
- { label: "拉美运营一部", value: "latam-ops-1" },
- { label: "中东运营部", value: "mena-ops" },
- { label: "泛娱乐增长组", value: "growth" }
-];
+export const socialBiDepartmentOptions = [{ label: "全部部门", value: ALL_VALUE }];
-export const socialBiRegionOptions = [
- {
- children: [
- { label: "巴西", value: "br" },
- { label: "墨西哥", value: "mx" },
- { label: "哥伦比亚", value: "co" }
- ],
- label: "拉美大区",
- value: "latam"
- },
- {
- children: [
- { label: "沙特", value: "sa" },
- { label: "阿联酋", value: "ae" },
- { label: "埃及", value: "eg" }
- ],
- label: "中东大区",
- value: "mena"
- },
- {
- children: [
- { label: "印度尼西亚", value: "id" },
- { label: "菲律宾", value: "ph" }
- ],
- label: "东南亚大区",
- value: "sea"
- }
-];
+export const socialBiRegionOptions = [];
export const socialBiFilterDefinitions = {
apps: { label: "App", options: socialBiAppOptions, type: "multi" },
@@ -62,10 +26,10 @@ export const socialBiFilterDefinitions = {
export const socialBiSections = [
{
cards: [
- { delta: 8.4, key: "recharge", label: "总充值 ($)", type: "money" },
- { delta: -2.6, key: "newUsers", label: "总新增人数", type: "number" },
- { delta: 3.8, key: "dau", label: "总日活 (DAU)", type: "number" },
- { delta: 1.7, key: "arppu", label: "综合 ARPPU ($)", type: "money" }
+ { key: "recharge", label: "总充值 ($)", type: "money" },
+ { key: "newUsers", label: "总新增人数", type: "number" },
+ { key: "dau", label: "总日活 (DAU)", type: "number" },
+ { key: "arppu", label: "综合 ARPPU ($)", type: "money" }
],
columns: [
textColumn("date", "日期"),
@@ -107,61 +71,15 @@ export const socialBiSections = [
filters: ["dateRange", "apps", "operators"],
key: "appData",
label: "App 数据",
- rows: [
- {
- appName: "Lalu",
- arppu: 42.7,
- coins: 49120000,
- date: "2026-06-30",
- dau: 18520,
- id: "app-lalu",
- newUsers: 843,
- recharge: 284300,
- retentionD1: 31.6,
- retentionD7: 18.4,
- retentionD30: 9.8,
- salary: 62310,
- turnover: 493820
- },
- {
- appName: "Aslan",
- arppu: 39.4,
- coins: 32760000,
- date: "2026-06-30",
- dau: 12740,
- id: "app-aslan",
- newUsers: 612,
- recharge: 196740,
- retentionD1: 29.2,
- retentionD7: 16.1,
- retentionD30: 8.7,
- salary: 42180,
- turnover: 338560
- },
- {
- appName: "Yumi",
- arppu: 36.2,
- coins: 24890000,
- date: "2026-06-30",
- dau: 9240,
- id: "app-yumi",
- newUsers: 438,
- recharge: 142860,
- retentionD1: 27.9,
- retentionD7: 15.5,
- retentionD30: 7.9,
- salary: 31860,
- turnover: 251420
- }
- ],
+ rows: [],
title: "App 数据"
},
{
cards: [
- { delta: 7.3, key: "recharge", label: "部门总充值 ($)", type: "money" },
- { delta: 4.8, key: "turnover", label: "部门总流水 ($)", type: "money" },
- { delta: -1.8, key: "newUsers", label: "部门总新增", type: "number" },
- { delta: 2.9, key: "dau", label: "部门总日活 (DAU)", type: "number" }
+ { key: "recharge", label: "部门区间充值 ($)", type: "money" },
+ { key: "monthRecharge", label: "部门当月充值 ($)", type: "money" },
+ { key: "newUsers", label: "部门区间新增", type: "number" },
+ { key: "monthKpiRate", label: "当月 KPI 达成率 (%)", type: "percent" }
],
columns: [
textColumn("date", "日期"),
@@ -176,55 +94,15 @@ export const socialBiSections = [
filters: ["dateRange", "apps", "departments"],
key: "department",
label: "部门看板",
- rows: [
- {
- appName: "Lalu",
- date: "2026-06-30",
- dau: 14220,
- dayProgress: progress(126800, 140000),
- department: "拉美运营一部",
- id: "dept-latam",
- monthProgress: progress(3020000, 3600000),
- newUsers: 516,
- recharge: 126800,
- region: "拉美大区",
- turnover: 225400
- },
- {
- appName: "Aslan",
- date: "2026-06-30",
- dau: 10540,
- dayProgress: progress(96800, 90000),
- department: "中东运营部",
- id: "dept-mena",
- monthProgress: progress(2416000, 2700000),
- newUsers: 452,
- recharge: 96800,
- region: "中东大区",
- turnover: 164300
- },
- {
- appName: "Yumi",
- date: "2026-06-30",
- dau: 8160,
- dayProgress: noGoal(),
- department: "泛娱乐增长组",
- id: "dept-growth",
- monthProgress: progress(1682000, 2100000),
- newUsers: 372,
- recharge: 74200,
- region: "东南亚大区",
- turnover: 118900
- }
- ],
+ rows: [],
title: "部门看板"
},
{
cards: [
- { delta: 6.5, key: "recharge", label: "人员充值合计 ($)", type: "money" },
- { delta: 4.4, key: "turnover", label: "人员流水合计 ($)", type: "money" },
- { delta: 2.1, key: "newUsers", label: "新增人数", type: "number" },
- { delta: -3.2, key: "monthKpiRate", label: "当月充值 KPI 达成率 (%)", type: "percent" }
+ { key: "recharge", label: "人员区间充值合计 ($)", type: "money" },
+ { key: "monthRecharge", label: "人员当月充值合计 ($)", type: "money" },
+ { key: "monthTarget", label: "当月目标合计 ($)", type: "money" },
+ { key: "monthKpiRate", label: "当月充值 KPI 达成率 (%)", type: "percent" }
],
columns: [
textColumn("date", "日期"),
@@ -239,54 +117,14 @@ export const socialBiSections = [
filters: ["dateRange", "apps", "operators"],
key: "personnel",
label: "人员绩效",
- rows: [
- {
- appName: "Lalu",
- date: "2026-06-30",
- dayProgress: progress(52800, 50000),
- id: "person-ava",
- monthKpiRate: 88.4,
- monthProgress: progress(1182000, 1350000),
- newUsers: 211,
- operator: "Ava Chen",
- recharge: 52800,
- region: "巴西 / 墨西哥",
- turnover: 94100
- },
- {
- appName: "Aslan",
- date: "2026-06-30",
- dayProgress: progress(41600, 45000),
- id: "person-omar",
- monthKpiRate: 82.1,
- monthProgress: progress(906000, 1100000),
- newUsers: 186,
- operator: "Omar Saleh",
- recharge: 41600,
- region: "沙特 / 阿联酋",
- turnover: 70400
- },
- {
- appName: "Yumi",
- date: "2026-06-30",
- dayProgress: noGoal(),
- id: "person-lina",
- monthKpiRate: 75.6,
- monthProgress: progress(642000, 850000),
- newUsers: 154,
- operator: "Lina Gomez",
- recharge: 32600,
- region: "菲律宾",
- turnover: 52900
- }
- ],
+ rows: [],
title: "人员绩效"
},
{
cards: [
- { delta: 9.2, key: "recharge", label: "区域总充值 ($)", type: "money" },
- { delta: -1.1, key: "newUsers", label: "区域总新增", type: "number" },
- { delta: 2.4, key: "arppu", label: "区域综合 ARPPU ($)", type: "money" }
+ { key: "recharge", label: "区域总充值 ($)", type: "money" },
+ { key: "newUsers", label: "区域总新增", type: "number" },
+ { key: "arppu", label: "区域综合 ARPPU ($)", type: "money" }
],
columns: [
textColumn("date", "日期"),
@@ -301,89 +139,25 @@ export const socialBiSections = [
percentColumn("retentionD7", "7日留"),
percentColumn("retentionD30", "30日留"),
moneyColumn("salary", "存量工资 ($)"),
- numberColumn("coins", "金币数量"),
- ratioColumn("agencyCount", "Agency数量"),
- ratioColumn("hostCount", "Host数量"),
- ratioColumn("bdCount", "BD数量"),
- ratioColumn("coinSellerCount", "币商数量"),
- numberColumn("coinSellerCoins", "币商金币数")
+ moneyColumn("googleRecharge", "谷歌充值"),
+ moneyColumn("thirdPartyRecharge", "三方充值"),
+ moneyColumn("newUserRecharge", "新用户充值"),
+ numberColumn("giftCoinSpent", "礼物流水"),
+ flowRateColumn("luckyGiftFlowProfit", "幸运礼物流水/利润率"),
+ numberColumn("coinSellerTransferCoin", "币商出货金币")
],
filters: ["dateRange", "apps", "regions"],
key: "regional",
label: "地区分析",
- rows: [
- {
- agencyCount: ratioCount(84, 112),
- appName: "Lalu",
- arppu: 44.1,
- bdCount: ratioCount(26, 34),
- coinSellerCoins: 9840000,
- coinSellerCount: ratioCount(18, 24),
- coins: 36120000,
- date: "2026-06-30",
- dau: 13280,
- hostCount: ratioCount(412, 520),
- id: "region-br",
- newUsers: 506,
- recharge: 206400,
- region: "巴西",
- retentionD1: 32.8,
- retentionD7: 19.4,
- retentionD30: 10.2,
- salary: 52200,
- turnover: 354200
- },
- {
- agencyCount: ratioCount(66, 95),
- appName: "Aslan",
- arppu: 41.5,
- bdCount: ratioCount(19, 28),
- coinSellerCoins: 7420000,
- coinSellerCount: ratioCount(15, 21),
- coins: 28450000,
- date: "2026-06-30",
- dau: 11140,
- hostCount: ratioCount(356, 480),
- id: "region-sa",
- newUsers: 442,
- recharge: 173200,
- region: "沙特",
- retentionD1: 30.4,
- retentionD7: 17.7,
- retentionD30: 9.1,
- salary: 44100,
- turnover: 292600
- },
- {
- agencyCount: ratioCount(41, 68),
- appName: "Yumi",
- arppu: 34.8,
- bdCount: ratioCount(14, 22),
- coinSellerCoins: 4180000,
- coinSellerCount: ratioCount(9, 17),
- coins: 17260000,
- date: "2026-06-30",
- dau: 6840,
- hostCount: ratioCount(228, 346),
- id: "region-ph",
- newUsers: 318,
- recharge: 94800,
- region: "菲律宾",
- retentionD1: 27.5,
- retentionD7: 14.9,
- retentionD30: 7.3,
- salary: 22600,
- turnover: 164900
- }
- ],
+ rows: [],
title: "地区分析"
},
{
cards: [
- { delta: -1.7, key: "newUsers", label: "总新增", type: "number" },
- { delta: 3.5, key: "dau", label: "总日活", type: "number" },
- { delta: 1.2, key: "retentionD1", label: "次日留存均值 (%)", type: "percent" },
- { delta: 4.1, key: "ecosystemActive", label: "生态角色总活跃数", type: "number" }
+ { key: "newUsers", label: "总新增", type: "number" },
+ { key: "dau", label: "总日活", type: "number" },
+ { key: "retentionD1", label: "次日留存均值 (%)", type: "percent" },
+ { key: "retentionD7", label: "7日留存均值 (%)", type: "percent" }
],
columns: [
textColumn("date", "日期"),
@@ -394,64 +168,14 @@ export const socialBiSections = [
percentColumn("retentionD1", "次日留存"),
percentColumn("retentionD7", "7日留存"),
percentColumn("retentionD30", "30日留存"),
- ratioColumn("agencyActive", "Agency活跃数量"),
- ratioColumn("hostActive", "Host活跃数量"),
- ratioColumn("bdActive", "BD活跃数量"),
- ratioColumn("coinSellerActive", "币商活跃数量")
+ percentColumn("paidConversionRate", "付费转化率"),
+ moneyColumn("arpu", "ARPU ($)", "average"),
+ moneyColumn("arppu", "ARPPU ($)", "average")
],
filters: ["dateRange", "apps", "regions"],
key: "retention",
label: "留存分析",
- rows: [
- {
- agencyActive: ratioCount(84, 112),
- appName: "Lalu",
- bdActive: ratioCount(26, 34),
- coinSellerActive: ratioCount(18, 24),
- date: "2026-06-30",
- dau: 13280,
- ecosystemActive: 540,
- hostActive: ratioCount(412, 520),
- id: "retain-br",
- newUsers: 506,
- region: "巴西",
- retentionD1: 32.8,
- retentionD7: 19.4,
- retentionD30: 10.2
- },
- {
- agencyActive: ratioCount(66, 95),
- appName: "Aslan",
- bdActive: ratioCount(19, 28),
- coinSellerActive: ratioCount(15, 21),
- date: "2026-06-30",
- dau: 11140,
- ecosystemActive: 456,
- hostActive: ratioCount(356, 480),
- id: "retain-sa",
- newUsers: 442,
- region: "沙特",
- retentionD1: 30.4,
- retentionD7: 17.7,
- retentionD30: 9.1
- },
- {
- agencyActive: ratioCount(41, 68),
- appName: "Yumi",
- bdActive: ratioCount(14, 22),
- coinSellerActive: ratioCount(9, 17),
- date: "2026-06-30",
- dau: 6840,
- ecosystemActive: 292,
- hostActive: ratioCount(228, 346),
- id: "retain-ph",
- newUsers: 318,
- region: "菲律宾",
- retentionD1: 27.5,
- retentionD7: 14.9,
- retentionD30: 7.3
- }
- ],
+ rows: [],
title: "留存分析"
}
];
@@ -476,10 +200,6 @@ function progressColumn(key, label) {
return { aggregate: "progress", align: "left", key, label, type: "progress" };
}
-function ratioColumn(key, label) {
- return { aggregate: "ratio", align: "right", key, label, type: "ratio" };
-}
-
function flowRateColumn(key, label) {
return { aggregate: "flowRate", align: "right", key, label, type: "flowRate" };
}
@@ -491,15 +211,3 @@ function consumeOutputColumn(key, label) {
function durationColumn(key, label, aggregate = "sum") {
return { aggregate: aggregate === "average" ? "durationAverage" : "duration", align: "right", key, label, type: "duration" };
}
-
-function progress(actual, target) {
- return { actual, target };
-}
-
-function noGoal() {
- return { actual: 0, noGoal: true, target: 0 };
-}
-
-function ratioCount(active, total) {
- return { active, total };
-}
diff --git a/databi/src/styles/social-bi.css b/databi/src/styles/social-bi.css
index 7d4b767..64540c7 100644
--- a/databi/src/styles/social-bi.css
+++ b/databi/src/styles/social-bi.css
@@ -654,3 +654,120 @@
transition-duration: 1ms !important;
}
}
+
+.social-bi-target-button.MuiButton-root {
+ height: var(--social-bi-control-height);
+ min-height: var(--social-bi-control-height);
+ border-radius: 6px;
+ border-color: #1688d9;
+ color: #1688d9;
+ font-weight: 720;
+}
+
+.social-bi-scope-hint {
+ margin: 0;
+ padding: 8px 14px;
+ border: 1px solid #cfe6f8;
+ border-radius: 8px;
+ background: #eef7fe;
+ color: #0f5d96;
+ font-size: 13px;
+ font-weight: 640;
+}
+
+.social-bi-dialog-backdrop {
+ position: fixed;
+ inset: 0;
+ z-index: 60;
+ display: grid;
+ place-items: center;
+ padding: 24px;
+ background: rgba(7, 22, 38, 0.45);
+}
+
+.social-bi-dialog {
+ display: flex;
+ width: min(860px, 100%);
+ max-height: min(80vh, 720px);
+ flex-direction: column;
+ overflow: hidden;
+ border-radius: 12px;
+ background: #ffffff;
+ box-shadow: 0 24px 64px rgba(9, 30, 51, 0.28);
+}
+
+.social-bi-dialog-header {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 18px 22px 12px;
+ border-bottom: 1px solid #e4ecf5;
+}
+
+.social-bi-dialog-header strong {
+ font-size: 16px;
+ color: #14263c;
+}
+
+.social-bi-dialog-header span {
+ color: #5b7089;
+ font-size: 13px;
+}
+
+.social-bi-dialog-body {
+ flex: 1;
+ overflow: auto;
+ padding: 8px 22px;
+}
+
+.social-bi-dialog-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 13px;
+}
+
+.social-bi-dialog-table th,
+.social-bi-dialog-table td {
+ padding: 9px 10px;
+ border-bottom: 1px solid #eef3f9;
+ text-align: left;
+ white-space: nowrap;
+}
+
+.social-bi-dialog-table th {
+ position: sticky;
+ top: 0;
+ background: #f7fafd;
+ color: #47607c;
+ font-weight: 720;
+}
+
+.social-bi-dialog-table input {
+ width: 120px;
+ height: 32px;
+ padding: 0 10px;
+ border: 1px solid #cddaea;
+ border-radius: 6px;
+ font-size: 13px;
+}
+
+.social-bi-dialog-table input:focus {
+ outline: none;
+ border-color: #1688d9;
+ box-shadow: 0 0 0 3px rgba(22, 136, 217, 0.14);
+}
+
+.social-bi-dialog-empty {
+ margin: 18px 4px;
+ color: #5b7089;
+ font-size: 13px;
+}
+
+.social-bi-dialog-footer {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 10px;
+ padding: 12px 22px 16px;
+ border-top: 1px solid #e4ecf5;
+}
diff --git a/finance/src/FinanceApp.jsx b/finance/src/FinanceApp.jsx
index b10f6ea..41b7c7d 100644
--- a/finance/src/FinanceApp.jsx
+++ b/finance/src/FinanceApp.jsx
@@ -729,6 +729,7 @@ export function FinanceApp() {
updateRechargeDetailFilters({ appCode, regionId: "" });
setActiveView("rechargeDetails");
}}
+ onOpenWithdrawals={() => setActiveView("withdrawals")}
/>
) : activeView === "recon" && session.canViewAppRechargeDetails ? (
);
})}
+
@@ -195,6 +202,45 @@ export function FinanceOverview({
);
}
+// WithdrawalKpi 是“用户提现”卡:工资转币商的 USDT + 审核通过的提现申请 USDT;点击进入提现审核页。
+// 提现不是充值渠道,不参与来源筛选联动,占比按“提现 / 充值总和”表达资金回流强度。
+function WithdrawalKpi({ loading, onOpenWithdrawals, totalUsd, withdrawal }) {
+ const data = withdrawal || {
+ approvedCount: 0,
+ approvedUsdMinor: 0,
+ totalUsdMinor: 0,
+ transferCount: 0,
+ transferUsdMinor: 0,
+ };
+ const metaParts = [];
+ if (totalUsd > 0) {
+ const share = (data.totalUsdMinor / totalUsd) * 100;
+ metaParts.push(`占充值 ${share.toLocaleString("zh-CN", { maximumFractionDigits: 1 })}%`);
+ }
+ metaParts.push(`${formatAmount(data.transferCount + data.approvedCount)} 笔`);
+ return (
+
+ );
+}
+
function kpiMeta(bucket, key, totalUsd, prevSummary) {
const parts = [`${formatAmount(bucket.billCount)} 笔`];
if (key !== "total" && totalUsd > 0) {
diff --git a/finance/src/format.js b/finance/src/format.js
index 34ee93d..da7a171 100644
--- a/finance/src/format.js
+++ b/finance/src/format.js
@@ -254,12 +254,20 @@ export const EMPTY_RECHARGE_OVERVIEW = {
unsyncedCount: 0,
},
regions: [],
+ withdrawal: {
+ approvedCount: 0,
+ approvedUsdMinor: 0,
+ totalUsdMinor: 0,
+ transferCount: 0,
+ transferUsdMinor: 0,
+ },
};
export function normalizeRechargeOverview(data = {}) {
const daily = Array.isArray(data.daily) ? data.daily : [];
const regions = Array.isArray(data.regions) ? data.regions : [];
const paid = data.googlePaid ?? data.google_paid ?? {};
+ const withdrawal = data.withdrawal ?? {};
return {
daily: daily.map((item) => ({
coinSellerCoinAmount: numberValue(item.coinSellerCoinAmount ?? item.coin_seller_coin_amount) || 0,
@@ -285,6 +293,13 @@ export function normalizeRechargeOverview(data = {}) {
regionId: numberOrNull(item.regionId ?? item.region_id),
usdMinorAmount: numberValue(item.usdMinorAmount ?? item.usd_minor_amount) || 0,
})),
+ withdrawal: {
+ approvedCount: numberValue(withdrawal.approvedCount ?? withdrawal.approved_count) || 0,
+ approvedUsdMinor: numberValue(withdrawal.approvedUsdMinor ?? withdrawal.approved_usd_minor) || 0,
+ totalUsdMinor: numberValue(withdrawal.totalUsdMinor ?? withdrawal.total_usd_minor) || 0,
+ transferCount: numberValue(withdrawal.transferCount ?? withdrawal.transfer_count) || 0,
+ transferUsdMinor: numberValue(withdrawal.transferUsdMinor ?? withdrawal.transfer_usd_minor) || 0,
+ },
};
}
@@ -314,6 +329,9 @@ export function mergeRechargeOverviews(overviews = []) {
for (const key of Object.keys(merged.googlePaid)) {
merged.googlePaid[key] += overview.googlePaid[key];
}
+ for (const key of Object.keys(merged.withdrawal)) {
+ merged.withdrawal[key] += overview.withdrawal[key];
+ }
}
merged.daily = [...dailyByDate.values()].sort((left, right) => left.date.localeCompare(right.date));
return merged;
diff --git a/finance/src/styles/index.css b/finance/src/styles/index.css
index d3054f6..3f2942f 100644
--- a/finance/src/styles/index.css
+++ b/finance/src/styles/index.css
@@ -212,7 +212,7 @@ a {
.finance-kpi-band {
display: grid;
- grid-template-columns: repeat(4, minmax(180px, 1fr));
+ grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: var(--space-4);
}
@@ -255,6 +255,10 @@ a {
border-left-color: #f9ab00;
}
+.finance-kpi--withdrawal {
+ border-left-color: #be185d;
+}
+
.finance-kpi--active {
border-color: var(--primary, #4285f4);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary, #4285f4) 24%, transparent);
diff --git a/src/app/permissions.ts b/src/app/permissions.ts
index 4f32208..2f176fd 100644
--- a/src/app/permissions.ts
+++ b/src/app/permissions.ts
@@ -42,6 +42,8 @@ export const PERMISSIONS = {
coinSellerStockCredit: "coin-seller:stock-credit",
coinSellerExchangeRate: "coin-seller:exchange-rate",
hostWithdrawalView: "host-withdrawal:view",
+ databiKpiManage: "databi-kpi:manage",
+ databiKpiViewAll: "databi-kpi:view-all",
financeView: "finance:view",
financeApplicationCreate: "finance-application:create",
financeApplicationAudit: "finance-application:audit",
diff --git a/src/shared/api/generated/endpoints.ts b/src/shared/api/generated/endpoints.ts
index 37f497a..dbc3ddb 100644
--- a/src/shared/api/generated/endpoints.ts
+++ b/src/shared/api/generated/endpoints.ts
@@ -139,6 +139,9 @@ export const API_OPERATIONS = {
getRoomTurnoverRewardConfig: "getRoomTurnoverRewardConfig",
getRoomWhitelistConfig: "getRoomWhitelistConfig",
getSevenDayCheckInConfig: "getSevenDayCheckInConfig",
+ getSocialBiKpi: "getSocialBiKpi",
+ getSocialBiMaster: "getSocialBiMaster",
+ getSocialBiOverview: "getSocialBiOverview",
getTemporaryPaymentLink: "getTemporaryPaymentLink",
getUser: "getUser",
getWeeklyStarCycle: "getWeeklyStarCycle",
@@ -216,6 +219,7 @@ export const API_OPERATIONS = {
listRoomTurnoverRewardSettlements: "listRoomTurnoverRewardSettlements",
listSelfGames: "listSelfGames",
listSevenDayCheckInClaims: "listSevenDayCheckInClaims",
+ listSocialBiKpiTargets: "listSocialBiKpiTargets",
listSplashScreens: "listSplashScreens",
listSystemMenus: "listSystemMenus",
listTaskDefinitions: "listTaskDefinitions",
@@ -248,6 +252,7 @@ export const API_OPERATIONS = {
replaceRegionCountries: "replaceRegionCountries",
replaceRoleDataScopes: "replaceRoleDataScopes",
replaceRolePermissions: "replaceRolePermissions",
+ replaceSocialBiKpiTargets: "replaceSocialBiKpiTargets",
replaceUserFinanceScopes: "replaceUserFinanceScopes",
resetUserPassword: "resetUserPassword",
retryRedPacketRefund: "retryRedPacketRefund",
@@ -1217,6 +1222,27 @@ export const API_ENDPOINTS: Record
= {
permission: "seven-day-checkin:view",
permissions: ["seven-day-checkin:view"]
},
+ getSocialBiKpi: {
+ method: "GET",
+ operationId: API_OPERATIONS.getSocialBiKpi,
+ path: "/v1/admin/databi/social/kpi",
+ permission: "overview:view",
+ permissions: ["overview:view"]
+ },
+ getSocialBiMaster: {
+ method: "GET",
+ operationId: API_OPERATIONS.getSocialBiMaster,
+ path: "/v1/admin/databi/social/master",
+ permission: "overview:view",
+ permissions: ["overview:view"]
+ },
+ getSocialBiOverview: {
+ method: "GET",
+ operationId: API_OPERATIONS.getSocialBiOverview,
+ path: "/v1/admin/databi/social/overview",
+ permission: "overview:view",
+ permissions: ["overview:view"]
+ },
getTemporaryPaymentLink: {
method: "GET",
operationId: API_OPERATIONS.getTemporaryPaymentLink,
@@ -1754,6 +1780,13 @@ export const API_ENDPOINTS: Record = {
permission: "seven-day-checkin:view",
permissions: ["seven-day-checkin:view"]
},
+ listSocialBiKpiTargets: {
+ method: "GET",
+ operationId: API_OPERATIONS.listSocialBiKpiTargets,
+ path: "/v1/admin/databi/social/kpi-targets",
+ permission: "overview:view",
+ permissions: ["overview:view"]
+ },
listSplashScreens: {
method: "GET",
operationId: API_OPERATIONS.listSplashScreens,
@@ -1968,6 +2001,13 @@ export const API_ENDPOINTS: Record = {
permission: "role:permission",
permissions: ["role:permission","role:manage"]
},
+ replaceSocialBiKpiTargets: {
+ method: "PUT",
+ operationId: API_OPERATIONS.replaceSocialBiKpiTargets,
+ path: "/v1/admin/databi/social/kpi-targets",
+ permission: "databi-kpi:manage",
+ permissions: ["databi-kpi:manage"]
+ },
replaceUserFinanceScopes: {
method: "PUT",
operationId: API_OPERATIONS.replaceUserFinanceScopes,
diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts
index 0c6f0ac..1dfd3a4 100644
--- a/src/shared/api/generated/schema.d.ts
+++ b/src/shared/api/generated/schema.d.ts
@@ -2004,6 +2004,70 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/admin/databi/social/master": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["getSocialBiMaster"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/databi/social/overview": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["getSocialBiOverview"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/databi/social/kpi": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["getSocialBiKpi"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/databi/social/kpi-targets": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["listSocialBiKpiTargets"];
+ put: operations["replaceSocialBiKpiTargets"];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/admin/finance/scope": {
parameters: {
query?: never;
@@ -6983,6 +7047,99 @@ export interface operations {
200: components["responses"]["EmptyResponse"];
};
};
+ getSocialBiMaster: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["StatisticsObjectResponse"];
+ };
+ };
+ getSocialBiOverview: {
+ parameters: {
+ query?: {
+ stat_tz?: string;
+ start_ms?: number;
+ end_ms?: number;
+ app_codes?: string;
+ region_id?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["StatisticsObjectResponse"];
+ };
+ };
+ getSocialBiKpi: {
+ parameters: {
+ query?: {
+ stat_tz?: string;
+ start_ms?: number;
+ end_ms?: number;
+ period_month?: string;
+ app_codes?: string;
+ operator_user_id?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["StatisticsObjectResponse"];
+ };
+ };
+ listSocialBiKpiTargets: {
+ parameters: {
+ query?: {
+ period_month?: string;
+ user_id?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["StatisticsObjectResponse"];
+ };
+ };
+ replaceSocialBiKpiTargets: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ items?: {
+ /** Format: int64 */
+ userId?: number;
+ appCode?: string;
+ /** Format: int64 */
+ regionId?: number;
+ periodMonth?: string;
+ /** Format: int64 */
+ targetUsdMinor?: number;
+ /** Format: int64 */
+ dailyTargetUsdMinor?: number;
+ }[];
+ };
+ };
+ };
+ responses: {
+ 200: components["responses"]["StatisticsObjectResponse"];
+ };
+ };
getFinanceScope: {
parameters: {
query?: never;