WorkOrderFormModal.vue 8.14 KB
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
import type { WorkOrder, WorkOrderPayload } from '~/types'

const props = withDefaults(defineProps<{
  modelValue: boolean
  mode: 'create' | 'edit'
  workOrder?: WorkOrder
}>(), {
  workOrder: undefined
})

const emit = defineEmits<{
  'update:modelValue': [value: boolean]
  'submitted': []
}>()

const { t } = useAppI18n()
const toast = useToast()
const { user } = useAuth()
const workOrderApi = useWorkOrderApi()
const baseInfoApi = useBaseInfoApi()
const settingsApi = useSettingsApi()

const MAX_ORDER_NO = 40
const MAX_DEVICE_CODE = 40
const MAX_BATCH = 40
const MAX_LINE = 30

const isSubmitting = ref(false)
const { data: deviceTypes } = await baseInfoApi.getDeviceTypes()
const { data: members } = await settingsApi.getMembers()
const deviceCodeItems = computed(() => {
  const modelSet = new Set(deviceTypes.value.map(item => item.model.trim()).filter(model => model.length > 0))
  const selectedDeviceCode = state.deviceCode.trim()

  if (selectedDeviceCode.length > 0) {
    modelSet.add(selectedDeviceCode)
  }

  const models = Array.from(modelSet).sort((a, b) => a.localeCompare(b, 'zh-CN'))

  return models.map(model => ({
    label: model,
    value: model
  }))
})

const ownerItems = computed(() => {
  return members.value.map(member => ({
    label: `${member.name} (${member.username})`,
    value: member.username
  }))
})

const buildSchema = () => z.object({
  orderNo: z.string().trim()
    .min(1, t('workOrders.form.validation.required'))
    .max(MAX_ORDER_NO, t('workOrders.form.validation.orderNoMax', { max: MAX_ORDER_NO })),
  deviceCode: z.string().trim()
    .min(1, t('workOrders.form.validation.required'))
    .max(MAX_DEVICE_CODE, t('workOrders.form.validation.deviceCodeMax', { max: MAX_DEVICE_CODE })),
  batchNo: z.string().trim()
    .min(1, t('workOrders.form.validation.required'))
    .max(MAX_BATCH, t('workOrders.form.validation.batchNoMax', { max: MAX_BATCH })),
  line: z.string().trim()
    .min(1, t('workOrders.form.validation.required'))
    .max(MAX_LINE, t('workOrders.form.validation.lineMax', { max: MAX_LINE })),
  ownerUsername: z.string().trim()
    .min(1, t('workOrders.form.validation.required')),
  plannedDate: z.string().trim()
    .min(1, t('workOrders.form.validation.required')),
  totalSn: z.coerce.number().int().positive(t('workOrders.form.validation.totalSnPositive'))
})

const schema = computed(buildSchema)

type Schema = z.output<ReturnType<typeof buildSchema>>

const state = reactive<Schema>({
  orderNo: '',
  deviceCode: '',
  batchNo: '',
  line: '',
  ownerUsername: '',
  plannedDate: '',
  totalSn: 1
})

const open = computed({
  get: () => props.modelValue,
  set: value => emit('update:modelValue', value)
})

const title = computed(() => {
  return props.mode === 'create'
    ? t('workOrders.form.createTitle')
    : t('workOrders.form.editTitle')
})

const description = computed(() => {
  return props.mode === 'create'
    ? t('workOrders.form.createDescription')
    : t('workOrders.form.editDescription')
})

const submitLabel = computed(() => {
  return props.mode === 'create' ? t('common.create') : t('common.update')
})

function resetState() {
  if (props.mode === 'edit' && props.workOrder) {
    state.orderNo = props.workOrder.orderNo
    state.deviceCode = props.workOrder.deviceCode
    state.batchNo = props.workOrder.batchNo
    state.line = props.workOrder.line
    state.ownerUsername = props.workOrder.ownerUsername
    state.plannedDate = props.workOrder.plannedDate
    state.totalSn = props.workOrder.progress.totalSn
    return
  }

  state.orderNo = ''
  state.deviceCode = ''
  state.batchNo = ''
  state.line = ''
  state.ownerUsername = ''
  state.plannedDate = ''
  state.totalSn = 1
}

