mock-operations.ts 8.89 KB
import type {
  OperationTask,
  OperationTaskAction,
  OperationTaskActionPayload,
  OperationTaskListQuery,
  OperationTaskMutationResponse,
  OperationTaskResult,
  OperationTaskStatus
} from '../../app/types/operation'

const ALLOWED_ACTIONS_BY_STATUS: Record<OperationTaskStatus, OperationTaskAction[]> = {
  pending: ['start', 'skip'],
  in_progress: ['complete_assembly', 'submit_test', 'skip'],
  pending_test: ['submit_test', 'skip'],
  failed: ['skip'],
  rework: ['complete_rework', 'skip'],
  completed: [],
  skipped: []
}

const STATUS_NEXT_ACTION: Record<OperationTaskStatus, string> = {
  pending: 'start',
  in_progress: 'complete_assembly',
  pending_test: 'submit_test',
  failed: 'create_exception',
  rework: 'complete_rework',
  completed: 'go_next_step',
  skipped: 'audit_review'
}

function now() {
  return new Date().toISOString()
}

function normalizeText(value: string | undefined) {
  if (!value) {
    return undefined
  }

  const trimmed = value.trim()
  return trimmed.length > 0 ? trimmed : undefined
}

function cloneTask(item: OperationTask): OperationTask {
  return {
    ...item,
    auditEvents: item.auditEvents.map(event => ({ ...event }))
  }
}

function withMutation(base: OperationTaskMutationResponse, item?: OperationTask) {
  return {
    ...base,
    state: item?.status,
    nextAction: item?.nextAction
  }
}

