276 lines
9.9 KiB
Swift
276 lines
9.9 KiB
Swift
import Foundation
|
|
|
|
/// 收款首页线下收款区域的加载状态与业务数据。
|
|
final class OfflineCollectionHomeViewModel {
|
|
private(set) var statistics: OfflineCollectionStatisticsResponse?
|
|
private(set) var isLoading = false
|
|
private(set) var errorMessage: String?
|
|
|
|
let context: OfflineCollectionContext
|
|
private let api: any OfflineCollectionServing
|
|
var onStateChange: (() -> Void)?
|
|
|
|
init(context: OfflineCollectionContext = .current(), api: any OfflineCollectionServing) {
|
|
self.context = context
|
|
self.api = api
|
|
}
|
|
|
|
var todayBusinessDate: String? { statistics?.today.date }
|
|
var todaySummary: OfflineDailySummary {
|
|
statistics?.todaySummary ?? .empty(todayBusinessDate ?? "")
|
|
}
|
|
var overdueDates: [OfflinePendingDate] {
|
|
guard let statistics else { return [] }
|
|
return statistics.pending.dates.filter { $0.date < statistics.today.date }
|
|
}
|
|
var earliestOverdueBusinessDate: String? { overdueDates.first?.date }
|
|
var overdueRecordCount: Int { overdueDates.reduce(0) { $0 + $1.unpaidCount } }
|
|
var overdueAmountFen: Int { overdueDates.reduce(0) { $0 + $1.unpaidAmountFen } }
|
|
|
|
/// 从真实统计接口刷新首页卡片。
|
|
func load() async {
|
|
guard context.scenicId > 0 else {
|
|
errorMessage = OfflineCollectionError.missingScenic.localizedDescription
|
|
onStateChange?()
|
|
return
|
|
}
|
|
isLoading = true
|
|
errorMessage = nil
|
|
onStateChange?()
|
|
do {
|
|
statistics = try await api.statistics(scenicId: context.scenicId)
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
}
|
|
isLoading = false
|
|
onStateChange?()
|
|
}
|
|
}
|
|
|
|
/// 线下收款登记页 ViewModel,负责表单校验和登记后的统计刷新。
|
|
final class OfflineCollectionRegistrationViewModel {
|
|
private(set) var amountText = ""
|
|
private(set) var paymentMethod: OfflineCollectionPaymentMethod = .wechat
|
|
private(set) var isSubmitting = false
|
|
|
|
let context: OfflineCollectionContext
|
|
let api: any OfflineCollectionServing
|
|
var onStateChange: (() -> Void)?
|
|
var onShowMessage: ((String) -> Void)?
|
|
var onRegistrationSuccess: ((OfflineCollectionRegistrationReceipt) -> Void)?
|
|
|
|
init(context: OfflineCollectionContext = .current(), api: any OfflineCollectionServing) {
|
|
self.context = context
|
|
self.api = api
|
|
}
|
|
|
|
var canSubmit: Bool {
|
|
!isSubmitting && context.scenicId > 0 && OfflineCollectionMoney.parseFen(amountText) != nil
|
|
}
|
|
|
|
func updateAmount(_ value: String) {
|
|
guard OfflineCollectionMoney.acceptsEditingText(value) else { return }
|
|
guard value != amountText else { return }
|
|
amountText = value
|
|
onStateChange?()
|
|
}
|
|
|
|
func selectPaymentMethod(_ method: OfflineCollectionPaymentMethod) {
|
|
guard method != paymentMethod else { return }
|
|
paymentMethod = method
|
|
onStateChange?()
|
|
}
|
|
|
|
/// 按后端现有契约提交一笔线下收款。
|
|
func submit() async {
|
|
guard !isSubmitting else { return }
|
|
guard context.scenicId > 0 else {
|
|
onShowMessage?(OfflineCollectionError.missingScenic.localizedDescription)
|
|
return
|
|
}
|
|
guard let amountFen = OfflineCollectionMoney.parseFen(amountText) else {
|
|
onShowMessage?(OfflineCollectionError.invalidAmount.localizedDescription)
|
|
return
|
|
}
|
|
|
|
isSubmitting = true
|
|
onStateChange?()
|
|
defer {
|
|
isSubmitting = false
|
|
onStateChange?()
|
|
}
|
|
|
|
do {
|
|
let response = try await api.register(OfflineCollectionRegisterRequest(
|
|
scenicId: context.scenicId,
|
|
amount: OfflineCollectionMoney.apiAmount(amountFen),
|
|
payMethod: paymentMethod.rawValue
|
|
))
|
|
let statistics = try? await api.statistics(scenicId: context.scenicId)
|
|
let responseDate = String(response.createdAt.prefix(10))
|
|
onRegistrationSuccess?(OfflineCollectionRegistrationReceipt(
|
|
record: response.record,
|
|
businessDate: statistics?.today.date ?? responseDate,
|
|
summary: statistics?.todaySummary
|
|
))
|
|
} catch {
|
|
onShowMessage?(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
func startAnotherRegistration() {
|
|
amountText = ""
|
|
onStateChange?()
|
|
}
|
|
}
|
|
|
|
/// 日清页的补缴呈现状态。
|
|
enum OfflineSettlementPresentationState: Sendable, Equatable {
|
|
case idle
|
|
case processing
|
|
case success(OfflineSettlementResult)
|
|
case failed(String, canRetry: Bool)
|
|
}
|
|
|
|
/// 日清页 ViewModel,管理服务端营业日、日期标记、详情请求与补缴快照。
|
|
final class OfflineCollectionDailyViewModel {
|
|
private(set) var businessDate: String
|
|
private(set) var serverToday: String
|
|
private(set) var summary: OfflineDailySummary
|
|
private(set) var records: [OfflineCollectionRecord] = []
|
|
private(set) var pendingDates: Set<String> = []
|
|
private(set) var isLoading = false
|
|
private(set) var errorMessage: String?
|
|
private(set) var settlementState: OfflineSettlementPresentationState = .idle
|
|
private(set) var preparedRequest: OfflineSettlementRequest?
|
|
|
|
let context: OfflineCollectionContext
|
|
private let api: any OfflineCollectionServing
|
|
private var detailRequestVersion = 0
|
|
var onStateChange: (() -> Void)?
|
|
|
|
init(businessDate: String, context: OfflineCollectionContext = .current(), api: any OfflineCollectionServing) {
|
|
self.businessDate = businessDate
|
|
serverToday = businessDate
|
|
summary = .empty(businessDate)
|
|
self.context = context
|
|
self.api = api
|
|
}
|
|
|
|
var isToday: Bool { businessDate == serverToday }
|
|
var canSettle: Bool {
|
|
summary.pendingAmountFen > 0 && summary.pendingCount > 0 && settlementState != .processing && !isLoading
|
|
}
|
|
|
|
/// 首次进入或页面重新出现时刷新统计和当前详情。
|
|
func refresh() async {
|
|
guard context.scenicId > 0 else {
|
|
errorMessage = OfflineCollectionError.missingScenic.localizedDescription
|
|
onStateChange?()
|
|
return
|
|
}
|
|
isLoading = true
|
|
errorMessage = nil
|
|
onStateChange?()
|
|
do {
|
|
let statistics = try await api.statistics(scenicId: context.scenicId)
|
|
serverToday = statistics.today.date
|
|
pendingDates = Set(statistics.pending.dates.map(\.date))
|
|
if businessDate.isEmpty || businessDate > serverToday { businessDate = serverToday }
|
|
await loadDetails(for: businessDate)
|
|
} catch {
|
|
isLoading = false
|
|
errorMessage = error.localizedDescription
|
|
onStateChange?()
|
|
}
|
|
}
|
|
|
|
/// 选择任意不晚于服务端今日的日期,并只接受最后一次请求结果。
|
|
func selectBusinessDate(_ date: String) async {
|
|
guard date <= serverToday, settlementState != .processing else { return }
|
|
businessDate = date
|
|
preparedRequest = nil
|
|
settlementState = .idle
|
|
await loadDetails(for: date)
|
|
}
|
|
|
|
private func loadDetails(for date: String) async {
|
|
detailRequestVersion += 1
|
|
let version = detailRequestVersion
|
|
isLoading = true
|
|
errorMessage = nil
|
|
records = []
|
|
summary = .empty(date)
|
|
onStateChange?()
|
|
do {
|
|
let details = try await api.details(scenicId: context.scenicId, date: date)
|
|
guard version == detailRequestVersion, date == businessDate else { return }
|
|
summary = details.summary
|
|
records = details.records(serverToday: serverToday)
|
|
isLoading = false
|
|
onStateChange?()
|
|
} catch is CancellationError {
|
|
return
|
|
} catch {
|
|
guard version == detailRequestVersion, date == businessDate else { return }
|
|
isLoading = false
|
|
errorMessage = error.localizedDescription
|
|
onStateChange?()
|
|
}
|
|
}
|
|
|
|
func prepareSettlement() throws -> OfflineSettlementRequest {
|
|
guard canSettle else { throw OfflineCollectionError.noPendingRecords }
|
|
let request = OfflineSettlementRequest(
|
|
scenicId: context.scenicId,
|
|
businessDate: businessDate,
|
|
amountFen: summary.pendingAmountFen,
|
|
pendingCount: summary.pendingCount
|
|
)
|
|
preparedRequest = request
|
|
return request
|
|
}
|
|
|
|
func cancelPreparedSettlement() {
|
|
guard settlementState != .processing else { return }
|
|
preparedRequest = nil
|
|
}
|
|
|
|
/// 按后端现有契约补缴指定日期;失败时允许用户再次提交。
|
|
func confirmSettlement() async {
|
|
guard settlementState != .processing, let request = preparedRequest else { return }
|
|
settlementState = .processing
|
|
onStateChange?()
|
|
do {
|
|
let result = try await api.supplement(request)
|
|
preparedRequest = nil
|
|
settlementState = .success(result)
|
|
await refreshAfterSettlement()
|
|
} catch {
|
|
settlementState = .failed(error.localizedDescription, canRetry: true)
|
|
onStateChange?()
|
|
}
|
|
}
|
|
|
|
private func refreshAfterSettlement() async {
|
|
if let statistics = try? await api.statistics(scenicId: context.scenicId) {
|
|
serverToday = statistics.today.date
|
|
pendingDates = Set(statistics.pending.dates.map(\.date))
|
|
}
|
|
detailRequestVersion += 1
|
|
let version = detailRequestVersion
|
|
if let details = try? await api.details(scenicId: context.scenicId, date: businessDate), version == detailRequestVersion {
|
|
summary = details.summary
|
|
records = details.records(serverToday: serverToday)
|
|
}
|
|
isLoading = false
|
|
onStateChange?()
|
|
}
|
|
|
|
func clearSettlementFeedback() {
|
|
guard settlementState != .processing else { return }
|
|
settlementState = .idle
|
|
onStateChange?()
|
|
}
|
|
}
|