teamMemberService.ts 11.5 KB
import { createApiClient } from "../lib/apiClient";
import { authorizedPostBlobDownload, authorizedPostMultipartJson } from "../lib/batchFileHttp";
import type {
  PagedResultDto,
  TeamMemberCreateInput,
  TeamMemberDto,
  TeamMemberGetListInput,
  TeamMemberUpdateInput,
} from "../types/teamMember";

const api = createApiClient({
  getToken: () => {
    try {
      return localStorage.getItem("access_token") ?? localStorage.getItem("token") ?? null;
    } catch {
      return null;
    }
  },
});

const PATH = "/team-member";

function toStringArray(v: unknown): string[] {
  if (Array.isArray(v)) return v.map((x) => String(x));
  return [];
}

function toIdArray(v: unknown): string[] {
  if (!Array.isArray(v)) return [];
  const out: string[] = [];
  for (const x of v) {
    if (x === null || x === undefined) continue;
    if (typeof x === "string" || typeof x === "number") {
      out.push(String(x));
      continue;
    }
    if (typeof x === "object") {
      const o = x as Record<string, unknown>;
      const id = o.id ?? o.Id ?? o.locationId ?? o.LocationId ?? o.location_id ?? o.locationID;
      if (id !== null && id !== undefined) out.push(String(id));
    }
  }
  return out;
}

function toLocationLabels(v: unknown): string[] {
  if (!Array.isArray(v)) return [];
  const out: string[] = [];
  for (const x of v) {
    if (x === null || x === undefined) continue;
    if (typeof x === "string" || typeof x === "number") {
      out.push(String(x));
      continue;
    }
    if (typeof x === "object") {
      const o = x as Record<string, unknown>;
      const code =
        (o.locationCode ?? o.LocationCode ?? o.code ?? o.Code ?? o.location_code ?? o.locationCodeId) as unknown;
      const name =
        (o.locationName ?? o.LocationName ?? o.name ?? o.Name ?? o.location_name) as unknown;
      const id = (o.id ?? o.Id ?? o.locationId ?? o.LocationId) as unknown;

      const codeS = code === null || code === undefined ? "" : String(code).trim();
      const nameS = name === null || name === undefined ? "" : String(name).trim();
      const idS = id === null || id === undefined ? "" : String(id).trim();

      if (codeS && nameS) out.push(`${codeS} - ${nameS}`);
      else if (nameS) out.push(nameS);
      else if (codeS) out.push(codeS);
      else if (idS) out.push(idS);
    }
  }
  return out;
}

