labelTemplateService.ts 13.4 KB
import { createApiClient } from "../lib/apiClient";
import {
  stripLabelConfigPrefixes,
  normalizeTemplateBorder,
  normalizePrintOrientation,
  type LabelElement,
  type LabelTemplateCreateInput,
  type LabelTemplateDto,
  type LabelTemplateGetListInput,
  type LabelTemplateProductDefaultDto,
  type LabelTemplateUpdateInput,
  type PagedResultDto,
} from "../types/labelTemplate";

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

const PATH = "/label-template";

function normalizeTemplateCode(raw: unknown): string {
  const r = raw as Record<string, unknown> | null | undefined;
  if (!r || typeof r !== "object") return "";
  const id = r.id ?? r.templateCode ?? r.TemplateCode;
  return typeof id === "string" ? id.trim() : String(id ?? "").trim();
}

/** 详情/列表里的 elements 兼容 PascalCase(如 InputKey) */
function normalizeTemplateElements(list: unknown): LabelElement[] {
  if (!Array.isArray(list)) return [];
  return list.map((raw) => {
    const e = raw as Record<string, unknown> & {
      InputKey?: unknown;
      inputKey?: unknown;
      ElementName?: unknown;
      elementName?: unknown;
      TypeAdd?: unknown;
      typeAdd?: unknown;
      LibraryCategory?: unknown;
      libraryCategory?: unknown;
    };
    const ik = e.inputKey ?? e.InputKey;
    const nameRaw = e.elementName ?? e.ElementName;
    const typeAddRaw = e.typeAdd ?? e.TypeAdd;
    const lcRaw = e.libraryCategory ?? e.LibraryCategory;
    let libraryCategory: LabelElement["libraryCategory"];
    if (typeof lcRaw === "string") {
      const t = lcRaw.trim();
      if (t) libraryCategory = t;
    }
    const rawCfg =
      e.config && typeof e.config === "object" && !Array.isArray(e.config)
        ? (e.config as Record<string, unknown>)
        : {};
    return {
      ...(e as object),
      elementName:
        typeof nameRaw === "string" ? nameRaw.trim() : undefined,
      typeAdd: typeof typeAddRaw === "string" ? typeAddRaw.trim() : undefined,
      inputKey: typeof ik === "string" ? ik : e.inputKey ?? null,
      libraryCategory,
      config: stripLabelConfigPrefixes(rawCfg) as LabelElement["config"],
    } as LabelElement;
  });
}

function normalizeDefaultValuesJson(raw: unknown): Record<string, string> {
  if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return {};
  const out: Record<string, string> = {};
  for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
    if (v == null) out[k] = "";
    else if (typeof v === "string") out[k] = v;
    else if (typeof v === "number" || typeof v === "boolean") out[k] = String(v);
    else out[k] = JSON.stringify(v);
  }
  return out;
}

function normalizeTemplateProductDefaultsList(raw: unknown): LabelTemplateProductDefaultDto[] {
  if (!Array.isArray(raw)) return [];
  return raw.map((item, index) => {
    const o = item as Record<string, unknown>;
    const dv = o.defaultValues ?? o.DefaultValues ?? o.defaultValuesJson ?? o.DefaultValuesJson;
    return {
      productId: String(o.productId ?? o.ProductId ?? "").trim(),
      labelTypeId: String(o.labelTypeId ?? o.LabelTypeId ?? "").trim(),
      defaultValues: normalizeDefaultValuesJson(dv),
      orderNum: Number(o.orderNum ?? o.OrderNum ?? index + 1) || index + 1,
    };
  });
}

function normalizeListContentItems(raw: unknown): string[] {
  const r = raw as Record<string, unknown> | null | undefined;
  if (!r || typeof r !== "object") return [];
  const list = r.contentItems ?? r.items ?? r.Items ?? r.itemNames ?? r.ItemNames;
  /** 列表接口 items 常为后端拼好的逗号分隔字符串 */
  if (typeof list === "string") {
    const t = list.trim();
    return t ? [t] : [];
  }
  if (!Array.isArray(list)) return [];
  const out: string[] = [];
  for (const x of list) {
    if (typeof x === "string") {
      const t = x.trim();
      if (t) out.push(t);
      continue;
    }
    if (x != null && typeof x === "object" && !Array.isArray(x)) {
      const o = x as Record<string, unknown>;
      const name = String(o.elementName ?? o.ElementName ?? o.name ?? o.Name ?? "").trim();
      if (name) out.push(name);
    }
  }
  return out;
}

/** 列表 Contents 展示文案:优先接口 items 字符串,否则数组 join */
function normalizeListContentsText(raw: unknown, contentItems: string[]): string | null {
  const r = raw as Record<string, unknown> | null | undefined;
  if (!r || typeof r !== "object") return contentItems.length ? contentItems.join(", ") : null;
  const list = r.contentItems ?? r.items ?? r.Items ?? r.itemNames ?? r.ItemNames;
  if (typeof list === "string") {
    const t = list.trim();
    return t || null;
  }
  if (contentItems.length) return contentItems.join(", ");
  return null;
}

