yyb
10 小时以前 a1a9521e1f537d742c4f3ebada9b102bfefa6583
src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
@@ -1,4 +1,13 @@
import dayjs from "dayjs";
import {
  createEmptyNode,
  formatDisplayTime,
  mapNodesFromApi,
  mapSignModeFromApi,
  mapSignModeToApi,
  normalizeFlowNodes,
  nodeSignModeLabel,
} from "../approve-template/approveTemplateConstants.js";
import { buildFormPayloadFromFields, parseFormConfigToData } from "../approve-template/formConfigUtils.js";
/** 审批类型(与后端字段 approvalType 对齐,后期可同步) */
export const APPROVAL_TYPE_OPTIONS = [
@@ -25,105 +34,400 @@
  { value: "cancelled", label: "已撤销" },
];
/** 审批方式 approvalMode */
export const APPROVAL_MODE_OPTIONS = [
  { value: "parallel", label: "与签" },
  { value: "or_sign", label: "或签" },
];
export const LEGACY_APPROVE_LIST_STORAGE_KEY = "oa_unified_approve_list_v1";
export function clearLegacyApproveListStorage() {
  try {
    localStorage.removeItem(LEGACY_APPROVE_LIST_STORAGE_KEY);
  } catch {
    /* ignore */
  }
}
/** 提交弹窗:模板卡片(来自后端列表) */
export function mapSubmitTemplateCard(row) {
  const cfg = parseFormConfigToData(row?.formConfig);
  return {
    id: row?.id,
    key: String(row?.id ?? ""),
    approvalType: cfg.approvalType || row?.approvalType || "",
    label: row?.templateName || "—",
    summaryPlaceholder: (row?.description || "").trim() || cfg.summaryPlaceholder || "点击填写并提交",
  };
}
/** 审批记录 approveAction → 页面 result */
export function mapRecordResultFromApi(action) {
  const s = String(action || "").toUpperCase();
  if (s === "APPROVED" || s === "APPROVE" || s === "PASS") return "approved";
  if (s === "REJECTED" || s === "REJECT" || s === "REFUSE") return "rejected";
  return "pending";
}
/** 后端 records → 时间线展示结构 */
export function mapRecordsFromApi(records) {
  const list = Array.isArray(records) ? records : [];
  return list.map((r) => ({
    id: r.id,
    operatorName: r.approverName || r.operatorName || r.createUserName || "",
    result: mapRecordResultFromApi(r.approveAction ?? r.action ?? r.status),
    opinion: r.approveComment || r.comment || r.opinion || "",
    time: formatDisplayTime(r.approveTime || r.createTime || r.time || ""),
    raw: r,
  }));
}
export function mapTaskStatusLabel(status) {
  const s = String(status || "").toUpperCase();
  if (s === "APPROVED") return "已通过";
  if (s === "REJECTED") return "已驳回";
  if (s === "PENDING") return "待审批";
  if (s === "CANCELLED") return "已撤销";
  return status || "—";
}
export function mapTaskStatusTagType(status) {
  const s = String(status || "").toUpperCase();
  if (s === "APPROVED") return "success";
  if (s === "REJECTED") return "danger";
  if (s === "CANCELLED") return "info";
  return "warning";
}
/** 后端 tasks → 页面 flowNodes(按 levelNo 分组,供流程编辑/展示) */
export function mapTasksToFlowNodes(tasks) {
  const list = Array.isArray(tasks) ? tasks : [];
  if (!list.length) return [];
  const byLevel = new Map();
  list.forEach((t) => {
    const level = Number(t.levelNo ?? t.taskLevel ?? t.nodeOrder ?? 1);
    if (!byLevel.has(level)) {
      byLevel.set(level, {
        id: t.nodeId,
        templateId: t.templateId,
        nodeOrder: level,
        signMode: mapSignModeFromApi(t.approveType),
        approvers: [],
        tasks: [],
      });
    }
    const node = byLevel.get(level);
    node.approvers.push({
      id: t.id,
      nodeId: t.nodeId,
      templateId: t.templateId,
      approverId: t.approverId,
      approverName: t.approverName || "",
      status: t.status,
      approveComment: t.approveComment,
      approveTime: t.approveTime,
    });
    node.tasks.push(t);
    if (t.approveType != null) {
      node.signMode = mapSignModeFromApi(t.approveType);
    }
  });
  return [...byLevel.entries()]
    .sort(([a], [b]) => a - b)
    .map(([, node]) => node);
}
/** 页面 flowNodes → 后端 tasks */
export function mapFlowNodesToTasks(flowNodes, { instanceId, templateId } = {}) {
  const nodes = normalizeFlowNodes(flowNodes);
  const tasks = [];
  nodes.forEach((n) => {
    const levelNo = n.nodeOrder ?? 1;
    const approveType = mapSignModeToApi(n.signMode);
    n.approvers.forEach((a, idx) => {
      const task = {
        levelNo,
        approveType,
        approverId: a.approverId,
        approverName: a.approverName || "",
        sortNo: a.sortNo ?? idx + 1,
      };
      if (a.id != null) task.id = a.id;
      if (a.nodeId != null) task.nodeId = a.nodeId;
      if (a.templateId != null) task.templateId = a.templateId;
      else if (templateId) task.templateId = templateId;
      if (instanceId) task.instanceId = instanceId;
      if (a.status != null) task.status = a.status;
      tasks.push(task);
    });
  });
  return tasks;
}
function guessFieldTypeFromValue(val) {
  if (Array.isArray(val) && val.length === 2) return "datetimerange";
  if (typeof val === "number") return "number";
  if (typeof val === "string" && /^\d{4}-\d{2}-\d{2}$/.test(val)) return "date";
  if (typeof val === "string" && val.length > 100) return "textarea";
  return "text";
}
/** 单字段展示值(详情只读) */
export function formatFieldDisplayValue(field, val) {
  if (val == null || val === "" || (Array.isArray(val) && !val.length)) return "—";
  if (field?.type === "select" && field.options?.length) {
    const hit = field.options.find((o) => String(o.value) === String(val));
    return hit?.label || String(val);
  }
  if (Array.isArray(val)) return val.join(" 至 ");
  return String(val);
}
/**
 * 提交审批模板(按类型一键填报,字段后期与后端模板同步)
 * 从行数据 / formConfig 解析填报字段定义与 formPayload(与新增提交结构一致)
 */