function closeModal() {
  open.value = false
}

function toPayload(data: Schema): WorkOrderPayload & { operator: string } {
  return {
    orderNo: data.orderNo.trim(),
    deviceCode: data.deviceCode.trim(),
    batchNo: data.batchNo.trim(),
    line: data.line.trim(),
    ownerUsername: data.ownerUsername.trim(),
    plannedDate: data.plannedDate.trim(),
    totalSn: Number(data.totalSn),
    operator: user.value?.name || '未知操作员'
  }
}

async function onSubmit(event: FormSubmitEvent<Schema>) {
  isSubmitting.value = true

  try {
    if (props.mode === 'create') {
      const result = await workOrderApi.createWorkOrder(toPayload(event.data))

      if (!result.success) {
        toast.add({
          title: t('common.error'),
          description: result.message,
          icon: 'i-lucide-circle-alert',
          color: 'error'
        })
        return
      }

      toast.add({
        title: t('settings.profile.toastTitle'),
        description: result.message,
        icon: 'i-lucide-check',
        color: 'success'
      })
      emit('submitted')
      closeModal()
      return
    }

    if (!props.workOrder) {
      toast.add({
        title: t('common.error'),
        description: t('common.requestFailed'),
        icon: 'i-lucide-circle-alert',
        color: 'error'
      })
      return
    }

    const result = await workOrderApi.updateWorkOrderDraft(props.workOrder.id, toPayload(event.data))

    if (!result.success) {
      toast.add({
        title: t('common.error'),
        description: result.message,
        icon: 'i-lucide-circle-alert',
        color: 'error'
      })
      return
    }

    toast.add({
      title: t('settings.profile.toastTitle'),
      description: result.message,
      icon: 'i-lucide-check',
      color: 'success'
    })
    emit('submitted')
    closeModal()
  } catch {
    toast.add({
      title: t('common.error'),
      description: t('common.requestFailed'),
      icon: 'i-lucide-circle-alert',
      color: 'error'
    })
  } finally {
    isSubmitting.value = false
  }
}

watch(
  () => [open.value, props.mode, props.workOrder],
  ([nextOpen]) => {
    if (nextOpen) {
      resetState()
    }
  },
  { immediate: true }
)
</script>

<template>
  <UModal v-model:open="open" :title="title" :description="description">
    <template #body>
      <UForm
        :schema="schema"
        :state="state"
        class="space-y-4"
        @submit="onSubmit"
      >
        <UFormField name="orderNo" :label="t('workOrders.form.fields.orderNo')" required>
          <UInput v-model="state.orderNo" class="w-full" />
        </UFormField>

        <UFormField name="deviceCode" :label="t('workOrders.form.fields.deviceCode')" required>
          <USelect
            v-model="state.deviceCode"
            :items="deviceCodeItems"
            :placeholder="t('workOrders.form.fields.deviceCode')"
            class="w-full"
          />
        </UFormField>

        <UFormField name="batchNo" :label="t('workOrders.form.fields.batchNo')" required>
          <UInput v-model="state.batchNo" class="w-full" />
        </UFormField>

        <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
          <UFormField name="line" :label="t('workOrders.form.fields.line')" required>
            <UInput v-model="state.line" class="w-full" />
          </UFormField>

          <UFormField name="ownerUsername" :label="t('workOrders.form.fields.owner')" required>
            <USelect v-model="state.ownerUsername" :items="ownerItems" class="w-full" />
          </UFormField>

          <UFormField name="plannedDate" :label="t('workOrders.form.fields.plannedDate')" required>
            <UInput v-model="state.plannedDate" type="date" class="w-full" />
          </UFormField>
        </div>

        <UFormField name="totalSn" :label="t('workOrders.form.fields.totalSn')" required>
          <UInput
            v-model.number="state.totalSn"
            type="number"
            min="1"
            class="w-full"
          />
        </UFormField>

        <div class="flex justify-end gap-2">
          <UButton
            :label="t('common.cancel')"
            color="neutral"
            variant="subtle"
            type="button"
            @click="closeModal"
          />
          <UButton
            :label="submitLabel"
            color="primary"
            variant="solid"
            type="submit"
            :loading="isSubmitting"
          />
        </div>
      </UForm>
    </template>
  </UModal>
</template>