function normalizeLabelTemplateDto(raw: unknown): LabelTemplateDto {
  const r = raw as Record<string, unknown>;
  const ids =
    (Array.isArray(r.appliedLocationIds) ? r.appliedLocationIds : null) ??
    (Array.isArray(r.AppliedLocationIds) ? r.AppliedLocationIds : null) ??
    [];
  const appliedLocationIds = ids.map((x) => String(x));
  const parseIdList = (v: unknown): string[] => {
    if (!Array.isArray(v)) return [];
    return [...new Set(v.map((x) => String(x).trim()).filter(Boolean))];
  };
  const regionIds = parseIdList(r.regionIds ?? r.RegionIds);
  const groupIds = parseIdList(r.groupIds ?? r.GroupIds);
  const locationIds = parseIdList(r.locationIds ?? r.LocationIds);
  const partnerIds = parseIdList(r.partnerIds ?? r.PartnerIds ?? r.companyIds ?? r.CompanyIds);
  const mergedRegionIds = [...new Set([...regionIds, ...groupIds])];
  const mergedLocationIds = [
    ...new Set([...locationIds, ...appliedLocationIds]),
  ];
  const mergedPartnerIds = partnerIds;
  const appliedPartnerTypeRaw = r.appliedPartnerType ?? r.AppliedPartnerType;
  const appliedRegionTypeRaw = r.appliedRegionType ?? r.AppliedRegionType;
  const appliedLocationTypeRaw =
    r.appliedLocationType ?? r.AppliedLocationType ?? r.appliedLocation ?? r.AppliedLocation;
  const companyTextVo = r.company ?? r.Company;
  const regionTextVo = r.region ?? r.Region;
  const locationFieldVo = r.location ?? r.Location;
  const id = normalizeTemplateCode(raw);

  const templateNameVo = r.templateName ?? r.TemplateName;
  const templateCodeVo = r.templateCode ?? r.TemplateCode;
  const locationTextVo = r.locationText ?? r.LocationText;
  const sizeTextVo = r.sizeText ?? r.SizeText;
  const ccRaw = r.contentsCount ?? r.ContentsCount;
  const contentsCountVo = typeof ccRaw === "number" ? ccRaw : undefined;
  const lastEditedVo = r.lastEdited ?? r.LastEdited;

  const nameFromList =
    (typeof r.name === "string" && r.name.trim() ? r.name : null) ??
    (typeof templateNameVo === "string" && String(templateNameVo).trim() ? String(templateNameVo) : null);

  const contentItems = normalizeListContentItems(r);
  const contentsText = normalizeListContentsText(r, contentItems);

  return {
    ...(r as object),
    id,
    name: nameFromList ?? (r.name as LabelTemplateDto["name"]),
    templateName: (typeof templateNameVo === "string" ? templateNameVo : null) ?? (r.templateName as string | null),
    templateCode: (typeof templateCodeVo === "string" ? templateCodeVo : null) ?? (r.templateCode as string | null),
    locationText: (typeof locationTextVo === "string" ? locationTextVo : null) ?? (r.locationText as string | null),
    company: (typeof companyTextVo === "string" ? companyTextVo : null) ?? (r.company as string | null),
    region: (typeof regionTextVo === "string" ? regionTextVo : null) ?? (r.region as string | null),
    location: (typeof locationFieldVo === "string" ? locationFieldVo : null) ?? (r.location as string | null),
    appliedPartnerType:
      (typeof appliedPartnerTypeRaw === "string" ? appliedPartnerTypeRaw : null) ??
      (r.appliedPartnerType as string | null),
    appliedRegionType:
      (typeof appliedRegionTypeRaw === "string" ? appliedRegionTypeRaw : null) ??
      (r.appliedRegionType as string | null),
    appliedLocationType:
      (typeof appliedLocationTypeRaw === "string" ? appliedLocationTypeRaw : null) ??
      (r.appliedLocationType as string | null),
    sizeText: (typeof sizeTextVo === "string" ? sizeTextVo : null) ?? (r.sizeText as string | null),
    contentsCount: contentsCountVo ?? (r.contentsCount as number | null),
    contentItems,
    contentsText,
    items: contentsText ?? (contentItems.length ? contentItems.join(", ") : null),
    lastEdited: (typeof lastEditedVo === "string" ? lastEditedVo : null) ?? (r.lastEdited as string | null),
    appliedLocationIds: mergedLocationIds.length ? mergedLocationIds : appliedLocationIds,
    partnerIds: mergedPartnerIds,
    companyIds: mergedPartnerIds,
    regionIds: mergedRegionIds,
    groupIds: mergedRegionIds,
    locationIds: mergedLocationIds,
    elements: normalizeTemplateElements(r.elements),
    templateProductDefaults: normalizeTemplateProductDefaultsList(
      r.templateProductDefaults ?? r.TemplateProductDefaults,
    ),
    border: normalizeTemplateBorder(r.border ?? r.BorderType ?? r.borderType),
    printOrientation: normalizePrintOrientation(r.printOrientation ?? r.PrintOrientation),
  } as LabelTemplateDto;
}