function normalizeTeamMemberDto(row: unknown): TeamMemberDto {
  if (!row || typeof row !== "object") return { id: "" };
  const r = row as Record<string, unknown>;

  // ABP 常见 camelCase + 少量 PascalCase 兼容
  const id = String(
    r.id ?? r.Id ?? r.userId ?? r.UserId ?? r.user_id ?? r.UserID ?? r.memberId ?? r.MemberId ?? "",
  );

  // name fields: fullName or name
  const fullName = (r.fullName ?? r.FullName ?? r.name ?? r.Name) as string | null | undefined;
  const userName = (r.userName ?? r.UserName ?? r.username ?? r.UserName) as string | null | undefined;
  const email = (r.email ?? r.Email) as string | null | undefined;
  const phone = (r.phone ?? r.Phone) as string | null | undefined;

  // role 可能是扁平字段(roleId/roleName),也可能是嵌套对象(role: { id, roleName })
  let roleId = (r.roleId ?? r.RoleId) as string | null | undefined;
  let roleName = (r.roleName ?? r.RoleName ?? r.roleName ?? r.Role) as string | null | undefined;
  const roleObj = (r.role ?? r.Role) as unknown;
  if ((!roleId || !roleName) && roleObj && typeof roleObj === "object") {
    const ro = roleObj as Record<string, unknown>;
    roleId = (ro.id ?? ro.Id ?? ro.roleId ?? ro.RoleId ?? roleId) as string | null | undefined;
    roleName =
      (ro.roleName ?? ro.RoleName ?? ro.name ?? ro.Name ?? ro.role ?? ro.Role ?? roleName) as
        | string
        | null
        | undefined;
  }

  const stateRaw = r.state ?? r.State;
  const state =
    typeof stateRaw === "boolean" ? (stateRaw as boolean) : stateRaw === "true" ? true : stateRaw === "false" ? false : undefined;

  const rawLocationIds =
    r.locationIds ?? r.LocationIds ?? r.assignedLocationIds ?? r.AssignedLocationIds ?? r.location_id_list ?? r.LocationIdList;
  let locationIds = toIdArray(rawLocationIds);

  const rawLocations =
    r.locations ?? r.Locations ?? r.assignedLocations ?? r.AssignedLocations ?? r.locationNames ?? r.LocationNames;
  // locations 可能包含对象数组:{ id, locationCode, locationName } 或类似
  let locations = toLocationLabels(rawLocations);

  // 如果 locations 返回的是对象数组但没有显式 locationIds,则从 locations 对象里抽 id
  if (locationIds.length === 0 && Array.isArray(rawLocations)) {
    const inferredIds: string[] = [];
    for (const x of rawLocations) {
      if (typeof x !== "object" || !x) continue;
      const o = x as Record<string, unknown>;
      const id = o.id ?? o.Id ?? o.locationId ?? o.LocationId;
      if (id !== null && id !== undefined) inferredIds.push(String(id));
    }
    if (inferredIds.length) locationIds = inferredIds;
  }

  const partnerIds = toIdArray(r.partnerIds ?? r.PartnerIds);
  const regionIds = toIdArray(
    r.regionIds ?? r.RegionIds ?? r.groupIds ?? r.GroupIds,
  );
  const partnerId =
    (r.partnerId ?? r.PartnerId ?? partnerIds[0] ?? null) as string | null | undefined;
  const groupId =
    (r.groupId ?? r.GroupId ?? regionIds[0] ?? null) as string | null | undefined;

  // 有些接口可能只返回一个 locations,但我们仍尽量填充
  return {
    id,
    fullName,
    userName,
    email,
    phone,
    roleId,
    roleName,
    locationIds,
    locations,
    partnerId: partnerId != null ? String(partnerId) : null,
    groupId: groupId != null ? String(groupId) : null,
    partnerIds,
    regionIds,
    groupIds: regionIds,
    state: state ?? (r.status ? (String(r.status).toLowerCase() === "active") : undefined),
  };
}

export async function getTeamMembers(
  input: TeamMemberGetListInput,
  signal?: AbortSignal,
): Promise<PagedResultDto<TeamMemberDto>> {
  const raw = await api.requestJson<PagedResultDto<unknown>>({
    path: PATH,
    method: "GET",
    query: {
      SkipCount: input.skipCount,
      MaxResultCount: input.maxResultCount,
      Keyword: input.keyword,
      RoleId: input.roleId,
      PartnerId: input.partnerId,
      GroupId: input.groupId,
      LocationId: input.locationId,
      State: input.state,
      Sorting: input.sorting,
    },
    signal,
  });

  // requestJson 已做分页 shape 规范化,但这里做 DTO 规范
  const items = (raw.items ?? []) as unknown[];
  return {
    totalCount: raw.totalCount ?? 0,
    items: items.map(normalizeTeamMemberDto),
  };
}

export async function getTeamMemberById(id: string, signal?: AbortSignal): Promise<TeamMemberDto> {
  const raw = await api.requestJson<unknown>({
    path: `${PATH}/${encodeURIComponent(id)}`,
    method: "GET",
    signal,
  });
  return normalizeTeamMemberDto(raw);
}

function toPhoneNumber(v: string | number | null | undefined): number | null {
  if (v === null || v === undefined || v === "") return null;
  const s = String(v).trim();
  if (!s) return null;
  const num = Number(s.replace(/\D/g, "")) || 0;
  return num;
}

function scopeIdsForApi(ids: string[] | undefined): string[] | undefined {
  const list = [...new Set((ids ?? []).map((x) => String(x).trim()).filter(Boolean))];
  return list.length ? list : undefined;
}

function buildCreatePayload(input: TeamMemberCreateInput): Record<string, unknown> {
  const phoneVal = input.phone != null && input.phone !== "" ? toPhoneNumber(String(input.phone)) : null;
  const partnerId = (input.partnerId ?? "").trim() || undefined;
  const regionIds = scopeIdsForApi(input.regionIds);
  return {
    fullName: input.fullName,
    userName: input.userName,
    password: input.password,
    email: input.email ?? null,
    phone: phoneVal,
    roleId: input.roleId,
    partnerId,
    partnerIds: partnerId ? [partnerId] : undefined,
    regionIds,
    groupIds: regionIds,
    locationIds: input.locationIds,
    locations: input.locationIds,
    state: input.state,
  };
}

