useSnApi.ts 5.98 KB
import type {
  SnActionPayload,
  SnImportPayload,
  SnImportResponse,
  SnItem,
  SnListQuery,
  SnMutationResponse,
  SnStatus
} from '~/types'
import { SN_EXCEPTION_STATUS_VALUES, SN_STATUS_VALUES } from '~/types/sn'
import { createSharedComposable } from '@vueuse/core'

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null
}

function isStatus(value: unknown): value is SnStatus {
  return typeof value === 'string' && SN_STATUS_VALUES.includes(value as SnStatus)
}

function isExceptionStatus(value: unknown): value is SnItem['exceptionStatus'] {
  return typeof value === 'string' && SN_EXCEPTION_STATUS_VALUES.includes(value as SnItem['exceptionStatus'])
}

function normalizeString(value: unknown, fallback = '') {
  return typeof value === 'string' ? value : fallback
}

function normalizeNumber(value: unknown, fallback = 0) {
  const num = Number(value)
  return Number.isFinite(num) ? num : fallback
}

function normalizeSnItems(payload: unknown): SnItem[] {
  if (!Array.isArray(payload)) {
    return []
  }

  return payload
    .filter(isRecord)
    .map((item) => {
      const audit = isRecord(item.audit) ? item.audit : {}
      const events = Array.isArray(item.events) ? item.events : []

      return {
        id: normalizeNumber(item.id),
        sn: normalizeString(item.sn),
        workOrderNo: normalizeString(item.workOrderNo),
        status: isStatus(item.status) ? item.status : 'pending',
        currentStep: normalizeString(item.currentStep),
        exceptionStatus: isExceptionStatus(item.exceptionStatus) ? item.exceptionStatus : 'none',
        freezeReason: normalizeString(item.freezeReason, undefined),
        scrapReason: normalizeString(item.scrapReason, undefined),
        audit: {
          createdBy: normalizeString(audit.createdBy),
          createdAt: normalizeString(audit.createdAt),
          updatedBy: normalizeString(audit.updatedBy),
          updatedAt: normalizeString(audit.updatedAt),
          lastAction: normalizeString(audit.lastAction) as SnItem['audit']['lastAction'],
          lastActionAt: normalizeString(audit.lastActionAt),
          lastActionBy: normalizeString(audit.lastActionBy)
        },
        events: events
          .filter(isRecord)
          .map(event => ({
            id: normalizeString(event.id),
            action: normalizeString(event.action) as SnItem['events'][number]['action'],
            fromStatus: isStatus(event.fromStatus) ? event.fromStatus : null,
            toStatus: isStatus(event.toStatus) ? event.toStatus : 'pending',
            operator: normalizeString(event.operator),
            at: normalizeString(event.at),
            reason: normalizeString(event.reason, undefined),
            evidencePath: normalizeString(event.evidencePath, undefined),
            currentStep: normalizeString(event.currentStep),
            exceptionStatus: isExceptionStatus(event.exceptionStatus) ? event.exceptionStatus : 'none'
          }))
      } satisfies SnItem
    })
    .filter(item => item.id > 0 && item.sn.length > 0)
}

function normalizeMutation(payload: unknown, fallbackMessage: string): SnMutationResponse {
  if (!isRecord(payload)) {
    return {
      success: false,
      errorCode: 'REQUEST_FAILED',
      message: fallbackMessage
    }
  }

  return {
    success: Boolean(payload.success),
    errorCode: typeof payload.errorCode === 'string' ? payload.errorCode : null,
    message: typeof payload.message === 'string' && payload.message.length > 0
      ? payload.message
      : fallbackMessage
  }
}

function normalizeImport(payload: unknown): SnImportResponse {
  const mutation = normalizeMutation(payload, 'SN 导入失败。')
  if (!isRecord(payload)) {
    return {
      ...mutation,
      importedCount: 0,
      skippedCount: 0,
      duplicateSnList: []
    }
  }

  const duplicateSnList = Array.isArray(payload.duplicateSnList)
    ? payload.duplicateSnList.filter(item => typeof item === 'string')
    : []

  return {
    ...mutation,
    importedCount: normalizeNumber(payload.importedCount),
    skippedCount: normalizeNumber(payload.skippedCount),
    duplicateSnList
  }
}

function buildQuery(query: SnListQuery) {
  const params = new URLSearchParams()

  if (query.sn) {
    params.set('sn', query.sn)
  }

  if (query.workOrderNo) {
    params.set('workOrderNo', query.workOrderNo)
  }

  if (query.status) {
    params.set('status', query.status)
  }

  if (query.currentStep) {
    params.set('currentStep', query.currentStep)
  }

  if (query.exceptionStatus) {
    params.set('exceptionStatus', query.exceptionStatus)
  }

  const suffix = params.toString()
  return suffix.length > 0 ? `?${suffix}` : ''
}

const _useSnApi = () => {
  const api = useApiGateway()

  const getSnItems = async (query: SnListQuery) => {
    const result = await api.request<unknown>(`/api/sn${buildQuery(query)}`, {
      method: 'GET'
    })

    return normalizeSnItems(result)
  }

  const importSnItems = async (payload: SnImportPayload) => {
    try {
      const result = await api.request<unknown>('/api/sn/import', {
        method: 'POST',
        body: payload
      })

      return normalizeImport(result)
    } catch {
      return {
        success: false,
        errorCode: 'REQUEST_FAILED',
        message: 'SN 导入失败。',
        importedCount: 0,
        skippedCount: 0,
        duplicateSnList: []
      } satisfies SnImportResponse
    }
  }

  const runSnAction = async (id: number, action: 'freeze' | 'unfreeze' | 'scrap', payload: SnActionPayload) => {
    try {
      const result = await api.request<unknown>(`/api/sn/${id}/actions/${action}`, {
        method: 'POST',
        body: payload
      })

      return normalizeMutation(result, 'SN 状态更新失败。')
    } catch {
      return {
        success: false,
        errorCode: 'REQUEST_FAILED',
        message: 'SN 状态更新失败。'
      } satisfies SnMutationResponse
    }
  }

  return {
    getSnItems,
    importSnItems,
    runSnAction
  }
}

export const useSnApi = createSharedComposable(_useSnApi)