export async function getLabelTemplates(input: LabelTemplateGetListInput, signal?: AbortSignal): Promise<PagedResultDto<LabelTemplateDto>> {
  const res = await api.requestJson<PagedResultDto<LabelTemplateDto>>({
    path: PATH,
    method: "GET",
    query: {
      SkipCount: input.skipCount,
      MaxResultCount: input.maxResultCount,
      Sorting: input.sorting,
      Keyword: input.keyword,
      PartnerId: input.partnerId,
      GroupId: input.groupId,
      LocationId: input.locationId,
      LabelType: input.labelType,
      State: input.state,
    },
    signal,
  });
  const items = (res.items ?? []).map((x) => normalizeLabelTemplateDto(x));
  return { ...res, items };
}

export async function getLabelTemplate(templateCode: string, signal?: AbortSignal): Promise<LabelTemplateDto> {
  const raw = await api.requestJson<LabelTemplateDto>({
    path: `${PATH}/${encodeURIComponent(templateCode)}`,
    method: "GET",
    signal,
  });
  return normalizeLabelTemplateDto(raw);
}

function buildLabelTemplateScopeBody(input: LabelTemplateCreateInput): Record<string, unknown> {
  const partnerIds = [
    ...new Set(
      [...(input.partnerIds ?? []), ...(input.companyIds ?? [])]
        .map((x) => String(x).trim())
        .filter(Boolean),
    ),
  ];
  const regionIds = [
    ...new Set(
      [...(input.regionIds ?? []), ...(input.groupIds ?? [])]
        .map((x) => String(x).trim())
        .filter(Boolean),
    ),
  ];
  const locationIds = [
    ...new Set(
      [...(input.locationIds ?? []), ...(input.appliedLocationIds ?? [])]
        .map((x) => String(x).trim())
        .filter(Boolean),
    ),
  ];
  const appliedPartnerType = String(input.appliedPartnerType ?? "").trim().toUpperCase() || (partnerIds.length ? "SPECIFIED" : "ALL");
  const appliedRegionType = String(input.appliedRegionType ?? "").trim().toUpperCase() || (regionIds.length ? "SPECIFIED" : "ALL");
  const appliedLocation = String(input.appliedLocation ?? "").trim().toUpperCase() || (locationIds.length ? "SPECIFIED" : "ALL");

  const body: Record<string, unknown> = {
    id: input.id,
    name: input.name,
    labelType: input.labelType,
    unit: input.unit,
    width: input.width,
    height: input.height,
    appliedPartnerType,
    appliedRegionType,
    appliedLocation,
    showRuler: input.showRuler ?? true,
    showGrid: input.showGrid ?? true,
    border: normalizeTemplateBorder(input.border),
    printOrientation: normalizePrintOrientation(input.printOrientation),
    state: input.state ?? true,
    elements: input.elements,
  };

  if (appliedPartnerType === "SPECIFIED" && partnerIds.length) {
    body.partnerIds = partnerIds;
    body.companyIds = partnerIds;
  } else {
    body.partnerIds = [];
    body.companyIds = [];
  }

  if (appliedRegionType === "SPECIFIED" && regionIds.length) {
    body.regionIds = regionIds;
    body.groupIds = regionIds;
  } else {
    body.regionIds = [];
    body.groupIds = [];
  }

  if (appliedLocation === "SPECIFIED" && locationIds.length) {
    body.locationIds = locationIds;
    body.appliedLocationIds = locationIds;
  } else {
    body.locationIds = [];
    body.appliedLocationIds = [];
  }

  return body;
}

export async function createLabelTemplate(input: LabelTemplateCreateInput): Promise<LabelTemplateDto> {
  const created = await api.requestJson<LabelTemplateDto>({
    path: PATH,
    method: "POST",
    body: buildLabelTemplateScopeBody(input),
  });
  return normalizeLabelTemplateDto(created);
}

export async function updateLabelTemplate(templateCode: string, input: LabelTemplateUpdateInput): Promise<LabelTemplateDto> {
  const body = buildLabelTemplateScopeBody(input);
  if (input.templateProductDefaults !== undefined) {
    body.templateProductDefaults = input.templateProductDefaults.map((row, i) => ({
      productId: row.productId,
      labelTypeId: row.labelTypeId,
      defaultValues: row.defaultValues,
      orderNum: row.orderNum ?? i + 1,
    }));
  }
  const updated = await api.requestJson<LabelTemplateDto>({
    path: `${PATH}/${encodeURIComponent(templateCode)}`,
    method: "PUT",
    body,
  });
  return normalizeLabelTemplateDto(updated);
}

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