From 05ff5e8815bbef60a613c214f598f1c14f26dc9f Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Fri, 31 Oct 2025 15:19:07 +0100 Subject: [PATCH 1/8] Adding BookingBadgeView --- .../Classes/Bookings/BookingBadgeView.swift | 53 +++++++++++++++++++ .../SwiftUI Components/BadgeView.swift | 27 +++++++--- 2 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 WooCommerce/Classes/Bookings/BookingBadgeView.swift diff --git a/WooCommerce/Classes/Bookings/BookingBadgeView.swift b/WooCommerce/Classes/Bookings/BookingBadgeView.swift new file mode 100644 index 00000000000..aac9e8e8c5e --- /dev/null +++ b/WooCommerce/Classes/Bookings/BookingBadgeView.swift @@ -0,0 +1,53 @@ +import SwiftUI +import enum Yosemite.BookingAttendanceStatus +import enum Yosemite.BookingStatus + +struct BookingBadgeView: View { + let text: String + let color: Color + + var body: some View { + BadgeView(text: text, + customizations: .init(textColor: Color(UIColor.label.resolvedColor(with: .init(userInterfaceStyle: .light))), + backgroundColor: color, + bordered: false, + bold: false)) + } +} + +extension BookingBadgeView { + init(_ status: BookingAttendanceStatus) { + self.init(text: status.localizedTitle, color: status.badgeColor) + } + + init(_ status: BookingStatus) { + self.init(text: status.localizedTitle, color: status.badgeColor) + } +} + +extension BookingAttendanceStatus { + var badgeColor: Color { + switch self { + case .noShow: + return BadgeColor.info + default: + return BadgeColor.default + } + } +} + +extension BookingStatus { + var badgeColor: Color { + switch self { + case .unpaid: + return BadgeColor.info + default: + return BadgeColor.default + } + } +} + +fileprivate enum BadgeColor { + static let `default` = Color(uiColor: .systemGray6.resolvedColor(with: .init(userInterfaceStyle: .light))) + static let info = try! Color(rgbString: "rgba(255, 227, 101, 1)") +} diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/BadgeView.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/BadgeView.swift index 2e3f3f5e1b8..dd365cdfa87 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/BadgeView.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/BadgeView.swift @@ -35,11 +35,17 @@ struct BadgeView: View { struct Customizations { let textColor: Color let backgroundColor: Color + let bordered: Bool + let bold: Bool init(textColor: Color = Color(.textBrand), - backgroundColor: Color = Color(.wooCommercePurple(.shade0))) { + backgroundColor: Color = Color(.wooCommercePurple(.shade0)), + bordered: Bool = true, + bold: Bool = true) { self.textColor = textColor self.backgroundColor = backgroundColor + self.bordered = bordered + self.bold = bold } } @@ -64,7 +70,9 @@ struct BadgeView: View { var body: some View { if let text = type.title { Text(text) - .bold() + .if(customizations.bold) { + $0.bold() + } .foregroundColor(customizations.textColor) .captionStyle() .padding(.leading, Layout.horizontalPadding) @@ -98,13 +106,18 @@ private extension BadgeView { case .circle: Circle() .fill(customizations.backgroundColor) - .stroke(Color.white, lineWidth: Layout.borderLineWidth) + .overlay( + Circle() + .stroke(Color.white, lineWidth: Layout.borderLineWidth) + .opacity(customizations.bordered ? 1 : 0) + ) case .roundedRectangle(let cornerRadius): RoundedRectangle(cornerRadius: cornerRadius) - .stroke(.white, lineWidth: Layout.borderLineWidth) - .background( + .fill(customizations.backgroundColor) + .overlay( RoundedRectangle(cornerRadius: cornerRadius) - .fill(customizations.backgroundColor) + .stroke(Color.white, lineWidth: Layout.borderLineWidth) + .opacity(customizations.bordered ? 1 : 0) ) } } @@ -119,7 +132,7 @@ private extension BadgeView.BadgeType { private extension BadgeView { enum Layout { - static let horizontalPadding: CGFloat = 6 + static let horizontalPadding: CGFloat = 8 static let verticalPadding: CGFloat = 4 static let borderLineWidth: CGFloat = 1 static let cornerRadius: CGFloat = 8 From de658b439371b0ba7cb594523dcb36fc22e32799 Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Fri, 31 Oct 2025 15:19:14 +0100 Subject: [PATCH 2/8] Update text --- .../BookingAttendanceStatus+Localization.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingAttendanceStatus+Localization.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingAttendanceStatus+Localization.swift index c121fea5664..1ed6abad359 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingAttendanceStatus+Localization.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingAttendanceStatus+Localization.swift @@ -13,7 +13,7 @@ extension BookingAttendanceStatus { case .checkedIn: return NSLocalizedString( "BookingAttendanceStatus.checkedIn", - value: "Checked In", + value: "Checked-in", comment: "Title for 'Checked In' booking attendance status." ) case .cancelled: @@ -25,7 +25,7 @@ extension BookingAttendanceStatus { case .noShow: return NSLocalizedString( "BookingAttendanceStatus.noShow", - value: "No Show", + value: "No-show", comment: "Title for 'No Show' booking attendance status." ) case .unknown: From 452ba269c943975373c8be48ca2f1acf51ae6919 Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Fri, 31 Oct 2025 15:19:29 +0100 Subject: [PATCH 3/8] Adopt BookingListView --- .../Bookings/BookingList/BookingListView.swift | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/WooCommerce/Classes/Bookings/BookingList/BookingListView.swift b/WooCommerce/Classes/Bookings/BookingList/BookingListView.swift index e662c436b3e..e9142b7eefc 100644 --- a/WooCommerce/Classes/Bookings/BookingList/BookingListView.swift +++ b/WooCommerce/Classes/Bookings/BookingList/BookingListView.swift @@ -134,25 +134,14 @@ private extension BookingListView { .foregroundStyle(Color.secondary) HStack { - // TODO: update this when attendance status is available - // Update badge colors if design changes as statuses are not clarified now. - statusBadge(text: booking.attendanceStatus.localizedTitle, color: Layout.defaultBadgeColor) - statusBadge(text: booking.bookingStatus.localizedTitle, color: Layout.defaultBadgeColor) + BookingBadgeView(booking.attendanceStatus) + BookingBadgeView(booking.bookingStatus) Spacer() } } } } - func statusBadge(text: String, color: Color) -> some View { - Text(text) - .font(.caption2) - .foregroundStyle(Color.primary) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(color.clipShape(RoundedRectangle(cornerRadius: 4))) - } - func emptyStateView(isSearching: Bool, onRefresh: @escaping () async -> Void) -> some View { GeometryReader { proxy in ScrollView { @@ -225,7 +214,6 @@ private extension BookingListView { static let viewPadding: CGFloat = 16 static let emptyStatePadding: CGFloat = 24 static let emptyStateImageWidth: CGFloat = 67 - static let defaultBadgeColor = Color(uiColor: .init(light: .systemGray6, dark: .systemGray5)) static let cornerRadius: CGFloat = 8 } From e6c635abf1afce90409466d9be882346bf78cf1e Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Mon, 3 Nov 2025 08:57:50 +0100 Subject: [PATCH 4/8] BookingBadgeable --- .../Classes/Bookings/BookingBadgeView.swift | 25 +++++++++++++------ .../Booking Details/HeaderContent.swift | 12 ++++----- .../Bookings/Booking Details/HeaderView.swift | 12 ++------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/WooCommerce/Classes/Bookings/BookingBadgeView.swift b/WooCommerce/Classes/Bookings/BookingBadgeView.swift index aac9e8e8c5e..cb317b03ee8 100644 --- a/WooCommerce/Classes/Bookings/BookingBadgeView.swift +++ b/WooCommerce/Classes/Bookings/BookingBadgeView.swift @@ -15,17 +15,18 @@ struct BookingBadgeView: View { } } -extension BookingBadgeView { - init(_ status: BookingAttendanceStatus) { - self.init(text: status.localizedTitle, color: status.badgeColor) - } +protocol BookingBadgeable { + var text: String { get } + var badgeColor: Color { get } +} - init(_ status: BookingStatus) { - self.init(text: status.localizedTitle, color: status.badgeColor) +extension BookingBadgeView { + init(_ badgeable: BookingBadgeable) { + self.init(text: badgeable.text, color: badgeable.badgeColor) } } -extension BookingAttendanceStatus { +extension BookingAttendanceStatus: BookingBadgeable { var badgeColor: Color { switch self { case .noShow: @@ -34,9 +35,13 @@ extension BookingAttendanceStatus { return BadgeColor.default } } + + var text: String { + self.localizedTitle + } } -extension BookingStatus { +extension BookingStatus: BookingBadgeable { var badgeColor: Color { switch self { case .unpaid: @@ -45,6 +50,10 @@ extension BookingStatus { return BadgeColor.default } } + + var text: String { + self.localizedTitle + } } fileprivate enum BadgeColor { diff --git a/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift index 88002c4b69a..eae058c9fb7 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift @@ -3,11 +3,14 @@ import struct Yosemite.Booking import struct Yosemite.BookingProductInfo import struct Yosemite.Customer import struct Yosemite.Address +import enum Yosemite.BookingAttendanceStatus +import enum Yosemite.BookingStatus extension BookingDetailsViewModel { final class HeaderContent: ObservableObject { @Published private(set) var bookingDate: String = "" - @Published private(set) var status: [String] = [] + @Published private(set) var attendanceStatus: BookingAttendanceStatus = .unknown + @Published private(set) var bookingStatus: BookingStatus = .unknown @Published private(set) var serviceAndCustomerLine: String = "" func update(with booking: Booking) { @@ -17,11 +20,8 @@ extension BookingDetailsViewModel { timeZone: BookingListTab.utcTimeZone ) serviceAndCustomerLine = booking.summaryText - - status = [ - booking.attendanceStatus.localizedTitle, - booking.bookingStatus.localizedTitle - ] + attendanceStatus = booking.attendanceStatus + bookingStatus = booking.bookingStatus } } } diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/HeaderView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/HeaderView.swift index 2eab2f64aaa..3e17b5024a4 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/HeaderView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/HeaderView.swift @@ -17,16 +17,8 @@ extension BookingDetailsView { .foregroundColor(.secondary) } HStack { - ForEach(content.status, id: \.self) { statusString in - Text(statusString) - .font(.caption2) - .padding(.vertical, 4.5) - .padding(.horizontal, 8) - .background( - BookingDetailsView.Layout.defaultBadgeColor - ) - .cornerRadius(4) - } + BookingBadgeView(content.attendanceStatus) + BookingBadgeView(content.bookingStatus) } .padding(.top, Layout.headerBadgesAdditionalTopPadding) } From a56c570fb1c7cd22cdb6af67496cb0dc84cad782 Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Mon, 3 Nov 2025 09:56:46 +0100 Subject: [PATCH 5/8] Update loc --- WooCommerce/Resources/en.lproj/Localizable.strings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Resources/en.lproj/Localizable.strings b/WooCommerce/Resources/en.lproj/Localizable.strings index 6b59f145141..05d341b383e 100644 --- a/WooCommerce/Resources/en.lproj/Localizable.strings +++ b/WooCommerce/Resources/en.lproj/Localizable.strings @@ -1840,10 +1840,10 @@ which should be translated separately and considered part of this sentence. */ "BookingAttendanceStatus.cancelled" = "Cancelled"; /* Title for 'Checked In' booking attendance status. */ -"BookingAttendanceStatus.checkedIn" = "Checked In"; +"BookingAttendanceStatus.checkedIn" = "Checked-in"; /* Title for 'No Show' booking attendance status. */ -"BookingAttendanceStatus.noShow" = "No Show"; +"BookingAttendanceStatus.noShow" = "No-show"; /* Title for 'Unknown' booking attendance status. */ "BookingAttendanceStatus.unknown" = "Unknown"; From fe8b1102e5bb6ea465950cdc03faff3fbae1e49d Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Mon, 3 Nov 2025 11:20:17 +0100 Subject: [PATCH 6/8] Update tests --- .../ViewRelated/Bookings/BookingDetailsViewModelTests.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingDetailsViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingDetailsViewModelTests.swift index e055e11b506..b857d06f723 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingDetailsViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingDetailsViewModelTests.swift @@ -306,7 +306,8 @@ final class BookingDetailsViewModelTests: XCTestCase { XCTFail("Header section not found") return } - XCTAssertEqual(headerContent.status, ["Checked In", "Paid"]) + XCTAssertEqual(headerContent.attendanceStatus.localizedTitle, "Checked-in") + XCTAssertEqual(headerContent.bookingStatus.localizedTitle, "Paid") } func test_init_whenBookingHasAttendanceStatus_updatesAttendanceContentWithCorrectLocalizedString() { @@ -332,7 +333,7 @@ final class BookingDetailsViewModelTests: XCTestCase { return } - XCTAssertEqual(attendanceContent.value, "No Show") + XCTAssertEqual(attendanceContent.value, "No-show") } func test_attendance_section_is_hidden_when_booking_is_cancelled() { From 91af87209a4ec4141d945419bc1bff7d643974ee Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Mon, 3 Nov 2025 12:16:15 +0100 Subject: [PATCH 7/8] Remove unused code --- .../Bookings/Booking Details/BookingDetailsView.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index a1064ecb958..bc9622b7c6f 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -18,12 +18,6 @@ struct BookingDetailsView: View { static let headerBadgesAdditionalTopPadding: CGFloat = 4 static let sectionFooterTextVerticalPadding: CGFloat = 8 static let rowTextVerticalPadding: CGFloat = 11 - static let defaultBadgeColor = Color( - uiColor: .init( - light: .systemGray6, - dark: .systemGray5 - ) - ) } enum TextFont { From 620f93742d0741b99bb2dbde05500270147ce488 Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Tue, 4 Nov 2025 12:31:23 +0100 Subject: [PATCH 8/8] Only draw the border when needed --- .../SwiftUI Components/BadgeView.swift | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/BadgeView.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/BadgeView.swift index dd365cdfa87..6d059920da6 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/BadgeView.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/BadgeView.swift @@ -106,19 +106,20 @@ private extension BadgeView { case .circle: Circle() .fill(customizations.backgroundColor) - .overlay( - Circle() - .stroke(Color.white, lineWidth: Layout.borderLineWidth) - .opacity(customizations.bordered ? 1 : 0) - ) + .if(customizations.bordered) { view in + view.overlay { + Circle().stroke(Color.white, lineWidth: Layout.borderLineWidth) + } + } case .roundedRectangle(let cornerRadius): RoundedRectangle(cornerRadius: cornerRadius) .fill(customizations.backgroundColor) - .overlay( - RoundedRectangle(cornerRadius: cornerRadius) - .stroke(Color.white, lineWidth: Layout.borderLineWidth) - .opacity(customizations.bordered ? 1 : 0) - ) + .if(customizations.bordered) { view in + view.overlay { + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(Color.white, lineWidth: Layout.borderLineWidth) + } + } } } }