Skip to content

Commit 1cfffc4

Browse files
authored
Shipping Labels: Handle merging shipments (#15448)
2 parents 2321e64 + b3ba7c9 commit 1cfffc4

File tree

4 files changed

+180
-12
lines changed

4 files changed

+180
-12
lines changed

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsDetailView.swift

Lines changed: 114 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,16 @@ struct WooShippingSplitShipmentsDetailView: View {
66

77
@ObservedObject var viewModel: WooShippingSplitShipmentsViewModel
88

9+
@State private var showingMergeAllSheet = false
10+
911
var body: some View {
1012
NavigationView {
1113
VStack {
1214
if viewModel.shipments.count > 1 {
13-
TopTabView(tabs: viewModel.topTabItems,
14-
showContent: false,
15-
selectedTabIndex: $viewModel.selectedShipmentIndex,
16-
tabsContainerHorizontalPadding: nil,
17-
selectedStateColor: .accentColor,
18-
unselectedStateColor: .secondary,
19-
selectedTabIndicatorHeight: Layout.selectedTabIndicatorHeight,
20-
tabPadding: Layout.tabPadding,
21-
tabsNameFont: Font.subheadline.bold(),
22-
tabsIconSize: nil,
23-
tabItemContentHorizontalPadding: Layout.tabItemContentHorizontalPadding,
24-
tabItemContentVerticalPadding: Layout.tabItemContentVerticalPadding)
15+
VStack(spacing: 0) {
16+
topTabView
17+
Divider()
18+
}
2519
}
2620

2721
ScrollView {
@@ -66,10 +60,55 @@ struct WooShippingSplitShipmentsDetailView: View {
6660
.onAppear {
6761
viewModel.onAppear()
6862
}
63+
.sheet(isPresented: $showingMergeAllSheet) {
64+
mergeAllUnfulfilledSheet
65+
}
6966
}
7067
}
7168

7269
private extension WooShippingSplitShipmentsDetailView {
70+
var topTabView: some View {
71+
HStack(spacing: 0) {
72+
TopTabView(tabs: viewModel.topTabItems,
73+
showContent: false,
74+
showDividerBelowTabs: false,
75+
selectedTabIndex: $viewModel.selectedShipmentIndex,
76+
tabsContainerHorizontalPadding: nil,
77+
selectedStateColor: .accentColor,
78+
unselectedStateColor: .secondary,
79+
selectedTabIndicatorHeight: Layout.selectedTabIndicatorHeight,
80+
tabPadding: Layout.tabPadding,
81+
tabsNameFont: Font.subheadline.bold(),
82+
tabsIconSize: nil,
83+
tabItemContentHorizontalPadding: Layout.tabItemContentHorizontalPadding,
84+
tabItemContentVerticalPadding: Layout.tabItemContentVerticalPadding)
85+
.overlay(alignment: .trailing) {
86+
LinearGradient(gradient: Gradient(colors: [.clear, Color(.basicBackground)]), startPoint: .leading, endPoint: .center)
87+
.frame(width: Layout.gradientViewWidth)
88+
.renderedIf(viewModel.selectedShipmentIndex < viewModel.topTabItems.count - 1)
89+
}
90+
91+
removeShipmentMenu
92+
}
93+
}
94+
95+
var removeShipmentMenu: some View {
96+
Menu {
97+
ForEach(viewModel.topTabItems, id: \.name) { tab in
98+
Button(String.localizedStringWithFormat(Localization.removeShipmentFormat, tab.name.lowercased())) {
99+
// TODO
100+
}
101+
}
102+
Divider()
103+
Button(Localization.mergeAll) {
104+
showingMergeAllSheet = true
105+
}
106+
} label: {
107+
Image(systemName: "ellipsis")
108+
.padding()
109+
}
110+
}
111+
73112
var noticeStack: some View {
74113
VStack(spacing: Layout.contentPadding) {
75114
if let message = viewModel.instructions {
@@ -97,6 +136,34 @@ private extension WooShippingSplitShipmentsDetailView {
97136
}
98137
}
99138
}
139+
140+
var mergeAllUnfulfilledSheet: some View {
141+
ScrollableVStack(alignment: .leading, spacing: Layout.contentPadding) {
142+
Text(Localization.MergeAllUnfulfilledSheet.title)
143+
.font(.title3)
144+
.bold()
145+
.multilineTextAlignment(.leading)
146+
.padding(.top)
147+
148+
Text(Localization.MergeAllUnfulfilledSheet.description)
149+
.font(.subheadline)
150+
.multilineTextAlignment(.leading)
151+
152+
Spacer()
153+
154+
Button(Localization.MergeAllUnfulfilledSheet.confirmCTA) {
155+
viewModel.mergeAllUnfulfilledShipments()
156+
showingMergeAllSheet = false
157+
}
158+
.buttonStyle(PrimaryButtonStyle())
159+
160+
Button(Localization.cancel) {
161+
showingMergeAllSheet = false
162+
}
163+
.buttonStyle(SecondaryButtonStyle())
164+
}
165+
.presentationDetents([.fraction(0.4), .medium, .large])
166+
}
100167
}
101168

102169
private struct MessageSnackBar<IconContent: View>: View {
@@ -155,6 +222,7 @@ fileprivate extension WooShippingSplitShipmentsDetailView {
155222
static let tabItemContentHorizontalPadding: CGFloat = 16.0
156223
static let tabItemContentVerticalPadding: CGFloat = 9.0
157224
static let cornerRadius: CGFloat = 8
225+
static let gradientViewWidth: CGFloat = 32
158226
}
159227

160228
enum Localization {
@@ -178,6 +246,40 @@ fileprivate extension WooShippingSplitShipmentsDetailView {
178246
value: "Undo",
179247
comment: "Button to revert moving items between shipments in the shipping label creation flow"
180248
)
249+
static let cancel = NSLocalizedString(
250+
"wooShippingSplitShipmentsDetailView.cancel",
251+
value: "Cancel",
252+
comment: "Button to dismiss a sheet in the shipping label creation flow"
253+
)
254+
static let removeShipmentFormat = NSLocalizedString(
255+
"wooShippingSplitShipmentsDetailView.removeShipmentFormat",
256+
value: "Remove %1$@",
257+
comment: "Button to remove a shipment in the shipping label creation flow. " +
258+
"The placeholder is the name of a shipment. Reads as: 'Remove shipment 1'."
259+
)
260+
static let mergeAll = NSLocalizedString(
261+
"wooShippingSplitShipmentsDetailView.mergeAll",
262+
value: "Merge all unfulfilled",
263+
comment: "Button to merge all unfulfilled shipments in the shipping label creation flow."
264+
)
265+
266+
enum MergeAllUnfulfilledSheet {
267+
static let title = NSLocalizedString(
268+
"wooShippingSplitShipmentsDetailView.mergeAllUnfulfilledSheet.title",
269+
value: "Merge all unfulfilled shipments",
270+
comment: "Title of the merge all unfulfilled shipments sheet in the shipping label creation flow."
271+
)
272+
static let description = NSLocalizedString(
273+
"wooShippingSplitShipmentsDetailView.mergeAllUnfulfilledSheet.description",
274+
value: "This will remove all unfulfilled split shipments and move all items into one shipment",
275+
comment: "Message on the merge all unfulfilled shipments sheet in the shipping label creation flow."
276+
)
277+
static let confirmCTA = NSLocalizedString(
278+
"wooShippingSplitShipmentsDetailView.mergeAllUnfulfilledSheet.confirmCTA",
279+
value: "Merge all shipments",
280+
comment: "Button to confirm merging all unfulfilled shipments sheet in the shipping label creation flow."
281+
)
282+
}
181283
}
182284
}
183285

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsViewModel.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,31 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject {
210210
undoMovingItemsHandler?()
211211
movingCompletionMessage = nil
212212
}
213+
214+
func mergeAllUnfulfilledShipments() {
215+
var mergedShipment = Shipment()
216+
217+
// TODO-15440: check for fulfilled shipments and remove them from the list.
218+
shipments.forEach { shipment in
219+
for item in shipment {
220+
let matchingItemIndex = mergedShipment.firstIndex(where: {
221+
$0.packageItem.productOrVariationID == item.packageItem.productOrVariationID
222+
})
223+
if let matchingItemIndex {
224+
// Merge the quantity if the same item is merged to the shipment
225+
let updatedQuantity = item.packageItem.quantity + mergedShipment[matchingItemIndex].packageItem.quantity
226+
let updatedItem = ShippingLabelPackageItem(copy: item.packageItem, quantity: updatedQuantity)
227+
mergedShipment[matchingItemIndex] = CollapsibleShipmentItemCardViewModel(item: updatedItem, currency: order.currency)
228+
} else {
229+
// Keep the item as-is
230+
mergedShipment.append(item)
231+
}
232+
}
233+
}
234+
235+
shipments = [mergedShipment]
236+
selectedShipmentIndex = 0
237+
}
213238
}
214239

215240
private extension WooShippingSplitShipmentsViewModel {

WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TopTabView.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ struct TopTabView<Content: View>: View {
2525
@State private var contentSize: CGSize = .zero
2626

2727
@Binding var showTabs: Bool
28+
2829
private let showContent: Bool
30+
private let showDividerBelowTabs: Bool
2931

3032
let tabs: [TopTabItem<Content>]
3133

@@ -54,6 +56,7 @@ struct TopTabView<Content: View>: View {
5456
init(tabs: [TopTabItem<Content>],
5557
showTabs: Binding<Bool> = .constant(true),
5658
showContent: Bool = true,
59+
showDividerBelowTabs: Bool = true,
5760
selectedTabIndex: Binding<Int> = .constant(0),
5861
tabsContainerHorizontalPadding: CGFloat? = 0.0,
5962
selectedStateColor: Color = Colors.selected,
@@ -67,6 +70,7 @@ struct TopTabView<Content: View>: View {
6770
self.tabs = tabs
6871
self._showTabs = showTabs
6972
self.showContent = showContent
73+
self.showDividerBelowTabs = showDividerBelowTabs
7074
self._selectedTab = selectedTabIndex
7175
_tabWidths = State(initialValue: [CGFloat](repeating: 0, count: tabs.count))
7276
self.tabsContainerHorizontalPadding = tabsContainerHorizontalPadding
@@ -151,6 +155,7 @@ struct TopTabView<Content: View>: View {
151155
}
152156
}
153157
Divider()
158+
.renderedIf(showDividerBelowTabs)
154159
}
155160

156161
if showContent {

WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/Split shipments/WooShippingSplitShipmentsViewModelTests.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,42 @@ final class WooShippingSplitShipmentsViewModelTests: XCTestCase {
467467
XCTAssertEqual(viewModel.shipments[0][1].packageItem.productOrVariationID, items[1].productOrVariationID)
468468
XCTAssertEqual(viewModel.shipments[0][1].packageItem.quantity, 1)
469469
}
470+
471+
// MARK: - `mergeAllUnfulfilledShipments`
472+
473+
func test_mergeAllUnfulfilledShipments_updates_shipments_correctly() {
474+
// Given
475+
let items = [sampleItem(id: 1, weight: 5, value: 10, quantity: 2),
476+
sampleItem(id: 2, weight: 3, value: 2.5, quantity: 1),
477+
sampleItem(id: 3, weight: 4, value: 5, quantity: 3)]
478+
let viewModel = WooShippingSplitShipmentsViewModel(order: sampleOrder,
479+
config: WooShippingConfig.fake(),
480+
items: items,
481+
currencySettings: currencySettings,
482+
shippingSettingsService: shippingSettingsService)
483+
484+
// Moving items to 2 new shipments
485+
viewModel.shipments.first?.last?.childItemRows.first?.handleTap()
486+
viewModel.moveSelectedItems(to: .newShipment)
487+
viewModel.shipments.first?.last?.childItemRows.last?.handleTap()
488+
viewModel.moveSelectedItems(to: .newShipment)
489+
490+
// Confidence checks
491+
XCTAssertEqual(viewModel.shipments.count, 3)
492+
493+
// When
494+
viewModel.mergeAllUnfulfilledShipments()
495+
496+
// Then
497+
XCTAssertEqual(viewModel.shipments.count, 1)
498+
XCTAssertEqual(viewModel.shipments[0].count, 3)
499+
XCTAssertEqual(viewModel.shipments[0][0].packageItem.productOrVariationID, items[0].productOrVariationID)
500+
XCTAssertEqual(viewModel.shipments[0][0].packageItem.quantity, 2)
501+
XCTAssertEqual(viewModel.shipments[0][1].packageItem.productOrVariationID, items[1].productOrVariationID)
502+
XCTAssertEqual(viewModel.shipments[0][1].packageItem.quantity, 1)
503+
XCTAssertEqual(viewModel.shipments[0][2].packageItem.productOrVariationID, items[2].productOrVariationID)
504+
XCTAssertEqual(viewModel.shipments[0][2].packageItem.quantity, 3)
505+
}
470506
}
471507

472508
private extension WooShippingSplitShipmentsViewModelTests {

0 commit comments

Comments
 (0)