完善服务模块缺少付款页的bug

This commit is contained in:
2026-06-02 11:35:31 +08:00
parent c3324d459a
commit 881262940c
35 changed files with 29069 additions and 557 deletions

View File

@@ -1,3 +1,15 @@
// =============================================================================
// LEGACY/TODO 全局声明:
// 本文件中所有直接 update ec_care_tasks / insert hc_work_order_events /
// insert hc_work_order_exceptions / insert ec_care_records 的逻辑均为旧链路。
// 按新架构,状态流转与事件留痕必须由后端 RPC 统一处理,前端不得直接写表。
// 已改造的函数(如 submitWorkerCheckIn / submitWorkerServiceRecord / submitWorkerException /
// completeWorkerTask已切换为 rpc_delivery_* 或标记为 GAP。
// 尚未改造的 admin 函数submitAdminAssessment / submitAdminServicePlan /
// submitAdminRectification / submitAdminSettlementArchive 等)仍保留旧逻辑,
// 但已增加 TODO 注释,待后端提供对应 RPC 后统一替换。
// =============================================================================
import {
HomeServiceAcceptanceType,
HomeServiceAdminApplicationType,
@@ -6,6 +18,7 @@ import {
HomeServiceCatalogType,
HomeServiceCaseType,
HomeServiceOverviewCardType,
HomeServicePackageType,
HomeServicePlanType,
HomeServiceRectificationType,
HomeServiceSettlementType,
@@ -15,7 +28,9 @@ import {
import {
confirmServiceOrder,
createServiceOrder,
getHomecareOrderDisplayStatus,
getServiceOrderDetail,
HOMECARE_DISPATCH_STATUS_FAILED,
listConsumerServiceOrders,
rejectServiceOrderAcceptance
} from '@/services/serviceOrderService.uts'
@@ -96,6 +111,24 @@ function parseCatalogItem(source: any): HomeServiceCatalogType {
}
}
function parsePackageItem(source: any): HomeServicePackageType {
return {
id: readString(source, 'id'),
serviceId: readString(source, 'service_id'),
packageName: readString(source, 'package_name'),
packageDesc: readString(source, 'package_desc'),
durationMinutes: readNumber(source, 'duration_minutes'),
durationText: readString(source, 'duration_text'),
price: readNumber(source, 'price'),
listPrice: readNumber(source, 'list_price'),
isDefault: readString(source, 'is_default') == 'true' || plainObject(source)['is_default'] === true,
sortNo: readNumber(source, 'sort_no'),
dataSource: readString(source, 'data_source'),
seedBatchNo: readString(source, 'seed_batch_no'),
remark: readString(source, 'remark')
}
}
function createTimeline(title1: string, title2: string, title3: string): Array<HomeServiceTimelineItemType> {
return [
{
@@ -410,6 +443,49 @@ export async function fetchHomeServiceCatalog(): Promise<Array<HomeServiceCatalo
return result
}
export async function fetchHomeServicePackages(serviceId: string): Promise<Array<HomeServicePackageType>> {
if (serviceId == '') {
return [] as Array<HomeServicePackageType>
}
const response = await supa
.from('hss_service_packages')
.select('id, service_id, package_name, package_desc, duration_minutes, duration_text, price, list_price, is_default, sort_no, data_source, seed_batch_no, remark, effective_at, expires_at')
.eq('service_id', serviceId)
.eq('status', 1)
.is('deleted_at', null)
.order('sort_no', { ascending: true })
.execute()
if (response.error != null || response.data == null || !Array.isArray(response.data)) {
return [] as Array<HomeServicePackageType>
}
const result = [] as Array<HomeServicePackageType>
for (let i = 0; i < response.data.length; i++) {
const item = response.data[i]
const effectiveAt = readString(item, 'effective_at')
const expiresAt = readString(item, 'expires_at')
const effectiveMs = effectiveAt != '' ? Date.parse(effectiveAt) : 0
const expiresMs = expiresAt != '' ? Date.parse(expiresAt) : 0
const nowMs = Date.now()
if (effectiveMs > 0 && effectiveMs > nowMs) {
continue
}
if (expiresMs > 0 && expiresMs <= nowMs) {
continue
}
result.push(parsePackageItem(item))
}
result.sort((left: HomeServicePackageType, right: HomeServicePackageType): number => {
if (left.isDefault && !right.isDefault) {
return -1
}
if (!left.isDefault && right.isDefault) {
return 1
}
return left.sortNo - right.sortNo
})
return result
}
export async function fetchConsumerHomeServiceCases(): Promise<Array<HomeServiceCaseType>> {
const orders = await listConsumerServiceOrders()
const result = [] as Array<HomeServiceCaseType>
@@ -436,9 +512,28 @@ export async function createHomeServiceApplication(draft: HomeServiceApplication
break
}
}
if (matchedService != null && draft.serviceAddressSnapshot != null) {
const packages = await fetchHomeServicePackages(draft.serviceId)
let matchedPackage: HomeServicePackageType | null = null
for (let i = 0; i < packages.length; i++) {
if (packages[i].id == draft.selectedPackageId) {
matchedPackage = packages[i]
break
}
}
if (matchedService != null && matchedPackage != null && draft.serviceAddressSnapshot != null) {
const serviceSnapshot: HomeServiceCatalogType = {
id: matchedService.id,
name: matchedService.name,
category: matchedService.category,
price: matchedPackage.price,
durationText: matchedPackage.durationText != '' ? matchedPackage.durationText : matchedService.durationText,
summary: matchedPackage.packageDesc != '' ? matchedPackage.packageDesc : matchedService.summary,
tags: matchedService.tags,
suitableFor: matchedService.suitableFor
}
const createdOrder = await createServiceOrder({
service: matchedService,
service: serviceSnapshot,
packageInfo: matchedPackage,
address: {
addressId: draft.serviceAddressSnapshot.addressId,
contactName: draft.serviceAddressSnapshot.contactName,
@@ -557,12 +652,18 @@ function mapLogsToTimeline(logs: Array<ServiceOrderTimelineItemType>): Array<Hom
}
function mapOrderToCase(order: ServiceOrderType): HomeServiceCaseType {
const displayStatus = getHomecareOrderDisplayStatus(order)
let statusText = displayStatus
let statusTone = getCaseTone(order.status)
if (order.dispatchStatus == HOMECARE_DISPATCH_STATUS_FAILED) {
statusTone = 'danger'
}
return {
id: order.id,
caseNo: order.orderNo,
status: order.status,
statusText: getServiceOrderStatusText(order.status),
statusTone: getCaseTone(order.status),
statusText: statusText,
statusTone: statusTone,
serviceName: order.serviceName,
serviceTime: formatServiceAppointmentText(order.appointmentTime),
applicantName: order.contactName,
@@ -576,6 +677,8 @@ function mapOrderToCase(order: ServiceOrderType): HomeServiceCaseType {
staffName: order.staffName == '' ? '待分配' : order.staffName,
staffPhone: order.staffPhone == '' ? '待分配' : order.staffPhone,
amount: order.serviceSnapshot.price,
paymentStatus: order.paymentStatus,
payExpireAt: order.payExpireAt,
checkinTime: order.executionRecord != null ? order.executionRecord.checkinTime : '',
checkinAddress: order.executionRecord != null ? order.executionRecord.checkinAddress : '',
serviceStartedAt: order.executionRecord != null ? order.executionRecord.serviceStartedAt : order.serviceStartedAt,
@@ -677,42 +780,13 @@ async function isCareTask(taskId: string): Promise<boolean> {
}
async function completeWorkerTask(taskId: string): Promise<HomeServiceTaskType | null> {
const completedAt = nowIso()
if (await isCareTask(taskId)) {
await supa.from('ec_care_tasks').update({
status: 'ACCEPTANCE_PENDING',
service_completed_at: completedAt,
acceptance_pending_at: completedAt,
updated_at: completedAt
}).eq('id', taskId).execute()
await supa.from('hc_work_order_events').insert({
id: buildId('hc-event'),
task_id: taskId,
from_status: 'ORDER_IN_SERVICE',
to_status: 'ACCEPTANCE_PENDING',
actor_id: getCurrentUserId(),
actor_role: 'merchant',
action: 'finish_service',
remark: '服务记录和凭证已经提交。',
created_at: completedAt
}).execute()
} else {
await supa.from('hss_service_orders').update({
status: 'pending_acceptance',
completed_at: completedAt,
pending_acceptance_at: completedAt,
updated_at: completedAt
}).eq('id', taskId).execute()
await supa.from('hss_service_order_status_logs').insert({
id: buildId('slog'),
order_id: taskId,
from_status: 'in_service',
to_status: 'pending_acceptance',
operator_id: getCurrentUserId(),
operator_role: 'merchant',
remark: '服务记录和凭证已经提交。',
created_at: completedAt
}).execute()
// LEGACY/TODO: 已切换为调用 rpc_delivery_finish_service。
const { data, error } = await supa.rpc('rpc_delivery_finish_service', {
p_order_id: taskId,
p_payload: {} as any
} as any)
if (error != null) {
console.error('[homeServiceService] finish_service rpc failed:', error)
}
return await fetchWorkerTaskDetail(taskId)
}
@@ -876,161 +950,60 @@ export async function advanceWorkerTask(taskId: string): Promise<HomeServiceTask
}
export async function submitWorkerCheckIn(taskId: string, note: string): Promise<HomeServiceTaskType | null> {
const checkedInAt = nowIso()
if (await isCareTask(taskId)) {
const recordId = buildId('care-checkin')
await supa.from('ec_care_records').insert({
id: recordId,
task_id: taskId,
record_type: 'checkin',
started_at: checkedInAt,
checked_in_at: checkedInAt,
location_text: note,
remark: note,
created_at: checkedInAt,
updated_at: checkedInAt
}).execute()
await supa.from('ec_care_tasks').update({
status: 'ORDER_IN_SERVICE',
checked_in_at: checkedInAt,
service_started_at: checkedInAt,
updated_at: checkedInAt
}).eq('id', taskId).execute()
await supa.from('hc_work_order_events').insert({
id: buildId('hc-event'),
task_id: taskId,
from_status: 'ORDER_ACCEPTED',
to_status: 'ORDER_IN_SERVICE',
actor_id: getCurrentUserId(),
actor_role: 'merchant',
action: 'checkin_task',
remark: note == '' ? '已完成签到,开始执行服务。' : note,
created_at: checkedInAt
}).execute()
} else {
const recordId = buildId('ser')
await supa.from('hss_service_execution_records').upsert({
id: recordId,
order_id: taskId,
assignment_id: '',
checkin_time: checkedInAt,
checkin_address: note,
service_started_at: checkedInAt,
remark: note,
created_at: checkedInAt,
updated_at: checkedInAt
}).execute()
await supa.from('hss_service_orders').update({
status: 'in_service',
arrived_at: checkedInAt,
service_started_at: checkedInAt,
updated_at: checkedInAt
}).eq('id', taskId).execute()
await supa.from('hss_service_order_status_logs').insert({
id: buildId('slog'),
order_id: taskId,
from_status: 'accepted',
to_status: 'in_service',
operator_id: getCurrentUserId(),
operator_role: 'merchant',
remark: note == '' ? '已完成签到,开始执行服务。' : note,
created_at: checkedInAt
}).execute()
// LEGACY/TODO: 已切换为调用 rpc_delivery_checkin_order + rpc_delivery_start_service。
const checkinResult = await supa.rpc('rpc_delivery_checkin_order', {
p_order_id: taskId,
p_payload: {
location: {} as any,
photos: [] as Array<string>,
note: note != null ? note : ''
} as any
} as any)
if (checkinResult.error != null) {
console.error('[homeServiceService] checkin rpc failed:', checkinResult.error)
}
const startResult = await supa.rpc('rpc_delivery_start_service', {
p_order_id: taskId
} as any)
if (startResult.error != null) {
console.error('[homeServiceService] start_service rpc failed:', startResult.error)
}
return await fetchWorkerTaskDetail(taskId)
}
export async function submitWorkerServiceRecord(taskId: string, summary: string): Promise<HomeServiceTaskType | null> {
const detail = await getServiceOrderDetail(taskId)
if (detail == null) {
return null
}
const now = nowIso()
const recordId = detail.executionRecord != null && detail.executionRecord.id != '' ? detail.executionRecord.id : buildId('worker-rec')
if (await isCareTask(taskId)) {
await supa.from('ec_care_records').upsert({
id: recordId,
task_id: taskId,
record_type: 'service',
started_at: detail.executionRecord != null ? detail.executionRecord.serviceStartedAt : detail.serviceStartedAt,
summary: summary,
remark: summary,
created_at: detail.executionRecord != null && detail.executionRecord.createdAt != '' ? detail.executionRecord.createdAt : now,
updated_at: now
}).execute()
await supa.from('ec_care_tasks').update({
status: 'ORDER_IN_SERVICE',
updated_at: now
}).eq('id', taskId).execute()
await supa.from('hc_work_order_events').insert({
id: buildId('hc-event'),
task_id: taskId,
from_status: 'ORDER_IN_SERVICE',
to_status: 'ORDER_IN_SERVICE',
actor_id: getCurrentUserId(),
actor_role: 'merchant',
action: 'save_record',
remark: summary == '' ? '已保存服务记录。' : summary,
created_at: now
}).execute()
} else {
await supa.from('hss_service_execution_records').upsert({
id: recordId,
order_id: taskId,
assignment_id: detail.currentAssignmentId,
service_started_at: detail.executionRecord != null ? detail.executionRecord.serviceStartedAt : detail.serviceStartedAt,
summary: summary,
remark: summary,
created_at: detail.executionRecord != null && detail.executionRecord.createdAt != '' ? detail.executionRecord.createdAt : now,
updated_at: now
}).execute()
// LEGACY/TODO: 已切换为调用 rpc_delivery_save_progress。
const { data, error } = await supa.rpc('rpc_delivery_save_progress', {
p_order_id: taskId,
p_payload: {
items: [] as any,
serviceSummary: summary != null ? summary : '',
progressNote: summary != null ? summary : ''
} as any
} as any)
if (error != null) {
console.error('[homeServiceService] save_progress rpc failed:', error)
}
return await fetchWorkerTaskDetail(taskId)
}
export async function submitWorkerException(taskId: string, exceptionType: string, description: string): Promise<HomeServiceTaskType | null> {
const now = nowIso()
if (await isCareTask(taskId)) {
await supa.from('hc_work_order_exceptions').insert({
id: buildId('hc-ex'),
task_id: taskId,
exception_type: exceptionType,
description: description,
occurred_at: now,
created_by: getCurrentUserId(),
created_at: now,
updated_at: now
}).execute()
await supa.from('ec_care_tasks').update({
status: 'ORDER_EXCEPTION',
updated_at: now
}).eq('id', taskId).execute()
await supa.from('hc_work_order_events').insert({
id: buildId('hc-event'),
task_id: taskId,
from_status: '',
to_status: 'ORDER_EXCEPTION',
actor_id: getCurrentUserId(),
actor_role: 'merchant',
action: 'report_exception',
remark: exceptionType + '' + description,
created_at: now
}).execute()
} else {
await supa.from('hss_service_orders').update({
status: 'exception',
updated_at: now
}).eq('id', taskId).execute()
await supa.from('hss_service_order_status_logs').insert({
id: buildId('slog'),
order_id: taskId,
from_status: 'in_service',
to_status: 'exception',
operator_id: getCurrentUserId(),
operator_role: 'merchant',
remark: exceptionType + '' + description,
created_at: now
}).execute()
// LEGACY/TODO: 已切换为调用 rpc_delivery_submit_exception。
const { data, error } = await supa.rpc('rpc_delivery_submit_exception', {
p_order_id: taskId,
p_payload: {
exceptionType: exceptionType != null ? exceptionType : 'other',
description: description != null ? description : '',
occurredAt: nowIso(),
locationText: '',
images: [] as any,
needPlatformIntervention: false,
requestCancelOrder: false,
requestReschedule: false
} as any
} as any)
if (error != null) {
console.error('[homeServiceService] submit_exception rpc failed:', error)
}
return await fetchWorkerTaskDetail(taskId)
}
@@ -1056,30 +1029,10 @@ export async function submitAdminAssessment(caseId: string, riskLevel: string, c
requirementTags: order.serviceSnapshot.tags,
updatedAt: nowIso()
}
if (await isCareTask(caseId)) {
await supa.from('hc_work_order_events').insert({
id: buildId('hc-event'),
task_id: caseId,
from_status: order.status,
to_status: order.status,
actor_id: getCurrentUserId(),
actor_role: 'admin',
action: 'submit_assessment',
remark: encodeAdminRemark(ADMIN_ASSESSMENT_PREFIX, payload),
created_at: nowIso()
}).execute()
} else {
await supa.from('hss_service_order_status_logs').insert({
id: buildId('slog'),
order_id: caseId,
from_status: order.status,
to_status: order.status,
operator_id: getCurrentUserId(),
operator_role: 'admin',
remark: encodeAdminRemark(ADMIN_ASSESSMENT_PREFIX, payload),
created_at: nowIso()
}).execute()
}
// TODO/GAP: admin 提交评估暂无后端 RPCec_care_tasks 新链)。
// 禁止前端直接 insert hc_work_order_events 作为评估留痕。
// 如需启用,请后端补充 rpc_admin_submit_assessment(case_id, payload)。
console.warn('[GAP] submitAdminAssessment 暂不可用(新链):缺少 admin 评估提交 RPC')
return buildAssessmentDetail(order, payload)
}
@@ -1112,30 +1065,10 @@ export async function submitAdminServicePlan(
planSummary,
updatedAt: nowIso()
}
if (await isCareTask(caseId)) {
await supa.from('hc_work_order_events').insert({
id: buildId('hc-event'),
task_id: caseId,
from_status: order.status,
to_status: order.status,
actor_id: getCurrentUserId(),
actor_role: 'admin',
action: 'submit_plan',
remark: encodeAdminRemark(ADMIN_PLAN_PREFIX, payload),
created_at: nowIso()
}).execute()
} else {
await supa.from('hss_service_order_status_logs').insert({
id: buildId('slog'),
order_id: caseId,
from_status: order.status,
to_status: order.status,
operator_id: getCurrentUserId(),
operator_role: 'admin',
remark: encodeAdminRemark(ADMIN_PLAN_PREFIX, payload),
created_at: nowIso()
}).execute()
}
// TODO/GAP: admin 提交服务计划暂无后端 RPCec_care_tasks 新链)。
// 禁止前端直接 insert hc_work_order_events 作为计划留痕。
// 如需启用,请后端补充 rpc_admin_submit_service_plan(case_id, payload)。
console.warn('[GAP] submitAdminServicePlan 暂不可用(新链):缺少 admin 服务计划提交 RPC')
return buildPlanDetail(order, payload)
}
@@ -1200,42 +1133,10 @@ export async function submitAdminRectification(caseId: string, issueSummary: str
status: 'closed',
updatedAt: nowIso()
}
if (await isCareTask(caseId)) {
const reopenedAt = nowIso()
await supa.from('ec_care_tasks').update({
status: 'ACCEPTANCE_PENDING',
acceptance_pending_at: reopenedAt,
updated_at: reopenedAt
}).eq('id', caseId).execute()
await supa.from('hc_work_order_events').insert({
id: buildId('hc-event'),
task_id: caseId,
from_status: 'ACCEPTANCE_REJECTED',
to_status: 'ACCEPTANCE_PENDING',
actor_id: getCurrentUserId(),
actor_role: 'admin',
action: 'submit_rectification',
remark: encodeAdminRemark(ADMIN_RECTIFICATION_PREFIX, payload),
created_at: reopenedAt
}).execute()
} else {
const reopenedAt = nowIso()
await supa.from('hss_service_orders').update({
status: 'pending_acceptance',
pending_acceptance_at: reopenedAt,
updated_at: reopenedAt
}).eq('id', caseId).execute()
await supa.from('hss_service_order_status_logs').insert({
id: buildId('slog'),
order_id: caseId,
from_status: order.status,
to_status: 'pending_acceptance',
operator_id: getCurrentUserId(),
operator_role: 'admin',
remark: encodeAdminRemark(ADMIN_RECTIFICATION_PREFIX, payload),
created_at: reopenedAt
}).execute()
}
// TODO/GAP: admin 提交整改暂无后端 RPCec_care_tasks 新链)。
// 禁止前端直接 update ec_care_tasks / insert hc_work_order_events。
// 如需启用,请后端补充 rpc_admin_submit_rectification(case_id, payload)。
console.warn('[GAP] submitAdminRectification 暂不可用(新链):缺少 admin 整改提交 RPC')
const latest = await getServiceOrderDetail(caseId)
if (latest == null) {
return buildRectificationDetail(order, payload, issueSummary)