function buildAuditEvent(
  action: OperationTaskAction,
  fromStatus: OperationTaskStatus | null,
  toStatus: OperationTaskStatus,
  operator: string,
  nextAction: string,
  remark?: string,
  evidencePath?: string
) {
  return {
    id: `op-evt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
    action,
    fromStatus,
    toStatus,
    operator,
    at: now(),
    remark: normalizeText(remark),
    evidencePath: normalizeText(evidencePath),
    nextAction
  }
}

function createSeed(input: {
  id: number
  workOrderNo: string
  sn: string
  stepName: string
  workstation: string
  device: string
  operator: string
  status: OperationTaskStatus
  result: OperationTaskResult
  startedAt?: string
  endedAt?: string
}) {
  const createdEvent = buildAuditEvent(
    'start',
    null,
    input.status,
    input.operator,
    STATUS_NEXT_ACTION[input.status],
    '任务初始化'
  )

  return {
    id: input.id,
    workOrderNo: input.workOrderNo,
    sn: input.sn,
    stepName: input.stepName,
    workstation: input.workstation,
    device: input.device,
    operator: input.operator,
    startedAt: input.startedAt,
    endedAt: input.endedAt,
    result: input.result,
    status: input.status,
    nextAction: STATUS_NEXT_ACTION[input.status],
    auditEvents: [createdEvent]
  } satisfies OperationTask
}

const mockOperationTasks: OperationTask[] = [
  createSeed({
    id: 9001,
    workOrderNo: 'WO-202605-001',
    sn: 'SN-RBTX5-0001',
    stepName: '装配',
    workstation: 'A01',
    device: 'ASM-JIG-01',
    operator: '装配工位操作员',
    status: 'pending',
    result: 'pending'
  }),
  createSeed({
    id: 9002,
    workOrderNo: 'WO-202605-001',
    sn: 'SN-RBTX5-0002',
    stepName: '装配',
    workstation: 'A02',
    device: 'ASM-JIG-02',
    operator: '装配工位操作员',
    status: 'in_progress',
    result: 'pending',
    startedAt: '2026-05-18T07:15:00.000Z'
  }),
  createSeed({
    id: 9003,
    workOrderNo: 'WO-202605-002',
    sn: 'SN-RBTX5-0003',
    stepName: '测试',
    workstation: 'T01',
    device: 'TEST-BENCH-07',
    operator: '质量经理',
    status: 'pending_test',
    result: 'pending',
    startedAt: '2026-05-18T06:45:00.000Z'
  }),
  createSeed({
    id: 9004,
    workOrderNo: 'WO-202605-003',
    sn: 'SN-RBTM2-0001',
    stepName: '终检',
    workstation: 'Q01',
    device: 'QC-STATION-03',
    operator: '质量经理',
    status: 'completed',
    result: 'pass',
    startedAt: '2026-05-18T05:00:00.000Z',
    endedAt: '2026-05-18T05:30:00.000Z'
  }),
  createSeed({
    id: 9005,
    workOrderNo: 'WO-202605-004',
    sn: 'SN-RBTQA-0001',
    stepName: '测试',
    workstation: 'T02',
    device: 'TEST-BENCH-11',
    operator: '质量经理',
    status: 'failed',
    result: 'fail',
    startedAt: '2026-05-18T04:10:00.000Z',
    endedAt: '2026-05-18T04:20:00.000Z'
  }),
  createSeed({
    id: 9006,
    workOrderNo: 'WO-202605-004',
    sn: 'SN-RBTQA-0002',
    stepName: '返修',
    workstation: 'R01',
    device: 'REPAIR-DESK-02',
    operator: '装配工位操作员',
    status: 'rework',
    result: 'pending',
    startedAt: '2026-05-18T03:00:00.000Z'
  }),
  createSeed({
    id: 9007,
    workOrderNo: 'WO-202605-005',
    sn: 'SN-RBTQX-0003',
    stepName: '装配',
    workstation: 'A03',
    device: 'ASM-JIG-05',
    operator: '装配工位操作员',
    status: 'skipped',
    result: 'pending'
  }),
  createSeed({
    id: 9008,
    workOrderNo: 'WO-202605-003',
    sn: 'SN-RBTM2-0003',
    stepName: '测试',
    workstation: 'T01',
    device: 'TEST-BENCH-07',
    operator: '质量经理',
    status: 'pending_test',
    result: 'pending',
    startedAt: '2026-05-18T02:10:00.000Z'
  })
]

function findById(id: number) {
  return mockOperationTasks.find(item => item.id === id)
}

function parseText(value: string | undefined) {
  return normalizeText(value)?.toLowerCase()
}

function matchesFilter(item: OperationTask, query: OperationTaskListQuery) {
  const workstation = parseText(query.workstation)
  const stepName = parseText(query.stepName)
  const operator = parseText(query.operator)
  const sn = parseText(query.sn)

  if (workstation && !item.workstation.toLowerCase().includes(workstation)) {
    return false
  }

  if (stepName && !item.stepName.toLowerCase().includes(stepName)) {
    return false
  }

  if (operator && !item.operator.toLowerCase().includes(operator)) {
    return false
  }

  if (sn && !item.sn.toLowerCase().includes(sn)) {
    return false
  }

  if (query.status && item.status !== query.status) {
    return false
  }

  return true
}

function applyAction(item: OperationTask, action: OperationTaskAction, payload: OperationTaskActionPayload) {
  const operator = payload.operator.trim()
  const remark = normalizeText(payload.remark)
  const evidencePath = normalizeText(payload.evidencePath)
  const fromStatus = item.status

  if (action === 'start') {
    item.status = 'in_progress'
    item.startedAt = item.startedAt || now()
    item.result = 'pending'
  }

  if (action === 'complete_assembly') {
    item.status = 'pending_test'
    item.result = 'pending'
  }

  if (action === 'complete_rework') {
    item.status = 'pending_test'
    item.result = 'pending'
    item.endedAt = undefined
  }

  if (action === 'submit_test') {
    const testPassed = payload.testPassed === true
    item.status = testPassed ? 'completed' : 'failed'
    item.result = testPassed ? 'pass' : 'fail'
    item.endedAt = now()
  }

  if (action === 'skip') {
    item.status = 'skipped'
    item.result = 'pending'
    item.endedAt = now()
  }

  item.operator = operator
  item.nextAction = STATUS_NEXT_ACTION[item.status]

  const event = buildAuditEvent(action, fromStatus, item.status, operator, item.nextAction, remark, evidencePath)
  item.auditEvents.unshift(event)
}

export function getMockOperationTasks(query: OperationTaskListQuery) {
  return mockOperationTasks
    .filter(item => matchesFilter(item, query))
    .sort((a, b) => {
      const aTime = a.auditEvents[0]?.at ?? ''
      const bTime = b.auditEvents[0]?.at ?? ''
      return bTime.localeCompare(aTime)
    })
    .map(item => cloneTask(item))
}

export function mutateMockOperationTask(
  id: number,
  action: OperationTaskAction,
  payload: OperationTaskActionPayload
): OperationTaskMutationResponse {
  const item = findById(id)

  if (!item) {
    return {
      success: false,
      errorCode: 'TASK_NOT_FOUND',
      message: '工序任务不存在。'
    }
  }

  const allowedActions = ALLOWED_ACTIONS_BY_STATUS[item.status] ?? []
  if (!allowedActions.includes(action)) {
    return withMutation({
      success: false,
      errorCode: 'ACTION_NOT_ALLOWED',
      message: '当前任务状态不允许执行该操作。'
    }, item)
  }

  if (action === 'skip' && !normalizeText(payload.remark)) {
    return withMutation({
      success: false,
      errorCode: 'SKIP_REASON_REQUIRED',
      message: '跳过工序必须填写原因。'
    }, item)
  }

  if (action === 'submit_test' && typeof payload.testPassed !== 'boolean') {
    return withMutation({
      success: false,
      errorCode: 'TEST_RESULT_REQUIRED',
      message: '提交测试结果时必须明确通过或失败。'
    }, item)
  }

  if (
    (action === 'complete_assembly' || action === 'submit_test')
    && !normalizeText(payload.evidencePath)
  ) {
    return withMutation({
      success: false,
      errorCode: 'EVIDENCE_REQUIRED',
      message: '完成装配或提交测试时必须上传图片或视频附件。'
    }, item)
  }

  applyAction(item, action, payload)

  return withMutation({
    success: true,
    errorCode: null,
    message: '工序任务状态已更新。'
  }, item)
}