Add scenic selection and cooperation order flows aligned with Android.

Upgrade scenic picker with search, location sorting, permission apply/status, and blocking home dialog; add cooperation order module and order source picker improvements with unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 10:48:43 +08:00
parent 3c4ca7eefb
commit 2bea05b1d9
50 changed files with 5603 additions and 191 deletions

View File

@ -20,7 +20,7 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
private var hasInitialized = false
private var isStoreMode: Bool { AppStore.shared.currentAppRole == .storeAdmin }
private var pendingOrderSourceNumber: String?
private var pendingOrderSourcePerson: OrderSourcePerson?
private weak var orderSourcePicker: OrderSourcePickerViewController?
private var pendingGiftOrderNumber: String?
private var isFetchingMultiVerifySpots = false
private let locationProvider = OrderLocationProvider()
@ -51,11 +51,17 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
override func bindActions() {
orderListViewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.applyViewModel() }
Task { @MainActor in
self?.applyViewModel()
self?.orderSourcePicker?.reloadFromViewModel()
}
}
orderListViewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
orderListViewModel.onNavigateBindAcquirer = { [weak self] saleUserId in
Task { @MainActor in self?.presentBindAcquirer(saleUserId: saleUserId) }
}
depositViewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.applyViewModel() }
}
@ -259,6 +265,13 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
present(nav, animated: true)
}
private func presentBindAcquirer(saleUserId: Int) {
dismiss(animated: true) { [weak self] in
let controller = BindAcquirerViewController(saleUserId: saleUserId)
self?.navigationController?.pushViewController(controller, animated: true)
}
}
private func fetchMultiVerifySpotsIfNeeded() async {
isFetchingMultiVerifySpots = true
defer { isFetchingMultiVerifySpots = false }
@ -322,7 +335,7 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
hideWriteActions: orderListViewModel.isScenicAdminReadOnly,
actions: makeOrderActions(for: order),
showOrderSource: orderListViewModel.shouldShowOrderSource(for: order),
sourceText: source.map { "\($0.person.name) · \($0.lead.displayText)" }
sourceSelection: source
)
listView.notifyLoadMoreIfNeeded(at: indexPath.row, shouldLoad: orderListViewModel.shouldLoadMore(at: indexPath.row))
return cell
@ -363,6 +376,9 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
actions.onOrderSourceTap = { [weak self] in
self?.presentOrderSourcePicker(for: order)
}
actions.onOrderSourceImageTap = { [weak self] images, index in
self?.presentImagePreview(images: images, startIndex: index)
}
if let travel = order.photoTravel, travel.canGiftRetouch {
actions.onSendGift = { [weak self] in
guard let self else { return }
@ -451,23 +467,23 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
pendingOrderSourceNumber = order.orderNumber
Task {
await orderListViewModel.onOrderSourceSheetOpen(api: orderAPI)
let controller = OrderSourcePickerViewController(
people: orderListViewModel.orderSourcePeople,
leads: [],
selectedPerson: nil
)
controller.onPersonSelected = { [weak self] person in
await MainActor.run { [weak self] in
guard let self else { return }
self.pendingOrderSourcePerson = person
Task {
await self.orderListViewModel.loadOrderSourceLeads(person: person, api: self.orderAPI)
let leadsController = OrderSourcePickerViewController(
people: self.orderListViewModel.orderSourcePeople,
leads: self.orderListViewModel.orderSourceLeads[person.key] ?? [],
selectedPerson: person
)
leadsController.onLeadSelected = { [weak self] selection in
let controller = OrderSourcePickerViewController(
orderNumber: order.orderNumber,
viewModel: self.orderListViewModel,
referralOrder: order.referralOrder,
onPersonSelected: { [weak self] person in
guard let self else { return }
Task {
await self.orderListViewModel.loadOrderSourceLeads(person: person, api: self.orderAPI)
}
},
onLeadBound: { [weak self] selection in
guard let self, let orderNumber = self.pendingOrderSourceNumber else { return }
self.dismiss(animated: true) {
self.orderSourcePicker = nil
}
Task {
await self.orderListViewModel.bindOrderSource(
orderNumber: orderNumber,
@ -475,11 +491,44 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
api: self.orderAPI
)
}
},
onCallPhone: { [weak self] phone in
self?.callPhone(phone)
},
onPreviewImages: { [weak self] urls, index in
self?.presentImagePreview(images: urls, startIndex: index)
}
)
self.orderSourcePicker = controller
let nav = UINavigationController(rootViewController: controller)
nav.setNavigationBarHidden(true, animated: false)
if let sheet = nav.sheetPresentationController {
sheet.prefersGrabberVisible = true
if #available(iOS 16.0, *) {
sheet.detents = [
.custom(identifier: .init("orderSource")) { context in
context.maximumDetentValue * 0.82
},
.large(),
]
sheet.selectedDetentIdentifier = .init("orderSource")
} else {
sheet.detents = [.large()]
}
self.present(leadsController, animated: true)
}
self.present(nav, animated: true)
}
present(controller, animated: true)
}
}
private func callPhone(_ phone: String) {
let trimmed = phone.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, let url = URL(string: "tel://\(trimmed)") else { return }
UIApplication.shared.open(url)
}
private func presentImagePreview(images: [String], startIndex: Int) {
guard !images.isEmpty else { return }
present(ImagePreviewViewController(imageURLs: images, startIndex: startIndex), animated: true)
}
}