完善下单逻辑及其ui展示,修复支付倒计时显示错误bug
This commit is contained in:
@@ -12,39 +12,80 @@ import {
|
||||
HomeServiceTaskType,
|
||||
HomeServiceTimelineItemType
|
||||
} from '@/types/home-service.uts'
|
||||
import {
|
||||
confirmServiceOrder,
|
||||
createServiceOrder,
|
||||
getServiceOrderDetail,
|
||||
listConsumerServiceOrders,
|
||||
rejectServiceOrderAcceptance
|
||||
} from '@/services/serviceOrderService.uts'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import {
|
||||
getServiceOrderStatusText,
|
||||
type ServiceOrderStatus,
|
||||
type ServiceOrderTimelineItemType,
|
||||
type ServiceOrderType
|
||||
} from '@/types/service-order.uts'
|
||||
|
||||
const SERVICE_CATALOG: Array<HomeServiceCatalogType> = [
|
||||
{
|
||||
id: 'svc-001',
|
||||
name: '基础上门护理',
|
||||
category: '日常照护',
|
||||
price: 168,
|
||||
durationText: '约 2 小时',
|
||||
summary: '覆盖生命体征监测、基础照护、风险提醒。',
|
||||
tags: ['适老化', '护理员上门', '支持家属陪同'],
|
||||
suitableFor: '行动不便、术后恢复、慢病随访老人'
|
||||
},
|
||||
{
|
||||
id: 'svc-002',
|
||||
name: '康复训练指导',
|
||||
category: '康复支持',
|
||||
price: 260,
|
||||
durationText: '约 3 小时',
|
||||
summary: '提供肢体训练、步态练习和居家康复建议。',
|
||||
tags: ['康复师', '步骤清晰', '可连续预约'],
|
||||
suitableFor: '卒中恢复、术后康复、失能半失能老人'
|
||||
},
|
||||
{
|
||||
id: 'svc-003',
|
||||
name: '慢病健康随访',
|
||||
category: '健康管理',
|
||||
price: 128,
|
||||
durationText: '约 90 分钟',
|
||||
summary: '完成血压血糖监测、用药核对与健康宣教。',
|
||||
tags: ['随访', '慢病', '可生成记录'],
|
||||
suitableFor: '高血压、糖尿病等长期管理老人'
|
||||
function plainObject(source: any): any {
|
||||
return JSON.parse(JSON.stringify(source)) as any
|
||||
}
|
||||
|
||||
function readString(source: any, key: string): string {
|
||||
const value = plainObject(source)[key]
|
||||
if (value == null) {
|
||||
return ''
|
||||
}
|
||||
]
|
||||
return typeof value == 'string' ? value : String(value)
|
||||
}
|
||||
|
||||
function readNumber(source: any, key: string): number {
|
||||
const value = plainObject(source)[key]
|
||||
if (typeof value == 'number') {
|
||||
return value
|
||||
}
|
||||
const parsed = Number(value)
|
||||
return isNaN(parsed) ? 0 : parsed
|
||||
}
|
||||
|
||||
function readStringArray(source: any, key: string): Array<string> {
|
||||
const value = plainObject(source)[key]
|
||||
if (Array.isArray(value)) {
|
||||
const result = [] as Array<string>
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
result.push(typeof value[i] == 'string' ? value[i] : String(value[i]))
|
||||
}
|
||||
return result
|
||||
}
|
||||
if (typeof value == 'string' && value != '') {
|
||||
try {
|
||||
const parsed = JSON.parse(value) as any
|
||||
if (Array.isArray(parsed)) {
|
||||
const result = [] as Array<string>
|
||||
for (let i = 0; i < parsed.length; i++) {
|
||||
result.push(typeof parsed[i] == 'string' ? parsed[i] : String(parsed[i]))
|
||||
}
|
||||
return result
|
||||
}
|
||||
} catch (error) {
|
||||
return [] as Array<string>
|
||||
}
|
||||
}
|
||||
return [] as Array<string>
|
||||
}
|
||||
|
||||
function parseCatalogItem(source: any): HomeServiceCatalogType {
|
||||
return {
|
||||
id: readString(source, 'id'),
|
||||
name: readString(source, 'name'),
|
||||
category: readString(source, 'category'),
|
||||
price: readNumber(source, 'price'),
|
||||
durationText: readString(source, 'duration_text'),
|
||||
summary: readString(source, 'summary'),
|
||||
tags: readStringArray(source, 'tags_json'),
|
||||
suitableFor: readString(source, 'suitable_for')
|
||||
}
|
||||
}
|
||||
|
||||
function createTimeline(title1: string, title2: string, title3: string): Array<HomeServiceTimelineItemType> {
|
||||
return [
|
||||
@@ -243,31 +284,6 @@ const ADMIN_PLANS: Array<HomeServicePlanType> = [
|
||||
}
|
||||
]
|
||||
|
||||
const CONSUMER_ACCEPTANCE: Array<HomeServiceAcceptanceType> = [
|
||||
{
|
||||
caseId: 'case-001',
|
||||
caseNo: 'HS202605130001',
|
||||
elderName: '李奶奶',
|
||||
serviceName: '基础上门护理',
|
||||
acceptanceStatus: 'waiting',
|
||||
acceptanceStatusText: '待验收',
|
||||
rating: 5,
|
||||
feedback: '护理员按时到达,服务过程清晰,老人状态稳定。',
|
||||
tags: ['准时上门', '沟通清楚', '动作规范']
|
||||
},
|
||||
{
|
||||
caseId: 'case-002',
|
||||
caseNo: 'HS202605130002',
|
||||
elderName: '张爷爷',
|
||||
serviceName: '慢病健康随访',
|
||||
acceptanceStatus: 'waiting',
|
||||
acceptanceStatusText: '待验收',
|
||||
rating: 5,
|
||||
feedback: '已查看记录,等待家属最终确认。',
|
||||
tags: ['记录完整', '态度耐心']
|
||||
}
|
||||
]
|
||||
|
||||
const ADMIN_RECTIFICATIONS: Array<HomeServiceRectificationType> = [
|
||||
{
|
||||
caseId: 'case-001',
|
||||
@@ -355,13 +371,6 @@ function clonePlan(item: HomeServicePlanType): HomeServicePlanType {
|
||||
}
|
||||
}
|
||||
|
||||
function cloneAcceptance(item: HomeServiceAcceptanceType): HomeServiceAcceptanceType {
|
||||
return {
|
||||
...item,
|
||||
tags: item.tags.slice(0)
|
||||
}
|
||||
}
|
||||
|
||||
function cloneRectification(item: HomeServiceRectificationType): HomeServiceRectificationType {
|
||||
return {
|
||||
...item
|
||||
@@ -375,68 +384,196 @@ function cloneSettlement(item: HomeServiceSettlementType): HomeServiceSettlement
|
||||
}
|
||||
|
||||
export async function fetchHomeServiceCatalog(): Promise<Array<HomeServiceCatalogType>> {
|
||||
await delay()
|
||||
return SERVICE_CATALOG.map((item) => ({ ...item, tags: item.tags.slice(0) }))
|
||||
const response = await supa
|
||||
.from('hss_service_catalog')
|
||||
.select('id, name, category, price, duration_text, summary, tags_json, suitable_for, sort_no')
|
||||
.eq('status', 1)
|
||||
.is('deleted_at', null)
|
||||
.order('sort_no', { ascending: true })
|
||||
.execute()
|
||||
if (response.error != null || response.data == null || !Array.isArray(response.data)) {
|
||||
return [] as Array<HomeServiceCatalogType>
|
||||
}
|
||||
const result = [] as Array<HomeServiceCatalogType>
|
||||
for (let i = 0; i < response.data.length; i++) {
|
||||
result.push(parseCatalogItem(response.data[i]))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export async function fetchConsumerHomeServiceCases(): Promise<Array<HomeServiceCaseType>> {
|
||||
await delay()
|
||||
return CASE_STORE.map((item) => cloneCase(item))
|
||||
const orders = await listConsumerServiceOrders()
|
||||
const result = [] as Array<HomeServiceCaseType>
|
||||
for (let i = 0; i < orders.length; i++) {
|
||||
result.push(mapOrderToCase(orders[i]))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export async function fetchConsumerHomeServiceCaseDetail(caseId: string): Promise<HomeServiceCaseType | null> {
|
||||
await delay()
|
||||
const target = CASE_STORE.find((item) => item.id == caseId)
|
||||
return target == null ? null : cloneCase(target)
|
||||
const detail = await getServiceOrderDetail(caseId)
|
||||
if (detail != null) {
|
||||
return mapOrderToCase(detail)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export async function createHomeServiceApplication(draft: HomeServiceApplicationDraftType): Promise<HomeServiceCaseType> {
|
||||
await delay()
|
||||
const matchedService = SERVICE_CATALOG.find((item) => item.id == draft.serviceId)
|
||||
const created: HomeServiceCaseType = {
|
||||
id: 'case-' + String(CASE_STORE.length + 1).padStart(3, '0'),
|
||||
caseNo: 'HS20260513' + String(CASE_STORE.length + 1).padStart(4, '0'),
|
||||
status: 'submitted',
|
||||
statusText: '已提交',
|
||||
statusTone: 'primary',
|
||||
serviceName: draft.serviceName,
|
||||
serviceTime: draft.preferredTime,
|
||||
applicantName: draft.applicantName,
|
||||
elderName: draft.elderName,
|
||||
age: draft.age,
|
||||
phone: draft.phone,
|
||||
address: draft.address,
|
||||
summary: draft.demandSummary,
|
||||
currentStep: 1,
|
||||
totalSteps: 8,
|
||||
staffName: '待分配',
|
||||
staffPhone: '待分配',
|
||||
amount: matchedService != null ? matchedService.price : 0,
|
||||
timeline: [
|
||||
{
|
||||
id: 'new-1',
|
||||
title: '已提交申请',
|
||||
time: '2026-05-13 10:00',
|
||||
description: '系统已接收申请,等待受理。'
|
||||
}
|
||||
]
|
||||
export async function createHomeServiceApplication(draft: HomeServiceApplicationDraftType): Promise<HomeServiceCaseType | null> {
|
||||
const catalog = await fetchHomeServiceCatalog()
|
||||
let matchedService: HomeServiceCatalogType | null = null
|
||||
for (let i = 0; i < catalog.length; i++) {
|
||||
if (catalog[i].id == draft.serviceId) {
|
||||
matchedService = catalog[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if (matchedService != null && draft.serviceAddressSnapshot != null) {
|
||||
const createdOrder = await createServiceOrder({
|
||||
service: matchedService,
|
||||
address: {
|
||||
addressId: draft.serviceAddressSnapshot.addressId,
|
||||
contactName: draft.serviceAddressSnapshot.contactName,
|
||||
contactPhone: draft.serviceAddressSnapshot.contactPhone,
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
detailAddress: draft.serviceAddressSnapshot.addressDetail,
|
||||
fullAddress: draft.serviceAddressSnapshot.fullAddress,
|
||||
latitude: draft.serviceAddressSnapshot.latitude,
|
||||
longitude: draft.serviceAddressSnapshot.longitude,
|
||||
coordinateType: draft.serviceAddressSnapshot.coordinateType,
|
||||
remark: draft.serviceAddressSnapshot.remark
|
||||
},
|
||||
recipientName: draft.elderName,
|
||||
recipientPhone: draft.phone,
|
||||
contactName: draft.applicantName,
|
||||
contactPhone: draft.phone,
|
||||
appointmentTime: draft.preferredTime,
|
||||
remark: draft.demandSummary
|
||||
})
|
||||
if (createdOrder != null) {
|
||||
return mapOrderToCase(createdOrder)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getCaseStep(status: ServiceOrderStatus): number {
|
||||
if (status == 'created') return 1
|
||||
if (status == 'assigned') return 3
|
||||
if (status == 'accepted') return 4
|
||||
if (status == 'departed') return 5
|
||||
if (status == 'arrived' || status == 'in_service') return 6
|
||||
if (status == 'pending_acceptance') return 7
|
||||
if (status == 'accepted_by_user' || status == 'reviewed' || status == 'settled') return 8
|
||||
if (status == 'rejected' || status == 'cancelled' || status == 'exception') return 7
|
||||
return 2
|
||||
}
|
||||
|
||||
function getCaseTone(status: ServiceOrderStatus): string {
|
||||
if (status == 'created' || status == 'assigned' || status == 'pending_acceptance') return 'warning'
|
||||
if (status == 'accepted' || status == 'departed' || status == 'arrived' || status == 'in_service') return 'primary'
|
||||
if (status == 'accepted_by_user' || status == 'reviewed' || status == 'settled') return 'success'
|
||||
if (status == 'exception' || status == 'cancelled' || status == 'rejected') return 'danger'
|
||||
return 'neutral'
|
||||
}
|
||||
|
||||
function getTimelineDescription(log: ServiceOrderTimelineItemType): string {
|
||||
if (log.remark != '') {
|
||||
return log.remark
|
||||
}
|
||||
if (log.toStatus == 'created') return '平台已接收预约申请,等待后续处理。'
|
||||
if (log.toStatus == 'assigned') return '系统已完成派单,正在通知服务人员。'
|
||||
if (log.toStatus == 'accepted') return '服务人员已接单,正在准备上门。'
|
||||
if (log.toStatus == 'departed') return '服务人员已出发,正在前往服务地点。'
|
||||
if (log.toStatus == 'arrived') return '服务人员已到达服务地点。'
|
||||
if (log.toStatus == 'in_service') return '服务已开始执行,请留意后续进度。'
|
||||
if (log.toStatus == 'completed') return '服务执行已完成,正在整理结果。'
|
||||
if (log.toStatus == 'pending_acceptance') return '服务记录已提交,等待家属验收。'
|
||||
if (log.toStatus == 'accepted_by_user') return '家属已确认本次服务结果。'
|
||||
if (log.toStatus == 'reviewed') return '家属已完成评价反馈。'
|
||||
if (log.toStatus == 'settled') return '本次服务已完成结算归档。'
|
||||
if (log.toStatus == 'cancelled') return '服务单已取消。'
|
||||
if (log.toStatus == 'rejected') return '服务人员未接受该工单。'
|
||||
return '服务过程状态已更新。'
|
||||
}
|
||||
|
||||
function formatServiceAppointmentText(value: string): string {
|
||||
if (value == '') {
|
||||
return ''
|
||||
}
|
||||
if (value.indexOf('上午') >= 0 || value.indexOf('下午') >= 0 || value.indexOf('晚上') >= 0) {
|
||||
return value
|
||||
}
|
||||
const parsed = Date.parse(value)
|
||||
if (!isNaN(parsed)) {
|
||||
const date = new Date(parsed)
|
||||
let year = date.getFullYear()
|
||||
const currentYear = new Date().getFullYear()
|
||||
if (year < currentYear - 1) {
|
||||
year = currentYear
|
||||
}
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
return year + '-' + month + '-' + day + ' ' + hour + ':' + minute
|
||||
}
|
||||
const monthDayMatch = value.match(/(\d{2})\/(\d{2})/)
|
||||
const timeMatch = value.match(/(\d{2}:\d{2}(\s*-\s*\d{2}:\d{2})?)/)
|
||||
if (monthDayMatch != null) {
|
||||
const month = monthDayMatch[1] ?? ''
|
||||
const day = monthDayMatch[2] ?? ''
|
||||
const timeText = timeMatch != null ? (timeMatch[1] ?? '') : ''
|
||||
if (month != '' && day != '') {
|
||||
return String(new Date().getFullYear()) + '-' + month + '-' + day + (timeText != '' ? ' ' + timeText.replace(/\s+/g, '') : '')
|
||||
}
|
||||
}
|
||||
return value.replace('T', ' ')
|
||||
}
|
||||
|
||||
function mapLogsToTimeline(logs: Array<ServiceOrderTimelineItemType>): Array<HomeServiceTimelineItemType> {
|
||||
const timeline = [] as Array<HomeServiceTimelineItemType>
|
||||
for (let i = 0; i < logs.length; i++) {
|
||||
timeline.push({
|
||||
id: logs[i].id,
|
||||
title: getServiceOrderStatusText(logs[i].toStatus),
|
||||
time: logs[i].createdAt,
|
||||
description: getTimelineDescription(logs[i])
|
||||
})
|
||||
}
|
||||
return timeline
|
||||
}
|
||||
|
||||
function mapOrderToCase(order: ServiceOrderType): HomeServiceCaseType {
|
||||
return {
|
||||
id: order.id,
|
||||
caseNo: order.orderNo,
|
||||
status: order.status,
|
||||
statusText: getServiceOrderStatusText(order.status),
|
||||
statusTone: getCaseTone(order.status),
|
||||
serviceName: order.serviceName,
|
||||
serviceTime: formatServiceAppointmentText(order.appointmentTime),
|
||||
applicantName: order.contactName,
|
||||
elderName: order.recipientName,
|
||||
age: 0,
|
||||
phone: order.contactPhone,
|
||||
address: order.addressSnapshot.fullAddress,
|
||||
summary: order.remark,
|
||||
currentStep: getCaseStep(order.status),
|
||||
totalSteps: 8,
|
||||
staffName: order.staffName == '' ? '待分配' : order.staffName,
|
||||
staffPhone: order.staffPhone == '' ? '待分配' : order.staffPhone,
|
||||
amount: order.serviceSnapshot.price,
|
||||
checkinTime: order.executionRecord != null ? order.executionRecord.checkinTime : '',
|
||||
checkinAddress: order.executionRecord != null ? order.executionRecord.checkinAddress : '',
|
||||
serviceStartedAt: order.executionRecord != null ? order.executionRecord.serviceStartedAt : order.serviceStartedAt,
|
||||
serviceFinishedAt: order.executionRecord != null ? order.executionRecord.serviceFinishedAt : order.completedAt,
|
||||
executionSummary: order.executionRecord != null ? (order.executionRecord.summary != '' ? order.executionRecord.summary : order.executionRecord.remark) : '',
|
||||
evidenceCount: order.evidenceFiles.length,
|
||||
serviceAddressSnapshot: null,
|
||||
timeline: mapLogsToTimeline(order.logs)
|
||||
}
|
||||
CASE_STORE.unshift(created)
|
||||
ADMIN_APPLICATIONS.unshift({
|
||||
id: 'admin-app-' + String(ADMIN_APPLICATIONS.length + 1).padStart(3, '0'),
|
||||
caseId: created.id,
|
||||
caseNo: created.caseNo,
|
||||
status: 'submitted',
|
||||
statusText: '已提交',
|
||||
statusTone: 'primary',
|
||||
elderName: created.elderName,
|
||||
serviceName: created.serviceName,
|
||||
preferredTime: created.serviceTime,
|
||||
assessmentResult: '待受理',
|
||||
dispatcherName: '待分配',
|
||||
staffName: '待分配'
|
||||
})
|
||||
return cloneCase(created)
|
||||
}
|
||||
|
||||
export async function fetchWorkerTasks(): Promise<Array<HomeServiceTaskType>> {
|
||||
@@ -738,9 +875,21 @@ export async function submitAdminServicePlan(
|
||||
}
|
||||
|
||||
export async function fetchConsumerAcceptanceDetail(caseId: string): Promise<HomeServiceAcceptanceType | null> {
|
||||
await delay()
|
||||
const target = CONSUMER_ACCEPTANCE.find((item) => item.caseId == caseId)
|
||||
return target == null ? null : cloneAcceptance(target)
|
||||
const detail = await getServiceOrderDetail(caseId)
|
||||
if (detail != null) {
|
||||
return {
|
||||
caseId: detail.id,
|
||||
caseNo: detail.orderNo,
|
||||
elderName: detail.recipientName,
|
||||
serviceName: detail.serviceName,
|
||||
acceptanceStatus: detail.status,
|
||||
acceptanceStatusText: getServiceOrderStatusText(detail.status),
|
||||
rating: detail.review != null ? detail.review.rating : 5,
|
||||
feedback: detail.review != null ? detail.review.content : '',
|
||||
tags: detail.review != null ? detail.review.tags : [] as Array<string>
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export async function submitConsumerAcceptance(
|
||||
@@ -750,40 +899,18 @@ export async function submitConsumerAcceptance(
|
||||
feedback: string,
|
||||
tags: Array<string>
|
||||
): Promise<HomeServiceAcceptanceType | null> {
|
||||
await delay()
|
||||
const target = CONSUMER_ACCEPTANCE.find((item) => item.caseId == caseId)
|
||||
if (target == null) {
|
||||
if (approved) {
|
||||
const result = await confirmServiceOrder(caseId, rating, feedback, tags)
|
||||
if (result != null) {
|
||||
return await fetchConsumerAcceptanceDetail(caseId)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
target.acceptanceStatus = approved ? 'approved' : 'rejected'
|
||||
target.acceptanceStatusText = approved ? '已验收' : '已退回'
|
||||
target.rating = rating
|
||||
target.feedback = feedback
|
||||
target.tags = tags.slice(0)
|
||||
|
||||
const relatedCase = CASE_STORE.find((item) => item.id == caseId)
|
||||
if (relatedCase != null) {
|
||||
relatedCase.status = approved ? 'completed' : 'revision_required'
|
||||
relatedCase.statusText = approved ? '已完成' : '待整改'
|
||||
relatedCase.statusTone = approved ? 'success' : 'warning'
|
||||
relatedCase.currentStep = approved ? 8 : 7
|
||||
relatedCase.timeline.unshift({
|
||||
id: 'case-accept-' + String(relatedCase.timeline.length + 1),
|
||||
title: approved ? '家属已验收' : '家属退回整改',
|
||||
time: '2026-05-13 17:10',
|
||||
description: feedback
|
||||
})
|
||||
const rejected = await rejectServiceOrderAcceptance(caseId, feedback)
|
||||
if (rejected != null) {
|
||||
return await fetchConsumerAcceptanceDetail(caseId)
|
||||
}
|
||||
|
||||
const relatedAdmin = ADMIN_APPLICATIONS.find((item) => item.caseId == caseId)
|
||||
if (relatedAdmin != null) {
|
||||
relatedAdmin.status = approved ? 'completed' : 'revision_required'
|
||||
relatedAdmin.statusText = approved ? '已完成' : '待整改'
|
||||
relatedAdmin.statusTone = approved ? 'success' : 'warning'
|
||||
}
|
||||
|
||||
return cloneAcceptance(target)
|
||||
return null
|
||||
}
|
||||
|
||||
export async function fetchAdminRectificationDetail(caseId: string): Promise<HomeServiceRectificationType | null> {
|
||||
|
||||
@@ -1,124 +1,135 @@
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { getCurrentUserId } from '@/utils/store.uts'
|
||||
import { getDeliveryProfileByUserId } from '@/api/delivery.uts'
|
||||
import { getServiceOrderStatusText, normalizeServiceOrderStatus, type ServiceOrderStatus } from '@/types/service-order.uts'
|
||||
import type {
|
||||
DeliveryCheckinPayloadType,
|
||||
DeliveryDashboardType,
|
||||
DeliveryLocationType,
|
||||
DeliveryOrderType,
|
||||
DeliveryServiceRecordType
|
||||
} from '@/types/delivery.uts'
|
||||
import { getCurrentUser } from '@/utils/store.uts'
|
||||
import type { UserAddress } from '@/utils/supabaseService.uts'
|
||||
import type { HomeServiceCatalogType } from '@/types/home-service.uts'
|
||||
import {
|
||||
getServiceOrderStatusText,
|
||||
normalizeServiceOrderStatus,
|
||||
type ServiceOrderAddressSnapshotType,
|
||||
type ServiceEvidenceFileType,
|
||||
type ServiceExecutionRecordType,
|
||||
type ServiceOrderStatus,
|
||||
type ServiceOrderTimelineItemType,
|
||||
type ServiceOrderType,
|
||||
type ServiceReviewType
|
||||
} from '@/types/service-order.uts'
|
||||
|
||||
function nowIso(): string {
|
||||
return new Date().toISOString()
|
||||
}
|
||||
export type CreateServiceOrderParams = {
|
||||
service: HomeServiceCatalogType
|
||||
address: ServiceOrderAddressSnapshotType
|
||||
recipientName: string
|
||||
recipientPhone: string
|
||||
contactName: string
|
||||
contactPhone: string
|
||||
appointmentTime: string
|
||||
remark: string
|
||||
}
|
||||
|
||||
function nowText(): string {
|
||||
return new Date().toISOString().replace('T', ' ').substring(0, 19)
|
||||
}
|
||||
|
||||
function buildId(prefix: string): string {
|
||||
return prefix + '-' + String(Date.now()) + '-' + String(Math.floor(Math.random() * 100000)).padStart(5, '0')
|
||||
}
|
||||
}
|
||||
|
||||
async function getCurrentStaffId(): Promise<string> {
|
||||
const userId = getCurrentUserId()
|
||||
if (userId == '') {
|
||||
return ''
|
||||
}
|
||||
const profile = await getDeliveryProfileByUserId(userId)
|
||||
return profile != null ? profile.id : ''
|
||||
}
|
||||
function buildOrderNo(): string {
|
||||
const date = new Date()
|
||||
const y = String(date.getFullYear())
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(date.getDate()).padStart(2, '0')
|
||||
const h = String(date.getHours()).padStart(2, '0')
|
||||
const i = String(date.getMinutes()).padStart(2, '0')
|
||||
const s = String(date.getSeconds()).padStart(2, '0')
|
||||
return 'HS' + y + m + d + h + i + s + String(Math.floor(Math.random() * 900) + 100)
|
||||
}
|
||||
|
||||
async function insertStatusLog(orderId: string, fromStatus: string, toStatus: ServiceOrderStatus, remark: string): Promise<void> {
|
||||
const userId = getCurrentUserId()
|
||||
await supa.from('hss_service_order_status_logs').insert({
|
||||
id: buildId('slog'),
|
||||
order_id: orderId,
|
||||
from_status: fromStatus,
|
||||
to_status: toStatus,
|
||||
operator_id: userId == '' ? null : userId,
|
||||
operator_role: 'delivery',
|
||||
remark,
|
||||
created_at: nowIso()
|
||||
}).execute()
|
||||
}
|
||||
function isUuidLike(value: string): boolean {
|
||||
return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/.test(value)
|
||||
}
|
||||
|
||||
function emptyOrder(): DeliveryOrderType {
|
||||
return {
|
||||
id: '',
|
||||
orderNo: '',
|
||||
serviceType: '',
|
||||
serviceName: '',
|
||||
serviceCategory: '',
|
||||
serviceItems: [] as Array<any>,
|
||||
elderId: '',
|
||||
elderName: '',
|
||||
elderNameMasked: '',
|
||||
elderGender: '',
|
||||
elderAge: 0,
|
||||
elderPhone: '',
|
||||
elderPhoneMasked: '',
|
||||
fullElderName: '',
|
||||
fullPhone: '',
|
||||
contactRelation: '家属',
|
||||
addressSummary: '',
|
||||
address: '',
|
||||
addressDetail: '',
|
||||
fullAddress: '',
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
appointmentTime: '',
|
||||
appointmentStartTime: '',
|
||||
appointmentEndTime: '',
|
||||
duration: 90,
|
||||
estimatedDuration: 90,
|
||||
price: 0,
|
||||
staffIncome: 0,
|
||||
distance: '',
|
||||
actualStartTime: '',
|
||||
actualEndTime: '',
|
||||
status: 'pending_assignment' as any,
|
||||
statusText: '',
|
||||
statusTone: 'warning',
|
||||
riskTags: [] as Array<string>,
|
||||
healthTags: [] as Array<string>,
|
||||
careLevel: '',
|
||||
needFamilyPresent: false,
|
||||
needMaterials: false,
|
||||
remark: '',
|
||||
merchantId: '',
|
||||
merchantName: '',
|
||||
deliveryStaffId: '',
|
||||
deliveryStaffName: '',
|
||||
acceptTime: '',
|
||||
departTime: '',
|
||||
arriveTime: '',
|
||||
checkinTime: '',
|
||||
finishTime: '',
|
||||
cancelReason: '',
|
||||
exceptionType: '',
|
||||
exceptionDesc: '',
|
||||
evidenceList: [] as Array<any>,
|
||||
signatureUrl: '',
|
||||
signatureName: '',
|
||||
satisfactionStatus: '',
|
||||
settlementStatus: '',
|
||||
archiveStatus: '',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
notices: [] as Array<string>,
|
||||
timeline: [] as Array<any>,
|
||||
statusLog: [] as Array<any>,
|
||||
serviceSummary: '',
|
||||
progressNote: '',
|
||||
distanceKm: '',
|
||||
allowCheckinRadiusMeters: 100,
|
||||
lastLocation: null,
|
||||
trackPoints: [] as Array<any>,
|
||||
serviceRecord: null,
|
||||
abnormalReport: null
|
||||
} as DeliveryOrderType
|
||||
function normalizeUuidOrNull(value: string): string | null {
|
||||
if (value == '') {
|
||||
return null
|
||||
}
|
||||
return isUuidLike(value) ? value : null
|
||||
}
|
||||
|
||||
function normalizeAppointmentTime(value: string): string | null {
|
||||
const text = value.trim()
|
||||
if (text == '') {
|
||||
return null
|
||||
}
|
||||
if (/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(\s*-\s*\d{2}:\d{2})?$/.test(text)) {
|
||||
return text
|
||||
}
|
||||
if (/^\d{4}-\d{2}-\d{2}(\s+(上午|下午|晚上))$/.test(text)) {
|
||||
return text
|
||||
}
|
||||
if (/^\d{4}-\d{2}-\d{2}T/.test(text)) {
|
||||
const parsed = Date.parse(text)
|
||||
if (!isNaN(parsed)) {
|
||||
const date = new Date(parsed)
|
||||
const year = String(date.getFullYear())
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
return year + '-' + month + '-' + day + ' ' + hour + ':' + minute
|
||||
}
|
||||
return text.replace('T', ' ')
|
||||
}
|
||||
const monthDayMatch = text.match(/(\d{2})\/(\d{2})/)
|
||||
if (monthDayMatch != null) {
|
||||
const month = monthDayMatch[1] ?? ''
|
||||
const day = monthDayMatch[2] ?? ''
|
||||
if (month == '' || day == '') {
|
||||
return text
|
||||
}
|
||||
const year = String(new Date().getFullYear())
|
||||
const rangeMatch = text.match(/(\d{2}:\d{2})(\s*-\s*\d{2}:\d{2})?/)
|
||||
if (rangeMatch != null) {
|
||||
const startTime = rangeMatch[1] ?? ''
|
||||
const endRange = rangeMatch[2] ?? ''
|
||||
if (startTime != '') {
|
||||
return year + '-' + month + '-' + day + ' ' + startTime + endRange.replace(/\s+/g, '')
|
||||
}
|
||||
}
|
||||
const tailText = text.substring(text.indexOf(month + '/' + day) + 5).trim()
|
||||
return year + '-' + month + '-' + day + (tailText != '' ? ' ' + tailText : '')
|
||||
}
|
||||
const explicitParsed = Date.parse(text)
|
||||
if (!isNaN(explicitParsed) && /^\d{4}\//.test(text)) {
|
||||
const date = new Date(explicitParsed)
|
||||
const year = String(date.getFullYear())
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
return year + '-' + month + '-' + day + ' ' + hour + ':' + minute
|
||||
}
|
||||
const year = String(new Date().getFullYear())
|
||||
return /^\d{2}-\d{2}\s+\d{2}:\d{2}/.test(text) ? year + '-' + text : text
|
||||
}
|
||||
|
||||
function safeParseObject(value: string): UTSJSONObject {
|
||||
if (value == '') {
|
||||
return JSON.parse('{}') as UTSJSONObject
|
||||
}
|
||||
return JSON.parse(value) as UTSJSONObject
|
||||
}
|
||||
|
||||
function safeParseArray(value: string): Array<string> {
|
||||
if (value == '') {
|
||||
return [] as Array<string>
|
||||
}
|
||||
const parsed = JSON.parse(value) as any
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed as Array<string>
|
||||
}
|
||||
return [] as Array<string>
|
||||
}
|
||||
|
||||
function safeJsonField(source: any, key: string): string {
|
||||
const plain = JSON.parse(JSON.stringify(source)) as any
|
||||
@@ -127,374 +138,387 @@ function safeJsonField(source: any, key: string): string {
|
||||
return ''
|
||||
}
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
}
|
||||
|
||||
function statusToDeliveryStatus(status: ServiceOrderStatus): string {
|
||||
if (status == 'assigned') return 'pending_assignment'
|
||||
if (status == 'accepted') return 'accepted'
|
||||
if (status == 'departed') return 'departed'
|
||||
if (status == 'arrived') return 'arrived'
|
||||
if (status == 'in_service') return 'in_service'
|
||||
if (status == 'pending_acceptance') return 'pending_acceptance'
|
||||
if (status == 'reviewed' || status == 'accepted_by_user' || status == 'settled') return 'completed'
|
||||
if (status == 'rejected') return 'rejected'
|
||||
if (status == 'cancelled') return 'cancelled'
|
||||
if (status == 'exception') return 'abnormal'
|
||||
return 'pending_assignment'
|
||||
}
|
||||
function plainObject(source: any): any {
|
||||
return JSON.parse(JSON.stringify(source)) as any
|
||||
}
|
||||
|
||||
function statusTone(status: ServiceOrderStatus): string {
|
||||
if (status == 'pending_acceptance' || status == 'assigned') return 'warning'
|
||||
if (status == 'accepted' || status == 'departed' || status == 'arrived' || status == 'in_service') return 'primary'
|
||||
if (status == 'accepted_by_user' || status == 'reviewed' || status == 'settled') return 'success'
|
||||
if (status == 'rejected' || status == 'cancelled' || status == 'exception') return 'danger'
|
||||
return 'warning'
|
||||
function readString(source: any, key: string): string {
|
||||
const value = plainObject(source)[key]
|
||||
if (value == null) {
|
||||
return ''
|
||||
}
|
||||
return typeof value == 'string' ? value : String(value)
|
||||
}
|
||||
|
||||
async function parseDeliveryOrder(orderId: string, item: any): Promise<DeliveryOrderType> {
|
||||
const order = emptyOrder()
|
||||
const obj = JSON.parse(JSON.stringify(item)) as UTSJSONObject
|
||||
const addressRaw = safeJsonField(item, 'address_snapshot_json')
|
||||
const serviceRaw = safeJsonField(item, 'service_snapshot_json')
|
||||
const addressObj = JSON.parse(addressRaw == '' ? '{}' : addressRaw) as UTSJSONObject
|
||||
const serviceObj = JSON.parse(serviceRaw == '' ? '{}' : serviceRaw) as UTSJSONObject
|
||||
const normalizedStatus = normalizeServiceOrderStatus(obj.getString('status') ?? '')
|
||||
order.id = obj.getString('id') ?? ''
|
||||
order.orderNo = obj.getString('order_no') ?? ''
|
||||
order.serviceType = serviceObj.getString('category') ?? '居家服务'
|
||||
order.serviceName = obj.getString('service_name') ?? ''
|
||||
order.serviceCategory = serviceObj.getString('category') ?? ''
|
||||
order.elderName = obj.getString('recipient_name') ?? ''
|
||||
order.elderNameMasked = order.elderName
|
||||
order.fullElderName = order.elderName
|
||||
order.elderPhone = obj.getString('recipient_phone') ?? ''
|
||||
order.elderPhoneMasked = order.elderPhone
|
||||
order.fullPhone = order.elderPhone
|
||||
order.contactName = obj.getString('contact_name') ?? ''
|
||||
order.contactPhone = obj.getString('contact_phone') ?? ''
|
||||
order.contactRelation = '家属'
|
||||
order.address = addressObj.getString('fullAddress') ?? ''
|
||||
order.addressDetail = addressObj.getString('detailAddress') ?? ''
|
||||
order.fullAddress = order.address
|
||||
order.latitude = addressObj.getNumber('latitude') ?? 0
|
||||
order.longitude = addressObj.getNumber('longitude') ?? 0
|
||||
order.appointmentTime = obj.getString('appointment_time') ?? ''
|
||||
order.appointmentStartTime = order.appointmentTime
|
||||
order.appointmentEndTime = order.appointmentTime
|
||||
order.duration = 90
|
||||
order.estimatedDuration = 90
|
||||
order.price = serviceObj.getNumber('price') ?? 0
|
||||
order.staffIncome = order.price
|
||||
order.status = statusToDeliveryStatus(normalizedStatus) as any
|
||||
order.statusText = getServiceOrderStatusText(normalizedStatus)
|
||||
order.statusTone = statusTone(normalizedStatus)
|
||||
order.remark = obj.getString('remark') ?? ''
|
||||
order.deliveryStaffId = obj.getString('current_staff_id') ?? ''
|
||||
order.acceptTime = obj.getString('accepted_at') ?? ''
|
||||
order.departTime = obj.getString('departed_at') ?? ''
|
||||
order.arriveTime = obj.getString('arrived_at') ?? ''
|
||||
order.actualStartTime = obj.getString('service_started_at') ?? ''
|
||||
order.startServiceTime = order.actualStartTime
|
||||
order.finishTime = obj.getString('completed_at') ?? ''
|
||||
order.createdAt = obj.getString('created_at') ?? ''
|
||||
order.updatedAt = obj.getString('updated_at') ?? ''
|
||||
order.allowCheckinRadiusMeters = 100
|
||||
const logsResponse = await supa.from('hss_service_order_status_logs').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
|
||||
if (logsResponse.data != null) {
|
||||
const rawLogs = logsResponse.data as any[]
|
||||
for (let i = 0; i < rawLogs.length; i++) {
|
||||
const logObj = JSON.parse(JSON.stringify(rawLogs[i])) as UTSJSONObject
|
||||
order.timeline.push({
|
||||
id: logObj.getString('id') ?? '',
|
||||
title: getServiceOrderStatusText(normalizeServiceOrderStatus(logObj.getString('to_status') ?? 'created')),
|
||||
time: logObj.getString('created_at') ?? '',
|
||||
description: logObj.getString('remark') ?? ''
|
||||
})
|
||||
}
|
||||
function readNumber(source: any, key: string): number {
|
||||
const value = plainObject(source)[key]
|
||||
if (typeof value == 'number') {
|
||||
return value
|
||||
}
|
||||
const recordResponse = await supa.from('hss_service_execution_records').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
|
||||
if (recordResponse.data != null) {
|
||||
const raw = recordResponse.data as any[]
|
||||
if (raw.length > 0) {
|
||||
const recordObj = JSON.parse(JSON.stringify(raw[0])) as UTSJSONObject
|
||||
order.checkinTime = recordObj.getString('checkin_time') ?? ''
|
||||
order.serviceRecord = {
|
||||
id: recordObj.getString('id') ?? '',
|
||||
orderId: orderId,
|
||||
startTime: recordObj.getString('service_started_at') ?? '',
|
||||
endTime: recordObj.getString('service_finished_at') ?? '',
|
||||
actualDurationMinutes: recordObj.getNumber('actual_duration_minutes') ?? 0,
|
||||
serviceItems: [] as Array<any>,
|
||||
serviceContent: [] as Array<string>,
|
||||
processNote: recordObj.getString('summary') ?? '',
|
||||
elderStatus: '',
|
||||
healthMetrics: { bloodPressure: '', heartRate: '', bloodSugar: '', bloodOxygen: '' },
|
||||
materialsUsed: '',
|
||||
abnormalNote: '',
|
||||
photos: [] as Array<string>,
|
||||
staffRemark: recordObj.getString('remark') ?? '',
|
||||
familyConfirmation: { method: 'none', code: '', signatureName: '', signatureUrl: '', confirmedAt: '' },
|
||||
createdAt: recordObj.getString('created_at') ?? '',
|
||||
updatedAt: recordObj.getString('updated_at') ?? ''
|
||||
} as any
|
||||
}
|
||||
}
|
||||
const evidenceResponse = await supa.from('hss_service_evidence_files').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
|
||||
if (evidenceResponse.data != null) {
|
||||
const rawEvidence = evidenceResponse.data as any[]
|
||||
for (let i = 0; i < rawEvidence.length; i++) {
|
||||
const evidenceObj = JSON.parse(JSON.stringify(rawEvidence[i])) as UTSJSONObject
|
||||
order.evidenceList.push({
|
||||
id: evidenceObj.getString('id') ?? '',
|
||||
orderId: orderId,
|
||||
phase: evidenceObj.getString('phase') ?? '',
|
||||
fileType: evidenceObj.getString('file_type') ?? 'image',
|
||||
name: evidenceObj.getString('storage_path') ?? '',
|
||||
url: evidenceObj.getString('file_url') ?? '',
|
||||
localPath: '',
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
retryable: false,
|
||||
createdAt: evidenceObj.getString('created_at') ?? ''
|
||||
})
|
||||
}
|
||||
}
|
||||
return order
|
||||
if (typeof value == 'string' && value != '') {
|
||||
const parsed = Number(value)
|
||||
return isNaN(parsed) ? 0 : parsed
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
export async function getDashboard(): Promise<DeliveryDashboardType> {
|
||||
const orders = await getOrdersByTab('all')
|
||||
let pending = 0
|
||||
let today = 0
|
||||
let serving = 0
|
||||
let completed = 0
|
||||
let nextOrder: DeliveryOrderType | null = null
|
||||
for (let i = 0; i < orders.length; i++) {
|
||||
const item = orders[i]
|
||||
if (item.status == 'pending_assignment') pending++
|
||||
if (item.status == 'pending_assignment' || item.status == 'accepted' || item.status == 'departed' || item.status == 'arrived' || item.status == 'in_service') today++
|
||||
if (item.status == 'in_service') serving++
|
||||
if (item.status == 'completed' || item.status == 'pending_acceptance') completed++
|
||||
if (nextOrder == null && item.status != 'completed' && item.status != 'cancelled' && item.status != 'abnormal') {
|
||||
nextOrder = item
|
||||
}
|
||||
}
|
||||
function parseTimeline(item: any): ServiceOrderTimelineItemType {
|
||||
return {
|
||||
pendingAssignmentCount: pending,
|
||||
pendingAcceptCount: pending,
|
||||
todayOrderCount: today,
|
||||
pendingDepartCount: 0,
|
||||
servingCount: serving,
|
||||
completedCount: completed,
|
||||
exceptionCount: 0,
|
||||
expectedIncome: 0,
|
||||
onlineStatus: 'online',
|
||||
nextOrder,
|
||||
recentOrders: orders.slice(0, 5)
|
||||
} as DeliveryDashboardType
|
||||
id: readString(item, 'id'),
|
||||
orderId: readString(item, 'order_id'),
|
||||
fromStatus: readString(item, 'from_status'),
|
||||
toStatus: normalizeServiceOrderStatus(readString(item, 'to_status')),
|
||||
operatorId: readString(item, 'operator_id'),
|
||||
operatorRole: readString(item, 'operator_role'),
|
||||
remark: readString(item, 'remark'),
|
||||
createdAt: readString(item, 'created_at')
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOrdersByTab(tab: string): Promise<Array<DeliveryOrderType>> {
|
||||
const staffId = await getCurrentStaffId()
|
||||
if (staffId == '') {
|
||||
return [] as Array<DeliveryOrderType>
|
||||
function parseReview(item: any): ServiceReviewType {
|
||||
return {
|
||||
id: readString(item, 'id'),
|
||||
orderId: readString(item, 'order_id'),
|
||||
userId: readString(item, 'user_id'),
|
||||
rating: readNumber(item, 'rating') == 0 ? 5 : readNumber(item, 'rating'),
|
||||
tags: safeParseArray(safeJsonField(item, 'tags_json')),
|
||||
content: readString(item, 'content'),
|
||||
createdAt: readString(item, 'created_at')
|
||||
}
|
||||
const response = await supa.from('hss_service_orders').select('*').eq('current_staff_id', staffId).order('created_at', { ascending: false }).execute()
|
||||
if (response.error != null || response.data == null) {
|
||||
return [] as Array<DeliveryOrderType>
|
||||
}
|
||||
const rawOrders = response.data as any[]
|
||||
const result = [] as Array<DeliveryOrderType>
|
||||
for (let i = 0; i < rawOrders.length; i++) {
|
||||
const orderObj = JSON.parse(JSON.stringify(rawOrders[i])) as UTSJSONObject
|
||||
const normalized = normalizeServiceOrderStatus(orderObj.getString('status') ?? '')
|
||||
let matched = true
|
||||
if (tab == 'pending') {
|
||||
matched = normalized == 'assigned'
|
||||
} else if (tab == 'today') {
|
||||
matched = normalized == 'assigned' || normalized == 'accepted' || normalized == 'departed' || normalized == 'arrived' || normalized == 'in_service' || normalized == 'pending_acceptance'
|
||||
} else if (tab == 'history') {
|
||||
matched = normalized == 'pending_acceptance' || normalized == 'accepted_by_user' || normalized == 'reviewed' || normalized == 'settled' || normalized == 'exception' || normalized == 'cancelled'
|
||||
|
||||
function parseExecutionRecord(item: any): ServiceExecutionRecordType {
|
||||
return {
|
||||
id: readString(item, 'id'),
|
||||
orderId: readString(item, 'order_id'),
|
||||
assignmentId: readString(item, 'assignment_id'),
|
||||
checkinTime: readString(item, 'checkin_time'),
|
||||
checkinLatitude: readNumber(item, 'checkin_latitude'),
|
||||
checkinLongitude: readNumber(item, 'checkin_longitude'),
|
||||
checkinAddress: readString(item, 'checkin_address'),
|
||||
serviceStartedAt: readString(item, 'service_started_at'),
|
||||
serviceFinishedAt: readString(item, 'service_finished_at'),
|
||||
actualDurationMinutes: readNumber(item, 'actual_duration_minutes'),
|
||||
serviceItemsJson: safeJsonField(item, 'service_items_json'),
|
||||
summary: readString(item, 'summary'),
|
||||
remark: readString(item, 'remark'),
|
||||
trackPointsJson: safeJsonField(item, 'track_points_json'),
|
||||
createdAt: readString(item, 'created_at'),
|
||||
updatedAt: readString(item, 'updated_at')
|
||||
}
|
||||
}
|
||||
|
||||
function parseEvidenceFile(item: any): ServiceEvidenceFileType {
|
||||
return {
|
||||
id: readString(item, 'id'),
|
||||
orderId: readString(item, 'order_id'),
|
||||
executionRecordId: readString(item, 'execution_record_id'),
|
||||
phase: readString(item, 'phase'),
|
||||
fileType: readString(item, 'file_type'),
|
||||
storagePath: readString(item, 'storage_path'),
|
||||
fileUrl: readString(item, 'file_url'),
|
||||
latitude: readNumber(item, 'latitude'),
|
||||
longitude: readNumber(item, 'longitude'),
|
||||
capturedAt: readString(item, 'captured_at'),
|
||||
createdAt: readString(item, 'created_at')
|
||||
}
|
||||
}
|
||||
|
||||
function parseServiceOrder(item: any, logs: Array<ServiceOrderTimelineItemType>, review: ServiceReviewType | null): ServiceOrderType {
|
||||
const addressSnapshot = safeJsonField(item, 'address_snapshot_json')
|
||||
const serviceSnapshot = safeJsonField(item, 'service_snapshot_json')
|
||||
const addressObj = plainObject(safeParseObject(addressSnapshot))
|
||||
const serviceObj = plainObject(safeParseObject(serviceSnapshot))
|
||||
return {
|
||||
id: readString(item, 'id'),
|
||||
orderNo: readString(item, 'order_no'),
|
||||
userId: readString(item, 'user_id'),
|
||||
serviceId: readString(item, 'service_id'),
|
||||
serviceName: readString(item, 'service_name'),
|
||||
serviceSnapshot: {
|
||||
serviceId: readString(serviceObj, 'serviceId') != '' ? readString(serviceObj, 'serviceId') : readString(item, 'service_id'),
|
||||
serviceName: readString(serviceObj, 'serviceName') != '' ? readString(serviceObj, 'serviceName') : readString(item, 'service_name'),
|
||||
category: readString(serviceObj, 'category'),
|
||||
price: readNumber(serviceObj, 'price'),
|
||||
durationText: readString(serviceObj, 'durationText'),
|
||||
summary: readString(serviceObj, 'summary'),
|
||||
tags: safeParseArray(safeJsonField(serviceObj, 'tags')),
|
||||
suitableFor: readString(serviceObj, 'suitableFor')
|
||||
},
|
||||
serviceAddressId: readString(item, 'service_address_id'),
|
||||
addressSnapshot: {
|
||||
addressId: readString(addressObj, 'addressId'),
|
||||
contactName: readString(addressObj, 'contactName'),
|
||||
contactPhone: readString(addressObj, 'contactPhone'),
|
||||
province: readString(addressObj, 'province'),
|
||||
city: readString(addressObj, 'city'),
|
||||
district: readString(addressObj, 'district'),
|
||||
detailAddress: readString(addressObj, 'detailAddress'),
|
||||
fullAddress: readString(addressObj, 'fullAddress'),
|
||||
latitude: readNumber(addressObj, 'latitude'),
|
||||
longitude: readNumber(addressObj, 'longitude'),
|
||||
coordinateType: readString(addressObj, 'coordinateType') == '' ? 'gcj02' : readString(addressObj, 'coordinateType'),
|
||||
remark: readString(addressObj, 'remark')
|
||||
},
|
||||
recipientName: readString(item, 'recipient_name'),
|
||||
recipientPhone: readString(item, 'recipient_phone'),
|
||||
contactName: readString(item, 'contact_name'),
|
||||
contactPhone: readString(item, 'contact_phone'),
|
||||
appointmentTime: readString(item, 'appointment_time'),
|
||||
remark: readString(item, 'remark'),
|
||||
status: normalizeServiceOrderStatus(readString(item, 'status')),
|
||||
currentAssignmentId: readString(item, 'current_assignment_id'),
|
||||
currentStaffId: readString(item, 'current_staff_id'),
|
||||
acceptedAt: readString(item, 'accepted_at'),
|
||||
departedAt: readString(item, 'departed_at'),
|
||||
arrivedAt: readString(item, 'arrived_at'),
|
||||
serviceStartedAt: readString(item, 'service_started_at'),
|
||||
completedAt: readString(item, 'completed_at'),
|
||||
pendingAcceptanceAt: readString(item, 'pending_acceptance_at'),
|
||||
acceptedByUserAt: readString(item, 'accepted_by_user_at'),
|
||||
reviewedAt: readString(item, 'reviewed_at'),
|
||||
createdAt: readString(item, 'created_at'),
|
||||
updatedAt: readString(item, 'updated_at'),
|
||||
staffName: '',
|
||||
staffPhone: '',
|
||||
logs,
|
||||
executionRecord: null,
|
||||
evidenceFiles: [] as Array<any>,
|
||||
review
|
||||
}
|
||||
}
|
||||
|
||||
async function insertStatusLog(orderId: string, fromStatus: string, toStatus: ServiceOrderStatus, operatorId: string, operatorRole: string, remark: string): Promise<void> {
|
||||
await supa.from('hss_service_order_status_logs').insert({
|
||||
id: buildId('slog'),
|
||||
order_id: orderId,
|
||||
from_status: fromStatus,
|
||||
to_status: toStatus,
|
||||
operator_id: operatorId == '' ? null : operatorId,
|
||||
operator_role: operatorRole,
|
||||
remark,
|
||||
created_at: new Date().toISOString()
|
||||
}).execute()
|
||||
}
|
||||
|
||||
export function buildAddressSnapshot(address: UserAddress, latitude: number, longitude: number): ServiceOrderAddressSnapshotType {
|
||||
return {
|
||||
addressId: address.id,
|
||||
contactName: address.recipient_name,
|
||||
contactPhone: address.phone,
|
||||
province: address.province,
|
||||
city: address.city,
|
||||
district: address.district,
|
||||
detailAddress: address.detail_address,
|
||||
fullAddress: address.province + address.city + address.district + ' ' + address.detail_address,
|
||||
latitude,
|
||||
longitude,
|
||||
coordinateType: 'gcj02',
|
||||
remark: address.label ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
export async function createServiceOrder(params: CreateServiceOrderParams): Promise<ServiceOrderType | null> {
|
||||
const userId = getCurrentUserId()
|
||||
if (userId == '') {
|
||||
return null
|
||||
}
|
||||
const orderId = buildId('so')
|
||||
const orderNo = buildOrderNo()
|
||||
const now = new Date().toISOString()
|
||||
const appointmentTime = normalizeAppointmentTime(params.appointmentTime)
|
||||
const response = await supa.from('hss_service_orders').insert({
|
||||
id: orderId,
|
||||
order_no: orderNo,
|
||||
user_id: userId,
|
||||
service_id: params.service.id,
|
||||
service_name: params.service.name,
|
||||
service_snapshot_json: params.service as any,
|
||||
service_address_id: normalizeUuidOrNull(params.address.addressId),
|
||||
address_snapshot_json: params.address as any,
|
||||
recipient_name: params.recipientName,
|
||||
recipient_phone: params.recipientPhone,
|
||||
contact_name: params.contactName,
|
||||
contact_phone: params.contactPhone,
|
||||
appointment_time: appointmentTime,
|
||||
remark: params.remark,
|
||||
status: 'created',
|
||||
created_at: now,
|
||||
updated_at: now
|
||||
}).execute()
|
||||
if (response.error != null) {
|
||||
console.error('createServiceOrder failed', response.error)
|
||||
return null
|
||||
}
|
||||
await insertStatusLog(orderId, '', 'created', userId, 'consumer', '创建服务订单')
|
||||
const staffResponse = await supa
|
||||
.from('ml_delivery_staff')
|
||||
.select('id, station_id, nickname, phone, status, deleted_at')
|
||||
.eq('status', 1)
|
||||
.order('created_at', { ascending: true })
|
||||
.execute()
|
||||
if (staffResponse.data != null) {
|
||||
const rawStaffList = staffResponse.data as any[]
|
||||
if (rawStaffList.length > 0) {
|
||||
const staffObj = plainObject(rawStaffList[0])
|
||||
const assignmentId = buildId('sa')
|
||||
await supa.from('hss_service_assignments').insert({
|
||||
id: assignmentId,
|
||||
order_id: orderId,
|
||||
staff_id: readString(staffObj, 'id'),
|
||||
station_id: readString(staffObj, 'station_id') == '' ? null : readString(staffObj, 'station_id'),
|
||||
status: 'assigned',
|
||||
assigned_at: now,
|
||||
created_at: now,
|
||||
updated_at: now
|
||||
}).execute()
|
||||
await supa.from('hss_service_orders').update({
|
||||
status: 'assigned',
|
||||
current_assignment_id: assignmentId,
|
||||
current_staff_id: readString(staffObj, 'id'),
|
||||
updated_at: now
|
||||
}).eq('id', orderId).execute()
|
||||
await insertStatusLog(orderId, 'created', 'assigned', userId, 'system', '系统已自动派单')
|
||||
}
|
||||
if (tab == 'all' || matched) {
|
||||
result.push(await parseDeliveryOrder(orderObj.getString('id') ?? '', rawOrders[i]))
|
||||
}
|
||||
return await getServiceOrderDetail(orderId)
|
||||
}
|
||||
|
||||
export async function listConsumerServiceOrders(): Promise<Array<ServiceOrderType>> {
|
||||
const userId = getCurrentUserId()
|
||||
if (userId == '') {
|
||||
return [] as Array<ServiceOrderType>
|
||||
}
|
||||
const response = await supa.from('hss_service_orders').select('*').eq('user_id', userId).order('created_at', { ascending: false }).execute()
|
||||
if (response.error != null || response.data == null) {
|
||||
return [] as Array<ServiceOrderType>
|
||||
}
|
||||
const list = response.data as any[]
|
||||
const result = [] as Array<ServiceOrderType>
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const parsed = await getServiceOrderDetail(JSON.parse(JSON.stringify(list[i]))['id'] as string)
|
||||
if (parsed != null) {
|
||||
result.push(parsed)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export async function getOrderDetail(orderId: string): Promise<DeliveryOrderType | null> {
|
||||
const response = await supa.from('hss_service_orders').select('*').eq('id', orderId).single().execute()
|
||||
if (response.error != null || response.data == null) {
|
||||
export async function getServiceOrderDetail(orderId: string): Promise<ServiceOrderType | null> {
|
||||
const orderResponse = await supa.from('hss_service_orders').select('*').eq('id', orderId).limit(1).execute()
|
||||
if (orderResponse.error != null || orderResponse.data == null) {
|
||||
return null
|
||||
}
|
||||
return await parseDeliveryOrder(orderId, response.data)
|
||||
const rawOrders = orderResponse.data as any[]
|
||||
if (rawOrders.length == 0) {
|
||||
return null
|
||||
}
|
||||
const logsResponse = await supa.from('hss_service_order_status_logs').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
|
||||
const reviewResponse = await supa.from('hss_service_reviews').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
|
||||
const executionResponse = await supa.from('hss_service_execution_records').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).limit(1).execute()
|
||||
const evidenceResponse = await supa.from('hss_service_evidence_files').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
|
||||
const logs = [] as Array<ServiceOrderTimelineItemType>
|
||||
if (logsResponse.data != null) {
|
||||
const rawLogs = logsResponse.data as any[]
|
||||
for (let i = 0; i < rawLogs.length; i++) {
|
||||
logs.push(parseTimeline(rawLogs[i]))
|
||||
}
|
||||
}
|
||||
let review: ServiceReviewType | null = null
|
||||
if (reviewResponse.data != null) {
|
||||
const rawReviews = reviewResponse.data as any[]
|
||||
if (rawReviews.length > 0) {
|
||||
review = parseReview(rawReviews[0])
|
||||
}
|
||||
}
|
||||
const parsed = parseServiceOrder(rawOrders[0], logs, review)
|
||||
if (executionResponse.data != null) {
|
||||
const rawExecutionList = executionResponse.data as any[]
|
||||
if (rawExecutionList.length > 0) {
|
||||
parsed.executionRecord = parseExecutionRecord(rawExecutionList[0])
|
||||
}
|
||||
}
|
||||
if (evidenceResponse.data != null) {
|
||||
const rawEvidenceList = evidenceResponse.data as any[]
|
||||
const evidenceFiles = [] as Array<ServiceEvidenceFileType>
|
||||
for (let i = 0; i < rawEvidenceList.length; i++) {
|
||||
evidenceFiles.push(parseEvidenceFile(rawEvidenceList[i]))
|
||||
}
|
||||
parsed.evidenceFiles = evidenceFiles
|
||||
}
|
||||
if (parsed.currentStaffId != '') {
|
||||
const staffResponse = await supa.from('ml_delivery_staff').select('nickname, phone').eq('id', parsed.currentStaffId).limit(1).execute()
|
||||
if (staffResponse.data != null) {
|
||||
const rawStaffList = staffResponse.data as any[]
|
||||
if (rawStaffList.length > 0) {
|
||||
const staffObj = plainObject(rawStaffList[0])
|
||||
parsed.staffName = readString(staffObj, 'nickname')
|
||||
parsed.staffPhone = readString(staffObj, 'phone')
|
||||
}
|
||||
}
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
async function updateOrderStatus(orderId: string, nextStatus: ServiceOrderStatus, updateData: UTSJSONObject, remark: string): Promise<DeliveryOrderType | null> {
|
||||
const current = await getOrderDetail(orderId)
|
||||
export async function confirmServiceOrder(orderId: string, rating: number, content: string, tags: Array<string>): Promise<ServiceOrderType | null> {
|
||||
const userId = getCurrentUserId()
|
||||
if (userId == '') {
|
||||
return null
|
||||
}
|
||||
const current = await getServiceOrderDetail(orderId)
|
||||
if (current == null) {
|
||||
return null
|
||||
}
|
||||
const map = JSON.parse('{}') as UTSJSONObject
|
||||
map.set('status', nextStatus)
|
||||
map.set('updated_at', nowIso())
|
||||
const iterator = updateData.keys()
|
||||
while (iterator.hasNext()) {
|
||||
const key = iterator.next()
|
||||
map.set(key, updateData.get(key))
|
||||
}
|
||||
const response = await supa.from('hss_service_orders').update(map).eq('id', orderId).execute()
|
||||
if (response.error != null) {
|
||||
console.error('updateOrderStatus failed', response.error)
|
||||
const acceptedAt = new Date().toISOString()
|
||||
const updateResponse = await supa.from('hss_service_orders').update({
|
||||
status: 'accepted_by_user',
|
||||
accepted_by_user_at: acceptedAt,
|
||||
updated_at: acceptedAt
|
||||
}).eq('id', orderId).execute()
|
||||
if (updateResponse.error != null) {
|
||||
return null
|
||||
}
|
||||
await insertStatusLog(orderId, normalizeServiceOrderStatus(current.status as string), nextStatus, remark)
|
||||
return await getOrderDetail(orderId)
|
||||
}
|
||||
|
||||
export async function acceptOrder(orderId: string): Promise<DeliveryOrderType | null> {
|
||||
const assignmentResponse = await supa.from('hss_service_assignments').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
|
||||
if (assignmentResponse.data != null) {
|
||||
const raw = assignmentResponse.data as any[]
|
||||
if (raw.length > 0) {
|
||||
const assignmentId = JSON.parse(JSON.stringify(raw[0]))['id'] as string
|
||||
await supa.from('hss_service_assignments').update({ status: 'accepted', accepted_at: nowIso(), updated_at: nowIso() }).eq('id', assignmentId).execute()
|
||||
}
|
||||
}
|
||||
const updateData = JSON.parse('{}') as UTSJSONObject
|
||||
updateData.set('accepted_at', nowIso())
|
||||
return await updateOrderStatus(orderId, 'accepted', updateData, '服务人员接单')
|
||||
}
|
||||
|
||||
export async function departOrder(orderId: string, location: DeliveryLocationType | null): Promise<DeliveryOrderType | null> {
|
||||
const updateData = JSON.parse('{}') as UTSJSONObject
|
||||
updateData.set('departed_at', nowIso())
|
||||
return await updateOrderStatus(orderId, 'departed', updateData, location == null ? '服务人员出发' : '服务人员出发:' + location.address)
|
||||
}
|
||||
|
||||
export async function arriveOrder(orderId: string, location: DeliveryLocationType | null): Promise<DeliveryOrderType | null> {
|
||||
const updateData = JSON.parse('{}') as UTSJSONObject
|
||||
updateData.set('arrived_at', nowIso())
|
||||
return await updateOrderStatus(orderId, 'arrived', updateData, location == null ? '服务人员到达' : '服务人员到达:' + location.address)
|
||||
}
|
||||
|
||||
export async function checkinOrder(orderId: string, payload: DeliveryCheckinPayloadType): Promise<DeliveryOrderType | null> {
|
||||
const current = await getOrderDetail(orderId)
|
||||
if (current == null) {
|
||||
return null
|
||||
}
|
||||
let assignmentId = ''
|
||||
const assignmentResponse = await supa.from('hss_service_assignments').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
|
||||
if (assignmentResponse.data != null) {
|
||||
const rawAssignments = assignmentResponse.data as any[]
|
||||
if (rawAssignments.length > 0) {
|
||||
assignmentId = JSON.parse(JSON.stringify(rawAssignments[0]))['id'] as string
|
||||
}
|
||||
}
|
||||
let recordId = buildId('ser')
|
||||
const recordResponse = await supa.from('hss_service_execution_records').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
|
||||
if (recordResponse.data != null) {
|
||||
const records = recordResponse.data as any[]
|
||||
if (records.length > 0) {
|
||||
recordId = JSON.parse(JSON.stringify(records[0]))['id'] as string
|
||||
await supa.from('hss_service_execution_records').update({
|
||||
checkin_time: nowIso(),
|
||||
checkin_latitude: payload.location.latitude,
|
||||
checkin_longitude: payload.location.longitude,
|
||||
checkin_address: payload.location.address,
|
||||
remark: payload.note,
|
||||
updated_at: nowIso()
|
||||
}).eq('id', recordId).execute()
|
||||
} else {
|
||||
await supa.from('hss_service_execution_records').insert({
|
||||
id: recordId,
|
||||
order_id: orderId,
|
||||
assignment_id: assignmentId,
|
||||
checkin_time: nowIso(),
|
||||
checkin_latitude: payload.location.latitude,
|
||||
checkin_longitude: payload.location.longitude,
|
||||
checkin_address: payload.location.address,
|
||||
remark: payload.note,
|
||||
created_at: nowIso(),
|
||||
updated_at: nowIso()
|
||||
}).execute()
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < payload.photos.length; i++) {
|
||||
await supa.from('hss_service_evidence_files').insert({
|
||||
id: buildId('sef'),
|
||||
order_id: orderId,
|
||||
execution_record_id: recordId,
|
||||
phase: 'checkin',
|
||||
file_type: 'image',
|
||||
storage_path: payload.photos[i],
|
||||
file_url: payload.photos[i],
|
||||
latitude: payload.location.latitude,
|
||||
longitude: payload.location.longitude,
|
||||
captured_at: nowIso(),
|
||||
created_at: nowIso()
|
||||
}).execute()
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
export async function startService(orderId: string): Promise<DeliveryOrderType | null> {
|
||||
let recordId = ''
|
||||
const recordResponse = await supa.from('hss_service_execution_records').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
|
||||
if (recordResponse.data != null) {
|
||||
const records = recordResponse.data as any[]
|
||||
if (records.length > 0) {
|
||||
recordId = JSON.parse(JSON.stringify(records[0]))['id'] as string
|
||||
await supa.from('hss_service_execution_records').update({ service_started_at: nowIso(), updated_at: nowIso() }).eq('id', recordId).execute()
|
||||
}
|
||||
}
|
||||
const updateData = JSON.parse('{}') as UTSJSONObject
|
||||
updateData.set('service_started_at', nowIso())
|
||||
return await updateOrderStatus(orderId, 'in_service', updateData, '开始服务')
|
||||
}
|
||||
|
||||
export async function saveServiceRecord(orderId: string, record: DeliveryServiceRecordType): Promise<DeliveryOrderType | null> {
|
||||
let assignmentId = ''
|
||||
const assignmentResponse = await supa.from('hss_service_assignments').select('*').eq('order_id', orderId).order('created_at', { ascending: false }).execute()
|
||||
if (assignmentResponse.data != null) {
|
||||
const assignments = assignmentResponse.data as any[]
|
||||
if (assignments.length > 0) {
|
||||
assignmentId = JSON.parse(JSON.stringify(assignments[0]))['id'] as string
|
||||
}
|
||||
}
|
||||
await supa.from('hss_service_execution_records').upsert({
|
||||
id: record.id,
|
||||
await insertStatusLog(orderId, current.status, 'accepted_by_user', userId, 'consumer', '用户确认验收')
|
||||
await supa.from('hss_service_reviews').insert({
|
||||
id: buildId('srv'),
|
||||
order_id: orderId,
|
||||
assignment_id: assignmentId,
|
||||
service_started_at: record.startTime,
|
||||
service_finished_at: record.endTime,
|
||||
actual_duration_minutes: record.actualDurationMinutes,
|
||||
service_items_json: record.serviceItems as any,
|
||||
summary: record.processNote,
|
||||
remark: record.staffRemark,
|
||||
updated_at: nowIso(),
|
||||
created_at: record.createdAt
|
||||
user_id: userId,
|
||||
rating,
|
||||
tags_json: tags as any,
|
||||
content,
|
||||
created_at: acceptedAt
|
||||
}).execute()
|
||||
for (let i = 0; i < record.photos.length; i++) {
|
||||
await supa.from('hss_service_evidence_files').insert({
|
||||
id: buildId('sef'),
|
||||
order_id: orderId,
|
||||
execution_record_id: record.id,
|
||||
phase: 'service',
|
||||
file_type: 'image',
|
||||
storage_path: record.photos[i],
|
||||
file_url: record.photos[i],
|
||||
captured_at: nowIso(),
|
||||
created_at: nowIso()
|
||||
}).execute()
|
||||
}
|
||||
return await getOrderDetail(orderId)
|
||||
await supa.from('hss_service_orders').update({
|
||||
status: 'reviewed',
|
||||
reviewed_at: acceptedAt,
|
||||
updated_at: acceptedAt
|
||||
}).eq('id', orderId).execute()
|
||||
await insertStatusLog(orderId, 'accepted_by_user', 'reviewed', userId, 'consumer', '用户提交评价')
|
||||
return await getServiceOrderDetail(orderId)
|
||||
}
|
||||
|
||||
export async function finishOrder(orderId: string): Promise<DeliveryOrderType | null> {
|
||||
const updateData = JSON.parse('{}') as UTSJSONObject
|
||||
updateData.set('completed_at', nowIso())
|
||||
updateData.set('pending_acceptance_at', nowIso())
|
||||
return await updateOrderStatus(orderId, 'pending_acceptance', updateData, '服务完成,等待用户验收')
|
||||
export async function rejectServiceOrderAcceptance(orderId: string, content: string): Promise<ServiceOrderType | null> {
|
||||
const userId = getCurrentUserId()
|
||||
if (userId == '') {
|
||||
return null
|
||||
}
|
||||
const current = await getServiceOrderDetail(orderId)
|
||||
if (current == null) {
|
||||
return null
|
||||
}
|
||||
const updateResponse = await supa.from('hss_service_orders').update({
|
||||
status: 'exception',
|
||||
updated_at: nowIso()
|
||||
}).eq('id', orderId).execute()
|
||||
if (updateResponse.error != null) {
|
||||
return null
|
||||
}
|
||||
await insertStatusLog(orderId, current.status, 'exception', userId, 'consumer', content == '' ? '用户退回整改' : content)
|
||||
return await getServiceOrderDetail(orderId)
|
||||
}
|
||||
|
||||
export async function getCurrentConsumerUser() {
|
||||
return await getCurrentUser()
|
||||
}
|
||||
Reference in New Issue
Block a user