Files

402 lines
15 KiB
Swift
Raw Permalink 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?
@Published var refundingOrder: OrderEntity?
@Published var sendGiftContext: SendGiftContext?
private var loadingOrderSourcePeople = false
/// /
struct SendGiftContext: Equatable, Identifiable {
var id: String { orderNumber }
let orderNumber: String
let nowPhotoNum: Int
let nowVideoNum: Int
var sendPhotoNum: Int
var sendVideoNum: Int
}
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,
orderStatus: nil,
orderVerifyStatus: nil,
startTime: nil,
endTime: nil,
userPhone: nil
)
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,
orderStatus: nil,
orderVerifyStatus: nil,
startTime: nil,
endTime: nil,
userPhone: nil
)
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,
orderStatus: nil,
orderVerifyStatus: nil,
startTime: nil,
endTime: nil,
userPhone: nil
)
writeOffOrders = refreshed.list
writeOffTotal = refreshed.total
writeOffPage = 1
writeOffHasMore = writeOffOrders.count < refreshed.total && !refreshed.list.isEmpty
}
/// 退
func shouldShowRefundButton(order: OrderEntity, appRole: AppRoleCode?) -> Bool {
guard !Self.isScenicAdminReadOnly(appRole: appRole) else { return false }
return (order.orderStatus == 18 || order.orderStatus == 30) && order.orderType != 19
}
///
func shouldShowVerifyButton(order: OrderEntity, appRole: AppRoleCode?) -> Bool {
guard !Self.isScenicAdminReadOnly(appRole: appRole) else { return false }
if order.orderType == 19 { return true }
guard let photoTravel = order.photoTravel else { return false }
return photoTravel.needCheckIn && order.orderStatus != 30
}
///
func updateRefinedChoice(api: OrderServing, orderNumber: String, isRefined: Int) async throws {
try await api.updateRefinedChoice(orderNumber: orderNumber, isRefined: isRefined)
if let index = storeOrders.firstIndex(where: { $0.orderNumber == orderNumber }) {
storeOrders[index].isRefined = isRefined
}
}
///
func cancelOrder(api: OrderServing, scenicId: Int?, appRole: AppRoleCode?, orderNumber: String) async throws {
try await api.cancelOrder(orderNumber: orderNumber)
try await reloadStoreOrders(api: api, scenicId: scenicId, appRole: appRole, showLoading: false)
}
/// /
func prepareSendGift(for order: OrderEntity) {
let photoTravel = order.photoTravel
sendGiftContext = SendGiftContext(
orderNumber: order.orderNumber,
nowPhotoNum: photoTravel?.orderPhotoNum ?? 0,
nowVideoNum: photoTravel?.orderVideoNum ?? 0,
sendPhotoNum: photoTravel?.retouchGiftPhotoNum ?? 0,
sendVideoNum: photoTravel?.retouchGiftVideoNum ?? 0
)
}
/// /
func submitSendGift(
api: OrderServing,
scenicId: Int?,
appRole: AppRoleCode?,
photoNum: Int,
videoNum: Int
) async throws {
guard let context = sendGiftContext else { return }
try await api.sendGift(orderNumber: context.orderNumber, photoNum: photoNum, videoNum: videoNum)
sendGiftContext = nil
try await reloadStoreOrders(api: api, scenicId: scenicId, appRole: appRole, showLoading: false)
}
///
var currentFilterTitle: String {
OrderFilters.statusFilters.first(where: { $0.id == selectedStatus })?.title ?? "全部"
}
///
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
}()
}