完善下单逻辑及其ui展示,修复支付倒计时显示错误bug

This commit is contained in:
2026-05-25 15:35:41 +08:00
parent d25f80ccdd
commit cecb51a8e2
40 changed files with 13040 additions and 3217 deletions

View File

@@ -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()
}