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 randomHex(len: number): string { let result = '' for (let i = 0; i < len; i++) { result += Math.floor(Math.random() * 16).toString(16) } return result } function generateUuid(): string { return randomHex(8) + '-' + randomHex(4) + '-4' + randomHex(3) + '-' + String(Math.floor(Math.random() * 4 + 8).toString(16)) + randomHex(3) + '-' + randomHex(12) } function buildOrderNo(): string { const now = new Date() const year = String(now.getFullYear()) const month = String(now.getMonth() + 1).padStart(2, '0') const day = String(now.getDate()).padStart(2, '0') const random = String(Math.floor(Math.random() * 1000000)).padStart(6, '0') return 'SO' + year + month + day + random } function normalizeAppointmentTime(time: string): string { if (time == '') { return new Date().toISOString() } const parsed = Date.parse(time) if (!isNaN(parsed)) { return new Date(parsed).toISOString() } return new Date().toISOString() } function normalizeUuidOrNull(id: string): string | null { if (id == '') { return null } if (id.indexOf('-') >= 0 && id.length == 36) { return id } return null } function plainObject(source: any): any { if (source instanceof UTSJSONObject) { return source } return JSON.parse(JSON.stringify(source)) as any } function readString(source: any, key: string): string { if (source instanceof UTSJSONObject) { const value = (source as UTSJSONObject).getString(key) return value != null ? value : '' } const value = plainObject(source)[key] if (value == null) { return '' } return typeof value == 'string' ? value : String(value) } function readNumber(source: any, key: string): number { if (source instanceof UTSJSONObject) { const value = (source as UTSJSONObject).getNumber(key) return value != null ? value : 0 } 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 readFirstString(source: any, keys: Array): string { for (let i = 0; i < keys.length; i++) { const value = readString(source, keys[i]) if (value != '') { return value } } return '' } function readJsonField(source: any, key: string): any { if (source != null && typeof source.getJSONObj === 'function') { const value = source.getJSONObj(key) return value != null ? value : null } const value = plainObject(source)[key] return value == null ? null : value } 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 ecCareTaskCreateUnavailable = false function shouldBypassEcCareTaskCreate(error: any): boolean { if (error == null) { return false } return hasMissingColumnError(error, 'address_snapshot_json') || hasMissingColumnError(error, 'service_snapshot_json') || hasMissingColumnError(error, 'scheduled_at') || hasMissingColumnError(error, 'appointment_time') || hasMissingColumnError(error, 'task_no') || hasMissingColumnError(error, 'service_catalog_id') || hasMissingColumnError(error, 'elder_name') || hasMissingColumnError(error, 'contact_name') } function readServiceItems(source: any): Array { const raw = readJsonField(source, 'service_items_json') const result = [] as Array if (raw == null) { return result } const items = Array.isArray(raw) ? raw : plainObject(raw) if (!Array.isArray(items)) { return result } for (let i = 0; i < items.length; i++) { result.push({ id: readFirstString(items[i], ['id']) != '' ? readFirstString(items[i], ['id']) : buildId('svc-item'), name: readFirstString(items[i], ['name', 'label']), required: readString(items[i], 'required') != 'false', completed: readString(items[i], 'completed') == 'true', incompleteReason: readString(items[i], 'incompleteReason'), remark: readString(items[i], 'remark'), updatedAt: readString(items[i], 'updatedAt') } as DeliveryServiceItemType) } return result } function buildDefaultServiceItems(order: DeliveryOrderType): Array { const names = [] as Array if (order.serviceName.indexOf('护理') >= 0) { names.push('上门签到确认') names.push('基础护理执行') names.push('生命体征记录') names.push('家属沟通反馈') } else if (order.serviceName.indexOf('随访') >= 0) { names.push('上门签到确认') names.push('慢病指标采集') names.push('用药情况核对') names.push('随访结论反馈') } else { names.push('上门签到确认') names.push('服务项目执行') names.push('服务过程记录') names.push('完成情况确认') } const result = [] as Array for (let i = 0; i < names.length; i++) { result.push({ id: order.id + '-item-' + String(i + 1), name: names[i], required: true, completed: false, incompleteReason: '', remark: '', updatedAt: '' } as DeliveryServiceItemType) } return result } async function getCurrentStaffId(): Promise { const userId = getCurrentUserId() if (userId == '') { return '' } const profile = await getDeliveryProfileByUserId(userId) return profile != null ? profile.id : '' } function getCurrentWorkerUserId(): string { return getCurrentUserId() } // ============================================================================= // 居家服务 delivery 动作 RPC 统一调用封装 // 说明:所有会改变履约状态的动作必须走后端 RPC,禁止前端直接 update/insert。 // ============================================================================= function parseRpcOrderResult(data: any): DeliveryOrderType | null { if (data == null) { return null } const plain = plainObject(data) if (Array.isArray(plain)) { if (plain.length == 0) { return null } return plain[0] as DeliveryOrderType } if (readString(plain, 'id') == '') { return null } return plain as DeliveryOrderType } async function callDeliveryActionRpc(rpcName: string, params: any): Promise { try { const { data, error } = await supa.rpc(rpcName, params as any) if (error != null) { console.error('[delivery-action] RPC failed:', rpcName, error) return null } return parseRpcOrderResult(data) } catch (e) { console.error('[delivery-action] RPC exception:', rpcName, e) return null } } // LEGACY: 以下两个函数为旧链路直接写表逻辑,已停用。新链路由 rpc_delivery_* 统一处理。 // 保留函数体仅为了避免引用处编译报错,实际不再执行写操作。 async function insertLegacyStatusLog(orderId: string, fromStatus: string, toStatus: ServiceOrderStatus, remark: string): Promise { // LEGACY/TODO: 旧链路直接写 hss_service_order_status_logs 已停用。 // 状态事件由后端 rpc_delivery_* 统一写入 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, action: string, remark: string): Promise { // LEGACY/TODO: 前端直接 insert hc_work_order_events 已禁止。 // 状态事件必须由后端 rpc_delivery_* 统一写入。 console.warn('[LEGACY] insertWorkOrderEvent skipped for', taskId, fromStatus, toStatus, action, remark) } function parseExecutionRecord(item: any): ServiceExecutionRecordType { return { id: '', orderNo: '', serviceType: '', serviceName: '', serviceCategory: '', serviceItems: [] as Array, 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, healthTags: [] as Array, careLevel: '', needFamilyPresent: false, needMaterials: false, remark: '', merchantId: '', merchantName: '', deliveryStaffId: '', deliveryStaffName: '', acceptTime: '', departTime: '', arriveTime: '', checkinTime: '', finishTime: '', cancelReason: '', exceptionType: '', exceptionDesc: '', evidenceList: [] as Array, signatureUrl: '', signatureName: '', satisfactionStatus: '', settlementStatus: '', archiveStatus: '', createdAt: '', updatedAt: '', contactName: '', contactPhone: '', notices: [] as Array, timeline: [] as Array, statusLog: [] as Array, serviceSummary: '', progressNote: '', distanceKm: '', allowCheckinRadiusMeters: 100, lastLocation: null, trackPoints: [] as Array, serviceRecord: null, abnormalReport: null } as DeliveryOrderType } function deriveTaskStatus(item: any): string { const rawStatus = readString(item, 'status') if (readFirstString(item, ['accepted_by_family_at']) != '') return 'ACCEPTED' if (readFirstString(item, ['acceptance_pending_at']) != '') return 'ACCEPTANCE_PENDING' if (readFirstString(item, ['service_started_at']) != '') return 'ORDER_IN_SERVICE' if (readFirstString(item, ['checked_in_at']) != '') return 'ORDER_CHECKED_IN' if (readFirstString(item, ['departed_at']) != '') return 'departed' if (readFirstString(item, ['accepted_at']) != '') return 'ORDER_ACCEPTED' return rawStatus } function statusToDeliveryStatus(status: ServiceOrderStatus, item: any): string { if (status == 'assigned') return 'pending_assignment' if (status == 'accepted') { return readFirstString(item, ['departed_at']) != '' ? 'departed' : '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 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 hasOrderCoreInfo(order: DeliveryOrderType | null): boolean { if (order == null) { return false } return order.serviceName != '' || order.elderName != '' || order.address != '' || order.contactName != '' } function mapCareTaskToLegacyShape(item: any): any { 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, content: readFirstString(item, ['content', 'summary', 'remark']), created_at: readString(item, 'created_at') } } function buildLegacyExecutionRecord(taskId: string, records: Array): 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, 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, created_at: readString(target, 'created_at'), updated_at: readString(target, 'updated_at') }) } async function getCareTaskDetail(taskId: string): Promise { 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 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 if (eventsResponse.data != null) { const rawEvents = eventsResponse.data as Array 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 : [] as Array 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 const evidenceFiles = [] as Array 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 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 { // 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 { 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 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 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 ?? '' } } async function getLegacyServiceOrderDetail(orderId: string): Promise { return await getOrderDetail(orderId) } async function tryCreateCareTask(params: CreateServiceOrderParams): Promise { // LEGACY/TODO: 本函数为前端直接 INSERT ec_care_tasks 的过渡逻辑。 // 按新架构,履约工单应在支付完成后由后端 RPC 生成/激活,前端不得直接创建。 // 当前因缺少后端接口(rpc_consumer_create_homecare_task 或支付回调自动创建)暂时保留。 if (ecCareTaskCreateUnavailable) { return null } const userId = getCurrentUserId() if (userId == '') { return null } const taskId = generateUuid() const taskNo = buildOrderNo() const now = nowIso() const scheduledAt = normalizeAppointmentTime(params.appointmentTime) const response = await supa.from('ec_care_tasks').insert({ id: taskId, task_no: taskNo, user_id: userId, service_catalog_id: params.service.id, service_name: params.service.name, service_category: params.service.category, service_snapshot_json: params.service as any, elder_name: params.recipientName, elder_phone: params.recipientPhone, contact_name: params.contactName, contact_phone: params.contactPhone, address_snapshot_json: params.address as any, scheduled_at: scheduledAt, appointment_time: scheduledAt, remark: params.remark, status: 'ORDER_CREATED', created_at: now, updated_at: now }).execute() if (shouldBypassEcCareTaskCreate(response.error)) { ecCareTaskCreateUnavailable = true console.error('[tryCreateCareTask] ec_care_tasks 缺少必要字段,已停用前端直接创建:', response.error) return null } if (response.error != null) { console.error('tryCreateCareTask failed', response.error) return null } // 已移除前端直接写入 hc_work_order_events;事件应由后端 RPC 统一生成。 console.warn('[LEGACY] tryCreateCareTask skipped direct insertWorkOrderEvent for', taskId) return await getOrderDetail(taskId) as any } export async function createServiceOrder(params: CreateServiceOrderParams): Promise { // 当前下单链路分为两层: // 1) 交易支付层:hss_service_orders(旧表,仍承担套餐价格快照与支付状态)。 // 2) 履约工单层:ec_care_tasks(新表,应由后端在支付成功后生成/激活)。 // 非套餐单当前走 tryCreateCareTask(LEGACY,前端直接写 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', '创建服务订单') const staffObj = await getAutoAssignableStaff() if (staffObj != null) { const plainStaff = plainObject(staffObj) const assignmentId = buildId('sa') await supa.from('hss_service_assignments').insert({ id: assignmentId, order_id: orderId, staff_id: readString(plainStaff, 'id'), station_id: readString(plainStaff, 'station_id') == '' ? null : readString(plainStaff, '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(plainStaff, 'id'), updated_at: now }).eq('id', orderId).execute() await insertLegacyStatusLog(orderId, 'created', 'assigned', '系统已自动派单') } return await getLegacyServiceOrderDetail(orderId) } export async function getOrdersByTab(tab: string): Promise> { const staffId = await getCurrentStaffId() if (staffId == '') { return [] as Array } // FIX: assigned_to / current_staff_id 存的是 ml_delivery_staff.id,不是 auth.uid const careTaskResponse = await supa.from('ec_care_tasks').select('*').eq('assigned_to', staffId).order('created_at', { ascending: false }).execute() if (careTaskResponse.error == null && careTaskResponse.data != null) { const rawTasks = careTaskResponse.data as Array const result = [] as Array for (let i = 0; i < rawTasks.length; i++) { const parsed = await parseCareTaskOrder(readString(rawTasks[i], 'id'), rawTasks[i]) let matched = true if (tab == 'pending') { matched = parsed.status == 'pending_assignment' } else if (tab == 'today') { matched = parsed.status == 'pending_assignment' || parsed.status == 'accepted' || parsed.status == 'departed' || parsed.status == 'arrived' || parsed.status == 'in_service' || parsed.status == 'pending_acceptance' } else if (tab == 'history') { matched = parsed.status == 'pending_acceptance' || parsed.status == 'completed' || parsed.status == 'abnormal' || parsed.status == 'cancelled' || parsed.status == 'rejected' } if (tab == 'all' || matched) { result.push(parsed) } } const validResult = [] as Array for (let i = 0; i < result.length; i++) { if (hasOrderCoreInfo(result[i])) { validResult.push(result[i]) } } if (validResult.length > 0) { return validResult } } 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 } const list = response.data as any[] const result = [] as Array 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 getOrderDetail(orderId: string): Promise { const careTaskId = normalizeUuidOrNull(orderId) if (careTaskId != null) { const careTaskResponse = await supa.from('ec_care_tasks').select('*').eq('id', careTaskId).limit(1).execute() if (careTaskResponse.error == null && careTaskResponse.data != null) { const rows = careTaskResponse.data as Array if (rows.length > 0) { return await parseCareTaskOrder(orderId, rows[0]) } } } const response = await supa.from('hss_service_orders').select('*').eq('id', orderId).single().execute() if (response.error != null || response.data == null) { return null } return await parseLegacyDeliveryOrder(orderId, response.data) } async function updateLegacyOrderStatus(orderId: string, nextStatus: ServiceOrderStatus, updateData: UTSJSONObject, remark: string): Promise { // LEGACY/TODO: 前端直接 update hss_service_orders.status 已禁止。 // 请使用 rpc_delivery_* 系列 RPC 完成状态流转。 console.warn('[LEGACY] updateLegacyOrderStatus skipped for', orderId, nextStatus, remark) return await getOrderDetail(orderId) } async function updateCareTask(orderId: string, nextStatus: string, updateData: any, action: string, remark: string): Promise { // LEGACY/TODO: 前端直接 update ec_care_tasks.status + insert hc_work_order_events 已严格禁止。 // 所有状态流转必须调用后端 rpc_delivery_* RPC。 console.warn('[LEGACY] updateCareTask skipped for', orderId, nextStatus, action, remark) return await getOrderDetail(orderId) } export async function acceptOrder(orderId: string): Promise { return await callDeliveryActionRpc('rpc_delivery_accept_order', { p_order_id: orderId }) } export async function rejectOrder(orderId: string, reason: string): Promise { return await callDeliveryActionRpc('rpc_delivery_reject_order', { p_order_id: orderId, p_reason: reason }) } export async function departOrder(orderId: string, location: DeliveryLocationType | null): Promise { return await callDeliveryActionRpc('rpc_delivery_start_depart', { p_order_id: orderId, p_location: location != null ? { latitude: location.latitude, longitude: location.longitude, address: location.address } : {} as any }) } export async function arriveOrder(orderId: string, location: DeliveryLocationType | null): Promise { return await callDeliveryActionRpc('rpc_delivery_arrive_order', { p_order_id: orderId, p_location: location != null ? { latitude: location.latitude, longitude: location.longitude, address: location.address } : {} as any }) } export async function checkinOrder(orderId: string, payload: DeliveryCheckinPayloadType): Promise { return await callDeliveryActionRpc('rpc_delivery_checkin_order', { p_order_id: orderId, p_payload: { location: payload.location != null ? { latitude: payload.location.latitude, longitude: payload.location.longitude, address: payload.location.address } : {} as any, photos: payload.photos != null ? payload.photos : [] as Array, note: payload.note != null ? payload.note : '' } as any }) } export async function startService(orderId: string): Promise { return await callDeliveryActionRpc('rpc_delivery_start_service', { p_order_id: orderId }) } export async function saveServiceRecord(orderId: string, record: DeliveryServiceRecordType): Promise { // 先保存服务进度 const progressResult = await callDeliveryActionRpc('rpc_delivery_save_progress', { p_order_id: orderId, p_payload: { items: record.serviceItems != null ? record.serviceItems as any : [] as any, serviceSummary: record.processNote != null ? record.processNote : '', progressNote: record.staffRemark != null ? record.staffRemark : '' } as any }) if (progressResult == null) { return null } // 再逐张上传证据照片 if (record.photos != null && record.photos.length > 0) { for (let i = 0; i < record.photos.length; i++) { await callDeliveryActionRpc('rpc_delivery_upload_evidence', { p_order_id: orderId, p_phase: 'service', p_file_url: record.photos[i], p_latitude: 0, p_longitude: 0 }) } } return await getOrderDetail(orderId) } export async function finishOrder(orderId: string): Promise { return await callDeliveryActionRpc('rpc_delivery_finish_service', { p_order_id: orderId, p_payload: {} as any }) } export async function submitException(orderId: string, payload: DeliveryExceptionPayloadType): Promise { return await callDeliveryActionRpc('rpc_delivery_submit_exception', { p_order_id: orderId, p_payload: { exceptionType: payload.exceptionType != null ? payload.exceptionType : 'other', description: payload.description != null ? payload.description : '', occurredAt: payload.occurredAt != null && payload.occurredAt != '' ? payload.occurredAt : nowIso(), locationText: payload.locationText != null ? payload.locationText : '', images: payload.images != null ? payload.images as any : [] as any, needPlatformIntervention: payload.needPlatformIntervention == true, requestCancelOrder: payload.requestCancelOrder == true, requestReschedule: payload.requestReschedule == true } as any }) } export async function changeOrderStatus(orderId: string, nextStatus: DeliveryOrderStatus, extraRemark: string = ''): Promise { if (nextStatus == 'accepted') return await acceptOrder(orderId) if (nextStatus == 'departed' || nextStatus == 'on_the_way') return await departOrder(orderId, null) if (nextStatus == 'arrived' || nextStatus == 'checked_in') return await arriveOrder(orderId, null) if (nextStatus == 'in_service' || nextStatus == 'serving') return await startService(orderId) if (nextStatus == 'pending_acceptance' || nextStatus == 'pending_confirm' || nextStatus == 'pending_submit' || nextStatus == 'completed') return await finishOrder(orderId) if (nextStatus == 'rejected') return await rejectOrder(orderId, extraRemark) if (nextStatus == 'abnormal' || nextStatus == 'exception_pending') { return await submitException(orderId, { exceptionType: 'other', description: extraRemark, occurredAt: nowIso(), locationText: '', images: [] as Array, needPlatformIntervention: false, requestCancelOrder: false, requestReschedule: false }) } return await getOrderDetail(orderId) } export async function confirmServiceOrder(orderId: string, rating: number, feedback: string, tags: Array): Promise { // TODO/GAP: consumer 确认验收暂无后端 RPC(ec_care_tasks 新链)。 // 当前禁止前端直接 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 getOrderDetail(orderId) } export async function rejectServiceOrderAcceptance(orderId: string, feedback: string): Promise { // TODO/GAP: consumer 拒绝验收暂无后端 RPC(ec_care_tasks 新链)。 // 当前禁止前端直接 update ec_care_tasks.status。 // 如需启用,请后端补充 rpc_consumer_reject_acceptance(task_id, feedback)。 console.warn('[GAP] rejectServiceOrderAcceptance 暂不可用:缺少 consumer 新链拒绝验收 RPC') uni.showToast({ title: '验收功能正在升级,请稍后重试', icon: 'none' }) return await getOrderDetail(orderId) }