Files
medical-mall/services/serviceOrderService.uts

1368 lines
52 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import supa from '@/components/supadb/aksupainstance.uts'
import { getCurrentUserId } from '@/utils/store.uts'
import { getCurrentUser } from '@/utils/store.uts'
import type { UserAddress } from '@/utils/supabaseService.uts'
import type { HomeServiceCatalogType, HomeServicePackageType } from '@/types/home-service.uts'
import type { DeliveryServiceRecordType } from '@/types/delivery.uts'
import {
getServiceOrderStatusText,
normalizeServiceOrderStatus,
type ServiceOrderAddressSnapshotType,
type ServiceEvidenceFileType,
type ServiceExecutionRecordType,
type ServiceOrderStatus,
type ServiceOrderTimelineItemType,
type ServiceOrderType,
type ServiceReviewType
} from '@/types/service-order.uts'
export type HomecareDispatchResult = {
success: boolean
code: string
message: string
display_type: string
retryable: boolean
dispatch_status?: string
order_id?: string
assignment_id?: string
staff_id?: string
station_id?: string
dispatch_distance_km?: number
}
export const HOMECARE_DISPATCH_STATUS_PENDING = 'pending'
export const HOMECARE_DISPATCH_STATUS_DISPATCHING = 'dispatching'
export const HOMECARE_DISPATCH_STATUS_ASSIGNED = 'assigned'
export const HOMECARE_DISPATCH_STATUS_FAILED = 'failed'
export type CreateServiceOrderParams = {
service: HomeServiceCatalogType
packageInfo: HomeServicePackageType
address: ServiceOrderAddressSnapshotType
recipientName: string
recipientPhone: string
recipientAge: number
recipientGender: string
contactName: string
contactPhone: string
appointmentTime: string
remark: string
}
function buildServiceSnapshot(params: CreateServiceOrderParams): any {
return {
serviceId: params.service.id,
serviceName: params.service.name,
category: params.service.category,
price: params.packageInfo.price,
durationText: params.packageInfo.durationText != '' ? params.packageInfo.durationText : params.service.durationText,
summary: params.packageInfo.packageDesc != '' ? params.packageInfo.packageDesc : params.service.summary,
tags: params.service.tags,
suitableFor: params.service.suitableFor,
packageId: params.packageInfo.id,
packageName: params.packageInfo.packageName,
packagePrice: params.packageInfo.price,
packageListPrice: params.packageInfo.listPrice,
packageDataSource: params.packageInfo.dataSource,
packageSeedBatchNo: params.packageInfo.seedBatchNo
} as any
}
function buildPricingSnapshot(params: CreateServiceOrderParams): any {
return {
service_id: params.service.id,
service_name: params.service.name,
package_id: params.packageInfo.id,
package_name: params.packageInfo.packageName,
package_desc: params.packageInfo.packageDesc,
duration_minutes: params.packageInfo.durationMinutes,
duration_text: params.packageInfo.durationText,
price: params.packageInfo.price,
list_price: params.packageInfo.listPrice,
data_source: params.packageInfo.dataSource,
seed_batch_no: params.packageInfo.seedBatchNo,
remark: params.packageInfo.remark
} as any
}
const HOMECARE_DISPATCH_CANDIDATE_RPC = 'rpc_homecare_dispatch_candidate'
function nowText(): string {
return new Date().toISOString().replace('T', ' ').substring(0, 19)
}
function nowIso(): string {
return new Date().toISOString()
}
function buildId(prefix: string): string {
return prefix + '-' + String(Date.now()) + '-' + String(Math.floor(Math.random() * 100000)).padStart(5, '0')
}
function buildHex(length: number): string {
const chars = '0123456789abcdef'
let result = ''
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
return result
}
function buildUuidLike(): string {
const segment1 = buildHex(8)
const segment2 = buildHex(4)
const segment3 = '4' + buildHex(3)
const variants = '89ab'
const segment4 = variants.charAt(Math.floor(Math.random() * variants.length)) + buildHex(3)
const segment5 = buildHex(12)
return segment1 + '-' + segment2 + '-' + segment3 + '-' + segment4 + '-' + segment5
}
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)
}
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 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
}
const fullRangeMatch = text.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})(\s*-\s*\d{2}:\d{2})?$/)
if (fullRangeMatch != null) {
const datePart = fullRangeMatch[1] ?? ''
const startTime = fullRangeMatch[2] ?? ''
const parsed = Date.parse(datePart + 'T' + startTime + ':00')
if (!isNaN(parsed)) {
return new Date(parsed).toISOString()
}
return datePart + ' ' + startTime
}
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] ?? ''
if (startTime != '') {
const parsed = Date.parse(year + '-' + month + '-' + day + 'T' + startTime + ':00')
if (!isNaN(parsed)) {
return new Date(parsed).toISOString()
}
return year + '-' + month + '-' + day + ' ' + startTime
}
}
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
const value = plain[key]
if (value == null) {
return ''
}
return JSON.stringify(value)
}
function safeFirstJsonField(source: any, keys: Array<string>): string {
const plain = JSON.parse(JSON.stringify(source)) as any
for (let i = 0; i < keys.length; i++) {
const value = plain[keys[i]]
if (value != null) {
return JSON.stringify(value)
}
}
return ''
}
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
}
if (typeof value == 'string' && value != '') {
const parsed = Number(value)
return isNaN(parsed) ? 0 : parsed
}
return 0
}
function hasMissingColumnError(error: any, columnName: string): boolean {
if (error == null || columnName == '') {
return false
}
const errorText = JSON.stringify(error).toLowerCase()
return errorText.indexOf('could not find the') >= 0 && errorText.indexOf(columnName.toLowerCase()) >= 0
}
let ecServiceRequestCreateUnavailable = false
function shouldBypassEcServiceRequestCreate(error: any): boolean {
if (error == null) {
return false
}
return hasMissingColumnError(error, 'address_snapshot')
|| hasMissingColumnError(error, 'address_snapshot_json')
|| hasMissingColumnError(error, 'contact_name')
|| hasMissingColumnError(error, 'contact_phone')
}
export function shouldUseCareTaskPath(orderId: string): boolean {
return isUuidLike(orderId)
}
function isStaffActive(staff: any): boolean {
const plain = plainObject(staff)
if (plain['deleted_at'] != null && String(plain['deleted_at']) != '') {
return false
}
if (plain['is_active'] === false) {
return false
}
return readNumber(staff, 'status') == 1
}
function getStaffPriority(staff: any): number {
let score = 0
const onlineStatus = readString(staff, 'online_status')
if (onlineStatus == 'online') {
score += 30
} else if (onlineStatus == 'resting' || onlineStatus == '') {
score += 20
} else if (onlineStatus == 'busy') {
score += 10
}
if (readString(staff, 'uid') != '') {
score += 5
}
if (readString(staff, 'station_id') != '') {
score += 1
}
return score
}
async function getAutoAssignableStaff(): Promise<any | null> {
try {
const rpcResponse: any = await supa.rpc(HOMECARE_DISPATCH_CANDIDATE_RPC, {} as any)
if (rpcResponse == null || rpcResponse.error != null || rpcResponse.data == null) {
return null
}
return rpcResponse.data
} catch (error) {
console.warn('getAutoAssignableStaff rpc failed', error)
return null
}
}
function buildEcServiceRequestPayload(params: CreateServiceOrderParams, userId: string, requestId: string, createdAt: string, appointmentTime: string | null, useAddressSnapshot: boolean): any {
const payload = {
id: requestId,
user_id: userId,
service_catalog_id: params.service.id,
service_name: params.service.name,
service_category: params.service.category,
elder_name: params.recipientName,
elder_phone: params.recipientPhone,
elder_age: params.recipientAge,
elder_gender: params.recipientGender,
contact_name: params.contactName,
contact_phone: params.contactPhone,
scheduled_at: appointmentTime,
remark: params.remark,
status: 'ORDER_CREATED',
created_at: createdAt,
updated_at: createdAt
} as any
if (useAddressSnapshot) {
payload.address_snapshot = params.address as any
} else {
payload.address_snapshot_json = params.address as any
}
return payload
}
function buildEcServiceRequestPayloadWithoutAddress(params: CreateServiceOrderParams, userId: string, requestId: string, createdAt: string, appointmentTime: string | null): any {
return {
id: requestId,
user_id: userId,
service_catalog_id: params.service.id,
service_name: params.service.name,
service_category: params.service.category,
elder_name: params.recipientName,
elder_phone: params.recipientPhone,
elder_age: params.recipientAge,
elder_gender: params.recipientGender,
contact_name: params.contactName,
contact_phone: params.contactPhone,
scheduled_at: appointmentTime,
remark: params.remark,
status: 'ORDER_CREATED',
created_at: createdAt,
updated_at: createdAt
} as any
}
function buildEcCareTaskPayload(params: CreateServiceOrderParams, userId: string, requestId: string, taskId: string, taskNo: string, createdAt: string, appointmentTime: string | null, useAddressSnapshot: boolean): any {
const payload = {
id: taskId,
task_no: taskNo,
request_id: requestId,
user_id: userId,
service_catalog_id: params.service.id,
service_name: params.service.name,
service_category: params.service.category,
service_snapshot_json: buildServiceSnapshot(params),
elder_name: params.recipientName,
elder_phone: params.recipientPhone,
elder_age: params.recipientAge,
elder_gender: params.recipientGender,
contact_name: params.contactName,
contact_phone: params.contactPhone,
scheduled_at: appointmentTime,
remark: params.remark,
status: 'ORDER_CREATED',
created_at: createdAt,
updated_at: createdAt
} as any
if (useAddressSnapshot) {
payload.address_snapshot = params.address as any
} else {
payload.address_snapshot_json = params.address as any
}
return payload
}
function buildEcCareTaskPayloadWithoutAddress(params: CreateServiceOrderParams, userId: string, requestId: string, taskId: string, taskNo: string, createdAt: string, appointmentTime: string | null): any {
return {
id: taskId,
task_no: taskNo,
request_id: requestId,
user_id: userId,
service_catalog_id: params.service.id,
service_name: params.service.name,
service_category: params.service.category,
service_snapshot_json: buildServiceSnapshot(params),
elder_name: params.recipientName,
elder_phone: params.recipientPhone,
elder_age: params.recipientAge,
elder_gender: params.recipientGender,
contact_name: params.contactName,
contact_phone: params.contactPhone,
scheduled_at: appointmentTime,
remark: params.remark,
status: 'ORDER_CREATED',
created_at: createdAt,
updated_at: createdAt
} as any
}
function parseTimeline(item: any): ServiceOrderTimelineItemType {
return {
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')
}
}
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')
}
}
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 = safeFirstJsonField(item, ['address_snapshot_json', 'address_snapshot'])
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'),
recipientAge: readFirstNumber(item, ['recipient_age', 'elder_age']),
recipientGender: readFirstString(item, ['recipient_gender', 'elder_gender']),
contactName: readString(item, 'contact_name'),
contactPhone: readString(item, 'contact_phone'),
appointmentTime: readString(item, 'appointment_time'),
remark: readString(item, 'remark'),
status: normalizeServiceOrderStatus(readString(item, 'status')),
paymentStatus: readNumber(item, 'payment_status'),
payExpireAt: readString(item, 'pay_expire_at'),
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,
dispatchStatus: readString(item, 'dispatch_status'),
dispatchErrorCode: readString(item, 'dispatch_error_code'),
dispatchErrorMessage: readString(item, 'dispatch_error_message')
}
}
async function insertLegacyStatusLog(orderId: string, fromStatus: string, toStatus: ServiceOrderStatus, operatorId: string, operatorRole: string, remark: string): Promise<void> {
// LEGACY/TODO: 旧链路直接写 hss_service_order_status_logs 已停用。
// 状态事件应由后端 RPC 统一写入 hc_work_order_events / hss_service_order_status_logs。
console.warn('[LEGACY] insertLegacyStatusLog skipped for', orderId, fromStatus, toStatus, remark)
}
async function insertWorkOrderEvent(taskId: string, fromStatus: string, toStatus: string, actorId: string, actorRole: string, action: string, remark: string): Promise<void> {
// LEGACY/TODO: 前端直接 insert hc_work_order_events 已严格禁止。
// 状态事件必须由后端 RPC 统一写入。
console.warn('[LEGACY] insertWorkOrderEvent skipped for', taskId, fromStatus, toStatus, action, remark)
}
function readFirstString(source: any, keys: Array<string>): string {
for (let i = 0; i < keys.length; i++) {
const value = readString(source, keys[i])
if (value != '') {
return value
}
}
return ''
}
function readFirstNumber(source: any, keys: Array<string>): number {
for (let i = 0; i < keys.length; i++) {
const value = readNumber(source, keys[i])
if (value != 0) {
return value
}
}
return 0
}
function readJsonObjectField(source: any, keys: Array<string>): any {
const plain = plainObject(source)
for (let i = 0; i < keys.length; i++) {
const value = plain[keys[i]]
if (value != null) {
return value
}
}
return null
}
function mapCareTaskRowToLegacyOrderRow(item: any): any {
const serviceSnapshotValue = readJsonObjectField(item, ['service_snapshot_json'])
const addressSnapshotValue = readJsonObjectField(item, ['address_snapshot', 'address_snapshot_json'])
let derivedStatus = readString(item, 'status')
if (readFirstString(item, ['accepted_by_family_at']) != '') {
derivedStatus = 'ACCEPTED'
} else if (readFirstString(item, ['acceptance_pending_at']) != '') {
derivedStatus = 'ACCEPTANCE_PENDING'
} else if (readString(item, 'service_started_at') != '') {
derivedStatus = 'ORDER_IN_SERVICE'
} else if (readFirstString(item, ['checked_in_at']) != '') {
derivedStatus = 'ORDER_CHECKED_IN'
} else if (readString(item, 'departed_at') != '') {
derivedStatus = 'departed'
} else if (readString(item, 'accepted_at') != '') {
derivedStatus = 'ORDER_ACCEPTED'
} else if (readFirstString(item, ['assigned_to']) != '') {
derivedStatus = 'ORDER_ASSIGNED'
}
const serviceSnapshot = serviceSnapshotValue != null ? serviceSnapshotValue : {
serviceId: readFirstString(item, ['service_catalog_id', 'service_id']),
serviceName: readString(item, 'service_name'),
category: readFirstString(item, ['service_category', 'category']),
price: readFirstNumber(item, ['service_price', 'price']),
durationText: readFirstString(item, ['service_duration_text', 'duration_text']),
summary: readFirstString(item, ['service_summary', 'summary']),
tags: [] as Array<string>,
suitableFor: readFirstString(item, ['suitable_for'])
}
return {
id: readString(item, 'id'),
order_no: readFirstString(item, ['task_no', 'order_no']),
user_id: readFirstString(item, ['user_id', 'requester_user_id']),
service_id: readFirstString(item, ['service_catalog_id', 'service_id']),
service_name: readString(item, 'service_name'),
service_snapshot_json: serviceSnapshot,
service_address_id: readFirstString(item, ['service_address_id', 'address_id']),
address_snapshot_json: addressSnapshotValue != null ? addressSnapshotValue : JSON.parse('{}'),
recipient_name: readFirstString(item, ['elder_name', 'recipient_name']),
recipient_phone: readFirstString(item, ['elder_phone', 'recipient_phone']),
recipient_age: readFirstNumber(item, ['elder_age', 'recipient_age']),
recipient_gender: readFirstString(item, ['elder_gender', 'recipient_gender']),
contact_name: readString(item, 'contact_name'),
contact_phone: readString(item, 'contact_phone'),
appointment_time: readFirstString(item, ['scheduled_at', 'appointment_time']),
remark: readString(item, 'remark'),
status: derivedStatus,
current_assignment_id: readFirstString(item, ['assignment_id', 'current_assignment_id']),
current_staff_id: readFirstString(item, ['assigned_to', 'current_staff_id']),
accepted_at: readString(item, 'accepted_at'),
departed_at: readString(item, 'departed_at'),
arrived_at: readFirstString(item, ['checked_in_at', 'arrived_at']),
service_started_at: readString(item, 'service_started_at'),
completed_at: readFirstString(item, ['service_completed_at', 'completed_at']),
pending_acceptance_at: readFirstString(item, ['acceptance_pending_at', 'pending_acceptance_at']),
accepted_by_user_at: readFirstString(item, ['accepted_by_family_at', 'accepted_by_user_at']),
reviewed_at: readString(item, 'reviewed_at'),
created_at: readString(item, 'created_at'),
updated_at: readString(item, 'updated_at')
}
}
function mapWorkOrderEventToLegacyLog(item: any): any {
return {
id: readString(item, 'id'),
order_id: readFirstString(item, ['task_id', 'order_id']),
from_status: readString(item, 'from_status'),
to_status: readFirstString(item, ['to_status', 'status']),
operator_id: readFirstString(item, ['actor_id', 'operator_id']),
operator_role: readFirstString(item, ['actor_role', 'operator_role']),
remark: readString(item, 'remark'),
created_at: readString(item, 'created_at')
}
}
function mapCareReviewRecordToLegacyReview(item: any): any {
return {
id: readString(item, 'id'),
order_id: readFirstString(item, ['task_id', 'order_id']),
user_id: readFirstString(item, ['created_by', 'user_id']),
rating: readFirstNumber(item, ['rating']),
tags_json: readJsonObjectField(item, ['tags_json']) != null ? readJsonObjectField(item, ['tags_json']) : [] as Array<string>,
content: readFirstString(item, ['content', 'summary', 'remark']),
created_at: readString(item, 'created_at')
}
}
function buildLegacyExecutionRecord(taskId: string, records: Array<any>): ServiceExecutionRecordType | null {
if (records.length == 0) {
return null
}
let checkinRecord: any = null
let serviceRecord: any = null
for (let i = 0; i < records.length; i++) {
const recordType = readFirstString(records[i], ['record_type', 'care_record_type'])
if (recordType == 'review') {
continue
}
if (recordType == 'checkin') {
checkinRecord = records[i]
continue
}
if (serviceRecord == null) {
serviceRecord = records[i]
}
}
const target = serviceRecord != null ? serviceRecord : (checkinRecord != null ? checkinRecord : records[0])
return parseExecutionRecord({
id: readString(target, 'id'),
order_id: taskId,
assignment_id: readFirstString(target, ['assignment_id']),
checkin_time: checkinRecord != null ? readFirstString(checkinRecord, ['checked_in_at', 'checkin_time', 'started_at']) : readFirstString(target, ['checked_in_at', 'checkin_time']),
checkin_latitude: checkinRecord != null ? readFirstNumber(checkinRecord, ['latitude', 'checkin_latitude']) : readFirstNumber(target, ['latitude', 'checkin_latitude']),
checkin_longitude: checkinRecord != null ? readFirstNumber(checkinRecord, ['longitude', 'checkin_longitude']) : readFirstNumber(target, ['longitude', 'checkin_longitude']),
checkin_address: checkinRecord != null ? readFirstString(checkinRecord, ['location_text', 'checkin_address', 'remark']) : readFirstString(target, ['location_text', 'checkin_address']),
service_started_at: readFirstString(target, ['started_at', 'service_started_at']),
service_finished_at: readFirstString(target, ['finished_at', 'service_finished_at']),
actual_duration_minutes: readFirstNumber(target, ['duration_minutes', 'actual_duration_minutes']),
service_items_json: readJsonObjectField(target, ['service_items_json']) != null ? readJsonObjectField(target, ['service_items_json']) : [] as Array<any>,
summary: readFirstString(target, ['summary', 'content']),
remark: readString(target, 'remark'),
track_points_json: readJsonObjectField(target, ['track_points_json']) != null ? readJsonObjectField(target, ['track_points_json']) : [] as Array<any>,
created_at: readString(target, 'created_at'),
updated_at: readString(target, 'updated_at')
})
}
async function getCareTaskDetail(taskId: string): Promise<ServiceOrderType | null> {
if (!isUuidLike(taskId)) {
return null
}
const taskResponse = await supa.from('ec_care_tasks').select('*').eq('id', taskId).limit(1).execute()
if (taskResponse.error != null || taskResponse.data == null) {
return null
}
const taskRows = taskResponse.data as Array<any>
if (taskRows.length == 0) {
return null
}
const eventsResponse = await supa.from('hc_work_order_events').select('*').eq('task_id', taskId).order('created_at', { ascending: false }).execute()
const recordsResponse = await supa.from('ec_care_records').select('*').eq('task_id', taskId).order('created_at', { ascending: false }).execute()
const evidenceResponse = await supa.from('hc_evidence_files').select('*').eq('task_id', taskId).order('created_at', { ascending: false }).execute()
const logs = [] as Array<ServiceOrderTimelineItemType>
if (eventsResponse.data != null) {
const rawEvents = eventsResponse.data as Array<any>
for (let i = 0; i < rawEvents.length; i++) {
logs.push(parseTimeline(mapWorkOrderEventToLegacyLog(rawEvents[i])))
}
}
let review: ServiceReviewType | null = null
const recordRows = recordsResponse.data != null ? recordsResponse.data as Array<any> : [] as Array<any>
for (let i = 0; i < recordRows.length; i++) {
const recordType = readFirstString(recordRows[i], ['record_type', 'care_record_type'])
if (recordType == 'review') {
review = parseReview(mapCareReviewRecordToLegacyReview(recordRows[i]))
break
}
}
const parsed = parseServiceOrder(mapCareTaskRowToLegacyOrderRow(taskRows[0]), logs, review)
parsed.executionRecord = buildLegacyExecutionRecord(taskId, recordRows)
if (evidenceResponse.data != null) {
const rawEvidence = evidenceResponse.data as Array<any>
const evidenceFiles = [] as Array<ServiceEvidenceFileType>
for (let i = 0; i < rawEvidence.length; i++) {
evidenceFiles.push(parseEvidenceFile({
id: readString(rawEvidence[i], 'id'),
order_id: taskId,
execution_record_id: readFirstString(rawEvidence[i], ['care_record_id', 'execution_record_id']),
phase: readString(rawEvidence[i], 'phase'),
file_type: readString(rawEvidence[i], 'file_type'),
storage_path: readString(rawEvidence[i], 'storage_path'),
file_url: readString(rawEvidence[i], 'file_url'),
latitude: readFirstNumber(rawEvidence[i], ['latitude']),
longitude: readFirstNumber(rawEvidence[i], ['longitude']),
captured_at: readString(rawEvidence[i], 'captured_at'),
created_at: readString(rawEvidence[i], 'created_at')
}))
}
parsed.evidenceFiles = evidenceFiles
}
if (parsed.currentStaffId != '') {
const staffResponse = await supa.from('ml_delivery_staff').select('nickname, phone').eq('uid', parsed.currentStaffId).limit(1).execute()
if (staffResponse.data != null) {
const rawStaffList = staffResponse.data as Array<any>
if (rawStaffList.length > 0) {
parsed.staffName = readString(rawStaffList[0], 'nickname')
parsed.staffPhone = readString(rawStaffList[0], 'phone')
}
}
}
parsed.paymentStatus = 2
parsed.dispatchStatus = parsed.currentStaffId != '' ? HOMECARE_DISPATCH_STATUS_ASSIGNED : HOMECARE_DISPATCH_STATUS_PENDING
parsed.dispatchErrorCode = ''
parsed.dispatchErrorMessage = ''
return parsed
}
async function tryCreateCareTask(params: CreateServiceOrderParams): Promise<ServiceOrderType | null> {
// LEGACY/TODO: 本函数为前端直接 INSERT ec_service_requests + ec_care_tasks 的过渡逻辑。
// 按新架构,履约工单应在支付完成后由后端 RPC 生成/激活,前端不得直接创建。
// 当前因缺少后端接口rpc_consumer_create_homecare_task 或支付回调自动创建)暂时保留,
// 但已移除前端直接派单(不再写入 assigned_to + ORDER_ASSIGNED
if (ecServiceRequestCreateUnavailable) {
return null
}
const userId = getCurrentUserId()
if (userId == '') {
return null
}
const requestId = buildUuidLike()
const taskId = buildUuidLike()
const taskNo = buildOrderNo()
const createdAt = nowIso()
const appointmentTime = normalizeAppointmentTime(params.appointmentTime)
let requestResponse = await supa.from('ec_service_requests').insert(
buildEcServiceRequestPayload(params, userId, requestId, createdAt, appointmentTime, true)
).execute()
if (shouldBypassEcServiceRequestCreate(requestResponse.error)) {
ecServiceRequestCreateUnavailable = true
return null
}
if (requestResponse.error != null) {
requestResponse = await supa.from('ec_service_requests').insert(
buildEcServiceRequestPayload(params, userId, requestId, createdAt, appointmentTime, false)
).execute()
}
if (shouldBypassEcServiceRequestCreate(requestResponse.error)) {
ecServiceRequestCreateUnavailable = true
return null
}
if (requestResponse.error != null) {
requestResponse = await supa.from('ec_service_requests').insert(
buildEcServiceRequestPayloadWithoutAddress(params, userId, requestId, createdAt, appointmentTime)
).execute()
}
if (shouldBypassEcServiceRequestCreate(requestResponse.error)) {
ecServiceRequestCreateUnavailable = true
return null
}
if (requestResponse.error != null) {
return null
}
let taskResponse = await supa.from('ec_care_tasks').insert(
buildEcCareTaskPayload(params, userId, requestId, taskId, taskNo, createdAt, appointmentTime, true)
).execute()
if (taskResponse.error != null) {
taskResponse = await supa.from('ec_care_tasks').insert(
buildEcCareTaskPayload(params, userId, requestId, taskId, taskNo, createdAt, appointmentTime, false)
).execute()
}
if (taskResponse.error != null) {
taskResponse = await supa.from('ec_care_tasks').insert(
buildEcCareTaskPayloadWithoutAddress(params, userId, requestId, taskId, taskNo, createdAt, appointmentTime)
).execute()
}
if (taskResponse.error != null) {
return null
}
// 已移除前端直接派单逻辑。派单应由后端调度系统完成。
await insertWorkOrderEvent(taskId, '', 'ORDER_CREATED', userId, 'consumer', 'create_task', '创建服务申请')
return await getCareTaskDetail(taskId)
}
async function getLegacyServiceOrderDetail(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
}
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
}
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 function getHomecareOrderDisplayStatus(order: ServiceOrderType): string {
if (order.paymentStatus == 1 && order.status == 'created' && isHomecarePaymentExpired(order)) {
return '已超时未支付'
}
if (order.paymentStatus == 1 && order.status == 'created') {
return '待付款'
}
if (order.paymentStatus == 2 && order.dispatchStatus == HOMECARE_DISPATCH_STATUS_PENDING) {
return '待派单'
}
if (order.paymentStatus == 2 && order.dispatchStatus == HOMECARE_DISPATCH_STATUS_DISPATCHING) {
return '正在派单'
}
if (order.paymentStatus == 2 && order.dispatchStatus == HOMECARE_DISPATCH_STATUS_FAILED) {
return '派单未成功'
}
if (order.paymentStatus == 2 && order.dispatchStatus == HOMECARE_DISPATCH_STATUS_ASSIGNED) {
return '已派单'
}
return getServiceOrderStatusText(order.status)
}
function isHomecarePaymentExpired(order: ServiceOrderType): boolean {
if (order.paymentStatus != 1 || order.status != 'created') {
return false
}
const payExpireAt = order.payExpireAt != null ? order.payExpireAt : ''
if (payExpireAt == '') {
return false
}
const expireMs = Date.parse(payExpireAt)
if (isNaN(expireMs)) {
return false
}
return expireMs <= Date.now()
}
export async function dispatchPaidHomecareOrder(orderId: string): Promise<HomecareDispatchResult> {
if (orderId == null || orderId.trim() == '') {
return {
success: false,
code: 'ORDER_ID_REQUIRED',
message: '订单信息异常,请返回后重试',
display_type: 'modal',
retryable: false
}
}
// LEGACY/TODO: rpc_homecare_auto_dispatch 当前只操作 hss_service_orders旧交易链
// 对于 ec_care_tasks 新链UUID 格式订单 ID不要调用旧 RPC避免错误回写旧表。
if (shouldUseCareTaskPath(orderId)) {
return {
success: true,
code: 'SYNC_IN_PROGRESS',
message: '付款成功,服务安排信息正在同步中,请稍后在我的服务中查看。',
display_type: 'toast',
retryable: false,
dispatch_status: HOMECARE_DISPATCH_STATUS_PENDING,
order_id: orderId
}
}
const { data, error } = await supa.rpc('rpc_homecare_auto_dispatch', {
p_order_id: orderId
} as any)
if (error != null) {
console.error('[homecare-dispatch] rpc failed:', error)
return {
success: false,
code: 'RPC_EXECUTION_FAILED',
message: '派单服务暂时异常,请稍后重试',
display_type: 'modal',
retryable: true,
dispatch_status: 'failed',
order_id: orderId
}
}
if (data == null) {
return {
success: false,
code: 'RPC_EMPTY_RESULT',
message: '未获取到派单结果,请稍后重试',
display_type: 'modal',
retryable: true,
dispatch_status: 'failed',
order_id: orderId
}
}
const result = plainObject(data)
const successValue = result['success']
const isSuccess = successValue === true || successValue === 'true' || (typeof successValue === 'boolean' && successValue)
const code = readString(result, 'code')
const message = readString(result, 'message')
const displayType = readString(result, 'display_type')
const retryable = result['retryable'] === true || result['retryable'] === 'true'
const dispatchStatus = readString(result, 'dispatch_status')
if (isSuccess || code == 'DISPATCH_ASSIGNED' || code == 'ALREADY_ASSIGNED' || code == 'ALREADY_ASSIGNED_RECOVERED') {
return {
success: true,
code: code != '' ? code : 'DISPATCH_ASSIGNED',
message: message != '' ? message : '系统已为您匹配服务人员',
display_type: displayType != '' ? displayType : 'none',
retryable: false,
dispatch_status: dispatchStatus != '' ? dispatchStatus : 'assigned',
order_id: readString(result, 'order_id'),
assignment_id: readString(result, 'assignment_id'),
staff_id: readString(result, 'staff_id'),
station_id: readString(result, 'station_id'),
dispatch_distance_km: readNumber(result, 'dispatch_distance_km')
}
}
return {
success: false,
code: code != '' ? code : 'DISPATCH_FAILED',
message: message != '' ? message : '派单失败,请稍后重试',
display_type: displayType != '' ? displayType : 'modal',
retryable: retryable,
dispatch_status: dispatchStatus != '' ? dispatchStatus : 'failed',
order_id: readString(result, 'order_id')
}
}
export function showHomecareDispatchFailureModal(orderId: string, result: HomecareDispatchResult, retryCallback: (id: string) => void): void {
const code = result.code
let title = '操作失败'
let allowRetry = false
if (code == 'ORDER_ID_REQUIRED') {
title = '操作失败'
allowRetry = false
} else if (code == 'UNAUTHENTICATED') {
title = '请重新登录'
allowRetry = false
} else if (code == 'USER_PROFILE_NOT_FOUND') {
title = '账户异常'
allowRetry = false
} else if (code == 'ORDER_NOT_FOUND') {
title = '订单异常'
allowRetry = false
} else if (code == 'ORDER_ACCESS_DENIED') {
title = '无权操作'
allowRetry = false
} else if (code == 'ORDER_NOT_PAID') {
title = '暂不能派单'
allowRetry = false
} else if (code == 'ORDER_STATUS_NOT_DISPATCHABLE') {
title = '暂不能派单'
allowRetry = false
} else if (code == 'NO_ONLINE_STAFF' || code == 'NO_STAFF_IN_SERVICE_STATION' || code == 'NO_QUALIFIED_STAFF' || code == 'NO_NEARBY_STAFF' || code == 'ALL_ELIGIBLE_STAFF_BUSY') {
title = '暂未匹配成功'
allowRetry = true
} else if (code == 'DISPATCH_CONFLICT_RETRY') {
title = '请重新尝试'
allowRetry = true
} else if (code == 'RPC_EXECUTION_FAILED' || code == 'RPC_EMPTY_RESULT') {
title = '派单服务异常'
allowRetry = true
} else {
title = '操作失败'
allowRetry = result.retryable
}
if (!allowRetry) {
uni.showModal({
title: title,
content: result.message,
showCancel: false,
confirmText: '我知道了'
})
return
}
uni.showModal({
title: title,
content: result.message,
showCancel: true,
cancelText: '稍后再试',
confirmText: '重新派单',
success: (res) => {
if (res.confirm) {
retryCallback(orderId)
}
}
})
}
export async function createServiceOrder(params: CreateServiceOrderParams): Promise<ServiceOrderType | null> {
// 当前下单链路分为两层:
// 1) 交易支付层hss_service_orders旧表仍承担套餐价格快照与支付状态
// 2) 履约工单层ec_care_tasks新表应由后端在支付成功后生成/激活)。
// 非套餐单当前走 tryCreateCareTaskLEGACY前端直接写 ec_service_requests + ec_care_tasks
// 待后端提供 rpc_consumer_create_homecare_task 后应统一切到后端创建。
if (params.packageInfo.id == '') {
const newTask = await tryCreateCareTask(params)
if (newTask != null) {
return newTask
}
}
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 pricingSnapshot = buildPricingSnapshot(params)
const serviceSnapshot = buildServiceSnapshot(params)
const payableAmount = params.packageInfo.price
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: serviceSnapshot,
service_package_id: params.packageInfo.id,
pricing_snapshot_json: pricingSnapshot,
original_amount: params.packageInfo.listPrice > 0 ? params.packageInfo.listPrice : payableAmount,
payable_amount: payableAmount,
total_amount: payableAmount,
service_address_id: normalizeUuidOrNull(params.address.addressId),
address_snapshot_json: params.address as any,
recipient_name: params.recipientName,
recipient_phone: params.recipientPhone,
recipient_age: params.recipientAge,
recipient_gender: params.recipientGender,
contact_name: params.contactName,
contact_phone: params.contactPhone,
appointment_time: appointmentTime,
remark: params.remark,
status: 'created',
payment_status: 1,
created_at: now,
updated_at: now
}).execute()
if (response.error != null) {
console.error('createServiceOrder failed', response.error)
return null
}
await insertLegacyStatusLog(orderId, '', 'created', userId, 'consumer', '创建服务订单')
return await getLegacyServiceOrderDetail(orderId)
}
export async function listConsumerServiceOrders(): Promise<Array<ServiceOrderType>> {
const userId = getCurrentUserId()
if (userId == '') {
return [] as Array<ServiceOrderType>
}
const careTaskResponse = await supa.from('ec_care_tasks').select('*').eq('user_id', userId).order('created_at', { ascending: false }).execute()
if (careTaskResponse.error == null && careTaskResponse.data != null) {
const rawTasks = careTaskResponse.data as Array<any>
const taskResult = [] as Array<ServiceOrderType>
for (let i = 0; i < rawTasks.length; i++) {
const parsed = await getCareTaskDetail(readString(rawTasks[i], 'id'))
if (parsed != null) {
taskResult.push(parsed)
}
}
return taskResult
}
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 getLegacyServiceOrderDetail(JSON.parse(JSON.stringify(list[i]))['id'] as string)
if (parsed != null) {
result.push(parsed)
}
}
return result
}
export async function getServiceOrderDetail(orderId: string): Promise<ServiceOrderType | null> {
if (shouldUseCareTaskPath(orderId)) {
const careTask = await getCareTaskDetail(orderId)
if (careTask != null) {
return careTask
}
}
return await getLegacyServiceOrderDetail(orderId)
}
export async function saveServiceRecord(orderId: string, record: DeliveryServiceRecordType): Promise<ServiceOrderType | null> {
const userId = getCurrentUserId()
if (userId == '') {
return null
}
const careTask = shouldUseCareTaskPath(orderId) ? await getCareTaskDetail(orderId) : null
if (careTask != null) {
// TODO/GAP: consumer 端保存服务记录ec_care_tasks 新链)暂无后端 RPC。
// 禁止前端直接 insert ec_care_records / update ec_care_tasks。
// 如需启用,请后端补充 rpc_consumer_save_service_record(task_id, record)。
console.warn('[GAP] saveServiceRecord 暂不可用(新链):缺少 consumer 保存服务记录 RPC')
uni.showToast({ title: '服务记录功能正在升级,请稍后重试', icon: 'none' })
return await getCareTaskDetail(orderId)
}
const current = await getLegacyServiceOrderDetail(orderId)
if (current == null) {
return null
}
const savedAt = nowIso()
const serviceSummary = record.serviceSummary != '' ? record.serviceSummary : record.processNote
const saveResponse = await supa.from('hss_service_execution_records').insert({
id: buildId('ser'),
order_id: orderId,
checkin_time: current.executionRecord != null ? current.executionRecord.checkin_time : savedAt,
checkin_latitude: current.executionRecord != null ? current.executionRecord.checkin_latitude : 0,
checkin_longitude: current.executionRecord != null ? current.executionRecord.checkin_longitude : 0,
checkin_address: current.executionRecord != null ? current.executionRecord.checkin_address : '',
service_content_json: record.serviceContent.length > 0 ? record.serviceContent as any : record.serviceItems.map(item => item.name) as any,
service_summary: serviceSummary,
completion_images_json: record.photos as any,
signature_image: '',
signature_name: record.familyConfirmation.familyMember,
created_at: savedAt,
updated_at: savedAt
}).execute()
if (saveResponse.error != null) {
return null
}
await supa.from('hss_service_orders').update({
updated_at: savedAt
}).eq('id', orderId).execute()
return await getLegacyServiceOrderDetail(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 careTask = shouldUseCareTaskPath(orderId) ? await getCareTaskDetail(orderId) : null
if (careTask != null) {
// TODO/GAP: consumer 确认验收ec_care_tasks 新链)暂无后端 RPC。
// 禁止前端直接 update ec_care_tasks / insert ec_care_records / insert hc_work_order_events。
// 如需启用,请后端补充 rpc_consumer_confirm_acceptance(task_id, rating, feedback, tags)。
console.warn('[GAP] confirmServiceOrder 暂不可用(新链):缺少 consumer 确认验收 RPC')
uni.showToast({ title: '验收功能正在升级,请稍后重试', icon: 'none' })
return await getCareTaskDetail(orderId)
}
// LEGACY: 以下仍为旧 hss_service_orders 链路,仅用于兼容已有交易订单。
const current = await getServiceOrderDetail(orderId)
if (current == null) {
return null
}
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 insertLegacyStatusLog(orderId, current.status, 'accepted_by_user', userId, 'consumer', '用户确认验收')
await supa.from('hss_service_reviews').insert({
id: buildId('srv'),
order_id: orderId,
user_id: userId,
rating,
tags_json: tags as any,
content,
created_at: acceptedAt
}).execute()
await supa.from('hss_service_orders').update({
status: 'reviewed',
reviewed_at: acceptedAt,
updated_at: acceptedAt
}).eq('id', orderId).execute()
await insertLegacyStatusLog(orderId, 'accepted_by_user', 'reviewed', userId, 'consumer', '用户提交评价')
return await getLegacyServiceOrderDetail(orderId)
}
export async function rejectServiceOrderAcceptance(orderId: string, content: string): Promise<ServiceOrderType | null> {
const userId = getCurrentUserId()
if (userId == '') {
return null
}
const careTask = shouldUseCareTaskPath(orderId) ? await getCareTaskDetail(orderId) : null
if (careTask != null) {
// TODO/GAP: consumer 拒绝验收ec_care_tasks 新链)暂无后端 RPC。
// 禁止前端直接 update ec_care_tasks / insert hc_work_order_exceptions。
// 如需启用,请后端补充 rpc_consumer_reject_acceptance(task_id, feedback)。
console.warn('[GAP] rejectServiceOrderAcceptance 暂不可用(新链):缺少 consumer 拒绝验收 RPC')
uni.showToast({ title: '验收功能正在升级,请稍后重试', icon: 'none' })
return await getCareTaskDetail(orderId)
}
// LEGACY: 以下仍为旧 hss_service_orders 链路。
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 insertLegacyStatusLog(orderId, current.status, 'exception', userId, 'consumer', content == '' ? '用户退回整改' : content)
return await getLegacyServiceOrderDetail(orderId)
}
export async function getCurrentConsumerUser() {
return await getCurrentUser()
}