Files
medical-mall/services/serviceOrderService.uts

1176 lines
44 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 } 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 CreateServiceOrderParams = {
service: HomeServiceCatalogType
address: ServiceOrderAddressSnapshotType
recipientName: string
recipientPhone: string
contactName: string
contactPhone: string
appointmentTime: string
remark: string
}
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
}
if (/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(\s*-\s*\d{2}:\d{2})?$/.test(text)) {
return text
}
if (/^\d{4}-\d{2}-\d{2}(\s+(上午|下午|晚上))$/.test(text)) {
return text
}
if (/^\d{4}-\d{2}-\d{2}T/.test(text)) {
const parsed = Date.parse(text)
if (!isNaN(parsed)) {
const date = new Date(parsed)
const year = String(date.getFullYear())
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return year + '-' + month + '-' + day + ' ' + hour + ':' + minute
}
return text.replace('T', ' ')
}
const monthDayMatch = text.match(/(\d{2})\/(\d{2})/)
if (monthDayMatch != null) {
const month = monthDayMatch[1] ?? ''
const day = monthDayMatch[2] ?? ''
if (month == '' || day == '') {
return text
}
const year = String(new Date().getFullYear())
const rangeMatch = text.match(/(\d{2}:\d{2})(\s*-\s*\d{2}:\d{2})?/)
if (rangeMatch != null) {
const startTime = rangeMatch[1] ?? ''
const endRange = rangeMatch[2] ?? ''
if (startTime != '') {
return year + '-' + month + '-' + day + ' ' + startTime + endRange.replace(/\s+/g, '')
}
}
const tailText = text.substring(text.indexOf(month + '/' + day) + 5).trim()
return year + '-' + month + '-' + day + (tailText != '' ? ' ' + tailText : '')
}
const explicitParsed = Date.parse(text)
if (!isNaN(explicitParsed) && /^\d{4}\//.test(text)) {
const date = new Date(explicitParsed)
const year = String(date.getFullYear())
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return year + '-' + month + '-' + day + ' ' + hour + ':' + minute
}
const year = String(new Date().getFullYear())
return /^\d{2}-\d{2}\s+\d{2}:\d{2}/.test(text) ? year + '-' + text : text
}
function safeParseObject(value: string): UTSJSONObject {
if (value == '') {
return JSON.parse('{}') as UTSJSONObject
}
return JSON.parse(value) as UTSJSONObject
}
function safeParseArray(value: string): Array<string> {
if (value == '') {
return [] as Array<string>
}
const parsed = JSON.parse(value) as any
if (Array.isArray(parsed)) {
return parsed as Array<string>
}
return [] as Array<string>
}
function safeJsonField(source: any, key: string): string {
const plain = JSON.parse(JSON.stringify(source)) as any
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')
}
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,
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,
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: params.service as any,
elder_name: params.recipientName,
elder_phone: params.recipientPhone,
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: params.service as any,
elder_name: params.recipientName,
elder_phone: params.recipientPhone,
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'),
contactName: readString(item, 'contact_name'),
contactPhone: readString(item, 'contact_phone'),
appointmentTime: readString(item, 'appointment_time'),
remark: readString(item, 'remark'),
status: normalizeServiceOrderStatus(readString(item, 'status')),
currentAssignmentId: readString(item, 'current_assignment_id'),
currentStaffId: readString(item, 'current_staff_id'),
acceptedAt: readString(item, 'accepted_at'),
departedAt: readString(item, 'departed_at'),
arrivedAt: readString(item, 'arrived_at'),
serviceStartedAt: readString(item, 'service_started_at'),
completedAt: readString(item, 'completed_at'),
pendingAcceptanceAt: readString(item, 'pending_acceptance_at'),
acceptedByUserAt: readString(item, 'accepted_by_user_at'),
reviewedAt: readString(item, 'reviewed_at'),
createdAt: readString(item, 'created_at'),
updatedAt: readString(item, 'updated_at'),
staffName: '',
staffPhone: '',
logs,
executionRecord: null,
evidenceFiles: [] as Array<any>,
review
}
}
async function insertLegacyStatusLog(orderId: string, fromStatus: string, toStatus: ServiceOrderStatus, operatorId: string, operatorRole: string, remark: string): Promise<void> {
await supa.from('hss_service_order_status_logs').insert({
id: buildId('slog'),
order_id: orderId,
from_status: fromStatus,
to_status: toStatus,
operator_id: operatorId == '' ? null : operatorId,
operator_role: operatorRole,
remark,
created_at: new Date().toISOString()
}).execute()
}
async function insertWorkOrderEvent(taskId: string, fromStatus: string, toStatus: string, actorId: string, actorRole: string, action: string, remark: string): Promise<void> {
await supa.from('hc_work_order_events').insert({
id: buildId('hc-event'),
task_id: taskId,
from_status: fromStatus == '' ? null : fromStatus,
to_status: toStatus,
actor_id: actorId == '' ? null : actorId,
actor_role: actorRole,
action,
remark,
created_at: nowIso()
}).execute()
}
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']),
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')
}
}
}
return parsed
}
async function tryCreateCareTask(params: CreateServiceOrderParams): Promise<ServiceOrderType | null> {
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', '创建服务申请')
const assignedStaff = await getAutoAssignableStaff()
if (assignedStaff != null) {
const assignedUserId = readString(assignedStaff, 'uid')
if (assignedUserId != '') {
await supa.from('ec_care_tasks').update({
status: 'ORDER_ASSIGNED',
assigned_to: assignedUserId,
updated_at: createdAt
}).eq('id', taskId).execute()
await insertWorkOrderEvent(taskId, 'ORDER_CREATED', 'ORDER_ASSIGNED', userId, 'system', 'assign_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 async function createServiceOrder(params: CreateServiceOrderParams): Promise<ServiceOrderType | null> {
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 response = await supa.from('hss_service_orders').insert({
id: orderId,
order_no: orderNo,
user_id: userId,
service_id: params.service.id,
service_name: params.service.name,
service_snapshot_json: params.service as any,
service_address_id: normalizeUuidOrNull(params.address.addressId),
address_snapshot_json: params.address as any,
recipient_name: params.recipientName,
recipient_phone: params.recipientPhone,
contact_name: params.contactName,
contact_phone: params.contactPhone,
appointment_time: appointmentTime,
remark: params.remark,
status: 'created',
created_at: now,
updated_at: now
}).execute()
if (response.error != null) {
console.error('createServiceOrder failed', response.error)
return null
}
await insertLegacyStatusLog(orderId, '', 'created', userId, 'consumer', '创建服务订单')
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', userId, 'system', '系统已自动派单')
}
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) {
const savedAt = nowIso()
const serviceContent = record.serviceContent.length > 0 ? record.serviceContent : record.serviceItems.map(item => item.name)
const serviceSummary = record.serviceSummary != '' ? record.serviceSummary : record.processNote
const healthMetrics = record.healthMetrics as any
const familyConfirmation = record.familyConfirmation as any
const insertResponse = await supa.from('ec_care_records').insert({
id: buildId('care-record'),
task_id: orderId,
record_type: 'service_record',
created_by: userId,
service_items_json: record.serviceItems as any,
service_content_json: serviceContent as any,
service_summary: serviceSummary,
process_note: record.processNote,
elder_status: record.elderStatus,
health_metrics_json: healthMetrics,
materials_used: record.materialsUsed,
abnormal_note: record.abnormalNote,
photos_json: record.photos as any,
staff_remark: record.staffRemark,
family_confirmation_json: familyConfirmation,
created_at: savedAt,
updated_at: savedAt
}).execute()
if (insertResponse.error != null) {
return null
}
const updateResponse = await supa.from('ec_care_tasks').update({
updated_at: savedAt
}).eq('id', orderId).execute()
if (updateResponse.error != null) {
return null
}
await insertWorkOrderEvent(orderId, careTask.status, careTask.status, userId, 'staff', 'save_service_record', serviceSummary == '' ? '服务记录已保存' : serviceSummary)
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) {
const acceptedAt = nowIso()
const updateResponse = await supa.from('ec_care_tasks').update({
status: 'ACCEPTED',
accepted_by_family_at: acceptedAt,
updated_at: acceptedAt
}).eq('id', orderId).execute()
if (updateResponse.error == null) {
await insertWorkOrderEvent(orderId, 'ACCEPTANCE_PENDING', 'ACCEPTED', userId, 'consumer', 'accept_task', '用户确认验收')
if (rating > 0 || content != '' || tags.length > 0) {
await supa.from('ec_care_records').insert({
id: buildId('care-review'),
task_id: orderId,
record_type: 'review',
created_by: userId,
rating,
tags_json: tags as any,
content,
created_at: acceptedAt,
updated_at: acceptedAt
}).execute()
await insertWorkOrderEvent(orderId, 'ACCEPTED', 'ACCEPTED', userId, 'consumer', 'submit_review', '用户提交评价')
}
return await getCareTaskDetail(orderId)
}
}
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) {
const rejectedAt = nowIso()
const updateResponse = await supa.from('ec_care_tasks').update({
status: 'ACCEPTANCE_REJECTED',
updated_at: rejectedAt
}).eq('id', orderId).execute()
if (updateResponse.error == null) {
await supa.from('hc_work_order_exceptions').insert({
id: buildId('hc-ex'),
task_id: orderId,
exception_type: 'acceptance_rejected',
description: content == '' ? '用户退回整改' : content,
occurred_at: rejectedAt,
location_text: '',
images_json: [] as Array<string>,
need_platform_intervention: false,
request_cancel_order: false,
request_reschedule: false,
created_by: userId,
created_at: rejectedAt,
updated_at: rejectedAt
}).execute()
await insertWorkOrderEvent(orderId, 'ACCEPTANCE_PENDING', 'ACCEPTANCE_REJECTED', userId, 'consumer', 'reject_acceptance', content == '' ? '用户退回整改' : content)
return await getCareTaskDetail(orderId)
}
}
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()
}