From a7375e972517ee778420208821166b7fb318beba Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Fri, 26 Sep 2025 15:27:20 +0300 Subject: [PATCH 01/20] Add reschedule and cancel booking buttons --- .../Booking Details/BookingDetailsView.swift | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index eef19b175a2..eecbd6a4953 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -8,6 +8,7 @@ struct BookingDetailsView: View { private enum Layout { static let contentSidePadding: CGFloat = 16 + static let contentVerticalPadding: CGFloat = 16 static let headerContentVerticalPadding: CGFloat = 6 static let headerBadgesAdditionalTopPadding: CGFloat = 4 } @@ -120,15 +121,37 @@ private extension BookingDetailsView { horizontalPadding: 0 // Parent section padding is added elsewhere, ) - if row.id != content.rows.last?.id { - Divider() - .padding(.trailing, -Layout.contentSidePadding) + Divider() + .padding(.trailing, -Layout.contentSidePadding) + } + + VStack(spacing: Layout.contentVerticalPadding) { + Button { + /// On reschedule button tap + } label: { + Text(Localization.rescheduleButtonTitle) + } + .buttonStyle(SecondaryButtonStyle()) + + Button { + /// On cancel booking button tap + } label: { + Text(Localization.cancelBooking) } + .buttonStyle(SecondaryButtonStyle()) } + .padding(.vertical, Layout.contentVerticalPadding) } } } +private extension BookingDetailsView { + enum Localization { + static let rescheduleButtonTitle = "Reschedule" + static let cancelBooking = "Cancel booking" + } +} + #if DEBUG struct BookingDetailsView_Previews: PreviewProvider { static var previews: some View { From 4c629acfff17b10c3035c353f61bad50a4e95bae Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Fri, 26 Sep 2025 15:37:31 +0300 Subject: [PATCH 02/20] Draft attendance content and footer --- .../Booking Details/AttendanceContent.swift | 6 ++++++ .../Booking Details/BookingDetailsViewModel.swift | 11 +++++++---- .../Bookings/Booking Details/BookingDetailsView.swift | 2 ++ WooCommerce/WooCommerce.xcodeproj/project.pbxproj | 4 ++++ 4 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 WooCommerce/Classes/ViewModels/Booking Details/AttendanceContent.swift diff --git a/WooCommerce/Classes/ViewModels/Booking Details/AttendanceContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/AttendanceContent.swift new file mode 100644 index 00000000000..fcc1977973e --- /dev/null +++ b/WooCommerce/Classes/ViewModels/Booking Details/AttendanceContent.swift @@ -0,0 +1,6 @@ +import Foundation + +extension BookingDetailsViewModel { + struct AttendanceContent { + } +} diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index 25e7997df4e..3fa33b82591 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -2,9 +2,6 @@ import Foundation import struct Networking.Booking extension BookingDetailsViewModel { - struct AttendanceContent { - } - struct PaymentContent { } @@ -28,9 +25,15 @@ final class BookingDetailsViewModel: ObservableObject { content: .appointmentDetails(AppointmentDetailsContent(booking)) ) + let attendanceSection = Section( + footerText: "Mark attendance to keep your reports accurate and spot booking trends.", + content: .attendance(AttendanceContent()) + ) + sections = [ headerSection, - appointmentDetailsSection + appointmentDetailsSection, + attendanceSection ] } } diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index eecbd6a4953..1a6f2887dbd 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -11,6 +11,7 @@ struct BookingDetailsView: View { static let contentVerticalPadding: CGFloat = 16 static let headerContentVerticalPadding: CGFloat = 6 static let headerBadgesAdditionalTopPadding: CGFloat = 4 + static let sectionFooterTextVerticalPadding: CGFloat = 8 } fileprivate enum TextFont { @@ -63,6 +64,7 @@ private extension BookingDetailsView { if let footerText = section.footerText { Text(footerText) .padding(.horizontal, Layout.contentSidePadding) + .padding(.vertical, Layout.sectionFooterTextVerticalPadding) .font(.footnote) .foregroundColor(.gray) } diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 054acb658e9..64843e46c02 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1248,6 +1248,7 @@ 2D05D1A42E82D266004111FD /* AppointmentDetailsContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05D1A32E82D25F004111FD /* AppointmentDetailsContent.swift */; }; 2D05D1A52E82D3F6004111FD /* BookingDetailsViewModel+SectionContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05D1A02E82D1EF004111FD /* BookingDetailsViewModel+SectionContent.swift */; }; 2D05D1A72E82D49F004111FD /* BookingDetailsViewModel+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05D1A62E82D49D004111FD /* BookingDetailsViewModel+Status.swift */; }; + 2D05E80F2E86BE50004111FD /* AttendanceContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05E80E2E86BE4F004111FD /* AttendanceContent.swift */; }; 2D09E0D12E61BC7F005C26F3 /* ApplicationPasswordsExperimentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D09E0D02E61BC7D005C26F3 /* ApplicationPasswordsExperimentState.swift */; }; 2D09E0D52E65C9B9005C26F3 /* ApplicationPasswordsExperimentStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D09E0D42E65C9B9005C26F3 /* ApplicationPasswordsExperimentStateTests.swift */; }; 2D7A3E232E7891DB00C46401 /* CIABEligibilityCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7A3E222E7891D200C46401 /* CIABEligibilityCheckerTests.swift */; }; @@ -4444,6 +4445,7 @@ 2D05D1A12E82D233004111FD /* HeaderContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderContent.swift; sourceTree = ""; }; 2D05D1A32E82D25F004111FD /* AppointmentDetailsContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppointmentDetailsContent.swift; sourceTree = ""; }; 2D05D1A62E82D49D004111FD /* BookingDetailsViewModel+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BookingDetailsViewModel+Status.swift"; sourceTree = ""; }; + 2D05E80E2E86BE4F004111FD /* AttendanceContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttendanceContent.swift; sourceTree = ""; }; 2D09E0D02E61BC7D005C26F3 /* ApplicationPasswordsExperimentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordsExperimentState.swift; sourceTree = ""; }; 2D09E0D42E65C9B9005C26F3 /* ApplicationPasswordsExperimentStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordsExperimentStateTests.swift; sourceTree = ""; }; 2D7A3E222E7891D200C46401 /* CIABEligibilityCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIABEligibilityCheckerTests.swift; sourceTree = ""; }; @@ -9119,6 +9121,7 @@ 2DAC251E2E829FF9008521AF /* Booking Details */ = { isa = PBXGroup; children = ( + 2D05E80E2E86BE4F004111FD /* AttendanceContent.swift */, 2D05D1A12E82D233004111FD /* HeaderContent.swift */, 2D05D1A32E82D25F004111FD /* AppointmentDetailsContent.swift */, 2DAC251F2E82A02C008521AF /* BookingDetailsViewModel.swift */, @@ -16722,6 +16725,7 @@ 021BCDF82D3648CD002E9F15 /* PointOfSaleItemListFullscreenErrorView.swift in Sources */, E1C5E78226C2A971008D4C47 /* InPersonPaymentsPluginNotSetup.swift in Sources */, 022F941E257F8E820011CD94 /* BoldableTextParser.swift in Sources */, + 2D05E80F2E86BE50004111FD /* AttendanceContent.swift in Sources */, 26FE09DD24D9F3F600B9BDF5 /* LoadingView.swift in Sources */, 457151AB243B6E8000EB2DFA /* ProductSlugViewController.swift in Sources */, 02F49ADA23BF356E00FA0BFA /* TitleAndTextFieldTableViewCell.ViewModel+State.swift in Sources */, From 6ee56dfacd00b7bbe930a2546a27c9eba28608a8 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Fri, 26 Sep 2025 16:08:40 +0300 Subject: [PATCH 03/20] Add attendance section --- .../Booking Details/AttendanceContent.swift | 3 +++ .../BookingDetailsViewModel+Section.swift | 13 +++++++--- .../BookingDetailsViewModel.swift | 3 ++- .../Booking Details/BookingDetailsView.swift | 25 +++++++++++++++++-- .../SwiftUI Components/TitleAndValueRow.swift | 5 +++- 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/AttendanceContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/AttendanceContent.swift index fcc1977973e..33b7c270f83 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/AttendanceContent.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/AttendanceContent.swift @@ -2,5 +2,8 @@ import Foundation extension BookingDetailsViewModel { struct AttendanceContent { + /// Hardcoded attendance value + /// Will be replaced with model value or binding + let value = "Booked" } } diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Section.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Section.swift index 451c2528a93..ece7cee70c5 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Section.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Section.swift @@ -6,18 +6,25 @@ extension BookingDetailsViewModel { return content.id } - let headerText: String? + let header: Header? let footerText: String? let content: SectionContent init( - headerText: String? = nil, + header: Header? = nil, footerText: String? = nil, content: SectionContent ) { - self.headerText = headerText + self.header = header self.footerText = footerText self.content = content } } } + +extension BookingDetailsViewModel.Section { + enum Header { + case empty + case title(String) + } +} diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index 3fa33b82591..d755a772f0a 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -21,11 +21,12 @@ final class BookingDetailsViewModel: ObservableObject { ) let appointmentDetailsSection = Section( - headerText: Localization.appointmentDetailsSectionHeaderTitle.uppercased(), + header: .title(Localization.appointmentDetailsSectionHeaderTitle.uppercased()), content: .appointmentDetails(AppointmentDetailsContent(booking)) ) let attendanceSection = Section( + header: .empty, footerText: "Mark attendance to keep your reports accurate and spot booking trends.", content: .attendance(AttendanceContent()) ) diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 1a6f2887dbd..ca20359856e 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -47,9 +47,18 @@ struct BookingDetailsView: View { private extension BookingDetailsView { func sectionView(with section: BookingDetailsViewModel.Section) -> some View { VStack(alignment: .leading, spacing: 0) { - if let headerText = section.headerText { + if let header = section.header { + let text = { + switch header { + case .empty: + return "" + case .title(let text): + return text + } + }() + ListHeaderView( - text: headerText, + text: text, alignment: .left ) .padding(.horizontal, insets: safeAreaInsets) @@ -78,6 +87,8 @@ private extension BookingDetailsView { headerView(with: content) case .appointmentDetails(let content): appointmentDetailsView(with: content) + case .attendance(let content): + attendanceView(with: content) default: EmptyView() } @@ -108,6 +119,15 @@ private extension BookingDetailsView { .padding(.vertical, 6) } + func attendanceView(with content: BookingDetailsViewModel.AttendanceContent) -> some View { + TitleAndValueRow( + title: Localization.attendanceRowTitle, + value: .placeholder(content.value), + selectionStyle: .disclosure, + horizontalPadding: 0 + ) + } + func appointmentDetailsView(with content: BookingDetailsViewModel.AppointmentDetailsContent) -> some View { VStack(alignment: .leading, spacing: 0) { ForEach(content.rows) { row in @@ -151,6 +171,7 @@ private extension BookingDetailsView { enum Localization { static let rescheduleButtonTitle = "Reschedule" static let cancelBooking = "Cancel booking" + static let attendanceRowTitle = "Attendance" } } diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TitleAndValueRow.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TitleAndValueRow.swift index 3db052bd1fa..672888ae236 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TitleAndValueRow.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TitleAndValueRow.swift @@ -18,6 +18,7 @@ struct TitleAndValueRow: View { private let bold: Bool private let selectionStyle: SelectionStyle private let isLoading: Bool + private let horizontalPadding: CGFloat private let action: () -> Void /// Static width for title label. Used to align values between different rows. @@ -46,6 +47,7 @@ struct TitleAndValueRow: View { bold: Bool = false, selectionStyle: SelectionStyle = .none, isLoading: Bool = false, + horizontalPadding: CGFloat = Constants.horizontalPadding, action: @escaping () -> Void = {}) { self.title = title self.titleSuffixImage = titleSuffixImage @@ -55,6 +57,7 @@ struct TitleAndValueRow: View { self.bold = bold self.selectionStyle = selectionStyle self.isLoading = isLoading + self.horizontalPadding = horizontalPadding self.action = action } @@ -92,7 +95,7 @@ struct TitleAndValueRow: View { }) .disabled(selectionStyle == .none) .frame(minHeight: Constants.minHeight) - .padding(.horizontal, Constants.horizontalPadding) + .padding(.horizontal, horizontalPadding) .accessibilityElement() .accessibilityLabel(Text(title)) .accessibilityValue(Text(value.text)) From f448da061b6a90d925bdcb06c26cc6586f87f46f Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Fri, 26 Sep 2025 18:30:02 +0300 Subject: [PATCH 04/20] Use TitleAndValueRow for appointment details section --- .../Booking Details/BookingDetailsView.swift | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index ca20359856e..6160910cd47 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -131,16 +131,10 @@ private extension BookingDetailsView { func appointmentDetailsView(with content: BookingDetailsViewModel.AppointmentDetailsContent) -> some View { VStack(alignment: .leading, spacing: 0) { ForEach(content.rows) { row in - TitleAndTextFieldRow( + TitleAndValueRow( title: row.title, - placeholder: String(), - text: .constant(row.value), - fieldAlignment: .trailing, - keyboardType: .default, - titleFont: BookingDetailsView.TextFont.bodyMedium, - valueColor: .secondary, - valueFont: BookingDetailsView.TextFont.bodyRegular, - horizontalPadding: 0 // Parent section padding is added elsewhere, + value: .placeholder(row.value), + horizontalPadding: 0 ) Divider() From 79c2128f59d09aedae0cbe0da8d34f506e2ac69f Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Mon, 29 Sep 2025 12:27:46 +0300 Subject: [PATCH 05/20] Update Attendance section to follow designs --- .../BookingDetailsViewModel.swift | 16 ++++++++++++++-- .../Booking Details/BookingDetailsView.swift | 6 ++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index d755a772f0a..88201670e74 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -26,8 +26,8 @@ final class BookingDetailsViewModel: ObservableObject { ) let attendanceSection = Section( - header: .empty, - footerText: "Mark attendance to keep your reports accurate and spot booking trends.", + header: .title(Localization.attendanceSectionHeaderTitle.uppercased()), + footerText: Localization.attendanceSectionFooterText, content: .attendance(AttendanceContent()) ) @@ -46,5 +46,17 @@ private extension BookingDetailsViewModel { value: "Appointment Details", comment: "Header title for the 'Appointment Details' section in the booking details screen." ) + + static let attendanceSectionHeaderTitle = NSLocalizedString( + "BookingDetailsView.attendance.headerTitle", + value: "Attendance", + comment: "Header title for the 'Attendance' section in the booking details screen." + ) + + static let attendanceSectionFooterText = NSLocalizedString( + "BookingDetailsView.attendance.footerText", + value: "Mark attendance to keep your reports accurate and spot booking trends.", + comment: "Footer text for the 'Attendance' section in the booking details screen." + ) } } diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 6160910cd47..96d93d104ad 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -121,7 +121,7 @@ private extension BookingDetailsView { func attendanceView(with content: BookingDetailsViewModel.AttendanceContent) -> some View { TitleAndValueRow( - title: Localization.attendanceRowTitle, + title: Localization.statusRowTitle, value: .placeholder(content.value), selectionStyle: .disclosure, horizontalPadding: 0 @@ -165,7 +165,9 @@ private extension BookingDetailsView { enum Localization { static let rescheduleButtonTitle = "Reschedule" static let cancelBooking = "Cancel booking" - static let attendanceRowTitle = "Attendance" + + /// Attendance section + static let statusRowTitle = "Status" } } From 997e29ef23d8476d9859d52d6eb9638b7d5edd03 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Mon, 29 Sep 2025 12:48:07 +0300 Subject: [PATCH 06/20] Update attendance rows content to follow designs --- .../AppointmentDetailsContent.swift | 30 +++++++++---------- .../Booking Details/BookingDetailsView.swift | 3 +- .../SwiftUI Components/TitleAndValueRow.swift | 5 ++++ 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift index 14971aee79f..ce155cef0d3 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift @@ -25,10 +25,10 @@ extension BookingDetailsViewModel { rows = [ Row(title: Localization.appointmentDetailsDateRowTitle, value: appointmentDate), Row(title: Localization.appointmentDetailsTimeRowTitle, value: appointmentTimeFrame), - Row(title: Localization.appointmentDetailsServiceTitle, value: "Women's Haircut"), - Row(title: Localization.appointmentDetailsQuantityTitle, value: "1"), + Row(title: Localization.appointmentDetailsAssignedStaffTitle, value: "Marianne Renoir"), /// Temporarily hardcoded + Row(title: Localization.appointmentDetailsLocationTitle, value: "238 Willow Creek Drive, Montgomery ..."), /// Temporarily hardcoded Row(title: Localization.appointmentDetailsDurationTitle, value: String(durationMinutes)), - Row(title: Localization.appointmentDetailsCostTitle, value: booking.cost) + Row(title: Localization.appointmentDetailsPriceTitle, value: booking.cost) ] } } @@ -48,16 +48,16 @@ private extension BookingDetailsViewModel.AppointmentDetailsContent { comment: "Time row title in appointment details section in booking details view." ) - static let appointmentDetailsServiceTitle = NSLocalizedString( - "BookingDetailsView.appointmentDetails.serviceRow.title", - value: "Service", - comment: "Service name row title in appointment details section in booking details view." + static let appointmentDetailsAssignedStaffTitle = NSLocalizedString( + "BookingDetailsView.appointmentDetails.assignedStaff.title", + value: "Assigned staff", + comment: "Assigned staff row title in appointment details section in booking details view." ) - static let appointmentDetailsQuantityTitle = NSLocalizedString( - "BookingDetailsView.appointmentDetails.quantityRow.title", - value: "Quantity", - comment: "Quantity row title in appointment details section in booking details view." + static let appointmentDetailsLocationTitle = NSLocalizedString( + "BookingDetailsView.appointmentDetails.locationRow.title", + value: "Location", + comment: "Location row title in appointment details section in booking details view." ) static let appointmentDetailsDurationTitle = NSLocalizedString( @@ -66,10 +66,10 @@ private extension BookingDetailsViewModel.AppointmentDetailsContent { comment: "Duration row title in appointment details section in booking details view." ) - static let appointmentDetailsCostTitle = NSLocalizedString( - "BookingDetailsView.appointmentDetails.costRow.title", - value: "Cost", - comment: "Cost row title in appointment details section in booking details view." + static let appointmentDetailsPriceTitle = NSLocalizedString( + "BookingDetailsView.appointmentDetails.priceRow.title", + value: "Price", + comment: "Price row title in appointment details section in booking details view." ) } } diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 96d93d104ad..e9a8fbee19d 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -134,7 +134,8 @@ private extension BookingDetailsView { TitleAndValueRow( title: row.title, value: .placeholder(row.value), - horizontalPadding: 0 + horizontalPadding: 0, + isMultiline: false ) Divider() diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TitleAndValueRow.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TitleAndValueRow.swift index 672888ae236..ecf3f4091e6 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TitleAndValueRow.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TitleAndValueRow.swift @@ -19,6 +19,7 @@ struct TitleAndValueRow: View { private let selectionStyle: SelectionStyle private let isLoading: Bool private let horizontalPadding: CGFloat + private let isMultiline: Bool private let action: () -> Void /// Static width for title label. Used to align values between different rows. @@ -48,6 +49,7 @@ struct TitleAndValueRow: View { selectionStyle: SelectionStyle = .none, isLoading: Bool = false, horizontalPadding: CGFloat = Constants.horizontalPadding, + isMultiline: Bool = true, action: @escaping () -> Void = {}) { self.title = title self.titleSuffixImage = titleSuffixImage @@ -58,6 +60,7 @@ struct TitleAndValueRow: View { self.selectionStyle = selectionStyle self.isLoading = isLoading self.horizontalPadding = horizontalPadding + self.isMultiline = isMultiline self.action = action } @@ -82,6 +85,8 @@ struct TitleAndValueRow: View { Text(value.text) .style(for: value, bold: bold, highlighted: false) .multilineTextAlignment(valueTextAlignment) + .lineLimit(isMultiline ? nil : 1) + .truncationMode(.tail) .frame(maxWidth: .infinity, alignment: valueFrameAlignment) .padding(.vertical, Constants.verticalPadding) .redacted(reason: isLoading ? .placeholder : []) From 8666990f9d8307843eb77efe78e0346742b73965 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Mon, 29 Sep 2025 12:50:55 +0300 Subject: [PATCH 07/20] Delete reschedule button --- .../Booking Details/BookingDetailsView.swift | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index e9a8fbee19d..07b74516098 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -142,21 +142,12 @@ private extension BookingDetailsView { .padding(.trailing, -Layout.contentSidePadding) } - VStack(spacing: Layout.contentVerticalPadding) { - Button { - /// On reschedule button tap - } label: { - Text(Localization.rescheduleButtonTitle) - } - .buttonStyle(SecondaryButtonStyle()) - - Button { - /// On cancel booking button tap - } label: { - Text(Localization.cancelBooking) - } - .buttonStyle(SecondaryButtonStyle()) + Button { + /// On cancel booking button tap + } label: { + Text(Localization.cancelBooking) } + .buttonStyle(SecondaryButtonStyle()) .padding(.vertical, Layout.contentVerticalPadding) } } @@ -164,7 +155,6 @@ private extension BookingDetailsView { private extension BookingDetailsView { enum Localization { - static let rescheduleButtonTitle = "Reschedule" static let cancelBooking = "Cancel booking" /// Attendance section From 22eef28a3715ab69114991b3857aa59089893fec Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Mon, 29 Sep 2025 13:36:38 +0300 Subject: [PATCH 08/20] Update header to follow designs --- .../BookingDetailsViewModel+Status.swift | 20 +++++++++---- .../Booking Details/HeaderContent.swift | 10 +++++-- .../Booking Details/BookingDetailsView.swift | 30 ++++++++++++------- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Status.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Status.swift index 71552a555d3..91c35425009 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Status.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Status.swift @@ -3,7 +3,7 @@ import SwiftUI extension BookingDetailsViewModel { enum Status { - case booked, paid + case booked, paid, payAtLocation } } @@ -14,6 +14,8 @@ extension BookingDetailsViewModel.Status { return Localization.bookingStatusBooked case .paid: return Localization.bookingStatusPaid + case .payAtLocation: + return Localization.bookingStatusPayAtLocation } } @@ -23,6 +25,8 @@ extension BookingDetailsViewModel.Status { return Color(UIColor.systemGray6) case .paid: return Color(UIColor.systemGray6) + case .payAtLocation: + return Color(UIColor(hexString: "FFE365")) } } } @@ -30,15 +34,21 @@ extension BookingDetailsViewModel.Status { private extension BookingDetailsViewModel.Status { enum Localization { static let bookingStatusBooked = NSLocalizedString( - "BookingDetailsView.appointmentDetails.statusLabel.booked", + "BookingDetailsView.statusLabel.booked", value: "Booked", - comment: "Title for the 'Booked' status label in the appointment details view." + comment: "Title for the 'Booked' status label in the header view." ) static let bookingStatusPaid = NSLocalizedString( - "BookingDetailsView.appointmentDetails.statusLabel.paid", + "BookingDetailsView.statusLabel.paid", value: "Paid", - comment: "Title for the 'Paid' status label in the appointment details view." + comment: "Title for the 'Paid' status label in the header view." + ) + + static let bookingStatusPayAtLocation = NSLocalizedString( + "BookingDetailsView.statusLabel.payAtLocation", + value: "Pay at location", + comment: "Title for the 'Pay at location' status label in the header view." ) } } diff --git a/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift index 9e7a9c2ee66..eced35ae7a9 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift @@ -9,10 +9,16 @@ extension BookingDetailsViewModel { let status: [Status] init(_ booking: Booking) { - bookingDate = booking.startDate.formatted(date: .numeric, time: .omitted) + bookingDate = booking.startDate.formatted(.dateTime + .month(.twoDigits) + .day(.twoDigits) + .year(.defaultDigits) + .hour(.twoDigits(amPM: .abbreviated)) + .minute(.twoDigits) + ) serviceName = "Women's Haircut" customerName = "Margarita Nikolaevna" - status = [.paid, .booked] + status = [.booked, .payAtLocation] } } } diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 07b74516098..decb67c8a48 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -12,6 +12,7 @@ struct BookingDetailsView: View { static let headerContentVerticalPadding: CGFloat = 6 static let headerBadgesAdditionalTopPadding: CGFloat = 4 static let sectionFooterTextVerticalPadding: CGFloat = 8 + static let circleSeparatorSize: CGFloat = 3 } fileprivate enum TextFont { @@ -97,18 +98,27 @@ private extension BookingDetailsView { func headerView(with headerContent: BookingDetailsViewModel.HeaderContent) -> some View { VStack(alignment: .leading, spacing: Layout.headerContentVerticalPadding) { Text(headerContent.bookingDate) - .font(.caption) - .foregroundColor(.secondary) - Text(headerContent.serviceName) - .font(TextFont.bodyMedium) - Text(headerContent.customerName) - .font(TextFont.bodyMedium) - .foregroundColor(.secondary) + .font(.body.weight(.medium)) + .foregroundColor(.primary) + HStack { + Text(headerContent.serviceName) + Circle() + .fill(.tertiary) + .frame( + width: Layout.circleSeparatorSize, + height: Layout.circleSeparatorSize + ) + Text(headerContent.customerName) + } + .font(.footnote.weight(.medium)) + .foregroundColor(.secondary) + HStack { ForEach(headerContent.status, id: \.self) { status in Text(status.labelText) - .font(.caption) - .padding(4) + .font(.caption2) + .padding(.vertical, 4.5) + .padding(.horizontal, 8) .background(status.labelColor) .cornerRadius(4) } @@ -116,7 +126,7 @@ private extension BookingDetailsView { .padding(.top, Layout.headerBadgesAdditionalTopPadding) } .frame(maxWidth: .infinity, alignment: .leading) - .padding(.vertical, 6) + .padding(.vertical) } func attendanceView(with content: BookingDetailsViewModel.AttendanceContent) -> some View { From 7204d39c5d7f3be01d62dcdb8ae4642ff948762d Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Mon, 29 Sep 2025 14:39:23 +0300 Subject: [PATCH 09/20] Add customer section --- .../BookingDetailsViewModel.swift | 25 +++++- .../Booking Details/CustomerContent.swift | 10 +++ .../Booking Details/BookingDetailsView.swift | 86 +++++++++++++++++++ .../WooCommerce.xcodeproj/project.pbxproj | 4 + 4 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 WooCommerce/Classes/ViewModels/Booking Details/CustomerContent.swift diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index 88201670e74..97931147cfe 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -5,9 +5,6 @@ extension BookingDetailsViewModel { struct PaymentContent { } - struct CustomerContent { - } - struct TeamMemberContent { } } @@ -31,9 +28,25 @@ final class BookingDetailsViewModel: ObservableObject { content: .attendance(AttendanceContent()) ) + let customerSection = Section( + header: .title(Localization.customerSectionHeaderTitle.uppercased()), + content: .customer( + /// Temporary hardcode + CustomerContent( + nameText: "Margarita Nikolaevna", + emailText: "margarita.n@mail.com", + phoneText: "+1 742582943798", + billingAddressText: """ + 238 Willow Creek Drive
Montgomery
AL 36109 + """ + ) + ) + ) + sections = [ headerSection, appointmentDetailsSection, + customerSection, attendanceSection ] } @@ -53,6 +66,12 @@ private extension BookingDetailsViewModel { comment: "Header title for the 'Attendance' section in the booking details screen." ) + static let customerSectionHeaderTitle = NSLocalizedString( + "BookingDetailsView.customer.headerTitle", + value: "Customer", + comment: "Header title for the 'Customer' section in the booking details screen." + ) + static let attendanceSectionFooterText = NSLocalizedString( "BookingDetailsView.attendance.footerText", value: "Mark attendance to keep your reports accurate and spot booking trends.", diff --git a/WooCommerce/Classes/ViewModels/Booking Details/CustomerContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/CustomerContent.swift new file mode 100644 index 00000000000..0a962538144 --- /dev/null +++ b/WooCommerce/Classes/ViewModels/Booking Details/CustomerContent.swift @@ -0,0 +1,10 @@ +import Foundation + +extension BookingDetailsViewModel { + struct CustomerContent { + let nameText: String + let emailText: String + let phoneText: String + let billingAddressText: String? + } +} diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index decb67c8a48..8261f6b338f 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -13,6 +13,7 @@ struct BookingDetailsView: View { static let headerBadgesAdditionalTopPadding: CGFloat = 4 static let sectionFooterTextVerticalPadding: CGFloat = 8 static let circleSeparatorSize: CGFloat = 3 + static let rowTextVerticalPadding: CGFloat = 11 } fileprivate enum TextFont { @@ -90,6 +91,8 @@ private extension BookingDetailsView { appointmentDetailsView(with: content) case .attendance(let content): attendanceView(with: content) + case .customer(let content): + customerDetailsView(with: content) default: EmptyView() } @@ -161,6 +164,89 @@ private extension BookingDetailsView { .padding(.vertical, Layout.contentVerticalPadding) } } + + func customerDetailsView(with content: BookingDetailsViewModel.CustomerContent) -> some View { + VStack(spacing: 0) { + /// Name + HStack { + Text(content.nameText) + .rowTextStyle() + Spacer() + } + .padding(.vertical, Layout.rowTextVerticalPadding) + + Divider() + .padding(.trailing, -Layout.contentSidePadding) + + /// Email + HStack { + Text(content.emailText) + .rowTextStyle() + Spacer() + Image(systemName: "doc.on.doc") + .font(.body.weight(.medium)) + .foregroundStyle(Color(UIColor.primary)) + } + .padding(.vertical, Layout.rowTextVerticalPadding) + .tappable { + print("On email copy") + } + + Divider() + .padding(.trailing, -Layout.contentSidePadding) + + /// Phone + HStack { + Text(content.phoneText) + .rowTextStyle() + Spacer() + Image(systemName: "ellipsis") + .font(.body.weight(.medium)) + .foregroundStyle(Color(UIColor.primary)) + } + .padding(.vertical, Layout.rowTextVerticalPadding) + .tappable { + print("On phone ellipsis") + } + + Divider() + .padding(.trailing, -Layout.contentSidePadding) + } + } +} + +private struct RowTextStyle: ViewModifier { + func body(content: Content) -> some View { + content + .font(.body.weight(.medium)) + .foregroundStyle(.primary) + .multilineTextAlignment(.leading) + } +} + +private struct TappableRowModifier: ViewModifier { + let onTap: () -> Void + + func body(content: Content) -> some View { + Button { + onTap() + } label: { + content + } + .buttonStyle(.plain) + } +} + +private extension Text { + func rowTextStyle() -> some View { + self.modifier(RowTextStyle()) + } +} + +private extension View { + func tappable(_ onTap: @escaping () -> Void) -> some View { + self.modifier(TappableRowModifier(onTap: onTap)) + } } private extension BookingDetailsView { diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 64843e46c02..01989e5abb2 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1249,6 +1249,7 @@ 2D05D1A52E82D3F6004111FD /* BookingDetailsViewModel+SectionContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05D1A02E82D1EF004111FD /* BookingDetailsViewModel+SectionContent.swift */; }; 2D05D1A72E82D49F004111FD /* BookingDetailsViewModel+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05D1A62E82D49D004111FD /* BookingDetailsViewModel+Status.swift */; }; 2D05E80F2E86BE50004111FD /* AttendanceContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05E80E2E86BE4F004111FD /* AttendanceContent.swift */; }; + 2D05E8112E8A9905004111FD /* CustomerContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05E8102E8A98FE004111FD /* CustomerContent.swift */; }; 2D09E0D12E61BC7F005C26F3 /* ApplicationPasswordsExperimentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D09E0D02E61BC7D005C26F3 /* ApplicationPasswordsExperimentState.swift */; }; 2D09E0D52E65C9B9005C26F3 /* ApplicationPasswordsExperimentStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D09E0D42E65C9B9005C26F3 /* ApplicationPasswordsExperimentStateTests.swift */; }; 2D7A3E232E7891DB00C46401 /* CIABEligibilityCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7A3E222E7891D200C46401 /* CIABEligibilityCheckerTests.swift */; }; @@ -4446,6 +4447,7 @@ 2D05D1A32E82D25F004111FD /* AppointmentDetailsContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppointmentDetailsContent.swift; sourceTree = ""; }; 2D05D1A62E82D49D004111FD /* BookingDetailsViewModel+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BookingDetailsViewModel+Status.swift"; sourceTree = ""; }; 2D05E80E2E86BE4F004111FD /* AttendanceContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttendanceContent.swift; sourceTree = ""; }; + 2D05E8102E8A98FE004111FD /* CustomerContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerContent.swift; sourceTree = ""; }; 2D09E0D02E61BC7D005C26F3 /* ApplicationPasswordsExperimentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordsExperimentState.swift; sourceTree = ""; }; 2D09E0D42E65C9B9005C26F3 /* ApplicationPasswordsExperimentStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordsExperimentStateTests.swift; sourceTree = ""; }; 2D7A3E222E7891D200C46401 /* CIABEligibilityCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIABEligibilityCheckerTests.swift; sourceTree = ""; }; @@ -9121,6 +9123,7 @@ 2DAC251E2E829FF9008521AF /* Booking Details */ = { isa = PBXGroup; children = ( + 2D05E8102E8A98FE004111FD /* CustomerContent.swift */, 2D05E80E2E86BE4F004111FD /* AttendanceContent.swift */, 2D05D1A12E82D233004111FD /* HeaderContent.swift */, 2D05D1A32E82D25F004111FD /* AppointmentDetailsContent.swift */, @@ -16259,6 +16262,7 @@ 0286B27B23C7051F003D784B /* ProductImagesCollectionViewController.swift in Sources */, E107FCE126C12B2700BAF51B /* InPersonPaymentsCountryNotSupported.swift in Sources */, 26E7EE7229301EBC00793045 /* ProductsReportCardViewModel.swift in Sources */, + 2D05E8112E8A9905004111FD /* CustomerContent.swift in Sources */, 027A2E142513124E00DA6ACB /* Keychain+Entries.swift in Sources */, B97C6E562B15E51A008A2BF2 /* UpdateProductInventoryView.swift in Sources */, 268EC45F26CEA50C00716F5C /* EditCustomerNote.swift in Sources */, From 5144ee541989807ddc0b24feaece55256da973fe Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Mon, 29 Sep 2025 14:49:52 +0300 Subject: [PATCH 10/20] Add billing address row to customer section --- .../Booking Details/BookingDetailsView.swift | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 8261f6b338f..33afc3f5774 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -211,6 +211,22 @@ private extension BookingDetailsView { Divider() .padding(.trailing, -Layout.contentSidePadding) + + /// Billing address + if let billingAddressText = content.billingAddressText, !billingAddressText.isEmpty { + HStack { + VStack(alignment: .leading) { + Text(Localization.billingAddressRowTitle) + .rowTextStyle() + Text(billingAddressText) + .font(.body.weight(.medium)) + .foregroundStyle(.secondary) + .multilineTextAlignment(.leading) + } + Spacer() + } + .padding(.vertical, Layout.rowTextVerticalPadding) + } } } } @@ -255,6 +271,13 @@ private extension BookingDetailsView { /// Attendance section static let statusRowTitle = "Status" + + /// Customer section + static let billingAddressRowTitle = NSLocalizedString( + "BookingDetailsView.customer.billingAddress.title", + value: "Billing address", + comment: "Billing address row title in customer section in booking details view." + ) } } From 591cb1dd6e69c64aef023efc71e2e6c8ba1e053e Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Mon, 29 Sep 2025 15:56:36 +0300 Subject: [PATCH 11/20] Add Payment section --- .../BookingDetailsViewModel.swift | 17 +++- .../Booking Details/PaymentContent.swift | 89 +++++++++++++++++++ .../Booking Details/BookingDetailsView.swift | 23 ++++- .../WooCommerce.xcodeproj/project.pbxproj | 4 + 4 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 WooCommerce/Classes/ViewModels/Booking Details/PaymentContent.swift diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index 97931147cfe..ffe3bfea2b3 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -2,9 +2,6 @@ import Foundation import struct Networking.Booking extension BookingDetailsViewModel { - struct PaymentContent { - } - struct TeamMemberContent { } } @@ -43,11 +40,17 @@ final class BookingDetailsViewModel: ObservableObject { ) ) + let paymentSection = Section( + header: .title(Localization.paymentSectionHeaderTitle.uppercased()), + content: .payment(PaymentContent(booking: booking)) + ) + sections = [ headerSection, appointmentDetailsSection, customerSection, - attendanceSection + attendanceSection, + paymentSection ] } } @@ -77,5 +80,11 @@ private extension BookingDetailsViewModel { value: "Mark attendance to keep your reports accurate and spot booking trends.", comment: "Footer text for the 'Attendance' section in the booking details screen." ) + + static let paymentSectionHeaderTitle = NSLocalizedString( + "BookingDetailsView.payment.headerTitle", + value: "Payment", + comment: "Header title for the 'Payment' section in the booking details screen." + ) } } diff --git a/WooCommerce/Classes/ViewModels/Booking Details/PaymentContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/PaymentContent.swift new file mode 100644 index 00000000000..c8b53b78ae8 --- /dev/null +++ b/WooCommerce/Classes/ViewModels/Booking Details/PaymentContent.swift @@ -0,0 +1,89 @@ +import Foundation +import struct Networking.Booking + +extension BookingDetailsViewModel { + struct PaymentContent { + let amounts: [Amount] + + init(booking: Booking) { + amounts = [ + .init(value: "$55.00", type: .service), + .init(value: "$0", type: .tax), + .init(value: "-", type: .discount), + .init(value: "$55.00", type: .total, emphasized: true), + ] + } + } +} + +extension BookingDetailsViewModel.PaymentContent { + struct Amount { + enum AmountType { + case service + case tax + case discount + case total + } + + let value: String + let type: AmountType + let emphasized: Bool + + var title: String { + return type.rowTitle + } + + init(value: String, type: AmountType, emphasized: Bool = false) { + self.value = value + self.type = type + self.emphasized = emphasized + } + } +} + +extension BookingDetailsViewModel.PaymentContent.Amount: Identifiable { + var id: String { + return title + } +} + +extension BookingDetailsViewModel.PaymentContent.Amount.AmountType { + var rowTitle: String { + switch self { + case .service: + return Localization.paymentServiceRowTitle + case .tax: + return Localization.paymentTaxRowTitle + case .discount: + return Localization.paymentDiscountRowTitle + case .total: + return Localization.paymentTotalRowTitle + } + } +} + +private enum Localization { + static let paymentServiceRowTitle = NSLocalizedString( + "BookingDetailsView.payment.serviceRow.title", + value: "Service", + comment: "Service row title in payment section in booking details view." + ) + + static let paymentTaxRowTitle = NSLocalizedString( + "BookingDetailsView.payment.taxRow.title", + value: "Tax", + comment: "Tax row title in payment section in booking details view." + ) + + static let paymentDiscountRowTitle = NSLocalizedString( + "BookingDetailsView.payment.discountRow.title", + value: "Discount", + comment: "Discount row title in payment section in booking details view." + ) + + static let paymentTotalRowTitle = NSLocalizedString( + "BookingDetailsView.payment.totalRow.title", + value: "Total", + comment: "Total row title in payment section in booking details view." + ) +} diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 33afc3f5774..5605894bbaf 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -93,6 +93,8 @@ private extension BookingDetailsView { attendanceView(with: content) case .customer(let content): customerDetailsView(with: content) + case .payment(let content): + paymentDetailsView(with: content) default: EmptyView() } @@ -229,6 +231,25 @@ private extension BookingDetailsView { } } } + + func paymentDetailsView(with content: BookingDetailsViewModel.PaymentContent) -> some View { + VStack(alignment: .leading, spacing: 8) { + ForEach(content.amounts) { amount in + HStack { + Text(amount.title) + Spacer() + Text(amount.value) + } + .if(!amount.emphasized) { view in + view.rowTextStyle() + } + .if(amount.emphasized) { view in + view.font(.body.weight(.bold)) + } + } + } + .padding(.vertical) + } } private struct RowTextStyle: ViewModifier { @@ -253,7 +274,7 @@ private struct TappableRowModifier: ViewModifier { } } -private extension Text { +private extension View { func rowTextStyle() -> some View { self.modifier(RowTextStyle()) } diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 01989e5abb2..4270e74db99 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1250,6 +1250,7 @@ 2D05D1A72E82D49F004111FD /* BookingDetailsViewModel+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05D1A62E82D49D004111FD /* BookingDetailsViewModel+Status.swift */; }; 2D05E80F2E86BE50004111FD /* AttendanceContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05E80E2E86BE4F004111FD /* AttendanceContent.swift */; }; 2D05E8112E8A9905004111FD /* CustomerContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05E8102E8A98FE004111FD /* CustomerContent.swift */; }; + 2D05E8132E8AADB9004111FD /* PaymentContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05E8122E8AADB2004111FD /* PaymentContent.swift */; }; 2D09E0D12E61BC7F005C26F3 /* ApplicationPasswordsExperimentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D09E0D02E61BC7D005C26F3 /* ApplicationPasswordsExperimentState.swift */; }; 2D09E0D52E65C9B9005C26F3 /* ApplicationPasswordsExperimentStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D09E0D42E65C9B9005C26F3 /* ApplicationPasswordsExperimentStateTests.swift */; }; 2D7A3E232E7891DB00C46401 /* CIABEligibilityCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7A3E222E7891D200C46401 /* CIABEligibilityCheckerTests.swift */; }; @@ -4448,6 +4449,7 @@ 2D05D1A62E82D49D004111FD /* BookingDetailsViewModel+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BookingDetailsViewModel+Status.swift"; sourceTree = ""; }; 2D05E80E2E86BE4F004111FD /* AttendanceContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttendanceContent.swift; sourceTree = ""; }; 2D05E8102E8A98FE004111FD /* CustomerContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerContent.swift; sourceTree = ""; }; + 2D05E8122E8AADB2004111FD /* PaymentContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentContent.swift; sourceTree = ""; }; 2D09E0D02E61BC7D005C26F3 /* ApplicationPasswordsExperimentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordsExperimentState.swift; sourceTree = ""; }; 2D09E0D42E65C9B9005C26F3 /* ApplicationPasswordsExperimentStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordsExperimentStateTests.swift; sourceTree = ""; }; 2D7A3E222E7891D200C46401 /* CIABEligibilityCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIABEligibilityCheckerTests.swift; sourceTree = ""; }; @@ -9123,6 +9125,7 @@ 2DAC251E2E829FF9008521AF /* Booking Details */ = { isa = PBXGroup; children = ( + 2D05E8122E8AADB2004111FD /* PaymentContent.swift */, 2D05E8102E8A98FE004111FD /* CustomerContent.swift */, 2D05E80E2E86BE4F004111FD /* AttendanceContent.swift */, 2D05D1A12E82D233004111FD /* HeaderContent.swift */, @@ -16756,6 +16759,7 @@ 4535EE7A281ADD56004212B4 /* CouponCodeInputFormatter.swift in Sources */, 0373A1312A1E3FD700731236 /* TapToPayBadgePromotionChecker.swift in Sources */, DEDB2D262845D31900CE7D35 /* CouponAllowedEmailsViewModel.swift in Sources */, + 2D05E8132E8AADB9004111FD /* PaymentContent.swift in Sources */, 035DBA45292D0164003E5125 /* CollectOrderPaymentUseCase.swift in Sources */, 0282DD96233C960C006A5FDB /* SearchResultCell.swift in Sources */, 027F83ED29B046D2002688C6 /* TopPerformersPeriodViewModel.swift in Sources */, From cc59e28a4c9ba88f46cd23286773c003675f8665 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Mon, 29 Sep 2025 16:18:52 +0300 Subject: [PATCH 12/20] Add button actions for payment section --- .../Booking Details/PaymentContent.swift | 58 +++++++++++++++++++ .../Booking Details/BookingDetailsView.swift | 45 ++++++++++---- 2 files changed, 92 insertions(+), 11 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/PaymentContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/PaymentContent.swift index c8b53b78ae8..3d186fd7267 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/PaymentContent.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/PaymentContent.swift @@ -4,6 +4,7 @@ import struct Networking.Booking extension BookingDetailsViewModel { struct PaymentContent { let amounts: [Amount] + let actions: [Action] init(booking: Booking) { amounts = [ @@ -12,6 +13,11 @@ extension BookingDetailsViewModel { .init(value: "-", type: .discount), .init(value: "$55.00", type: .total, emphasized: true), ] + + actions = [ + .markAsPaid, + .viewOrder + ] } } } @@ -62,6 +68,40 @@ extension BookingDetailsViewModel.PaymentContent.Amount.AmountType { } } +extension BookingDetailsViewModel.PaymentContent { + enum Action: String, Identifiable { + case markAsPaid + case markAsRefunded + case viewOrder + + var id: String { + return rawValue + } + } +} + +extension BookingDetailsViewModel.PaymentContent.Action { + var buttonTitle: String { + switch self { + case .markAsPaid: + return Localization.paymentMarkAsPaidButtonTitle + case .markAsRefunded: + return Localization.paymentMarkAsRefundedButtonTitle + case .viewOrder: + return Localization.paymentViewOrderButtonTitle + } + } + + var isEmphasized: Bool { + switch self { + case .markAsPaid: + return true + case .markAsRefunded, .viewOrder: + return false + } + } +} + private enum Localization { static let paymentServiceRowTitle = NSLocalizedString( "BookingDetailsView.payment.serviceRow.title", @@ -86,4 +126,22 @@ private enum Localization { value: "Total", comment: "Total row title in payment section in booking details view." ) + + static let paymentMarkAsPaidButtonTitle = NSLocalizedString( + "BookingDetailsView.payment.markAsPaid.title", + value: "Mark as paid", + comment: "Title for 'Mark as paid' button in payment section in booking details view." + ) + + static let paymentMarkAsRefundedButtonTitle = NSLocalizedString( + "BookingDetailsView.payment.markAsRefunded.title", + value: "Mark as refunded", + comment: "Title for 'Mark as refunded' button in payment section in booking details view." + ) + + static let paymentViewOrderButtonTitle = NSLocalizedString( + "BookingDetailsView.payment.viewOrder.title", + value: "View order", + comment: "Title for 'View order' button in payment section in booking details view." + ) } diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 5605894bbaf..77c906afc7b 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -233,20 +233,43 @@ private extension BookingDetailsView { } func paymentDetailsView(with content: BookingDetailsViewModel.PaymentContent) -> some View { - VStack(alignment: .leading, spacing: 8) { - ForEach(content.amounts) { amount in - HStack { - Text(amount.title) - Spacer() - Text(amount.value) - } - .if(!amount.emphasized) { view in - view.rowTextStyle() + VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 8) { + ForEach(content.amounts) { amount in + HStack { + Text(amount.title) + Spacer() + Text(amount.value) + } + .if(!amount.emphasized) { view in + view.rowTextStyle() + } + .if(amount.emphasized) { view in + view.font(.body.weight(.bold)) + } } - .if(amount.emphasized) { view in - view.font(.body.weight(.bold)) + } + .padding(.bottom) + + Divider() + .padding(.trailing, -Layout.contentSidePadding) + + VStack(alignment: .leading, spacing: Layout.contentVerticalPadding) { + ForEach(content.actions) { action in + Button { + /// On action tap + } label: { + Text(action.buttonTitle) + } + .if(action.isEmphasized) { + $0.buttonStyle(PrimaryButtonStyle()) + } + .if(!action.isEmphasized) { + $0.buttonStyle(SecondaryButtonStyle()) + } } } + .padding(.top) } .padding(.vertical) } From bf418914e513691ae6a4714f12aa47c1a177cbe5 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Mon, 29 Sep 2025 17:28:42 +0300 Subject: [PATCH 13/20] Add Booking Notes section --- ...okingDetailsViewModel+SectionContent.swift | 6 ++--- .../BookingDetailsViewModel.swift | 19 +++++++++----- .../Booking Details/BookingDetailsView.swift | 26 +++++++++++++++++-- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+SectionContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+SectionContent.swift index 78e1e8992ff..4dda47f5a36 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+SectionContent.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+SectionContent.swift @@ -7,7 +7,7 @@ extension BookingDetailsViewModel { case attendance(AttendanceContent) case payment(PaymentContent) case customer(CustomerContent) - case teamMember(TeamMemberContent) + case bookingNotes } } @@ -24,8 +24,8 @@ extension BookingDetailsViewModel.SectionContent: Identifiable { return "payment" case .customer: return "customer" - case .teamMember: - return "teamMember" + case .bookingNotes: + return "bookingNotes" } } } diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index ffe3bfea2b3..11c7beb62e7 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -1,11 +1,6 @@ import Foundation import struct Networking.Booking -extension BookingDetailsViewModel { - struct TeamMemberContent { - } -} - final class BookingDetailsViewModel: ObservableObject { let sections: [Section] @@ -45,12 +40,18 @@ final class BookingDetailsViewModel: ObservableObject { content: .payment(PaymentContent(booking: booking)) ) + let bookingNotes = Section( + header: .title(Localization.bookingNotesSectionHeaderTitle.uppercased()), + content: .bookingNotes + ) + sections = [ headerSection, appointmentDetailsSection, customerSection, attendanceSection, - paymentSection + paymentSection, + bookingNotes ] } } @@ -86,5 +87,11 @@ private extension BookingDetailsViewModel { value: "Payment", comment: "Header title for the 'Payment' section in the booking details screen." ) + + static let bookingNotesSectionHeaderTitle = NSLocalizedString( + "BookingDetailsView.bookingNotes.headerTitle", + value: "Booking notes", + comment: "Header title for the 'Booking notes' section in the booking details screen." + ) } } diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 77c906afc7b..77ce1d34a5a 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -95,8 +95,8 @@ private extension BookingDetailsView { customerDetailsView(with: content) case .payment(let content): paymentDetailsView(with: content) - default: - EmptyView() + case .bookingNotes: + bookingNotesView() } } @@ -273,6 +273,21 @@ private extension BookingDetailsView { } .padding(.vertical) } + + func bookingNotesView() -> some View { + HStack(spacing: Layout.contentSidePadding) { + Image(systemName: "plus") + .font(.title3.weight(.medium)) + Text(Localization.bookingNotesRowText) + .rowTextStyle() + Spacer() + } + .foregroundStyle(Color(UIColor.primary)) + .padding(.vertical, Layout.rowTextVerticalPadding) + .tappable { + print("On Add a note tap") + } + } } private struct RowTextStyle: ViewModifier { @@ -322,6 +337,13 @@ private extension BookingDetailsView { value: "Billing address", comment: "Billing address row title in customer section in booking details view." ) + + /// Booking notes + static let bookingNotesRowText = NSLocalizedString( + "BookingDetailsView.bookingNotes.addANoteRow.title", + value: "Add a note", + comment: "Add a note row title in booking notes section in booking details view." + ) } } From 1b1cadb42860e6eee5460f7f2a5d595229f35cb9 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Mon, 29 Sep 2025 17:45:45 +0300 Subject: [PATCH 14/20] Resolve unused code --- .../Booking Details/PaymentContent.swift | 2 +- .../Booking Details/BookingDetailsView.swift | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/PaymentContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/PaymentContent.swift index 3d186fd7267..c8577638511 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/PaymentContent.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/PaymentContent.swift @@ -8,7 +8,7 @@ extension BookingDetailsViewModel { init(booking: Booking) { amounts = [ - .init(value: "$55.00", type: .service), + .init(value: booking.cost, type: .service), .init(value: "$0", type: .tax), .init(value: "-", type: .discount), .init(value: "$55.00", type: .total, emphasized: true), diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 77ce1d34a5a..bb8240dfe46 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -20,10 +20,6 @@ struct BookingDetailsView: View { static var bodyMedium: Font { Font.body.weight(.medium) } - - static var bodyRegular: Font { - Font.body.weight(.regular) - } } init(_ viewModel: BookingDetailsViewModel) { @@ -103,7 +99,7 @@ private extension BookingDetailsView { func headerView(with headerContent: BookingDetailsViewModel.HeaderContent) -> some View { VStack(alignment: .leading, spacing: Layout.headerContentVerticalPadding) { Text(headerContent.bookingDate) - .font(.body.weight(.medium)) + .font(TextFont.bodyMedium) .foregroundColor(.primary) HStack { Text(headerContent.serviceName) @@ -186,7 +182,7 @@ private extension BookingDetailsView { .rowTextStyle() Spacer() Image(systemName: "doc.on.doc") - .font(.body.weight(.medium)) + .font(TextFont.bodyMedium) .foregroundStyle(Color(UIColor.primary)) } .padding(.vertical, Layout.rowTextVerticalPadding) @@ -203,7 +199,7 @@ private extension BookingDetailsView { .rowTextStyle() Spacer() Image(systemName: "ellipsis") - .font(.body.weight(.medium)) + .font(TextFont.bodyMedium) .foregroundStyle(Color(UIColor.primary)) } .padding(.vertical, Layout.rowTextVerticalPadding) @@ -221,7 +217,7 @@ private extension BookingDetailsView { Text(Localization.billingAddressRowTitle) .rowTextStyle() Text(billingAddressText) - .font(.body.weight(.medium)) + .font(TextFont.bodyMedium) .foregroundStyle(.secondary) .multilineTextAlignment(.leading) } @@ -293,7 +289,7 @@ private extension BookingDetailsView { private struct RowTextStyle: ViewModifier { func body(content: Content) -> some View { content - .font(.body.weight(.medium)) + .font(BookingDetailsView.TextFont.bodyMedium) .foregroundStyle(.primary) .multilineTextAlignment(.leading) } @@ -356,7 +352,7 @@ struct BookingDetailsView_Previews: PreviewProvider { siteID: 1, bookingID: 123, allDay: false, - cost: "70.00", + cost: "$70.00", customerID: 456, dateCreated: now, dateModified: now, From f1e1feb6b94bda48d21cbfa135f0d1282ee1d9e9 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Tue, 30 Sep 2025 13:33:56 +0300 Subject: [PATCH 15/20] Switch to Color instead of UIColor --- .../Bookings/Booking Details/BookingDetailsView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index bb8240dfe46..163329a20f7 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -183,7 +183,7 @@ private extension BookingDetailsView { Spacer() Image(systemName: "doc.on.doc") .font(TextFont.bodyMedium) - .foregroundStyle(Color(UIColor.primary)) + .foregroundStyle(.primary) } .padding(.vertical, Layout.rowTextVerticalPadding) .tappable { @@ -200,7 +200,7 @@ private extension BookingDetailsView { Spacer() Image(systemName: "ellipsis") .font(TextFont.bodyMedium) - .foregroundStyle(Color(UIColor.primary)) + .foregroundStyle(.primary) } .padding(.vertical, Layout.rowTextVerticalPadding) .tappable { @@ -278,7 +278,7 @@ private extension BookingDetailsView { .rowTextStyle() Spacer() } - .foregroundStyle(Color(UIColor.primary)) + .foregroundStyle(.primary) .padding(.vertical, Layout.rowTextVerticalPadding) .tappable { print("On Add a note tap") From 83f47151a005e210f601f2ab7b3b7f23d2e454e9 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Tue, 30 Sep 2025 13:34:19 +0300 Subject: [PATCH 16/20] Make tappable modifier reusable --- .../View Modifiers/View+Tappable.swift | 20 +++++++++++++++++++ .../Booking Details/BookingDetailsView.swift | 19 ------------------ .../WooCommerce.xcodeproj/project.pbxproj | 4 ++++ 3 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 WooCommerce/Classes/View Modifiers/View+Tappable.swift diff --git a/WooCommerce/Classes/View Modifiers/View+Tappable.swift b/WooCommerce/Classes/View Modifiers/View+Tappable.swift new file mode 100644 index 00000000000..7d02d739f91 --- /dev/null +++ b/WooCommerce/Classes/View Modifiers/View+Tappable.swift @@ -0,0 +1,20 @@ +import SwiftUI + +private struct TappableViewModifier: ViewModifier { + let onTap: () -> Void + + func body(content: Content) -> some View { + Button { + onTap() + } label: { + content + } + .buttonStyle(.plain) + } +} + +extension View { + func tappable(_ onTap: @escaping () -> Void) -> some View { + self.modifier(TappableViewModifier(onTap: onTap)) + } +} diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 163329a20f7..0368c819b06 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -295,31 +295,12 @@ private struct RowTextStyle: ViewModifier { } } -private struct TappableRowModifier: ViewModifier { - let onTap: () -> Void - - func body(content: Content) -> some View { - Button { - onTap() - } label: { - content - } - .buttonStyle(.plain) - } -} - private extension View { func rowTextStyle() -> some View { self.modifier(RowTextStyle()) } } -private extension View { - func tappable(_ onTap: @escaping () -> Void) -> some View { - self.modifier(TappableRowModifier(onTap: onTap)) - } -} - private extension BookingDetailsView { enum Localization { static let cancelBooking = "Cancel booking" diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 4270e74db99..ba4e569558a 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1251,6 +1251,7 @@ 2D05E80F2E86BE50004111FD /* AttendanceContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05E80E2E86BE4F004111FD /* AttendanceContent.swift */; }; 2D05E8112E8A9905004111FD /* CustomerContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05E8102E8A98FE004111FD /* CustomerContent.swift */; }; 2D05E8132E8AADB9004111FD /* PaymentContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05E8122E8AADB2004111FD /* PaymentContent.swift */; }; + 2D05F7102E8BE921004111FD /* View+Tappable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05F70F2E8BE91E004111FD /* View+Tappable.swift */; }; 2D09E0D12E61BC7F005C26F3 /* ApplicationPasswordsExperimentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D09E0D02E61BC7D005C26F3 /* ApplicationPasswordsExperimentState.swift */; }; 2D09E0D52E65C9B9005C26F3 /* ApplicationPasswordsExperimentStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D09E0D42E65C9B9005C26F3 /* ApplicationPasswordsExperimentStateTests.swift */; }; 2D7A3E232E7891DB00C46401 /* CIABEligibilityCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7A3E222E7891D200C46401 /* CIABEligibilityCheckerTests.swift */; }; @@ -4450,6 +4451,7 @@ 2D05E80E2E86BE4F004111FD /* AttendanceContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttendanceContent.swift; sourceTree = ""; }; 2D05E8102E8A98FE004111FD /* CustomerContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerContent.swift; sourceTree = ""; }; 2D05E8122E8AADB2004111FD /* PaymentContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentContent.swift; sourceTree = ""; }; + 2D05F70F2E8BE91E004111FD /* View+Tappable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Tappable.swift"; sourceTree = ""; }; 2D09E0D02E61BC7D005C26F3 /* ApplicationPasswordsExperimentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordsExperimentState.swift; sourceTree = ""; }; 2D09E0D42E65C9B9005C26F3 /* ApplicationPasswordsExperimentStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordsExperimentStateTests.swift; sourceTree = ""; }; 2D7A3E222E7891D200C46401 /* CIABEligibilityCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIABEligibilityCheckerTests.swift; sourceTree = ""; }; @@ -8644,6 +8646,7 @@ 262A097F2628A8BF0033AD20 /* View Modifiers */ = { isa = PBXGroup; children = ( + 2D05F70F2E8BE91E004111FD /* View+Tappable.swift */, DE4B3B5526A68DD000EEF2D8 /* View+InsetPaddings.swift */, 26281775278F0B0100C836D3 /* View+NoticesModifier.swift */, AE77EA4F27A47C99006A21BD /* View+AddingDividers.swift */, @@ -15353,6 +15356,7 @@ 265284022624937600F91BA1 /* AddOnCrossreferenceUseCase.swift in Sources */, 02F3A6842A618CD7004CD2E8 /* WordPressMediaLibraryPickerCoordinator.swift in Sources */, DE6F99802BEE04310007B2DD /* InboxDashboardCardViewModel.swift in Sources */, + 2D05F7102E8BE921004111FD /* View+Tappable.swift in Sources */, 026D68492A0E060A00D8C22C /* LocalNotificationScheduler.swift in Sources */, 02CA63DB23D1ADD100BBF148 /* MediaPickingCoordinator.swift in Sources */, 028FA466257E021100F88A48 /* RefundShippingLabelViewModel.swift in Sources */, From 23b179999bb42b21fd082e5a2eb835f5ab9760a0 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Tue, 30 Sep 2025 13:40:34 +0300 Subject: [PATCH 17/20] Add missing localizations --- .../Booking Details/BookingDetailsView.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 0368c819b06..f59b9deab20 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -303,10 +303,18 @@ private extension View { private extension BookingDetailsView { enum Localization { - static let cancelBooking = "Cancel booking" + static let cancelBooking = NSLocalizedString( + "BookingDetailsView.customer.cancelBookingButton.title", + value: "Cancel booking", + comment: "'Cancel booking' button title in appointment details section in booking details view." + ) /// Attendance section - static let statusRowTitle = "Status" + static let statusRowTitle = NSLocalizedString( + "BookingDetailsView.customer.status.title", + value: "Status", + comment: "'Status' row title in attendance section in booking details view." + ) /// Customer section static let billingAddressRowTitle = NSLocalizedString( From 5b4c2792ba3ad00bbebe6fabf6dbc38270acd07f Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Tue, 30 Sep 2025 13:52:00 +0300 Subject: [PATCH 18/20] Use string dot separator instead of Circle shape --- .../Booking Details/HeaderContent.swift | 27 ++++++++++++------- .../Booking Details/BookingDetailsView.swift | 16 +++-------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift index eced35ae7a9..a88f1fc1380 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift @@ -4,21 +4,28 @@ import struct Networking.Booking extension BookingDetailsViewModel { struct HeaderContent: Hashable { let bookingDate: String - let serviceName: String - let customerName: String + let serviceAndCustomerLine: String let status: [Status] init(_ booking: Booking) { - bookingDate = booking.startDate.formatted(.dateTime - .month(.twoDigits) - .day(.twoDigits) - .year(.defaultDigits) - .hour(.twoDigits(amPM: .abbreviated)) - .minute(.twoDigits) + bookingDate = booking.startDate.formatted( + date: .numeric, + time: .shortened ) - serviceName = "Women's Haircut" - customerName = "Margarita Nikolaevna" + + /// Temporary hardcode + serviceAndCustomerLine = [ + "Women's Haircut", + "Margarita Nikolaevna" + ].joined(separator: Constants.dotSeparator) + status = [.booked, .payAtLocation] } } } + +private extension BookingDetailsViewModel { + enum Constants { + static let dotSeparator: String = " • " + } +} diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index f59b9deab20..970ef05f2cf 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -101,19 +101,9 @@ private extension BookingDetailsView { Text(headerContent.bookingDate) .font(TextFont.bodyMedium) .foregroundColor(.primary) - HStack { - Text(headerContent.serviceName) - Circle() - .fill(.tertiary) - .frame( - width: Layout.circleSeparatorSize, - height: Layout.circleSeparatorSize - ) - Text(headerContent.customerName) - } - .font(.footnote.weight(.medium)) - .foregroundColor(.secondary) - + Text(headerContent.serviceAndCustomerLine) + .font(.footnote.weight(.medium)) + .foregroundColor(.secondary) HStack { ForEach(headerContent.status, id: \.self) { status in Text(status.labelText) From a8c1c8812225d387f2e499ec98e56f38efea1628 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Tue, 30 Sep 2025 14:14:47 +0300 Subject: [PATCH 19/20] Used accent color instead of primary for icons --- .../Bookings/Booking Details/BookingDetailsView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 970ef05f2cf..d05bf7b0759 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -173,7 +173,7 @@ private extension BookingDetailsView { Spacer() Image(systemName: "doc.on.doc") .font(TextFont.bodyMedium) - .foregroundStyle(.primary) + .foregroundStyle(Color.accentColor) } .padding(.vertical, Layout.rowTextVerticalPadding) .tappable { @@ -190,7 +190,7 @@ private extension BookingDetailsView { Spacer() Image(systemName: "ellipsis") .font(TextFont.bodyMedium) - .foregroundStyle(.primary) + .foregroundStyle(Color.accentColor) } .padding(.vertical, Layout.rowTextVerticalPadding) .tappable { @@ -268,7 +268,7 @@ private extension BookingDetailsView { .rowTextStyle() Spacer() } - .foregroundStyle(.primary) + .foregroundStyle(Color.accentColor) .padding(.vertical, Layout.rowTextVerticalPadding) .tappable { print("On Add a note tap") From affe0f344bfca7e34379f67c727b38304bd8ccd8 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Tue, 30 Sep 2025 14:30:42 +0300 Subject: [PATCH 20/20] Delete unused circle size constant --- .../Bookings/Booking Details/BookingDetailsView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index d05bf7b0759..7c160994206 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -12,7 +12,6 @@ struct BookingDetailsView: View { static let headerContentVerticalPadding: CGFloat = 6 static let headerBadgesAdditionalTopPadding: CGFloat = 4 static let sectionFooterTextVerticalPadding: CGFloat = 8 - static let circleSeparatorSize: CGFloat = 3 static let rowTextVerticalPadding: CGFloat = 11 }