Files
suixinkan_ios_new/suixinkan/Features/Orders/ViewModels/OrdersViewModel.swift
汉秋 1970572edd 迁移合作订单完整能力,并统一 AppRoleCode 角色权限与首页菜单展示。
新增 CooperationOrder 模块(双 Tab 列表、获客员绑定、主 Tab 扫码)、订单来源/带客单绑定及配套 API 测试;同步引入 roleCode 权限匹配与相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 13:33:00 +08:00

300 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// OrdersViewModel.swift
// suixinkan
//
// Created by Codex on 2026/6/22.
//
import Foundation
import Combine
@MainActor
/// ViewModel
final class OrdersViewModel: ObservableObject {
@Published var selectedEntry: OrdersEntry = .storeOrders
@Published var selectedStatus = 0
@Published var searchPhone = ""
@Published var filterStartDate: Date?
@Published var filterEndDate: Date?
@Published private(set) var loading = false
@Published private(set) var loadingMore = false
@Published private(set) var isVerifying = false
@Published private(set) var currentVerifyingOrderNumber: String?
@Published private(set) var storeOrders: [OrderEntity] = []
@Published private(set) var storeTotal = 0
@Published private(set) var storePage = 1
@Published private(set) var storeHasMore = false
@Published private(set) var writeOffOrders: [WriteOffOrderItem] = []
@Published private(set) var writeOffTotal = 0
@Published private(set) var writeOffPage = 1
@Published private(set) var writeOffHasMore = false
@Published var orderSourcePeople: [OrderSourcePerson] = []
@Published var orderSourceLeads: [String: [OrderSourceLead]] = [:]
@Published var orderSourceSelections: [String: OrderSourceSelection] = [:]
@Published var loadingOrderSourceLeadsFor: String?
private var loadingOrderSourcePeople = false
static let orderTypePhotographerFollow = 11
static let orderTypePhotographerFollowOfflineScan = 14
///
var normalizedPhone: String? {
let text = searchPhone.trimmingCharacters(in: .whitespacesAndNewlines)
return text.isEmpty ? nil : text
}
///
func reload(api: OrderServing, scenicId: Int?, storeId: Int? = nil, appRole: AppRoleCode?, showLoading: Bool = true) async throws {
switch selectedEntry {
case .storeOrders:
try await reloadStoreOrders(api: api, scenicId: scenicId, appRole: appRole, showLoading: showLoading)
case .verificationOrders:
try await reloadWriteOffOrders(api: api, scenicId: scenicId, storeId: storeId, showLoading: showLoading)
}
}
///
func reloadStoreOrders(api: OrderServing, scenicId: Int?, appRole: AppRoleCode?, showLoading: Bool = true) async throws {
guard let scenicId else {
storeOrders = []
storeTotal = 0
storePage = 1
storeHasMore = false
return
}
if showLoading { loading = true }
defer { if showLoading { loading = false } }
let result = try await api.orderList(
scenicId: scenicId,
page: 1,
pageSize: pageSize,
orderStatus: orderStatusQuery,
userPhone: normalizedPhone,
startTime: startTimeQuery,
endTime: endTimeQuery,
isRefined: refinedQuery,
isScenicAdmin: appRole?.isScenicAdmin == true
)
storeOrders = result.list
storeTotal = result.total
storePage = 1
storeHasMore = storeOrders.count < result.total && !result.list.isEmpty
syncOrderSourceSelections(from: result.list)
}
///
func loadMoreStoreOrders(api: OrderServing, scenicId: Int?, appRole: AppRoleCode?) async throws {
guard !loadingMore, storeHasMore, let scenicId else { return }
loadingMore = true
defer { loadingMore = false }
let nextPage = storePage + 1
let result = try await api.orderList(
scenicId: scenicId,
page: nextPage,
pageSize: pageSize,
orderStatus: orderStatusQuery,
userPhone: normalizedPhone,
startTime: startTimeQuery,
endTime: endTimeQuery,
isRefined: refinedQuery,
isScenicAdmin: appRole?.isScenicAdmin == true
)
storeOrders.append(contentsOf: result.list)
storeTotal = result.total
storePage = nextPage
storeHasMore = storeOrders.count < result.total && !result.list.isEmpty
}
///
func reloadWriteOffOrders(api: OrderServing, scenicId: Int?, storeId: Int? = nil, showLoading: Bool = true) async throws {
guard let scenicId else {
writeOffOrders = []
writeOffTotal = 0
writeOffPage = 1
writeOffHasMore = false
return
}
if showLoading { loading = true }
defer { if showLoading { loading = false } }
let result = try await api.writeOffList(scenicId: scenicId, storeId: storeId, page: 1, pageSize: pageSize)
writeOffOrders = result.list
writeOffTotal = result.total
writeOffPage = 1
writeOffHasMore = writeOffOrders.count < result.total && !result.list.isEmpty
}
///
func loadMoreWriteOffOrders(api: OrderServing, scenicId: Int?, storeId: Int? = nil) async throws {
guard !loadingMore, writeOffHasMore, let scenicId else { return }
loadingMore = true
defer { loadingMore = false }
let nextPage = writeOffPage + 1
let result = try await api.writeOffList(scenicId: scenicId, storeId: storeId, page: nextPage, pageSize: pageSize)
writeOffOrders.append(contentsOf: result.list)
writeOffTotal = result.total
writeOffPage = nextPage
writeOffHasMore = writeOffOrders.count < result.total && !result.list.isEmpty
}
///
func verify(api: OrderServing, scenicId: Int, storeId: Int? = nil, orderNumber: String) async throws {
guard !isVerifying else { return }
isVerifying = true
currentVerifyingOrderNumber = orderNumber
defer {
isVerifying = false
currentVerifyingOrderNumber = nil
}
try await api.writeOff(orderNumber: orderNumber)
let refreshed = try await api.writeOffList(scenicId: scenicId, storeId: storeId, page: 1, pageSize: pageSize)
writeOffOrders = refreshed.list
writeOffTotal = refreshed.total
writeOffPage = 1
writeOffHasMore = writeOffOrders.count < refreshed.total && !refreshed.list.isEmpty
}
///
func shouldShowOrderSource(order: OrderEntity, appRole: AppRoleCode?) -> Bool {
guard !Self.isScenicAdminReadOnly(appRole: appRole) else { return false }
return order.orderType == Self.orderTypePhotographerFollow
|| order.orderType == Self.orderTypePhotographerFollowOfflineScan
}
///
static func isScenicAdminReadOnly(appRole: AppRoleCode?) -> Bool {
appRole?.isScenicAdmin == true
}
///
func getOrderSourceSelection(for order: OrderEntity) -> OrderSourceSelection? {
orderSourceSelections[order.orderNumber] ?? order.toBoundOrderSourceSelection()
}
///
func syncOrderSourceSelections(from orders: [OrderEntity]) {
var updated = orderSourceSelections
for order in orders {
if let selection = order.toBoundOrderSourceSelection() {
updated[order.orderNumber] = selection
}
}
orderSourceSelections = updated
}
/// Sheet
func prepareOrderSourceSheet(api: CooperationOrderServing, storeId: Int?) async {
orderSourceLeads = [:]
await loadOrderSourcePeople(api: api, storeId: storeId)
}
///
func loadOrderSourcePeople(api: CooperationOrderServing, storeId: Int?) async {
if loadingOrderSourcePeople { return }
loadingOrderSourcePeople = true
defer { loadingOrderSourcePeople = false }
do {
let storeParam = storeId.flatMap { $0 > 0 ? $0 : nil }
let data = try await api.cooperativeSalers(storeId: storeParam, page: 1, pageSize: 50)
orderSourcePeople = data.list.map { $0.toOrderSourcePerson() }
} catch {
orderSourcePeople = []
}
}
///
func loadOrderSourceLeads(api: CooperationOrderServing, person: OrderSourcePerson) async {
if loadingOrderSourceLeadsFor == person.key { return }
loadingOrderSourceLeadsFor = person.key
defer { loadingOrderSourceLeadsFor = nil }
guard let saleUserId = Int(person.key) else { return }
do {
let data = try await api.saleUserReferralOrders(saleUserId: saleUserId, page: 1, pageSize: 50)
orderSourceLeads[person.key] = data.list.map { $0.toOrderSourceLead() }
} catch {
orderSourceLeads[person.key] = []
}
}
///
func bindOrderSource(
api: CooperationOrderServing,
orderNumber: String,
selection: OrderSourceSelection
) async throws {
try await api.saleUserBindReferralOrder(
orderNumber: orderNumber,
referralOrderNo: selection.lead.key
)
orderSourceSelections[orderNumber] = selection
}
///
func matchedWriteOffOrder(for rawCode: String) -> (orderNumber: String, matched: WriteOffOrderItem?)? {
guard let orderNumber = OrderNumberParser.parse(rawCode) else {
return nil
}
let matched = writeOffOrders.first {
$0.orderNumber.caseInsensitiveCompare(orderNumber) == .orderedSame
}
return (matched?.orderNumber ?? orderNumber, matched)
}
///
func writeOffOrder(matching orderNumber: String) -> WriteOffOrderItem? {
writeOffOrders.first {
$0.orderNumber.caseInsensitiveCompare(orderNumber) == .orderedSame
}
}
///
func markVerifyingForTests(orderNumber: String) {
isVerifying = true
currentVerifyingOrderNumber = orderNumber
}
private let pageSize = 20
private var orderStatusQuery: Int? {
selectedStatus > 0 ? selectedStatus : nil
}
private var startTimeQuery: String? {
filterStartDate.map { Self.dayFormatter.string(from: $0) }
}
private var endTimeQuery: String? {
filterEndDate.map { Self.dayFormatter.string(from: $0) }
}
private var refinedQuery: Int? {
switch selectedStatus {
case -1:
return 1
case -2:
return 2
default:
return nil
}
}
private static let dayFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .gregorian)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()
}