function buildUpdatePayload(input: TeamMemberUpdateInput): Record<string, unknown> {
  const phoneVal = input.phone != null && input.phone !== "" ? toPhoneNumber(String(input.phone)) : null;
  const partnerId = (input.partnerId ?? "").trim() || undefined;
  const regionIds = scopeIdsForApi(input.regionIds);
  const payload: Record<string, unknown> = {
    fullName: input.fullName,
    userName: input.userName,
    email: input.email ?? null,
    phone: phoneVal,
    roleId: input.roleId,
    partnerId,
    partnerIds: partnerId ? [partnerId] : undefined,
    regionIds,
    groupIds: regionIds,
    locationIds: input.locationIds,
    locations: input.locationIds,
    state: input.state,
  };
  if (input.password) payload.password = input.password;
  return payload;
}

export async function createTeamMember(input: TeamMemberCreateInput): Promise<TeamMemberDto> {
  const raw = await api.requestJson<unknown>({
    path: PATH,
    method: "POST",
    body: buildCreatePayload(input),
  });
  return normalizeTeamMemberDto(raw);
}

export async function updateTeamMember(id: string, input: TeamMemberUpdateInput): Promise<TeamMemberDto> {
  const raw = await api.requestJson<unknown>({
    path: `${PATH}/${encodeURIComponent(id)}`,
    method: "PUT",
    body: buildUpdatePayload(input),
  });
  return normalizeTeamMemberDto(raw);
}

export async function deleteTeamMember(id: string): Promise<void> {
  await api.requestJson<unknown>({
    path: `${PATH}/${encodeURIComponent(id)}`,
    method: "DELETE",
  });
}

/** PDF 全量导出筛选(与列表一致,不含分页)。 */
export type TeamMemberExportQueryInput = {
  keyword?: string;
  roleId?: string;
  locationId?: string;
  state?: boolean;
  sorting?: string;
};

export type TeamMemberBatchImportResultDto = {
  successCount: number;
  failCount: number;
  errors?: Array<{ rowNumber?: number; userName?: string; message?: string }>;
};

export type TeamMemberBulkUpdateItemVo = {
  id: string;
  fullName: string;
  userName: string;
  password?: string | null;
  email?: string | null;
  phone?: number | null;
  roleId: string;
  locationIds: string[];
  state: boolean;
};

export type TeamMemberBulkUpdateResultDto = {
  successCount: number;
  failCount: number;
  errors?: Array<{ rowNumber?: number; id?: string; message?: string }>;
};

export async function downloadTeamMemberImportTemplate(signal?: AbortSignal): Promise<void> {
  await authorizedPostBlobDownload({
    path: `${PATH}/download-team-member-import-template`,
    defaultFileName: "Team-Member-template.xlsx",
    signal,
  });
}

export async function exportTeamMembersPdf(input: TeamMemberExportQueryInput, signal?: AbortSignal): Promise<void> {
  await authorizedPostBlobDownload({
    path: `${PATH}/export-team-members-pdf`,
    query: {
      Keyword: input.keyword,
      RoleId: input.roleId,
      LocationId: input.locationId,
      State: input.state,
      Sorting: input.sorting,
    },
    defaultFileName: "team-members.pdf",
    signal,
  });
}

export async function importTeamMembersBatch(file: File, signal?: AbortSignal): Promise<TeamMemberBatchImportResultDto> {
  return authorizedPostMultipartJson<TeamMemberBatchImportResultDto>({
    path: `${PATH}/import-team-members-batch`,
    fieldName: "file",
    file,
    signal,
  });
}

export async function updateTeamMembersBulk(
  body: { items: TeamMemberBulkUpdateItemVo[] },
): Promise<TeamMemberBulkUpdateResultDto> {
  // ABP 约定 URL:`UpdateTeamMembersBulkAsync` 去掉动词前缀后为 `team-members-bulk`(勿用 `update-...`,否则会被 `PUT …/{id}` 当成 Guid)
  return api.requestJson<TeamMemberBulkUpdateResultDto>({
    path: `${PATH}/team-members-bulk`,
    method: "PUT",
    body,
  });
}