BEGIN; -- ===================================================================================== -- Delivery homecare action RPCs -- Purpose: �?delivery 端补�?rpc_delivery_* 契约,优先兼�?ec/hc 新链,回退 hss 旧链�? -- ===================================================================================== CREATE OR REPLACE FUNCTION public.delivery_current_user_id() RETURNS UUID LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT u.id FROM public.ak_users u WHERE u.auth_id = auth.uid() LIMIT 1 $$; CREATE OR REPLACE FUNCTION public.delivery_current_staff_id() RETURNS UUID LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT s.id FROM public.ml_delivery_staff s WHERE s.uid = public.delivery_current_user_id() AND s.deleted_at IS NULL ORDER BY COALESCE(s.updated_at, s.created_at) DESC, s.created_at DESC LIMIT 1 $$; CREATE OR REPLACE FUNCTION public.delivery_table_exists(p_table_name TEXT) RETURNS BOOLEAN LANGUAGE sql STABLE SET search_path = public AS $$ SELECT to_regclass('public.' || p_table_name) IS NOT NULL $$; CREATE OR REPLACE FUNCTION public.delivery_front_status(p_normalized_status TEXT, p_raw JSONB) RETURNS TEXT LANGUAGE plpgsql IMMUTABLE AS $$ BEGIN IF p_normalized_status = 'assigned' THEN RETURN 'pending_assignment'; END IF; IF p_normalized_status = 'accepted' THEN IF COALESCE(p_raw ->> 'departed_at', '') <> '' THEN RETURN 'departed'; END IF; RETURN 'accepted'; END IF; IF p_normalized_status = 'departed' THEN RETURN 'departed'; END IF; IF p_normalized_status = 'arrived' THEN RETURN 'arrived'; END IF; IF p_normalized_status = 'in_service' THEN RETURN 'in_service'; END IF; IF p_normalized_status = 'pending_acceptance' THEN RETURN 'pending_acceptance'; END IF; IF p_normalized_status IN ('reviewed', 'accepted_by_user', 'settled', 'completed') THEN RETURN 'completed'; END IF; IF p_normalized_status = 'rejected' THEN RETURN 'rejected'; END IF; IF p_normalized_status = 'cancelled' THEN RETURN 'cancelled'; END IF; IF p_normalized_status = 'exception' THEN RETURN 'abnormal'; END IF; RETURN 'pending_assignment'; END; $$; CREATE OR REPLACE FUNCTION public.delivery_status_text(p_front_status TEXT) RETURNS TEXT LANGUAGE plpgsql IMMUTABLE AS $$ BEGIN CASE p_front_status WHEN 'pending_assignment' THEN RETURN '待接�?; WHEN 'pending_accept' THEN RETURN '待接�?; WHEN 'accepted' THEN RETURN '待出�?; WHEN 'waiting_departure' THEN RETURN '待出�?; WHEN 'departed' THEN RETURN '已出�?; WHEN 'on_the_way' THEN RETURN '途中'; WHEN 'arrived' THEN RETURN '待签�?; WHEN 'checked_in' THEN RETURN '已签�?; WHEN 'in_service' THEN RETURN '服务�?; WHEN 'serving' THEN RETURN '服务�?; WHEN 'pending_confirm' THEN RETURN '待确�?; WHEN 'pending_submit' THEN RETURN '待提�?; WHEN 'pending_acceptance' THEN RETURN '待验�?; WHEN 'completed' THEN RETURN '已完�?; WHEN 'rejected' THEN RETURN '已拒�?; WHEN 'abnormal' THEN RETURN '异常处理�?; WHEN 'cancelled' THEN RETURN '已取�?; WHEN 'settled' THEN RETURN '已结�?; WHEN 'archived' THEN RETURN '已归�?; ELSE RETURN '待接�?; END CASE; END; $$; CREATE OR REPLACE FUNCTION public.delivery_status_tone(p_front_status TEXT) RETURNS TEXT LANGUAGE plpgsql IMMUTABLE AS $$ BEGIN IF p_front_status IN ('pending_assignment', 'pending_accept', 'pending_acceptance', 'pending_confirm', 'pending_submit') THEN RETURN 'warning'; END IF; IF p_front_status IN ('accepted', 'waiting_departure', 'departed', 'on_the_way', 'arrived', 'checked_in', 'in_service', 'serving') THEN RETURN 'primary'; END IF; IF p_front_status IN ('completed', 'settled', 'archived') THEN RETURN 'success'; END IF; IF p_front_status IN ('rejected', 'cancelled', 'abnormal', 'exception_pending', 'terminated') THEN RETURN 'danger'; END IF; RETURN 'warning'; END; $$; CREATE OR REPLACE FUNCTION public.delivery_default_service_items(p_order_id TEXT, p_service_name TEXT) RETURNS JSONB LANGUAGE sql IMMUTABLE AS $$ SELECT jsonb_build_array( jsonb_build_object('id', p_order_id || '-item-1', 'name', '上门签到确认', 'required', true, 'completed', false, 'incompleteReason', '', 'remark', '', 'updatedAt', ''), jsonb_build_object('id', p_order_id || '-item-2', 'name', CASE WHEN position('护理' IN COALESCE(p_service_name, '')) > 0 THEN '基础护理执行' WHEN position('随访' IN COALESCE(p_service_name, '')) > 0 THEN '慢病指标采集' ELSE '服务项目执行' END, 'required', true, 'completed', false, 'incompleteReason', '', 'remark', '', 'updatedAt', ''), jsonb_build_object('id', p_order_id || '-item-3', 'name', '服务过程记录', 'required', true, 'completed', false, 'incompleteReason', '', 'remark', '', 'updatedAt', ''), jsonb_build_object('id', p_order_id || '-item-4', 'name', '家属沟通反�?, 'required', true, 'completed', false, 'incompleteReason', '', 'remark', '', 'updatedAt', '') ) $$; CREATE OR REPLACE FUNCTION public.delivery_build_timeline(p_logs JSONB) RETURNS JSONB LANGUAGE sql IMMUTABLE AS $$ SELECT COALESCE( ( SELECT jsonb_agg( jsonb_build_object( 'id', COALESCE(item ->> 'id', md5(COALESCE(item::TEXT, ''))), 'title', public.delivery_status_text( public.delivery_front_status( CASE WHEN COALESCE(item ->> 'to_status', item ->> 'status', '') IN ('ORDER_ACCEPTED') THEN 'accepted' WHEN COALESCE(item ->> 'to_status', item ->> 'status', '') IN ('ORDER_CHECKED_IN') THEN 'arrived' WHEN COALESCE(item ->> 'to_status', item ->> 'status', '') IN ('ORDER_IN_SERVICE') THEN 'in_service' WHEN COALESCE(item ->> 'to_status', item ->> 'status', '') IN ('ACCEPTANCE_PENDING') THEN 'pending_acceptance' WHEN COALESCE(item ->> 'to_status', item ->> 'status', '') IN ('ORDER_EXCEPTION') THEN 'exception' WHEN COALESCE(item ->> 'to_status', item ->> 'status', '') IN ('ORDER_REJECTED') THEN 'rejected' ELSE lower(COALESCE(item ->> 'to_status', item ->> 'status', 'assigned')) END, item ) ), 'time', COALESCE(item ->> 'created_at', ''), 'description', COALESCE(item ->> 'remark', '') ) ORDER BY COALESCE(item ->> 'created_at', '') DESC ) FROM jsonb_array_elements(COALESCE(p_logs, '[]'::jsonb)) item ), '[]'::jsonb ) $$; CREATE OR REPLACE FUNCTION public.delivery_build_status_logs(p_logs JSONB, p_order_id TEXT) RETURNS JSONB LANGUAGE sql IMMUTABLE AS $$ SELECT COALESCE( ( SELECT jsonb_agg( jsonb_build_object( 'id', COALESCE(item ->> 'id', md5(COALESCE(item::TEXT, ''))), 'orderId', p_order_id, 'fromStatus', public.delivery_front_status(lower(COALESCE(item ->> 'from_status', 'assigned')), item), 'toStatus', public.delivery_front_status( CASE WHEN COALESCE(item ->> 'to_status', item ->> 'status', '') IN ('ORDER_ACCEPTED') THEN 'accepted' WHEN COALESCE(item ->> 'to_status', item ->> 'status', '') IN ('ORDER_CHECKED_IN') THEN 'arrived' WHEN COALESCE(item ->> 'to_status', item ->> 'status', '') IN ('ORDER_IN_SERVICE') THEN 'in_service' WHEN COALESCE(item ->> 'to_status', item ->> 'status', '') IN ('ACCEPTANCE_PENDING') THEN 'pending_acceptance' WHEN COALESCE(item ->> 'to_status', item ->> 'status', '') IN ('ORDER_EXCEPTION') THEN 'exception' WHEN COALESCE(item ->> 'to_status', item ->> 'status', '') IN ('ORDER_REJECTED') THEN 'rejected' ELSE lower(COALESCE(item ->> 'to_status', item ->> 'status', 'assigned')) END, item ), 'operatorRole', COALESCE(item ->> 'operator_role', item ->> 'actor_role', ''), 'operatorId', COALESCE(item ->> 'operator_id', item ->> 'actor_id', ''), 'remark', COALESCE(item ->> 'remark', ''), 'createdAt', COALESCE(item ->> 'created_at', '') ) ORDER BY COALESCE(item ->> 'created_at', '') DESC ) FROM jsonb_array_elements(COALESCE(p_logs, '[]'::jsonb)) item ), '[]'::jsonb ) $$; CREATE OR REPLACE FUNCTION public.delivery_build_evidence(p_evidence JSONB, p_order_id TEXT) RETURNS JSONB LANGUAGE sql IMMUTABLE AS $$ SELECT COALESCE( ( SELECT jsonb_agg( jsonb_build_object( 'id', COALESCE(item ->> 'id', md5(COALESCE(item::TEXT, ''))), 'orderId', p_order_id, 'phase', COALESCE(item ->> 'phase', 'service'), 'fileType', COALESCE(NULLIF(item ->> 'file_type', ''), 'image'), 'name', COALESCE(NULLIF(item ->> 'storage_path', ''), item ->> 'file_url', ''), 'url', COALESCE(item ->> 'file_url', ''), 'localPath', '', 'status', 'success', 'progress', 100, 'retryable', false, 'createdAt', COALESCE(item ->> 'created_at', '') ) ORDER BY COALESCE(item ->> 'created_at', '') DESC ) FROM jsonb_array_elements(COALESCE(p_evidence, '[]'::jsonb)) item ), '[]'::jsonb ) $$; CREATE OR REPLACE FUNCTION public.delivery_build_abnormal(p_exception JSONB, p_order_id TEXT) RETURNS JSONB LANGUAGE sql IMMUTABLE AS $$ SELECT CASE WHEN p_exception IS NULL THEN NULL ELSE jsonb_build_object( 'id', COALESCE(p_exception ->> 'id', ''), 'orderId', p_order_id, 'type', COALESCE(p_exception ->> 'exception_type', 'other'), 'description', COALESCE(p_exception ->> 'description', p_exception ->> 'remark', ''), 'occurredAt', COALESCE(p_exception ->> 'occurred_at', ''), 'locationText', COALESCE(p_exception ->> 'location_text', ''), 'images', COALESCE(p_exception -> 'images_json', '[]'::jsonb), 'needPlatformIntervention', COALESCE((p_exception ->> 'need_platform_intervention')::BOOLEAN, false), 'requestCancelOrder', COALESCE((p_exception ->> 'request_cancel_order')::BOOLEAN, false), 'requestReschedule', COALESCE((p_exception ->> 'request_reschedule')::BOOLEAN, false), 'createdAt', COALESCE(p_exception ->> 'created_at', '') ) END $$; CREATE OR REPLACE FUNCTION public.delivery_build_order_json( p_raw JSONB, p_logs JSONB DEFAULT '[]'::jsonb, p_records JSONB DEFAULT '[]'::jsonb, p_evidence JSONB DEFAULT '[]'::jsonb, p_exception JSONB DEFAULT NULL, p_source TEXT DEFAULT 'legacy' ) RETURNS JSONB LANGUAGE plpgsql IMMUTABLE AS $$ DECLARE v_service JSONB := COALESCE(p_raw -> 'service_snapshot_json', jsonb_build_object('category', COALESCE(p_raw ->> 'service_category', ''), 'price', COALESCE((p_raw ->> 'service_price')::NUMERIC, 0))); v_address JSONB := COALESCE(p_raw -> 'address_snapshot_json', '{}'::jsonb); v_raw_status TEXT := COALESCE(p_raw ->> 'status', 'assigned'); v_normalized_status TEXT; v_front_status TEXT; v_checkin_record JSONB; v_service_record JSONB; v_service_items JSONB; v_record_json JSONB; v_timeline JSONB; v_status_logs JSONB; v_evidence_list JSONB; BEGIN IF p_source = 'care' THEN IF COALESCE(p_raw ->> 'accepted_by_family_at', '') <> '' THEN v_normalized_status := 'completed'; ELSIF COALESCE(p_raw ->> 'acceptance_pending_at', '') <> '' THEN v_normalized_status := 'pending_acceptance'; ELSIF COALESCE(p_raw ->> 'service_started_at', '') <> '' THEN v_normalized_status := 'in_service'; ELSIF COALESCE(p_raw ->> 'checked_in_at', '') <> '' THEN v_normalized_status := 'arrived'; ELSIF COALESCE(p_raw ->> 'departed_at', '') <> '' THEN v_normalized_status := 'departed'; ELSIF COALESCE(p_raw ->> 'accepted_at', '') <> '' THEN v_normalized_status := 'accepted'; ELSE v_normalized_status := CASE v_raw_status WHEN 'ORDER_ACCEPTED' THEN 'accepted' WHEN 'ORDER_CHECKED_IN' THEN 'arrived' WHEN 'ORDER_IN_SERVICE' THEN 'in_service' WHEN 'ACCEPTANCE_PENDING' THEN 'pending_acceptance' WHEN 'ORDER_EXCEPTION' THEN 'exception' WHEN 'ORDER_REJECTED' THEN 'rejected' WHEN 'ORDER_CANCELLED' THEN 'cancelled' WHEN 'ORDER_COMPLETED' THEN 'completed' ELSE 'assigned' END; END IF; ELSE v_normalized_status := lower(v_raw_status); END IF; v_front_status := public.delivery_front_status(v_normalized_status, p_raw); v_timeline := public.delivery_build_timeline(p_logs); v_status_logs := public.delivery_build_status_logs(p_logs, COALESCE(p_raw ->> 'id', '')); v_evidence_list := public.delivery_build_evidence(p_evidence, COALESCE(p_raw ->> 'id', '')); SELECT item INTO v_checkin_record FROM jsonb_array_elements(COALESCE(p_records, '[]'::jsonb)) item WHERE COALESCE(item ->> 'record_type', item ->> 'care_record_type', '') = 'checkin' ORDER BY COALESCE(item ->> 'created_at', '') DESC LIMIT 1; SELECT item INTO v_service_record FROM jsonb_array_elements(COALESCE(p_records, '[]'::jsonb)) item WHERE COALESCE(item ->> 'record_type', item ->> 'care_record_type', '') <> 'checkin' AND COALESCE(item ->> 'record_type', item ->> 'care_record_type', '') <> 'review' ORDER BY COALESCE(item ->> 'created_at', '') DESC LIMIT 1; IF v_service_record IS NULL THEN v_service_record := v_checkin_record; END IF; v_service_items := COALESCE(v_service_record -> 'service_items_json', public.delivery_default_service_items(COALESCE(p_raw ->> 'id', ''), COALESCE(p_raw ->> 'service_name', ''))); IF v_service_record IS NULL THEN v_record_json := NULL; ELSE v_record_json := jsonb_build_object( 'id', COALESCE(v_service_record ->> 'id', ''), 'orderId', COALESCE(p_raw ->> 'id', ''), 'startTime', COALESCE(v_service_record ->> 'started_at', v_service_record ->> 'service_started_at', ''), 'endTime', COALESCE(v_service_record ->> 'finished_at', v_service_record ->> 'service_finished_at', ''), 'actualDurationMinutes', COALESCE((v_service_record ->> 'duration_minutes')::INTEGER, (v_service_record ->> 'actual_duration_minutes')::INTEGER, 0), 'serviceItems', COALESCE(v_service_items, '[]'::jsonb), 'serviceContent', COALESCE( ( SELECT jsonb_agg(elem ->> 'name') FROM jsonb_array_elements(COALESCE(v_service_items, '[]'::jsonb)) elem WHERE COALESCE((elem ->> 'completed')::BOOLEAN, false) ), '[]'::jsonb ), 'processNote', COALESCE(v_service_record ->> 'summary', v_service_record ->> 'content', ''), 'elderStatus', '', 'healthMetrics', jsonb_build_object('bloodPressure', '', 'heartRate', '', 'bloodSugar', '', 'bloodOxygen', ''), 'materialsUsed', '', 'abnormalNote', '', 'photos', COALESCE( ( SELECT jsonb_agg(item ->> 'file_url') FROM jsonb_array_elements(COALESCE(p_evidence, '[]'::jsonb)) item WHERE COALESCE(item ->> 'phase', '') = 'service' ), '[]'::jsonb ), 'staffRemark', COALESCE(v_service_record ->> 'remark', ''), 'familyConfirmation', jsonb_build_object('method', 'none', 'code', '', 'signatureName', '', 'signatureUrl', '', 'confirmedAt', ''), 'createdAt', COALESCE(v_service_record ->> 'created_at', ''), 'updatedAt', COALESCE(v_service_record ->> 'updated_at', '') ); END IF; RETURN jsonb_build_object( 'id', COALESCE(p_raw ->> 'id', ''), 'orderNo', COALESCE(NULLIF(p_raw ->> 'task_no', ''), p_raw ->> 'order_no', ''), 'serviceType', COALESCE(NULLIF(v_service ->> 'category', ''), '居家服务'), 'serviceName', COALESCE(p_raw ->> 'service_name', ''), 'serviceCategory', COALESCE(v_service ->> 'category', ''), 'serviceItems', COALESCE(v_service_items, '[]'::jsonb), 'elderId', COALESCE(p_raw ->> 'elder_id', p_raw ->> 'user_id', ''), 'elderName', COALESCE(NULLIF(p_raw ->> 'recipient_name', ''), p_raw ->> 'elder_name', ''), 'elderNameMasked', COALESCE(NULLIF(p_raw ->> 'recipient_name', ''), p_raw ->> 'elder_name', ''), 'elderGender', '', 'elderAge', 0, 'elderPhone', COALESCE(NULLIF(p_raw ->> 'recipient_phone', ''), p_raw ->> 'elder_phone', ''), 'elderPhoneMasked', COALESCE(NULLIF(p_raw ->> 'recipient_phone', ''), p_raw ->> 'elder_phone', ''), 'fullElderName', COALESCE(NULLIF(p_raw ->> 'recipient_name', ''), p_raw ->> 'elder_name', ''), 'fullPhone', COALESCE(NULLIF(p_raw ->> 'recipient_phone', ''), p_raw ->> 'elder_phone', ''), 'contactRelation', '家属', 'addressSummary', COALESCE(NULLIF(v_address ->> 'fullAddress', ''), NULLIF(v_address ->> 'full_address', ''), NULLIF(v_address ->> 'address', ''), ''), 'address', COALESCE(NULLIF(v_address ->> 'fullAddress', ''), NULLIF(v_address ->> 'full_address', ''), NULLIF(v_address ->> 'address', ''), ''), 'addressDetail', COALESCE(NULLIF(v_address ->> 'detailAddress', ''), NULLIF(v_address ->> 'detail_address', ''), ''), 'fullAddress', COALESCE(NULLIF(v_address ->> 'fullAddress', ''), NULLIF(v_address ->> 'full_address', ''), NULLIF(v_address ->> 'address', ''), ''), 'latitude', COALESCE((v_address ->> 'latitude')::NUMERIC, 0), 'longitude', COALESCE((v_address ->> 'longitude')::NUMERIC, 0), 'appointmentTime', COALESCE(NULLIF(p_raw ->> 'appointment_time', ''), p_raw ->> 'scheduled_at', ''), 'appointmentStartTime', COALESCE(NULLIF(p_raw ->> 'appointment_time', ''), p_raw ->> 'scheduled_at', ''), 'appointmentEndTime', COALESCE(NULLIF(p_raw ->> 'appointment_time', ''), p_raw ->> 'scheduled_at', ''), 'duration', COALESCE((p_raw ->> 'duration_minutes')::INTEGER, 90), 'estimatedDuration', COALESCE((p_raw ->> 'duration_minutes')::INTEGER, 90), 'price', COALESCE((v_service ->> 'price')::NUMERIC, 0), 'staffIncome', COALESCE((v_service ->> 'price')::NUMERIC, 0), 'distance', '', 'actualStartTime', COALESCE(p_raw ->> 'service_started_at', ''), 'actualEndTime', COALESCE(NULLIF(p_raw ->> 'completed_at', ''), p_raw ->> 'service_completed_at', ''), 'status', v_front_status, 'statusText', public.delivery_status_text(v_front_status), 'statusTone', public.delivery_status_tone(v_front_status), 'riskTags', '[]'::jsonb, 'healthTags', '[]'::jsonb, 'careLevel', COALESCE(v_service ->> 'category', ''), 'needFamilyPresent', false, 'needMaterials', false, 'remark', COALESCE(p_raw ->> 'remark', ''), 'merchantId', COALESCE(p_raw ->> 'merchant_id', ''), 'merchantName', COALESCE(p_raw ->> 'merchant_name', ''), 'deliveryStaffId', COALESCE(NULLIF(p_raw ->> 'current_staff_id', ''), p_raw ->> 'assigned_to', ''), 'deliveryStaffName', COALESCE(p_raw ->> 'delivery_staff_name', ''), 'acceptTime', COALESCE(p_raw ->> 'accepted_at', ''), 'departTime', COALESCE(p_raw ->> 'departed_at', ''), 'arriveTime', COALESCE(NULLIF(p_raw ->> 'arrived_at', ''), p_raw ->> 'checked_in_at', ''), 'checkinTime', COALESCE(NULLIF(p_raw ->> 'checked_in_at', ''), p_raw ->> 'arrived_at', ''), 'startServiceTime', COALESCE(p_raw ->> 'service_started_at', ''), 'finishTime', COALESCE(NULLIF(p_raw ->> 'completed_at', ''), p_raw ->> 'service_completed_at', ''), 'cancelReason', COALESCE(p_raw ->> 'cancel_reason', ''), 'exceptionType', COALESCE(p_exception ->> 'exception_type', ''), 'exceptionDesc', COALESCE(p_exception ->> 'description', p_exception ->> 'remark', ''), 'evidenceList', COALESCE(v_evidence_list, '[]'::jsonb), 'signatureUrl', '', 'signatureName', '', 'satisfactionStatus', CASE WHEN v_front_status = 'pending_acceptance' THEN '待验�? WHEN v_front_status = 'completed' THEN '已验�? ELSE '待评�? END, 'settlementStatus', CASE WHEN v_front_status = 'completed' THEN '待结�? ELSE '待确�? END, 'archiveStatus', '未归�?, 'createdAt', COALESCE(p_raw ->> 'created_at', ''), 'updatedAt', COALESCE(p_raw ->> 'updated_at', ''), 'contactName', COALESCE(p_raw ->> 'contact_name', ''), 'contactPhone', COALESCE(p_raw ->> 'contact_phone', ''), 'notices', '[]'::jsonb, 'timeline', COALESCE(v_timeline, '[]'::jsonb), 'statusLog', COALESCE(v_status_logs, '[]'::jsonb), 'serviceSummary', COALESCE(v_service_record ->> 'summary', v_service_record ->> 'content', ''), 'progressNote', COALESCE(v_service_record ->> 'remark', ''), 'distanceKm', '', 'allowCheckinRadiusMeters', 100, 'lastLocation', CASE WHEN v_checkin_record IS NULL THEN NULL ELSE jsonb_build_object( 'latitude', COALESCE((v_checkin_record ->> 'latitude')::NUMERIC, (v_checkin_record ->> 'checkin_latitude')::NUMERIC, 0), 'longitude', COALESCE((v_checkin_record ->> 'longitude')::NUMERIC, (v_checkin_record ->> 'checkin_longitude')::NUMERIC, 0), 'address', COALESCE(v_checkin_record ->> 'location_text', v_checkin_record ->> 'checkin_address', ''), 'time', COALESCE(v_checkin_record ->> 'checked_in_at', v_checkin_record ->> 'checkin_time', '') ) END, 'trackPoints', COALESCE(v_service_record -> 'track_points_json', '[]'::jsonb), 'serviceRecord', v_record_json, 'abnormalReport', public.delivery_build_abnormal(p_exception, COALESCE(p_raw ->> 'id', '')) ); END; $$; CREATE OR REPLACE FUNCTION public.delivery_get_legacy_order_json(p_order_id TEXT) RETURNS JSONB LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = public AS $$ DECLARE v_staff_id UUID := public.delivery_current_staff_id(); v_raw JSONB; v_logs JSONB := '[]'::jsonb; v_records JSONB := '[]'::jsonb; v_evidence JSONB := '[]'::jsonb; BEGIN SELECT to_jsonb(o) INTO v_raw FROM public.hss_service_orders o WHERE o.id = p_order_id AND o.deleted_at IS NULL AND (v_staff_id IS NULL OR o.current_staff_id = v_staff_id) LIMIT 1; IF v_raw IS NULL THEN RETURN NULL; END IF; SELECT COALESCE(jsonb_agg(to_jsonb(l) ORDER BY l.created_at DESC), '[]'::jsonb) INTO v_logs FROM public.hss_service_order_status_logs l WHERE l.order_id = p_order_id; SELECT COALESCE(jsonb_agg(to_jsonb(r) ORDER BY r.created_at DESC), '[]'::jsonb) INTO v_records FROM public.hss_service_execution_records r WHERE r.order_id = p_order_id; SELECT COALESCE(jsonb_agg(to_jsonb(e) ORDER BY e.created_at DESC), '[]'::jsonb) INTO v_evidence FROM public.hss_service_evidence_files e WHERE e.order_id = p_order_id; RETURN public.delivery_build_order_json(v_raw, v_logs, v_records, v_evidence, NULL, 'legacy'); END; $$; CREATE OR REPLACE FUNCTION public.delivery_get_care_order_json(p_order_id TEXT) RETURNS JSONB LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = public AS $$ DECLARE v_user_id UUID := public.delivery_current_user_id(); v_raw JSONB; v_logs JSONB := '[]'::jsonb; v_records JSONB := '[]'::jsonb; v_evidence JSONB := '[]'::jsonb; v_exception JSONB; BEGIN IF NOT public.delivery_table_exists('ec_care_tasks') THEN RETURN NULL; END IF; BEGIN EXECUTE 'SELECT to_jsonb(t) FROM public.ec_care_tasks t WHERE t.id = $1::uuid AND ($2 IS NULL OR t.assigned_to = $2) LIMIT 1' INTO v_raw USING p_order_id, v_user_id; EXCEPTION WHEN OTHERS THEN RETURN NULL; END; IF v_raw IS NULL THEN RETURN NULL; END IF; BEGIN IF public.delivery_table_exists('hc_work_order_events') THEN EXECUTE 'SELECT COALESCE(jsonb_agg(to_jsonb(e) ORDER BY e.created_at DESC), ''[]''::jsonb) FROM public.hc_work_order_events e WHERE e.task_id = $1::uuid' INTO v_logs USING p_order_id; END IF; IF public.delivery_table_exists('ec_care_records') THEN EXECUTE 'SELECT COALESCE(jsonb_agg(to_jsonb(r) ORDER BY r.created_at DESC), ''[]''::jsonb) FROM public.ec_care_records r WHERE r.task_id = $1::uuid' INTO v_records USING p_order_id; END IF; IF public.delivery_table_exists('hc_evidence_files') THEN EXECUTE 'SELECT COALESCE(jsonb_agg(to_jsonb(f) ORDER BY f.created_at DESC), ''[]''::jsonb) FROM public.hc_evidence_files f WHERE f.task_id = $1::uuid' INTO v_evidence USING p_order_id; END IF; IF public.delivery_table_exists('hc_work_order_exceptions') THEN EXECUTE 'SELECT to_jsonb(x) FROM public.hc_work_order_exceptions x WHERE x.task_id = $1::uuid ORDER BY x.created_at DESC LIMIT 1' INTO v_exception USING p_order_id; END IF; EXCEPTION WHEN OTHERS THEN NULL; END; RETURN public.delivery_build_order_json(v_raw, v_logs, v_records, v_evidence, v_exception, 'care'); END; $$; CREATE OR REPLACE FUNCTION public.delivery_get_order_json(p_order_id TEXT) RETURNS JSONB LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = public AS $$ DECLARE v_order JSONB; BEGIN v_order := public.delivery_get_care_order_json(p_order_id); IF v_order IS NOT NULL THEN RETURN v_order; END IF; RETURN public.delivery_get_legacy_order_json(p_order_id); END; $$; CREATE OR REPLACE FUNCTION public.delivery_order_matches(p_order JSONB, p_tab TEXT, p_keyword TEXT) RETURNS BOOLEAN LANGUAGE plpgsql IMMUTABLE AS $$ DECLARE v_status TEXT := COALESCE(p_order ->> 'status', 'pending_assignment'); v_keyword TEXT := btrim(COALESCE(p_keyword, '')); BEGIN IF COALESCE(p_tab, '') NOT IN ('', 'all') THEN CASE p_tab WHEN 'pending_accept' THEN IF v_status NOT IN ('pending_assignment', 'pending_accept') THEN RETURN FALSE; END IF; WHEN 'accepted' THEN IF v_status NOT IN ('accepted', 'waiting_departure', 'departed', 'on_the_way', 'arrived') THEN RETURN FALSE; END IF; WHEN 'serving' THEN IF v_status NOT IN ('in_service', 'serving') THEN RETURN FALSE; END IF; WHEN 'pending_submit' THEN IF v_status NOT IN ('pending_confirm', 'pending_submit', 'pending_acceptance') THEN RETURN FALSE; END IF; WHEN 'completed' THEN IF v_status NOT IN ('completed', 'settled', 'archived') THEN RETURN FALSE; END IF; WHEN 'exception' THEN IF v_status NOT IN ('abnormal', 'exception_pending') THEN RETURN FALSE; END IF; END CASE; END IF; IF v_keyword <> '' THEN IF COALESCE(p_order ->> 'orderNo', '') NOT ILIKE '%' || v_keyword || '%' AND COALESCE(p_order ->> 'serviceName', '') NOT ILIKE '%' || v_keyword || '%' AND COALESCE(p_order ->> 'elderNameMasked', '') NOT ILIKE '%' || v_keyword || '%' AND COALESCE(p_order ->> 'addressSummary', '') NOT ILIKE '%' || v_keyword || '%' THEN RETURN FALSE; END IF; END IF; RETURN TRUE; END; $$; CREATE OR REPLACE FUNCTION public.delivery_append_if_match(p_result JSONB, p_order JSONB, p_tab TEXT, p_keyword TEXT) RETURNS JSONB LANGUAGE plpgsql IMMUTABLE AS $$ BEGIN IF p_order IS NULL THEN RETURN COALESCE(p_result, '[]'::jsonb); END IF; IF public.delivery_order_matches(p_order, p_tab, p_keyword) THEN RETURN COALESCE(p_result, '[]'::jsonb) || jsonb_build_array(p_order); END IF; RETURN COALESCE(p_result, '[]'::jsonb); END; $$; CREATE OR REPLACE FUNCTION public.delivery_build_staff_info(p_staff_id UUID) RETURNS JSONB LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT jsonb_build_object( 'id', s.id, 'userId', COALESCE(s.uid::TEXT, ''), 'staffNo', COALESCE(NULLIF(s.staff_no, ''), 'DEL-' || upper(substr(s.id::TEXT, 1, 8))), 'name', COALESCE(NULLIF(s.nickname, ''), '服务人员'), 'phone', COALESCE(s.phone, ''), 'role', 'delivery', 'status', CASE WHEN s.status = 0 OR COALESCE(s.is_active, true) = false THEN 'disabled' ELSE 'active' END, 'organizationId', COALESCE(s.station_id::TEXT, ''), 'organizationName', COALESCE(st.name, COALESCE(s.station_id::TEXT, '')), 'certificates', '[]'::jsonb, 'certificateStatus', CASE WHEN COALESCE(s.certificate_status, '') IN ('valid', 'expired') THEN s.certificate_status ELSE 'pending' END, 'certificateExpireAt', COALESCE(to_char(s.certificate_expire_at, 'YYYY-MM-DD'), ''), 'onlineStatus', CASE WHEN s.online_status IN ('online', 'busy') THEN s.online_status ELSE 'resting' END, 'serviceArea', COALESCE(s.service_area, ''), 'skills', CASE WHEN COALESCE(s.skills, '') = '' THEN '[]'::jsonb ELSE COALESCE(s.skills::jsonb, '[]'::jsonb) END, 'avatarUrl', COALESCE(s.avatar, ''), 'todayAccepted', 0, 'todayServing', 0, 'todayCompleted', 0, 'usesMock', false ) FROM public.ml_delivery_staff s LEFT JOIN public.ml_delivery_stations st ON st.id = s.station_id WHERE s.id = p_staff_id AND s.deleted_at IS NULL LIMIT 1 $$; CREATE OR REPLACE FUNCTION public.delivery_assert_staff_access(p_staff_id UUID) RETURNS VOID LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = public AS $$ DECLARE v_current_staff_id UUID := public.delivery_current_staff_id(); BEGIN IF auth.uid() IS NULL THEN RAISE EXCEPTION 'Permission denied'; END IF; IF p_staff_id IS NOT NULL AND v_current_staff_id IS NOT NULL AND p_staff_id <> v_current_staff_id THEN RAISE EXCEPTION 'Permission denied'; END IF; END; $$; CREATE OR REPLACE FUNCTION public.delivery_try_update_care_task(p_order_id TEXT, p_status TEXT, p_patch JSONB, p_action TEXT, p_remark TEXT) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_user_id UUID := public.delivery_current_user_id(); v_now TIMESTAMPTZ := now(); BEGIN IF NOT public.delivery_table_exists('ec_care_tasks') THEN RETURN NULL; END IF; BEGIN EXECUTE $sql$ UPDATE public.ec_care_tasks SET status = COALESCE($2, status), accepted_at = COALESCE(($3 ->> 'accepted_at')::timestamptz, accepted_at), departed_at = COALESCE(($3 ->> 'departed_at')::timestamptz, departed_at), checked_in_at = COALESCE(($3 ->> 'checked_in_at')::timestamptz, checked_in_at), service_started_at = COALESCE(($3 ->> 'service_started_at')::timestamptz, service_started_at), service_completed_at = COALESCE(($3 ->> 'service_completed_at')::timestamptz, service_completed_at), acceptance_pending_at = COALESCE(($3 ->> 'acceptance_pending_at')::timestamptz, acceptance_pending_at), reject_reason = COALESCE($3 ->> 'reject_reason', reject_reason), updated_at = COALESCE(($3 ->> 'updated_at')::timestamptz, $4) WHERE id = $1::uuid AND ($5 IS NULL OR assigned_to = $5) $sql$ USING p_order_id, p_status, COALESCE(p_patch, '{}'::jsonb), v_now, v_user_id; EXCEPTION WHEN OTHERS THEN RETURN NULL; END; BEGIN IF public.delivery_table_exists('hc_work_order_events') THEN EXECUTE $sql$ INSERT INTO public.hc_work_order_events(id, task_id, from_status, to_status, actor_id, actor_role, action, remark, created_at) VALUES (md5(random()::text || clock_timestamp()::text), $1::uuid, NULL, $2, $3, 'delivery', $4, $5, $6) $sql$ USING p_order_id, p_status, v_user_id, COALESCE(p_action, ''), COALESCE(p_remark, ''), v_now; END IF; EXCEPTION WHEN OTHERS THEN NULL; END; RETURN public.delivery_get_care_order_json(p_order_id); END; $$; CREATE OR REPLACE FUNCTION public.rpc_delivery_order_detail(p_order_id TEXT) RETURNS JSONB LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = public AS $$ BEGIN IF auth.uid() IS NULL THEN RAISE EXCEPTION 'Permission denied'; END IF; RETURN public.delivery_get_order_json(p_order_id); END; $$; CREATE OR REPLACE FUNCTION public.rpc_delivery_order_list( p_staff_id UUID DEFAULT NULL, p_tab TEXT DEFAULT 'all', p_keyword TEXT DEFAULT '' ) RETURNS JSONB LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = public AS $$ DECLARE v_user_id UUID := public.delivery_current_user_id(); v_staff_id UUID := COALESCE(p_staff_id, public.delivery_current_staff_id()); v_result JSONB := '[]'::jsonb; v_task JSONB; v_order JSONB; v_order_id TEXT; BEGIN PERFORM public.delivery_assert_staff_access(v_staff_id); IF public.delivery_table_exists('ec_care_tasks') THEN BEGIN FOR v_task IN EXECUTE 'SELECT to_jsonb(t) FROM public.ec_care_tasks t WHERE ($1 IS NULL OR t.assigned_to = $1) ORDER BY t.created_at DESC' USING v_user_id LOOP v_order := public.delivery_get_care_order_json(v_task ->> 'id'); v_result := public.delivery_append_if_match(v_result, v_order, p_tab, p_keyword); END LOOP; EXCEPTION WHEN OTHERS THEN NULL; END; END IF; FOR v_order_id IN SELECT o.id FROM public.hss_service_orders o WHERE o.deleted_at IS NULL AND (v_staff_id IS NULL OR o.current_staff_id = v_staff_id) ORDER BY COALESCE(o.updated_at, o.created_at) DESC, o.created_at DESC LOOP v_order := public.delivery_get_legacy_order_json(v_order_id); v_result := public.delivery_append_if_match(v_result, v_order, p_tab, p_keyword); END LOOP; RETURN COALESCE(v_result, '[]'::jsonb); END; $$; CREATE OR REPLACE FUNCTION public.rpc_delivery_dashboard(p_staff_id UUID DEFAULT NULL) RETURNS JSONB LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = public AS $$ DECLARE v_staff_id UUID := COALESCE(p_staff_id, public.delivery_current_staff_id()); v_orders JSONB := public.rpc_delivery_order_list(v_staff_id, 'all', ''); v_item JSONB; v_pending_assignment_count INTEGER := 0; v_pending_accept_count INTEGER := 0; v_today_order_count INTEGER := 0; v_pending_depart_count INTEGER := 0; v_serving_count INTEGER := 0; v_completed_count INTEGER := 0; v_exception_count INTEGER := 0; v_next_order JSONB; BEGIN PERFORM public.delivery_assert_staff_access(v_staff_id); FOR v_item IN SELECT value FROM jsonb_array_elements(COALESCE(v_orders, '[]'::jsonb)) LOOP IF COALESCE(v_item ->> 'status', '') IN ('pending_assignment', 'pending_accept') THEN v_pending_assignment_count := v_pending_assignment_count + 1; v_pending_accept_count := v_pending_accept_count + 1; END IF; IF COALESCE(v_item ->> 'status', '') IN ('accepted', 'waiting_departure', 'departed', 'on_the_way', 'arrived') THEN v_pending_depart_count := v_pending_depart_count + 1; END IF; IF COALESCE(v_item ->> 'status', '') IN ('pending_assignment', 'pending_accept', 'accepted', 'waiting_departure', 'departed', 'on_the_way', 'arrived', 'in_service', 'serving', 'pending_acceptance') THEN v_today_order_count := v_today_order_count + 1; END IF; IF COALESCE(v_item ->> 'status', '') IN ('in_service', 'serving') THEN v_serving_count := v_serving_count + 1; END IF; IF COALESCE(v_item ->> 'status', '') IN ('completed', 'pending_acceptance', 'settled', 'archived') THEN v_completed_count := v_completed_count + 1; END IF; IF COALESCE(v_item ->> 'status', '') IN ('abnormal', 'exception_pending') THEN v_exception_count := v_exception_count + 1; END IF; IF v_next_order IS NULL AND COALESCE(v_item ->> 'status', '') NOT IN ('completed', 'settled', 'archived', 'cancelled', 'rejected', 'abnormal') THEN v_next_order := v_item; END IF; END LOOP; RETURN jsonb_build_object( 'pendingAssignmentCount', v_pending_assignment_count, 'pendingAcceptCount', v_pending_accept_count, 'todayOrderCount', v_today_order_count, 'pendingDepartCount', v_pending_depart_count, 'servingCount', v_serving_count, 'completedCount', v_completed_count, 'exceptionCount', v_exception_count, 'expectedIncome', 0, 'onlineStatus', COALESCE(public.delivery_build_staff_info(v_staff_id) ->> 'onlineStatus', 'resting'), 'nextOrder', v_next_order, 'recentOrders', COALESCE((SELECT jsonb_agg(value) FROM (SELECT value FROM jsonb_array_elements(COALESCE(v_orders, '[]'::jsonb)) LIMIT 5) q), '[]'::jsonb) ); END; $$; CREATE OR REPLACE FUNCTION public.rpc_delivery_accept_order(p_order_id TEXT) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_now TIMESTAMPTZ := now(); v_staff_id UUID := public.delivery_current_staff_id(); BEGIN IF public.delivery_get_care_order_json(p_order_id) IS NOT NULL THEN RETURN public.delivery_try_update_care_task( p_order_id, 'ORDER_ACCEPTED', jsonb_build_object('accepted_at', v_now, 'updated_at', v_now), 'accept_task', '服务人员接单' ); END IF; UPDATE public.hss_service_assignments SET status = 'accepted', accepted_at = v_now, updated_at = v_now WHERE order_id = p_order_id AND (v_staff_id IS NULL OR staff_id = v_staff_id); UPDATE public.hss_service_orders SET status = 'accepted', accepted_at = v_now, updated_at = v_now WHERE id = p_order_id AND deleted_at IS NULL AND (v_staff_id IS NULL OR current_staff_id = v_staff_id); INSERT INTO public.hss_service_order_status_logs(id, order_id, from_status, to_status, operator_id, operator_role, remark, created_at) VALUES (md5(random()::text || clock_timestamp()::text), p_order_id, 'assigned', 'accepted', public.delivery_current_user_id(), 'delivery', '服务人员接单', v_now); RETURN public.delivery_get_legacy_order_json(p_order_id); END; $$; CREATE OR REPLACE FUNCTION public.rpc_delivery_reject_order(p_order_id TEXT, p_reason TEXT DEFAULT '') RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_now TIMESTAMPTZ := now(); v_staff_id UUID := public.delivery_current_staff_id(); v_reason TEXT := COALESCE(p_reason, ''); BEGIN IF public.delivery_get_care_order_json(p_order_id) IS NOT NULL THEN RETURN public.delivery_try_update_care_task( p_order_id, 'ORDER_REJECTED', jsonb_build_object('reject_reason', v_reason, 'updated_at', v_now), 'reject_task', CASE WHEN v_reason = '' THEN '服务人员拒单' ELSE v_reason END ); END IF; UPDATE public.hss_service_assignments SET status = 'rejected', rejected_at = v_now, reject_reason = v_reason, updated_at = v_now WHERE order_id = p_order_id AND (v_staff_id IS NULL OR staff_id = v_staff_id); UPDATE public.hss_service_orders SET status = 'rejected', updated_at = v_now WHERE id = p_order_id AND deleted_at IS NULL AND (v_staff_id IS NULL OR current_staff_id = v_staff_id); INSERT INTO public.hss_service_order_status_logs(id, order_id, from_status, to_status, operator_id, operator_role, remark, created_at) VALUES (md5(random()::text || clock_timestamp()::text), p_order_id, 'assigned', 'rejected', public.delivery_current_user_id(), 'delivery', CASE WHEN v_reason = '' THEN '服务人员拒单' ELSE v_reason END, v_now); RETURN public.delivery_get_legacy_order_json(p_order_id); END; $$; CREATE OR REPLACE FUNCTION public.rpc_delivery_start_depart(p_order_id TEXT, p_location JSONB DEFAULT '{}'::jsonb) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_now TIMESTAMPTZ := now(); v_staff_id UUID := public.delivery_current_staff_id(); v_remark TEXT := CASE WHEN COALESCE(p_location ->> 'address', '') = '' THEN '服务人员出发' ELSE '服务人员出发�? || (p_location ->> 'address') END; BEGIN IF public.delivery_get_care_order_json(p_order_id) IS NOT NULL THEN RETURN public.delivery_try_update_care_task( p_order_id, 'ORDER_ACCEPTED', jsonb_build_object('departed_at', v_now, 'updated_at', v_now), 'depart_task', v_remark ); END IF; UPDATE public.hss_service_orders SET status = 'departed', departed_at = v_now, updated_at = v_now WHERE id = p_order_id AND deleted_at IS NULL AND (v_staff_id IS NULL OR current_staff_id = v_staff_id); INSERT INTO public.hss_service_order_status_logs(id, order_id, from_status, to_status, operator_id, operator_role, remark, created_at) VALUES (md5(random()::text || clock_timestamp()::text), p_order_id, 'accepted', 'departed', public.delivery_current_user_id(), 'delivery', v_remark, v_now); RETURN public.delivery_get_legacy_order_json(p_order_id); END; $$; CREATE OR REPLACE FUNCTION public.rpc_delivery_arrive_order(p_order_id TEXT, p_location JSONB DEFAULT '{}'::jsonb) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_now TIMESTAMPTZ := now(); v_staff_id UUID := public.delivery_current_staff_id(); v_remark TEXT := CASE WHEN COALESCE(p_location ->> 'address', '') = '' THEN '服务人员到达' ELSE '服务人员到达�? || (p_location ->> 'address') END; BEGIN IF public.delivery_get_care_order_json(p_order_id) IS NOT NULL THEN RETURN public.delivery_try_update_care_task( p_order_id, 'ORDER_CHECKED_IN', jsonb_build_object('checked_in_at', v_now, 'updated_at', v_now), 'arrive_task', v_remark ); END IF; UPDATE public.hss_service_orders SET status = 'arrived', arrived_at = v_now, updated_at = v_now WHERE id = p_order_id AND deleted_at IS NULL AND (v_staff_id IS NULL OR current_staff_id = v_staff_id); INSERT INTO public.hss_service_order_status_logs(id, order_id, from_status, to_status, operator_id, operator_role, remark, created_at) VALUES (md5(random()::text || clock_timestamp()::text), p_order_id, 'departed', 'arrived', public.delivery_current_user_id(), 'delivery', v_remark, v_now); RETURN public.delivery_get_legacy_order_json(p_order_id); END; $$; CREATE OR REPLACE FUNCTION public.rpc_delivery_checkin_order(p_order_id TEXT, p_payload JSONB DEFAULT '{}'::jsonb) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_now TIMESTAMPTZ := now(); v_staff_id UUID := public.delivery_current_staff_id(); v_assignment_id TEXT := ''; v_record_id TEXT := 'ser-' || replace(gen_random_uuid()::TEXT, '-', ''); v_file TEXT; BEGIN IF public.delivery_get_care_order_json(p_order_id) IS NOT NULL THEN BEGIN IF public.delivery_table_exists('ec_care_records') THEN EXECUTE $sql$ INSERT INTO public.ec_care_records(id, task_id, record_type, started_at, checked_in_at, latitude, longitude, location_text, remark, created_at, updated_at) VALUES ($1, $2::uuid, 'checkin', $3, $3, ($4 -> 'location' ->> 'latitude')::double precision, ($4 -> 'location' ->> 'longitude')::double precision, COALESCE($4 -> 'location' ->> 'address', ''), COALESCE($4 ->> 'note', ''), $3, $3) $sql$ USING 'care-checkin-' || replace(gen_random_uuid()::TEXT, '-', ''), p_order_id, v_now, COALESCE(p_payload, '{}'::jsonb); END IF; IF public.delivery_table_exists('hc_evidence_files') THEN FOR v_file IN SELECT jsonb_array_elements_text(COALESCE(p_payload -> 'photos', '[]'::jsonb)) LOOP EXECUTE $sql$ INSERT INTO public.hc_evidence_files(id, task_id, care_record_id, phase, file_type, storage_path, file_url, latitude, longitude, captured_at, created_at) VALUES ($1, $2::uuid, NULL, 'checkin', 'image', $3, $3, ($4 -> 'location' ->> 'latitude')::double precision, ($4 -> 'location' ->> 'longitude')::double precision, $5, $5) $sql$ USING 'hc-ef-' || replace(gen_random_uuid()::TEXT, '-', ''), p_order_id, v_file, COALESCE(p_payload, '{}'::jsonb), v_now; END LOOP; END IF; EXCEPTION WHEN OTHERS THEN NULL; END; RETURN public.delivery_try_update_care_task( p_order_id, 'ORDER_CHECKED_IN', jsonb_build_object('checked_in_at', v_now, 'updated_at', v_now), 'checkin_task', COALESCE(NULLIF(p_payload ->> 'note', ''), '到场签到') ); END IF; SELECT COALESCE(a.id, '') INTO v_assignment_id FROM public.hss_service_assignments a WHERE a.order_id = p_order_id AND (v_staff_id IS NULL OR a.staff_id = v_staff_id) ORDER BY a.created_at DESC LIMIT 1; INSERT INTO public.hss_service_execution_records( id, order_id, assignment_id, checkin_time, checkin_latitude, checkin_longitude, checkin_address, remark, created_at, updated_at ) VALUES ( v_record_id, p_order_id, v_assignment_id, v_now, COALESCE((p_payload -> 'location' ->> 'latitude')::double precision, 0), COALESCE((p_payload -> 'location' ->> 'longitude')::double precision, 0), COALESCE(p_payload -> 'location' ->> 'address', ''), COALESCE(p_payload ->> 'note', ''), v_now, v_now ) ON CONFLICT (id) DO UPDATE SET checkin_time = EXCLUDED.checkin_time, checkin_latitude = EXCLUDED.checkin_latitude, checkin_longitude = EXCLUDED.checkin_longitude, checkin_address = EXCLUDED.checkin_address, remark = EXCLUDED.remark, updated_at = EXCLUDED.updated_at; FOR v_file IN SELECT jsonb_array_elements_text(COALESCE(p_payload -> 'photos', '[]'::jsonb)) LOOP INSERT INTO public.hss_service_evidence_files(id, order_id, execution_record_id, phase, file_type, storage_path, file_url, latitude, longitude, captured_at, created_at) VALUES ( 'sef-' || replace(gen_random_uuid()::TEXT, '-', ''), p_order_id, v_record_id, 'checkin', 'image', v_file, v_file, COALESCE((p_payload -> 'location' ->> 'latitude')::double precision, 0), COALESCE((p_payload -> 'location' ->> 'longitude')::double precision, 0), v_now, v_now ); END LOOP; RETURN public.delivery_get_legacy_order_json(p_order_id); END; $$; CREATE OR REPLACE FUNCTION public.rpc_delivery_start_service(p_order_id TEXT) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_now TIMESTAMPTZ := now(); v_staff_id UUID := public.delivery_current_staff_id(); BEGIN IF public.delivery_get_care_order_json(p_order_id) IS NOT NULL THEN RETURN public.delivery_try_update_care_task( p_order_id, 'ORDER_IN_SERVICE', jsonb_build_object('service_started_at', v_now, 'updated_at', v_now), 'start_service', '开始服�? ); END IF; UPDATE public.hss_service_execution_records SET service_started_at = v_now, updated_at = v_now WHERE order_id = p_order_id; UPDATE public.hss_service_orders SET status = 'in_service', service_started_at = v_now, updated_at = v_now WHERE id = p_order_id AND deleted_at IS NULL AND (v_staff_id IS NULL OR current_staff_id = v_staff_id); INSERT INTO public.hss_service_order_status_logs(id, order_id, from_status, to_status, operator_id, operator_role, remark, created_at) VALUES (md5(random()::text || clock_timestamp()::text), p_order_id, 'arrived', 'in_service', public.delivery_current_user_id(), 'delivery', '开始服�?, v_now); RETURN public.delivery_get_legacy_order_json(p_order_id); END; $$; CREATE OR REPLACE FUNCTION public.rpc_delivery_save_progress(p_order_id TEXT, p_payload JSONB DEFAULT '{}'::jsonb) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_now TIMESTAMPTZ := now(); v_staff_id UUID := public.delivery_current_staff_id(); v_assignment_id TEXT := ''; v_record_id TEXT := 'progress-' || replace(gen_random_uuid()::TEXT, '-', ''); BEGIN IF public.delivery_get_care_order_json(p_order_id) IS NOT NULL THEN BEGIN IF public.delivery_table_exists('ec_care_records') THEN EXECUTE $sql$ INSERT INTO public.ec_care_records(id, task_id, record_type, started_at, finished_at, duration_minutes, service_items_json, summary, remark, created_at, updated_at) VALUES ($1, $2::uuid, 'service', now(), NULL, 0, COALESCE($3 -> 'items', '[]'::jsonb), COALESCE($3 ->> 'serviceSummary', ''), COALESCE($3 ->> 'progressNote', ''), $4, $4) ON CONFLICT (id) DO UPDATE SET service_items_json = EXCLUDED.service_items_json, summary = EXCLUDED.summary, remark = EXCLUDED.remark, updated_at = EXCLUDED.updated_at $sql$ USING v_record_id, p_order_id, COALESCE(p_payload, '{}'::jsonb), v_now; END IF; EXCEPTION WHEN OTHERS THEN NULL; END; RETURN public.delivery_get_care_order_json(p_order_id); END IF; SELECT COALESCE(a.id, '') INTO v_assignment_id FROM public.hss_service_assignments a WHERE a.order_id = p_order_id AND (v_staff_id IS NULL OR a.staff_id = v_staff_id) ORDER BY a.created_at DESC LIMIT 1; INSERT INTO public.hss_service_execution_records( id, order_id, assignment_id, service_started_at, service_finished_at, actual_duration_minutes, service_items_json, summary, remark, created_at, updated_at ) VALUES ( v_record_id, p_order_id, v_assignment_id, v_now, NULL, 0, COALESCE(p_payload -> 'items', '[]'::jsonb), COALESCE(p_payload ->> 'serviceSummary', ''), COALESCE(p_payload ->> 'progressNote', ''), v_now, v_now ) ON CONFLICT (id) DO UPDATE SET service_items_json = EXCLUDED.service_items_json, summary = EXCLUDED.summary, remark = EXCLUDED.remark, updated_at = EXCLUDED.updated_at; RETURN public.delivery_get_legacy_order_json(p_order_id); END; $$; CREATE OR REPLACE FUNCTION public.rpc_delivery_upload_evidence( p_order_id TEXT, p_phase TEXT DEFAULT 'service', p_files TEXT[] DEFAULT ARRAY[]::TEXT[] ) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_now TIMESTAMPTZ := now(); v_file TEXT; v_result JSONB := '[]'::jsonb; BEGIN IF public.delivery_get_care_order_json(p_order_id) IS NOT NULL THEN BEGIN IF public.delivery_table_exists('hc_evidence_files') THEN FOREACH v_file IN ARRAY COALESCE(p_files, ARRAY[]::TEXT[]) LOOP EXECUTE $sql$ INSERT INTO public.hc_evidence_files(id, task_id, care_record_id, phase, file_type, storage_path, file_url, captured_at, created_at) VALUES ($1, $2::uuid, NULL, $3, 'image', $4, $4, $5, $5) $sql$ USING 'hc-ef-' || replace(gen_random_uuid()::TEXT, '-', ''), p_order_id, COALESCE(p_phase, 'service'), v_file, v_now; END LOOP; END IF; EXCEPTION WHEN OTHERS THEN NULL; END; v_result := COALESCE(public.delivery_get_care_order_json(p_order_id) -> 'evidenceList', '[]'::jsonb); RETURN v_result; END IF; FOREACH v_file IN ARRAY COALESCE(p_files, ARRAY[]::TEXT[]) LOOP INSERT INTO public.hss_service_evidence_files(id, order_id, execution_record_id, phase, file_type, storage_path, file_url, captured_at, created_at) VALUES ( 'sef-' || replace(gen_random_uuid()::TEXT, '-', ''), p_order_id, NULL, COALESCE(p_phase, 'service'), 'image', v_file, v_file, v_now, v_now ); END LOOP; RETURN COALESCE(public.delivery_get_legacy_order_json(p_order_id) -> 'evidenceList', '[]'::jsonb); END; $$; CREATE OR REPLACE FUNCTION public.rpc_delivery_retry_evidence(p_order_id TEXT, p_evidence_id TEXT) RETURNS JSONB LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = public AS $$ DECLARE v_order JSONB := public.delivery_get_order_json(p_order_id); v_item JSONB; BEGIN FOR v_item IN SELECT value FROM jsonb_array_elements(COALESCE(v_order -> 'evidenceList', '[]'::jsonb)) LOOP IF COALESCE(v_item ->> 'id', '') = COALESCE(p_evidence_id, '') THEN RETURN v_item; END IF; END LOOP; RETURN NULL; END; $$; CREATE OR REPLACE FUNCTION public.rpc_delivery_submit_exception(p_order_id TEXT, p_payload JSONB DEFAULT '{}'::jsonb) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_now TIMESTAMPTZ := now(); v_staff_id UUID := public.delivery_current_staff_id(); BEGIN IF public.delivery_get_care_order_json(p_order_id) IS NOT NULL THEN BEGIN IF public.delivery_table_exists('hc_work_order_exceptions') THEN EXECUTE $sql$ INSERT INTO public.hc_work_order_exceptions(id, task_id, exception_type, description, occurred_at, location_text, images_json, need_platform_intervention, request_cancel_order, request_reschedule, created_by, created_at, updated_at) VALUES ($1, $2::uuid, COALESCE($3 ->> 'exceptionType', 'other'), COALESCE($3 ->> 'description', ''), COALESCE(($3 ->> 'occurredAt')::timestamptz, $4), COALESCE($3 ->> 'locationText', ''), COALESCE($3 -> 'images', '[]'::jsonb), COALESCE(($3 ->> 'needPlatformIntervention')::boolean, false), COALESCE(($3 ->> 'requestCancelOrder')::boolean, false), COALESCE(($3 ->> 'requestReschedule')::boolean, false), $5, $4, $4) $sql$ USING 'hc-ex-' || replace(gen_random_uuid()::TEXT, '-', ''), p_order_id, COALESCE(p_payload, '{}'::jsonb), v_now, public.delivery_current_user_id(); END IF; EXCEPTION WHEN OTHERS THEN NULL; END; RETURN public.delivery_try_update_care_task( p_order_id, 'ORDER_EXCEPTION', jsonb_build_object('updated_at', v_now), 'report_exception', COALESCE(NULLIF(p_payload ->> 'description', ''), '异常上报') ); END IF; UPDATE public.hss_service_orders SET status = 'exception', updated_at = v_now WHERE id = p_order_id AND deleted_at IS NULL AND (v_staff_id IS NULL OR current_staff_id = v_staff_id); INSERT INTO public.hss_service_order_status_logs(id, order_id, from_status, to_status, operator_id, operator_role, remark, created_at) VALUES (md5(random()::text || clock_timestamp()::text), p_order_id, 'in_service', 'exception', public.delivery_current_user_id(), 'delivery', COALESCE(NULLIF(p_payload ->> 'description', ''), '异常上报'), v_now); RETURN public.delivery_get_legacy_order_json(p_order_id); END; $$; CREATE OR REPLACE FUNCTION public.rpc_delivery_finish_service(p_order_id TEXT, p_payload JSONB DEFAULT '{}'::jsonb) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_now TIMESTAMPTZ := now(); v_staff_id UUID := public.delivery_current_staff_id(); BEGIN IF public.delivery_get_care_order_json(p_order_id) IS NOT NULL THEN RETURN public.delivery_try_update_care_task( p_order_id, 'ACCEPTANCE_PENDING', jsonb_build_object('service_completed_at', v_now, 'acceptance_pending_at', v_now, 'updated_at', v_now), 'finish_service', '服务完成,等待用户验�? ); END IF; UPDATE public.hss_service_execution_records SET service_finished_at = v_now, updated_at = v_now WHERE order_id = p_order_id; UPDATE public.hss_service_orders SET status = 'pending_acceptance', completed_at = v_now, pending_acceptance_at = v_now, updated_at = v_now WHERE id = p_order_id AND deleted_at IS NULL AND (v_staff_id IS NULL OR current_staff_id = v_staff_id); INSERT INTO public.hss_service_order_status_logs(id, order_id, from_status, to_status, operator_id, operator_role, remark, created_at) VALUES (md5(random()::text || clock_timestamp()::text), p_order_id, 'in_service', 'pending_acceptance', public.delivery_current_user_id(), 'delivery', '服务完成,等待用户验�?, v_now); RETURN public.delivery_get_legacy_order_json(p_order_id); END; $$; CREATE OR REPLACE FUNCTION public.rpc_delivery_record_list(p_staff_id UUID DEFAULT NULL) RETURNS JSONB LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = public AS $$ DECLARE v_staff_id UUID := COALESCE(p_staff_id, public.delivery_current_staff_id()); v_orders JSONB := public.rpc_delivery_order_list(v_staff_id, 'all', ''); v_result JSONB := '[]'::jsonb; v_item JSONB; BEGIN PERFORM public.delivery_assert_staff_access(v_staff_id); FOR v_item IN SELECT value FROM jsonb_array_elements(COALESCE(v_orders, '[]'::jsonb)) LOOP IF COALESCE(v_item ->> 'status', '') IN ('pending_acceptance', 'completed', 'settled', 'archived', 'abnormal', 'exception_pending', 'cancelled', 'rejected') THEN v_result := v_result || jsonb_build_array(jsonb_build_object( 'id', 'record-' || COALESCE(v_item ->> 'id', ''), 'orderId', COALESCE(v_item ->> 'id', ''), 'orderNo', COALESCE(v_item ->> 'orderNo', ''), 'serviceName', COALESCE(v_item ->> 'serviceName', ''), 'elderName', COALESCE(v_item ->> 'elderName', ''), 'elderNameMasked', COALESCE(v_item ->> 'elderNameMasked', ''), 'status', COALESCE(v_item ->> 'status', ''), 'statusText', COALESCE(v_item ->> 'statusText', ''), 'appointmentStartTime', COALESCE(v_item ->> 'appointmentStartTime', ''), 'appointmentTime', COALESCE(v_item ->> 'appointmentTime', ''), 'actualStartTime', COALESCE(v_item ->> 'actualStartTime', ''), 'actualEndTime', COALESCE(v_item ->> 'actualEndTime', COALESCE(v_item ->> 'updatedAt', '')), 'staffIncome', COALESCE((v_item ->> 'staffIncome')::NUMERIC, 0), 'settlementStatus', COALESCE(v_item ->> 'settlementStatus', ''), 'acceptanceStatus', COALESCE(v_item ->> 'satisfactionStatus', ''), 'exceptionDesc', COALESCE(v_item ->> 'exceptionDesc', ''), 'ratingText', CASE WHEN COALESCE(v_item ->> 'status', '') IN ('completed', 'settled', 'archived') THEN '已验�? WHEN COALESCE(v_item ->> 'status', '') = 'pending_acceptance' THEN '待验�? ELSE '待补�? END, 'hasServiceRecord', (v_item -> 'serviceRecord') IS NOT NULL )); END IF; END LOOP; RETURN v_result; END; $$; CREATE OR REPLACE FUNCTION public.rpc_delivery_message_list() RETURNS JSONB LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = public AS $$ DECLARE v_orders JSONB := public.rpc_delivery_order_list(public.delivery_current_staff_id(), 'all', ''); v_result JSONB := '[]'::jsonb; v_item JSONB; BEGIN FOR v_item IN SELECT value FROM jsonb_array_elements(COALESCE(v_orders, '[]'::jsonb)) LIMIT 8 LOOP v_result := v_result || jsonb_build_array(jsonb_build_object( 'id', 'msg-' || COALESCE(v_item ->> 'id', ''), 'title', COALESCE(v_item ->> 'statusText', '工单提醒'), 'content', COALESCE(v_item ->> 'serviceName', '居家服务') || ',服务对象:' || COALESCE(v_item ->> 'elderNameMasked', '') || ',预约时间:' || COALESCE(v_item ->> 'appointmentStartTime', ''), 'type', 'order', 'createdAt', COALESCE(v_item ->> 'updatedAt', v_item ->> 'createdAt', ''), 'read', false, 'orderId', COALESCE(v_item ->> 'id', '') )); END LOOP; RETURN v_result; END; $$; CREATE OR REPLACE FUNCTION public.rpc_delivery_set_online_status(p_staff_id UUID, p_status TEXT) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_staff_id UUID := COALESCE(p_staff_id, public.delivery_current_staff_id()); v_info JSONB; BEGIN PERFORM public.delivery_assert_staff_access(v_staff_id); UPDATE public.ml_delivery_staff SET online_status = CASE WHEN p_status IN ('online', 'busy') THEN p_status ELSE 'resting' END, updated_at = now() WHERE id = v_staff_id AND deleted_at IS NULL; v_info := public.delivery_build_staff_info(v_staff_id); IF v_info IS NULL THEN RETURN NULL; END IF; RETURN v_info || jsonb_build_object('onlineStatus', CASE WHEN p_status IN ('online', 'busy') THEN p_status ELSE 'resting' END); END; $$; REVOKE ALL ON FUNCTION public.rpc_delivery_order_detail(TEXT) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.rpc_delivery_order_detail(TEXT) TO authenticated; REVOKE ALL ON FUNCTION public.rpc_delivery_order_list(UUID, TEXT, TEXT) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.rpc_delivery_order_list(UUID, TEXT, TEXT) TO authenticated; REVOKE ALL ON FUNCTION public.rpc_delivery_dashboard(UUID) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.rpc_delivery_dashboard(UUID) TO authenticated; REVOKE ALL ON FUNCTION public.rpc_delivery_accept_order(TEXT) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.rpc_delivery_accept_order(TEXT) TO authenticated; REVOKE ALL ON FUNCTION public.rpc_delivery_reject_order(TEXT, TEXT) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.rpc_delivery_reject_order(TEXT, TEXT) TO authenticated; REVOKE ALL ON FUNCTION public.rpc_delivery_start_depart(TEXT, JSONB) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.rpc_delivery_start_depart(TEXT, JSONB) TO authenticated; REVOKE ALL ON FUNCTION public.rpc_delivery_arrive_order(TEXT, JSONB) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.rpc_delivery_arrive_order(TEXT, JSONB) TO authenticated; REVOKE ALL ON FUNCTION public.rpc_delivery_checkin_order(TEXT, JSONB) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.rpc_delivery_checkin_order(TEXT, JSONB) TO authenticated; REVOKE ALL ON FUNCTION public.rpc_delivery_start_service(TEXT) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.rpc_delivery_start_service(TEXT) TO authenticated; REVOKE ALL ON FUNCTION public.rpc_delivery_save_progress(TEXT, JSONB) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.rpc_delivery_save_progress(TEXT, JSONB) TO authenticated; REVOKE ALL ON FUNCTION public.rpc_delivery_upload_evidence(TEXT, TEXT, TEXT[]) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.rpc_delivery_upload_evidence(TEXT, TEXT, TEXT[]) TO authenticated; REVOKE ALL ON FUNCTION public.rpc_delivery_retry_evidence(TEXT, TEXT) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.rpc_delivery_retry_evidence(TEXT, TEXT) TO authenticated; REVOKE ALL ON FUNCTION public.rpc_delivery_submit_exception(TEXT, JSONB) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.rpc_delivery_submit_exception(TEXT, JSONB) TO authenticated; REVOKE ALL ON FUNCTION public.rpc_delivery_finish_service(TEXT, JSONB) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.rpc_delivery_finish_service(TEXT, JSONB) TO authenticated; REVOKE ALL ON FUNCTION public.rpc_delivery_record_list(UUID) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.rpc_delivery_record_list(UUID) TO authenticated; REVOKE ALL ON FUNCTION public.rpc_delivery_message_list() FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.rpc_delivery_message_list() TO authenticated; REVOKE ALL ON FUNCTION public.rpc_delivery_set_online_status(UUID, TEXT) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.rpc_delivery_set_online_status(UUID, TEXT) TO authenticated; COMMIT;