1132 lines
43 KiB
Plaintext
1132 lines
43 KiB
Plaintext
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>): 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<DeliveryServiceItemType> {
|
||
const raw = readJsonField(source, 'service_items_json')
|
||
const result = [] as Array<DeliveryServiceItemType>
|
||
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<DeliveryServiceItemType> {
|
||
const names = [] as Array<string>
|
||
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<DeliveryServiceItemType>
|
||
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<string> {
|
||
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<DeliveryOrderType | null> {
|
||
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<void> {
|
||
// 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<void> {
|
||
// 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<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 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<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 ?? ''
|
||
}
|
||
}
|
||
|
||
async function getLegacyServiceOrderDetail(orderId: string): Promise<DeliveryOrderType | null> {
|
||
return await getOrderDetail(orderId)
|
||
}
|
||
|
||
async function tryCreateCareTask(params: CreateServiceOrderParams): Promise<ServiceOrderType | null> {
|
||
// 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<ServiceOrderType | null> {
|
||
// 当前下单链路分为两层:
|
||
// 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<Array<DeliveryOrderType>> {
|
||
const staffId = await getCurrentStaffId()
|
||
if (staffId == '') {
|
||
return [] as Array<DeliveryOrderType>
|
||
}
|
||
|
||
// 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<any>
|
||
const result = [] as Array<DeliveryOrderType>
|
||
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<DeliveryOrderType>
|
||
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<DeliveryOrderType>
|
||
}
|
||
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 getOrderDetail(orderId: string): Promise<DeliveryOrderType | null> {
|
||
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<any>
|
||
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<DeliveryOrderType | null> {
|
||
// 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<DeliveryOrderType | null> {
|
||
// 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<DeliveryOrderType | null> {
|
||
return await callDeliveryActionRpc('rpc_delivery_accept_order', {
|
||
p_order_id: orderId
|
||
})
|
||
}
|
||
|
||
export async function rejectOrder(orderId: string, reason: string): Promise<DeliveryOrderType | null> {
|
||
return await callDeliveryActionRpc('rpc_delivery_reject_order', {
|
||
p_order_id: orderId,
|
||
p_reason: reason
|
||
})
|
||
}
|
||
|
||
export async function departOrder(orderId: string, location: DeliveryLocationType | null): Promise<DeliveryOrderType | null> {
|
||
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<DeliveryOrderType | null> {
|
||
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<DeliveryOrderType | null> {
|
||
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<string>,
|
||
note: payload.note != null ? payload.note : ''
|
||
} as any
|
||
})
|
||
}
|
||
|
||
export async function startService(orderId: string): Promise<DeliveryOrderType | null> {
|
||
return await callDeliveryActionRpc('rpc_delivery_start_service', {
|
||
p_order_id: orderId
|
||
})
|
||
}
|
||
|
||
export async function saveServiceRecord(orderId: string, record: DeliveryServiceRecordType): Promise<DeliveryOrderType | null> {
|
||
// 先保存服务进度
|
||
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<DeliveryOrderType | null> {
|
||
return await callDeliveryActionRpc('rpc_delivery_finish_service', {
|
||
p_order_id: orderId,
|
||
p_payload: {} as any
|
||
})
|
||
}
|
||
|
||
export async function submitException(orderId: string, payload: DeliveryExceptionPayloadType): Promise<DeliveryOrderType | null> {
|
||
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<DeliveryOrderType | null> {
|
||
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<string>,
|
||
needPlatformIntervention: false,
|
||
requestCancelOrder: false,
|
||
requestReschedule: false
|
||
})
|
||
}
|
||
return await getOrderDetail(orderId)
|
||
}
|
||
export async function confirmServiceOrder(orderId: string, rating: number, feedback: string, tags: Array<string>): Promise<DeliveryOrderType | null> {
|
||
// 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<DeliveryOrderType | null> {
|
||
// 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)
|
||
}
|