524 lines
19 KiB
Plaintext
524 lines
19 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 {
|
|
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
|
|
}
|
|
|
|
function nowText(): string {
|
|
return new Date().toISOString().replace('T', ' ').substring(0, 19)
|
|
}
|
|
|
|
function buildId(prefix: string): string {
|
|
return prefix + '-' + String(Date.now()) + '-' + String(Math.floor(Math.random() * 100000)).padStart(5, '0')
|
|
}
|
|
|
|
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 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 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 = safeJsonField(item, 'address_snapshot_json')
|
|
const serviceSnapshot = safeJsonField(item, 'service_snapshot_json')
|
|
const addressObj = plainObject(safeParseObject(addressSnapshot))
|
|
const serviceObj = plainObject(safeParseObject(serviceSnapshot))
|
|
return {
|
|
id: readString(item, 'id'),
|
|
orderNo: readString(item, 'order_no'),
|
|
userId: readString(item, 'user_id'),
|
|
serviceId: readString(item, 'service_id'),
|
|
serviceName: readString(item, 'service_name'),
|
|
serviceSnapshot: {
|
|
serviceId: readString(serviceObj, 'serviceId') != '' ? readString(serviceObj, 'serviceId') : readString(item, 'service_id'),
|
|
serviceName: readString(serviceObj, 'serviceName') != '' ? readString(serviceObj, 'serviceName') : readString(item, 'service_name'),
|
|
category: readString(serviceObj, 'category'),
|
|
price: readNumber(serviceObj, 'price'),
|
|
durationText: readString(serviceObj, 'durationText'),
|
|
summary: readString(serviceObj, 'summary'),
|
|
tags: safeParseArray(safeJsonField(serviceObj, 'tags')),
|
|
suitableFor: readString(serviceObj, 'suitableFor')
|
|
},
|
|
serviceAddressId: readString(item, 'service_address_id'),
|
|
addressSnapshot: {
|
|
addressId: readString(addressObj, 'addressId'),
|
|
contactName: readString(addressObj, 'contactName'),
|
|
contactPhone: readString(addressObj, 'contactPhone'),
|
|
province: readString(addressObj, 'province'),
|
|
city: readString(addressObj, 'city'),
|
|
district: readString(addressObj, 'district'),
|
|
detailAddress: readString(addressObj, 'detailAddress'),
|
|
fullAddress: readString(addressObj, 'fullAddress'),
|
|
latitude: readNumber(addressObj, 'latitude'),
|
|
longitude: readNumber(addressObj, 'longitude'),
|
|
coordinateType: readString(addressObj, 'coordinateType') == '' ? 'gcj02' : readString(addressObj, 'coordinateType'),
|
|
remark: readString(addressObj, 'remark')
|
|
},
|
|
recipientName: readString(item, 'recipient_name'),
|
|
recipientPhone: readString(item, 'recipient_phone'),
|
|
contactName: readString(item, 'contact_name'),
|
|
contactPhone: readString(item, 'contact_phone'),
|
|
appointmentTime: readString(item, 'appointment_time'),
|
|
remark: readString(item, 'remark'),
|
|
status: normalizeServiceOrderStatus(readString(item, 'status')),
|
|
currentAssignmentId: readString(item, 'current_assignment_id'),
|
|
currentStaffId: readString(item, 'current_staff_id'),
|
|
acceptedAt: readString(item, 'accepted_at'),
|
|
departedAt: readString(item, 'departed_at'),
|
|
arrivedAt: readString(item, 'arrived_at'),
|
|
serviceStartedAt: readString(item, 'service_started_at'),
|
|
completedAt: readString(item, 'completed_at'),
|
|
pendingAcceptanceAt: readString(item, 'pending_acceptance_at'),
|
|
acceptedByUserAt: readString(item, 'accepted_by_user_at'),
|
|
reviewedAt: readString(item, 'reviewed_at'),
|
|
createdAt: readString(item, 'created_at'),
|
|
updatedAt: readString(item, 'updated_at'),
|
|
staffName: '',
|
|
staffPhone: '',
|
|
logs,
|
|
executionRecord: null,
|
|
evidenceFiles: [] as Array<any>,
|
|
review
|
|
}
|
|
}
|
|
|
|
async function insertStatusLog(orderId: string, fromStatus: string, toStatus: ServiceOrderStatus, operatorId: string, operatorRole: string, remark: string): Promise<void> {
|
|
await supa.from('hss_service_order_status_logs').insert({
|
|
id: buildId('slog'),
|
|
order_id: orderId,
|
|
from_status: fromStatus,
|
|
to_status: toStatus,
|
|
operator_id: operatorId == '' ? null : operatorId,
|
|
operator_role: operatorRole,
|
|
remark,
|
|
created_at: new Date().toISOString()
|
|
}).execute()
|
|
}
|
|
|
|
export function buildAddressSnapshot(address: UserAddress, latitude: number, longitude: number): ServiceOrderAddressSnapshotType {
|
|
return {
|
|
addressId: address.id,
|
|
contactName: address.recipient_name,
|
|
contactPhone: address.phone,
|
|
province: address.province,
|
|
city: address.city,
|
|
district: address.district,
|
|
detailAddress: address.detail_address,
|
|
fullAddress: address.province + address.city + address.district + ' ' + address.detail_address,
|
|
latitude,
|
|
longitude,
|
|
coordinateType: 'gcj02',
|
|
remark: address.label ?? ''
|
|
}
|
|
}
|
|
|
|
export async function createServiceOrder(params: CreateServiceOrderParams): Promise<ServiceOrderType | null> {
|
|
const userId = getCurrentUserId()
|
|
if (userId == '') {
|
|
return null
|
|
}
|
|
const orderId = buildId('so')
|
|
const orderNo = buildOrderNo()
|
|
const now = new Date().toISOString()
|
|
const appointmentTime = normalizeAppointmentTime(params.appointmentTime)
|
|
const response = await supa.from('hss_service_orders').insert({
|
|
id: orderId,
|
|
order_no: orderNo,
|
|
user_id: userId,
|
|
service_id: params.service.id,
|
|
service_name: params.service.name,
|
|
service_snapshot_json: params.service as any,
|
|
service_address_id: normalizeUuidOrNull(params.address.addressId),
|
|
address_snapshot_json: params.address as any,
|
|
recipient_name: params.recipientName,
|
|
recipient_phone: params.recipientPhone,
|
|
contact_name: params.contactName,
|
|
contact_phone: params.contactPhone,
|
|
appointment_time: appointmentTime,
|
|
remark: params.remark,
|
|
status: 'created',
|
|
created_at: now,
|
|
updated_at: now
|
|
}).execute()
|
|
if (response.error != null) {
|
|
console.error('createServiceOrder failed', response.error)
|
|
return null
|
|
}
|
|
await insertStatusLog(orderId, '', 'created', userId, 'consumer', '创建服务订单')
|
|
const staffResponse = await supa
|
|
.from('ml_delivery_staff')
|
|
.select('id, station_id, nickname, phone, status, deleted_at')
|
|
.eq('status', 1)
|
|
.order('created_at', { ascending: true })
|
|
.execute()
|
|
if (staffResponse.data != null) {
|
|
const rawStaffList = staffResponse.data as any[]
|
|
if (rawStaffList.length > 0) {
|
|
const staffObj = plainObject(rawStaffList[0])
|
|
const assignmentId = buildId('sa')
|
|
await supa.from('hss_service_assignments').insert({
|
|
id: assignmentId,
|
|
order_id: orderId,
|
|
staff_id: readString(staffObj, 'id'),
|
|
station_id: readString(staffObj, 'station_id') == '' ? null : readString(staffObj, 'station_id'),
|
|
status: 'assigned',
|
|
assigned_at: now,
|
|
created_at: now,
|
|
updated_at: now
|
|
}).execute()
|
|
await supa.from('hss_service_orders').update({
|
|
status: 'assigned',
|
|
current_assignment_id: assignmentId,
|
|
current_staff_id: readString(staffObj, 'id'),
|
|
updated_at: now
|
|
}).eq('id', orderId).execute()
|
|
await insertStatusLog(orderId, 'created', 'assigned', userId, 'system', '系统已自动派单')
|
|
}
|
|
}
|
|
return await getServiceOrderDetail(orderId)
|
|
}
|
|
|
|
export async function listConsumerServiceOrders(): Promise<Array<ServiceOrderType>> {
|
|
const userId = getCurrentUserId()
|
|
if (userId == '') {
|
|
return [] as Array<ServiceOrderType>
|
|
}
|
|
const response = await supa.from('hss_service_orders').select('*').eq('user_id', userId).order('created_at', { ascending: false }).execute()
|
|
if (response.error != null || response.data == null) {
|
|
return [] as Array<ServiceOrderType>
|
|
}
|
|
const list = response.data as any[]
|
|
const result = [] as Array<ServiceOrderType>
|
|
for (let i = 0; i < list.length; i++) {
|
|
const parsed = await getServiceOrderDetail(JSON.parse(JSON.stringify(list[i]))['id'] as string)
|
|
if (parsed != null) {
|
|
result.push(parsed)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
export async function getServiceOrderDetail(orderId: string): Promise<ServiceOrderType | null> {
|
|
const orderResponse = await supa.from('hss_service_orders').select('*').eq('id', orderId).limit(1).execute()
|
|
if (orderResponse.error != null || orderResponse.data == null) {
|
|
return null
|
|
}
|
|
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 async function confirmServiceOrder(orderId: string, rating: number, content: string, tags: Array<string>): Promise<ServiceOrderType | null> {
|
|
const userId = getCurrentUserId()
|
|
if (userId == '') {
|
|
return null
|
|
}
|
|
const current = await getServiceOrderDetail(orderId)
|
|
if (current == null) {
|
|
return null
|
|
}
|
|
const acceptedAt = new Date().toISOString()
|
|
const updateResponse = await supa.from('hss_service_orders').update({
|
|
status: 'accepted_by_user',
|
|
accepted_by_user_at: acceptedAt,
|
|
updated_at: acceptedAt
|
|
}).eq('id', orderId).execute()
|
|
if (updateResponse.error != null) {
|
|
return null
|
|
}
|
|
await insertStatusLog(orderId, 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 insertStatusLog(orderId, 'accepted_by_user', 'reviewed', userId, 'consumer', '用户提交评价')
|
|
return await getServiceOrderDetail(orderId)
|
|
}
|
|
|
|
export async function rejectServiceOrderAcceptance(orderId: string, content: string): Promise<ServiceOrderType | null> {
|
|
const userId = getCurrentUserId()
|
|
if (userId == '') {
|
|
return null
|
|
}
|
|
const current = await getServiceOrderDetail(orderId)
|
|
if (current == null) {
|
|
return null
|
|
}
|
|
const updateResponse = await supa.from('hss_service_orders').update({
|
|
status: 'exception',
|
|
updated_at: nowIso()
|
|
}).eq('id', orderId).execute()
|
|
if (updateResponse.error != null) {
|
|
return null
|
|
}
|
|
await insertStatusLog(orderId, current.status, 'exception', userId, 'consumer', content == '' ? '用户退回整改' : content)
|
|
return await getServiceOrderDetail(orderId)
|
|
}
|
|
|
|
export async function getCurrentConsumerUser() {
|
|
return await getCurrentUser()
|
|
} |