feat(schedule): 生成工单 UI 入口(打通计划→派单闭环)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-21 11:36:56 +08:00
parent abf76e22d7
commit 2ae7aec861

View File

@ -4,6 +4,7 @@
<div class="flex items-center gap-sm"> <div class="flex items-center gap-sm">
<el-date-picker v-model="date" type="date" value-format="YYYY-MM-DD" :clearable="false" style="width: 160px" /> <el-date-picker v-model="date" type="date" value-format="YYYY-MM-DD" :clearable="false" style="width: 160px" />
<el-button type="primary" :loading="loading" @click="refresh">刷新</el-button> <el-button type="primary" :loading="loading" @click="refresh">刷新</el-button>
<el-button v-permission="'order:generate'" @click="openGenerate">生成工单</el-button>
<el-button v-permission="'order:assign'" @click="recommendVisible = true">批量推荐</el-button> <el-button v-permission="'order:assign'" @click="recommendVisible = true">批量推荐</el-button>
<span class="ml-auto text-xs text-text-placeholder">横轴时段 08:00–18:00 · 纵轴护理员 · 冲突红框 · 空缺橙框</span> <span class="ml-auto text-xs text-text-placeholder">横轴时段 08:00–18:00 · 纵轴护理员 · 冲突红框 · 空缺橙框</span>
</div> </div>
@ -116,6 +117,50 @@
<span class="ml-xs text-xs text-text-placeholder">选择工单后自动带入老人与时段,逐条确认后才派</span> <span class="ml-xs text-xs text-text-placeholder">选择工单后自动带入老人与时段,逐条确认后才派</span>
</div> </div>
</el-dialog> </el-dialog>
<el-dialog v-model="generateVisible" title="生成工单" width="520px">
<el-form :model="generateForm" label-width="90px">
<el-form-item label="老人">
<el-select v-model="generateForm.elderId" filterable placeholder="选择老人" style="width: 100%" @change="onGenerateElderChange">
<el-option v-for="e in elders" :key="e.id" :label="`${e.name}(${e.idCardNo})`" :value="e.id" />
</el-select>
</el-form-item>
<el-form-item label="服务计划">
<div style="width: 100%">
<el-select
v-model="generateForm.planId"
filterable
placeholder="选择生效中的计划"
style="width: 100%"
:disabled="!generateForm.elderId"
:loading="generatePlansLoading"
>
<el-option v-for="p in activePlans" :key="p.id" :label="planLabel(p)" :value="p.id" />
</el-select>
<el-alert
v-if="generateForm.elderId && !generatePlansLoading && activePlans.length === 0"
class="mt-xs"
type="warning"
:closable="false"
title="该老人无生效中的计划"
/>
</div>
</el-form-item>
<el-form-item label="服务日期">
<el-date-picker v-model="generateForm.serviceDate" type="date" value-format="YYYY-MM-DD" :clearable="false" style="width: 100%" />
</el-form-item>
<el-form-item label="时段起">
<el-time-picker v-model="generateForm.timeSlotStart" value-format="HH:mm:ss" format="HH:mm" :clearable="false" style="width: 100%" />
</el-form-item>
<el-form-item label="时段止">
<el-time-picker v-model="generateForm.timeSlotEnd" value-format="HH:mm:ss" format="HH:mm" :clearable="false" style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="generateVisible = false">取消</el-button>
<el-button type="primary" :disabled="!generateForm.planId" :loading="generating" @click="confirmGenerate">生成工单</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
@ -124,10 +169,12 @@ import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { ApiError } from '@/api/http' import { ApiError } from '@/api/http'
import { listElders, type Elder } from '@/api/elder' import { listElders, type Elder } from '@/api/elder'
import { listPlansByElder, type Plan } from '@/api/plan'
import { listStaff, type Staff } from '@/api/staff' import { listStaff, type Staff } from '@/api/staff'
import { import {
assignOrder, assignOrder,
batchRecommend, batchRecommend,
generateOrder,
listOrders, listOrders,
reassignOrder, reassignOrder,
type BatchRecommendItem, type BatchRecommendItem,
@ -164,6 +211,22 @@ const recommending = ref(false)
const recommendations = ref<BatchRecommendItem[]>([]) const recommendations = ref<BatchRecommendItem[]>([])
const recommendForm = reactive({ elderId: '', timeSlotStart: '', timeSlotEnd: '', orderId: '' }) const recommendForm = reactive({ elderId: '', timeSlotStart: '', timeSlotEnd: '', orderId: '' })
// ---- 生成工单 ----
const generateVisible = ref(false)
const generating = ref(false)
const generatePlans = ref<Plan[]>([])
const generatePlansLoading = ref(false)
const generateForm = reactive({
elderId: '',
planId: '',
serviceDate: todayStr(),
timeSlotStart: '09:00:00',
timeSlotEnd: '10:00:00',
})
/** 老人名下生效中的计划(plan_status === active)。 */
const activePlans = computed(() => generatePlans.value.filter((p) => p.planStatus === 'active'))
/** 当日待派(to_assign)工单,供「待派工单」下拉选择。 */ /** 当日待派(to_assign)工单,供「待派工单」下拉选择。 */
const toAssignOrders = computed(() => orders.value.filter((o) => o.orderStatus === 'to_assign')) const toAssignOrders = computed(() => orders.value.filter((o) => o.orderStatus === 'to_assign'))
@ -182,6 +245,12 @@ function fmtTime(ms?: number | null): string {
return `${pad(d.getHours())}:${pad(d.getMinutes())}` return `${pad(d.getHours())}:${pad(d.getMinutes())}`
} }
function fmtDate(ms?: number | null): string {
if (ms == null) return '-'
const d = new Date(ms)
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
}
/** 毫秒时间戳 → 'YYYY-MM-DDTHH:mm:ss'(用于 datetime picker 的 value-format)。 */ /** 毫秒时间戳 → 'YYYY-MM-DDTHH:mm:ss'(用于 datetime picker 的 value-format)。 */
function toDateTimeStr(ms?: number | null): string { function toDateTimeStr(ms?: number | null): string {
if (ms == null) return '' if (ms == null) return ''
@ -365,6 +434,64 @@ async function confirmRecommend(staffId: string, name: string): Promise<void> {
} }
} }
// ---- 生成工单 ----
function planLabel(p: Plan): string {
return `${fmtDate(p.planPeriodStart)} ~ ${fmtDate(p.planPeriodEnd)} · ${p.careLevel}`
}
function openGenerate(): void {
generateForm.elderId = ''
generateForm.planId = ''
generateForm.serviceDate = todayStr()
generateForm.timeSlotStart = '09:00:00'
generateForm.timeSlotEnd = '10:00:00'
generatePlans.value = []
generateVisible.value = true
}
/** 选中老人后加载其计划列表,仅 active 计划可生成工单。 */
async function onGenerateElderChange(elderId: string): Promise<void> {
generateForm.planId = ''
generatePlans.value = []
if (!elderId) return
generatePlansLoading.value = true
try {
generatePlans.value = await listPlansByElder(elderId)
} catch (e) {
if (e instanceof ApiError) ElMessage.error(e.message)
} finally {
generatePlansLoading.value = false
}
}
async function confirmGenerate(): Promise<void> {
if (!generateForm.elderId) {
ElMessage.warning('请选择老人')
return
}
if (!generateForm.planId) {
ElMessage.warning('请选择生效中的计划')
return
}
generating.value = true
try {
await generateOrder({
elderId: generateForm.elderId,
planId: generateForm.planId,
serviceDate: generateForm.serviceDate,
timeSlotStart: `${generateForm.serviceDate}T${generateForm.timeSlotStart}`,
timeSlotEnd: `${generateForm.serviceDate}T${generateForm.timeSlotEnd}`,
})
ElMessage.success('已生成待派工单')
generateVisible.value = false
await refresh()
} catch (e) {
if (e instanceof ApiError) ElMessage.error(e.message)
} finally {
generating.value = false
}
}
onMounted(() => { onMounted(() => {
void refresh() void refresh()
}) })