export const SUBMIT_TEMPLATES = {
  cost_reimburse: {
    approvalType: "cost_reimburse",
    label: "费用报销",
    summaryPlaceholder: "请填写报销事由、金额等",
    fields: [
      { key: "summary", label: "申请事由", type: "textarea", required: true, rows: 3 },
      { key: "amount", label: "报销金额(元)", type: "number", required: true, min: 0, precision: 2 },
    ],
    approvalMode: "parallel",
  },
  travel_reimburse: {
    approvalType: "travel_reimburse",
    label: "差旅报销",
    summaryPlaceholder: "出差行程与费用说明",
    fields: [
      { key: "summary", label: "差旅说明", type: "textarea", required: true, rows: 3 },
      { key: "amount", label: "报销金额(元)", type: "number", required: true, min: 0, precision: 2 },
      { key: "tripDays", label: "出差天数", type: "number", required: false, min: 0, precision: 0 },
    ],
    approvalMode: "parallel",
  },
  overtime: {
    approvalType: "overtime",
    label: "加班申请",
    fields: [
      { key: "summary", label: "加班事由", type: "textarea", required: true, rows: 3 },
      { key: "overtimeDate", label: "加班日期", type: "date", required: true },
      { key: "hours", label: "加班时长(小时)", type: "number", required: true, min: 0.5, precision: 1 },
    ],
    approvalMode: "parallel",
  },
  leave: {
    approvalType: "leave",
    label: "请假申请",
    fields: [
      { key: "leaveType", label: "请假类型", type: "select", required: true, options: [
        { label: "年假", value: "annual" },
        { label: "病假", value: "sick" },
        { label: "事假", value: "personal" },
        { label: "调休", value: "compensatory" },
      ] },
      { key: "summary", label: "请假事由", type: "textarea", required: true, rows: 2 },
      { key: "dateRange", label: "请假时间", type: "datetimerange", required: true },
    ],
    approvalMode: "parallel",
  },
  work_handover: {
    approvalType: "work_handover",
    label: "工作交接",
    fields: [
      { key: "summary", label: "交接说明", type: "textarea", required: true, rows: 3 },
      { key: "handoverTo", label: "交接对象", type: "text", required: true },
    ],
    approvalMode: "parallel",
  },
  regular: {
    approvalType: "regular",
    label: "转正申请",
    fields: [
      { key: "summary", label: "转正说明", type: "textarea", required: true, rows: 3 },
      { key: "regularDate", label: "拟转正日期", type: "date", required: true },
    ],
    approvalMode: "parallel",
  },
  resign: {
    approvalType: "resign",
    label: "离职申请",
    fields: [
      { key: "summary", label: "离职原因", type: "textarea", required: true, rows: 3 },
      { key: "lastWorkDay", label: "最后工作日", type: "date", required: true },
    ],
    approvalMode: "or_sign",
  },
  transfer: {
    approvalType: "transfer",
    label: "调岗申请",
    fields: [
      { key: "summary", label: "调岗说明", type: "textarea", required: true, rows: 2 },
      { key: "targetDept", label: "目标部门", type: "text", required: true },
      { key: "targetPost", label: "目标岗位", type: "text", required: true },
    ],
    approvalMode: "parallel",
  },
};
export function resolveInstanceFormFields(row) {
  const cfg = parseInstanceFormConfig(row?.formConfig);
  let fields = (row?.formFieldDefs?.length ? row.formFieldDefs : cfg.fields) || [];
  const formPayload = {
    ...(fields.length ? buildFormPayloadFromFields(fields) : {}),
    ...cfg.formPayload,
    ...(row?.formPayload || {}),
  };
  if (!fields.length && Object.keys(formPayload).length) {
    fields = Object.keys(formPayload)
      .filter((k) => k && k !== "summary")
      .map((k) => ({
        key: k,
        label: k,
        type: guessFieldTypeFromValue(formPayload[k]),
        required: false,
        rows: 3,
        min: 0,
        precision: 0,
        options: [],
      }));
  }
  const templateSnapshot = {
    label: row?.templateName || row?.title || "审批",
    approvalType: cfg.approvalType || row?.approvalType || "",
    summaryPlaceholder: cfg.summaryPlaceholder || "",
    templateId: row?.templateId,
    fields,
  };
  return { fields, formPayload, templateSnapshot, formConfigData: cfg };
}
export const STORAGE_KEY = "oa_unified_approve_list_v1";
/** 解析实例 formConfig */
export function parseInstanceFormConfig(formConfig) {
  let raw = {};
  if (formConfig) {
    if (typeof formConfig === "object") raw = formConfig;
    else {
      try {
        raw = JSON.parse(formConfig);
      } catch {
        raw = {};
      }
    }
  }
  const data = parseFormConfigToData(formConfig);
  const payload = raw.formPayload;
  return {
    summaryPlaceholder: raw.summaryPlaceholder || data.summaryPlaceholder || "",
    approvalType: raw.approvalType || "",
    fields: data.fields || [],
    formPayload: payload && typeof payload === "object" ? payload : {},
  };
}
export function unwrapInstanceDetail(res) {
  const data = res?.data ?? res;
  if (!data || typeof data !== "object") return {};
  if (data.id != null || data.instanceNo) return data;
  if (data.approvalInstanceVo) return data.approvalInstanceVo;
  return data;
}
/** 填报内容 + 模板字段定义 → formConfig JSON */
export function buildInstanceFormConfigJson(templateSnapshot, formPayload) {
  const payload = formPayload || {};
  return JSON.stringify({
    summaryPlaceholder: templateSnapshot?.summaryPlaceholder || "",
    approvalType: templateSnapshot?.approvalType || "",
    fields: templateSnapshot?.fields || [],
    formPayload: payload,
  });
}
/** 组装保存/更新审批 DTO */
export function buildInstanceDto({ submitForm, activeTemplate, userStore, flowNodes, existingRow }) {
  const payload = submitForm?.formPayload || {};
  const tpl = activeTemplate || {};
  const title =
    String(payload.summary || payload.title || "").trim() ||
    tpl.label ||
    submitForm?.templateName ||
    "审批申请";
  const templateId = submitForm?.templateId || tpl.templateId;
  const instanceId = existingRow?.id ?? submitForm?.instanceId;
  const taskList = mapFlowNodesToTasks(flowNodes || submitForm?.flowNodes, {
    instanceId,
    templateId,
  });
  const isUpdate = Boolean(instanceId);
  const dto = {
    templateId,
    templateName: submitForm?.templateName || tpl.label || "",
    title,
    formConfig: buildInstanceFormConfigJson({ ...tpl, fields: tpl.fields || submitForm?.formFieldDefs }, payload),
    tasks: taskList,
  };
  if (isUpdate) {
    dto.id = existingRow?.id ?? submitForm?.instanceId;
    dto.instanceNo = existingRow?.instanceNo ?? submitForm?.instanceNo ?? "";
    dto.status =
      existingRow?.statusRaw || mapInstanceStatusToApi(existingRow?.approvalStatus) || "PENDING";
    dto.currentLevel = existingRow?.currentLevel ?? submitForm?.currentLevel ?? 1;
    dto.applicantId = existingRow?.applicantId ?? existingRow?.applicantNo;
    dto.applicantName = existingRow?.applicantName || "";
  } else {
    dto.status = "PENDING";
    dto.currentLevel = 1;
    dto.applicantId = userStore?.id;
    dto.applicantName = userStore?.nickName || userStore?.name || "";
  }
  return dto;
}
/** @deprecated 使用 buildInstanceDto */
export function buildSaveInstanceDto(params) {
  return buildInstanceDto(params);
}
/** 校验提交审批流程(与模板页规则一致) */
export function validateSubmitFlowNodes(flowNodes) {
  const nodes = normalizeFlowNodes(flowNodes);
  if (!nodes.length) return { ok: false, message: "请至少配置一个审批节点" };
  for (let i = 0; i < nodes.length; i++) {
    if (!nodes[i].approvers.length) {
      return { ok: false, message: `请为第 ${i + 1} 个节点选择至少一名审批人` };
    }
  }
  return { ok: true, nodes };
}
/** 后端 status → 页面 approvalStatus */
export function mapInstanceStatusFromApi(status) {
  const s = String(status || "").toUpperCase();
  if (s === "APPROVED") return "approved";
  if (s === "REJECTED") return "rejected";
  if (s === "CANCELLED") return "cancelled";
  return "pending";
}
/** 页面 approvalStatus → 后端 status */
export function mapInstanceStatusToApi(approvalStatus) {
  const s = String(approvalStatus || "").toLowerCase();
  if (s === "approved") return "APPROVED";
  if (s === "rejected") return "REJECTED";
  if (s === "cancelled") return "CANCELLED";
  return "PENDING";
}
export function unwrapInstancePage(res) {
  const data = res?.data ?? res;
  return {
    records: Array.isArray(data?.records) ? data.records : [],
    total: Number(data?.total ?? 0),
  };
}
/** 分页列表项 → 表格行 */
export function mapInstanceFromApi(row) {
  if (!row) return {};
  const approvalStatus = mapInstanceStatusFromApi(row.status);
  const createTime = formatDisplayTime(row.createTime ?? row.applyTime ?? "");
  const applyTime = formatDisplayTime(row.applyTime ?? "");
  const finishTime = formatDisplayTime(row.finishTime ?? "");
  const resolved = resolveInstanceFormFields(row);
  const { fields, formPayload, templateSnapshot } = resolved;
  const tasks = Array.isArray(row.tasks) ? row.tasks : [];
  const flowNodes = tasks.length
    ? mapTasksToFlowNodes(tasks)
    : mapNodesFromApi(row.nodes || row.flowNodes);
  const approvalRecords = mapRecordsFromApi(row.records);
  return {
    id: row.id,
    bizId: row.instanceNo || String(row.id ?? ""),
    instanceNo: row.instanceNo || "",
    templateId: row.templateId,
    templateName: row.templateName || "",
    businessId: row.businessId,
    businessType: row.businessType,
    applicantId: row.applicantId,
    applicantNo: row.applicantId != null ? String(row.applicantId) : "",
    applicantName: row.applicantName || "",
    approvalType: row.templateName || "",
    unread: Boolean(row.isApprove) && approvalStatus === "pending",
    isApprove: Boolean(row.isApprove),
    approvalStatus,
    statusRaw: row.status,
    createTime,
    applyTime: applyTime === "—" ? "" : applyTime,
    finishTime: finishTime === "—" ? "" : finishTime,
    title: row.title || "",
    summary: row.title || row.templateName || "",
    currentLevel: row.currentLevel,
    formConfig: row.formConfig,
    formPayload,
    formFieldDefs: fields,
    templateSnapshot,
    tasks,
    records: Array.isArray(row.records) ? row.records : [],
    flowNodes,
    approvalFlowNodes: [],
    currentNodeIndex: 0,
    approvalRecords,
    rejectReason:
      approvalRecords.find((r) => r.result === "rejected")?.opinion || "",
  };
}
/** 审批操作:与后端 status 枚举一致 */
export const APPROVE_ACTION_APPROVED = "APPROVED";
export const APPROVE_ACTION_REJECTED = "REJECTED";
/** 页面操作 → approveAction */
export function mapApproveActionToApi(uiResult) {
  return uiResult === "rejected" ? APPROVE_ACTION_REJECTED : APPROVE_ACTION_APPROVED;
}
/** 组装审批提交 DTO */
export function buildApproveInstanceDto(row, uiResult, comment) {
  const opinion = (comment || "").trim();
  return {
    id: row?.id,
    approveAction: mapApproveActionToApi(uiResult),
    approveComment: opinion || (uiResult === "approved" ? "同意" : ""),
  };
}
export function buildApprovalInstanceListParams({ page, searchForm }) {
  const params = {
    current: page.current,
    size: page.size,
  };
  const dto = {};
  const kw = (searchForm?.applicantKeyword || "").trim();
  if (kw) dto.applicantName = kw;
  if (searchForm?.approvalType) {
    const opt = APPROVAL_TYPE_OPTIONS.find((x) => x.value === searchForm.approvalType);
    if (opt?.label) dto.templateName = opt.label;
  }
  if (Object.keys(dto).length) params.approvalInstanceDto = dto;
  return params;
}
export function approvalTypeLabel(v) {
  return APPROVAL_TYPE_OPTIONS.find((x) => x.value === v)?.label || "—";
  return APPROVAL_TYPE_OPTIONS.find((x) => x.value === v)?.label || v || "—";
}
export function approvalTypeStyle(v) {
@@ -147,170 +451,57 @@
  return "primary";
}
export function approvalModeLabel(v) {
  if (v === "countersign") return "或签";
  return APPROVAL_MODE_OPTIONS.find((x) => x.value === v)?.label || "与签";
}
export function unreadLabel(v) {
  return v ? "是" : "否";
}
export function buildDefaultFlowNodes() {
  return [
    {
      approverId: "mock_supervisor",
      approverName: "直属上级",
      sortOrder: 1,
      nodeOrder: 1,
      nodeStatus: "process",
      approveOpinion: "",
      approveTime: "",
    },
    {
      approverId: "mock_manager",
      approverName: "部门经理",
      sortOrder: 2,
      nodeOrder: 2,
      nodeStatus: "wait",
      approveOpinion: "",
      approveTime: "",
    },
  ];
}
/** 列表行 → 编辑表单(仅用行数据回显) */
export function buildEditFormFromInstanceRow(row) {
  const { fields, formPayload, templateSnapshot } = resolveInstanceFormFields(row);
  const normalized = normalizeFlowNodes(
    row?.flowNodes?.length ? row.flowNodes : mapTasksToFlowNodes(row?.tasks)
  );
  const flowNodes = normalized.length
    ? JSON.parse(JSON.stringify(normalized))
    : [createEmptyNode(1)];
function demoRow(partial) {
  const now = dayjs().format("YYYY-MM-DD HH:mm:ss");
  return {
    id: partial.id,
    bizId: partial.bizId || partial.id,
    applicantNo: partial.applicantNo,
    applicantName: partial.applicantName,
    approvalType: partial.approvalType,
    approvalMode: partial.approvalMode || "parallel",
    unread: partial.unread ?? false,
    approvalStatus: partial.approvalStatus || "pending",
    createTime: partial.createTime || now,
    summary: partial.summary || "",
    formPayload: partial.formPayload || {},
    approvalFlowNodes: partial.approvalFlowNodes || buildDefaultFlowNodes(),
    currentNodeIndex: partial.currentNodeIndex ?? 0,
    approvalRecords: partial.approvalRecords || [],
    rejectReason: partial.rejectReason || "",
    sourceRoute: partial.sourceRoute || "",
    templateKey: String(row?.templateId || ""),
    templateId: row?.templateId,
    templateName: row?.templateName || templateSnapshot.label,
    instanceId: row?.id,
    instanceNo: row?.instanceNo || "",
    statusRaw: row?.statusRaw || row?.status || "PENDING",
    currentLevel: row?.currentLevel ?? 1,
    applicantId: row?.applicantId,
    applicantName: row?.applicantName || "",
    templateSnapshot,
    formFieldDefs: fields,
    formPayload,
    flowNodes,
  };
}
/** 初始演示数据(共 22 条,与原型数量一致) */
export function createInitialMockRows() {
  const types = [
    "cost_reimburse",
    "travel_reimburse",
    "overtime",
    "leave",
    "work_handover",
    "regular",
    "resign",
    "transfer",
    "cost_reimburse",
    "leave",
    "overtime",
    "travel_reimburse",
    "work_handover",
    "regular",
    "cost_reimburse",
    "leave",
    "transfer",
    "resign",
    "overtime",
    "travel_reimburse",
    "cost_reimburse",
    "leave",
  ];
  const applicants = [
    { no: "007", name: "苹果" },
    { no: "Guest001", name: "外部用户" },
    { no: "0056", name: "王五" },
    { no: "0042", name: "李四" },
    { no: "0088", name: "猫猫" },
    { no: "0012", name: "张三" },
    { no: "0033", name: "赵六" },
  ];
  const summaries = [
    "办公用品采购报销",
    "上海出差差旅费",
    "周末项目加班",
    "年假 3 天",
    "离职工作交接",
    "试用期转正申请",
    "个人原因离职",
    "调至销售部",
    "客户接待餐费",
    "病假 1 天",
    "节假日值班加班",
    "北京培训差旅",
    "项目文档交接",
    "研发岗转正",
    "通讯费报销",
    "事假半天",
    "调岗至市场部",
    "协商离职",
    "工作日延时加班",
    "成都展会差旅",
    "交通费报销",
    "调休 1 天",
  ];
  const statuses = ["pending", "pending", "pending", "approved", "pending", "pending", "rejected", "pending"];
  return types.map((approvalType, i) => {
    const ap = applicants[i % applicants.length];
    const daysAgo = i % 14;
    return demoRow({
      id: `mock_${i + 1}`,
      bizId: `BIZ${String(2025031400 + i)}`,
      applicantNo: ap.no,
      applicantName: ap.name,
      approvalType,
      approvalMode: i % 5 === 0 ? "or_sign" : "parallel",
      unread: i % 3 === 0,
      approvalStatus: statuses[i % statuses.length],
      createTime: dayjs().subtract(daysAgo, "day").hour(9 + (i % 8)).minute((i * 7) % 60).second(0).format("YYYY-MM-DD HH:mm:ss"),
      summary: summaries[i],
      formPayload: { summary: summaries[i] },
    });
  });
}
export function loadStoredRows() {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    return Array.isArray(parsed) ? parsed : null;
  } catch {
    return null;
  }
}
export function saveStoredRows(rows) {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(rows));
  } catch {
    /* ignore quota */
  }
}
export function createEmptySubmitForm(templateKey) {
  const tpl = SUBMIT_TEMPLATES[templateKey];
  const payload = { summary: "" };
  (tpl?.fields || []).forEach((f) => {
    if (f.type === "number") payload[f.key] = undefined;
    else if (f.type === "datetimerange") payload[f.key] = [];
    else payload[f.key] = "";
  });
export function createEmptySubmitForm(templateKey, templateOverride, flowNodesOverride) {
  const tpl = templateOverride || null;
  const payload = tpl?.fields?.length ? buildFormPayloadFromFields(tpl.fields) : { summary: "" };
  const normalized = normalizeFlowNodes(flowNodesOverride);
  const flowNodes = normalized.length
    ? JSON.parse(JSON.stringify(normalized))
    : [createEmptyNode(1)];
  return {
    templateKey: templateKey || "",
    approvalMode: tpl?.approvalMode || "parallel",
    templateId: tpl?.templateId || "",
    templateName: tpl?.label || "",
    instanceId: "",
    instanceNo: "",
    statusRaw: "",
    currentLevel: 1,
    applicantId: null,
    applicantName: "",
    templateSnapshot: templateOverride || null,
    formFieldDefs: tpl?.fields || [],
    formPayload: payload,
    approvalFlowNodes: buildDefaultFlowNodes(),
    flowNodes,
  };
}