diff --git a/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift new file mode 100644 index 00000000000..14971aee79f --- /dev/null +++ b/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift @@ -0,0 +1,75 @@ +import Foundation +import struct Networking.Booking + +extension BookingDetailsViewModel { + struct AppointmentDetailsContent { + struct Row: Identifiable { + let title: String + let value: String + + var id: String { + return title + } + } + + let rows: [Row] + + init(_ booking: Booking) { + let durationMinutes = Int(booking.endDate.timeIntervalSince(booking.startDate) / 60) + let appointmentDate = booking.startDate.formatted(date: .numeric, time: .omitted) + let appointmentTimeFrame = [ + booking.startDate.formatted(date: .omitted, time: .shortened), + booking.endDate.formatted(date: .omitted, time: .shortened) + ].joined(separator: " - ") + + 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.appointmentDetailsDurationTitle, value: String(durationMinutes)), + Row(title: Localization.appointmentDetailsCostTitle, value: booking.cost) + ] + } + } +} + +private extension BookingDetailsViewModel.AppointmentDetailsContent { + enum Localization { + static let appointmentDetailsDateRowTitle = NSLocalizedString( + "BookingDetailsView.appointmentDetails.dateRow.title", + value: "Date", + comment: "Date row title in appointment details section in booking details view." + ) + + static let appointmentDetailsTimeRowTitle = NSLocalizedString( + "BookingDetailsView.appointmentDetails.timeRow.title", + value: "Time", + 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 appointmentDetailsQuantityTitle = NSLocalizedString( + "BookingDetailsView.appointmentDetails.quantityRow.title", + value: "Quantity", + comment: "Quantity row title in appointment details section in booking details view." + ) + + static let appointmentDetailsDurationTitle = NSLocalizedString( + "BookingDetailsView.appointmentDetails.durationRow.title", + value: "Duration", + 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." + ) + } +} diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Section.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Section.swift new file mode 100644 index 00000000000..451c2528a93 --- /dev/null +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Section.swift @@ -0,0 +1,23 @@ +import Foundation + +extension BookingDetailsViewModel { + struct Section: Identifiable { + var id: String { + return content.id + } + + let headerText: String? + let footerText: String? + let content: SectionContent + + init( + headerText: String? = nil, + footerText: String? = nil, + content: SectionContent + ) { + self.headerText = headerText + self.footerText = footerText + self.content = content + } + } +} diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+SectionContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+SectionContent.swift new file mode 100644 index 00000000000..78e1e8992ff --- /dev/null +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+SectionContent.swift @@ -0,0 +1,31 @@ +import Foundation + +extension BookingDetailsViewModel { + enum SectionContent { + case header(HeaderContent) + case appointmentDetails(AppointmentDetailsContent) + case attendance(AttendanceContent) + case payment(PaymentContent) + case customer(CustomerContent) + case teamMember(TeamMemberContent) + } +} + +extension BookingDetailsViewModel.SectionContent: Identifiable { + var id: String { + switch self { + case .header: + return "header" + case .appointmentDetails: + return "appointmentDetails" + case .attendance: + return "attendance" + case .payment: + return "payment" + case .customer: + return "customer" + case .teamMember: + return "teamMember" + } + } +} diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Status.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Status.swift new file mode 100644 index 00000000000..71552a555d3 --- /dev/null +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Status.swift @@ -0,0 +1,44 @@ +import Foundation +import SwiftUI + +extension BookingDetailsViewModel { + enum Status { + case booked, paid + } +} + +extension BookingDetailsViewModel.Status { + var labelText: String { + switch self { + case .booked: + return Localization.bookingStatusBooked + case .paid: + return Localization.bookingStatusPaid + } + } + + var labelColor: Color { + switch self { + case .booked: + return Color(UIColor.systemGray6) + case .paid: + return Color(UIColor.systemGray6) + } + } +} + +private extension BookingDetailsViewModel.Status { + enum Localization { + static let bookingStatusBooked = NSLocalizedString( + "BookingDetailsView.appointmentDetails.statusLabel.booked", + value: "Booked", + comment: "Title for the 'Booked' status label in the appointment details view." + ) + + static let bookingStatusPaid = NSLocalizedString( + "BookingDetailsView.appointmentDetails.statusLabel.paid", + value: "Paid", + comment: "Title for the 'Paid' status label in the appointment details view." + ) + } +} diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift new file mode 100644 index 00000000000..25e7997df4e --- /dev/null +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -0,0 +1,46 @@ +import Foundation +import struct Networking.Booking + +extension BookingDetailsViewModel { + struct AttendanceContent { + } + + struct PaymentContent { + } + + struct CustomerContent { + } + + struct TeamMemberContent { + } +} + +final class BookingDetailsViewModel: ObservableObject { + let sections: [Section] + + init(booking: Booking) { + let headerSection = Section.init( + content: .header(HeaderContent(booking)) + ) + + let appointmentDetailsSection = Section( + headerText: Localization.appointmentDetailsSectionHeaderTitle.uppercased(), + content: .appointmentDetails(AppointmentDetailsContent(booking)) + ) + + sections = [ + headerSection, + appointmentDetailsSection + ] + } +} + +private extension BookingDetailsViewModel { + enum Localization { + static let appointmentDetailsSectionHeaderTitle = NSLocalizedString( + "BookingDetailsView.appointmentDetails.headerTitle", + value: "Appointment Details", + comment: "Header title for the 'Appointment Details' section in the booking details screen." + ) + } +} diff --git a/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift new file mode 100644 index 00000000000..9e7a9c2ee66 --- /dev/null +++ b/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift @@ -0,0 +1,18 @@ +import Foundation +import struct Networking.Booking + +extension BookingDetailsViewModel { + struct HeaderContent: Hashable { + let bookingDate: String + let serviceName: String + let customerName: String + let status: [Status] + + init(_ booking: Booking) { + bookingDate = booking.startDate.formatted(date: .numeric, time: .omitted) + serviceName = "Women's Haircut" + customerName = "Margarita Nikolaevna" + status = [.paid, .booked] + } + } +} diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift new file mode 100644 index 00000000000..eef19b175a2 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -0,0 +1,160 @@ +import SwiftUI +import Networking + +struct BookingDetailsView: View { + @Environment(\.safeAreaInsets) var safeAreaInsets: EdgeInsets + + @ObservedObject private var viewModel: BookingDetailsViewModel + + private enum Layout { + static let contentSidePadding: CGFloat = 16 + static let headerContentVerticalPadding: CGFloat = 6 + static let headerBadgesAdditionalTopPadding: CGFloat = 4 + } + + fileprivate enum TextFont { + static var bodyMedium: Font { + Font.body.weight(.medium) + } + + static var bodyRegular: Font { + Font.body.weight(.regular) + } + } + + init(_ viewModel: BookingDetailsViewModel) { + self.viewModel = viewModel + } + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: .zero) { + ForEach(viewModel.sections) { section in + sectionView(with: section) + } + } + } + .refreshable { + print("Refresh triggered") + } + .navigationBarTitleDisplayMode(.inline) + .background(Color(uiColor: .systemGroupedBackground)) + } +} + +private extension BookingDetailsView { + func sectionView(with section: BookingDetailsViewModel.Section) -> some View { + VStack(alignment: .leading, spacing: 0) { + if let headerText = section.headerText { + ListHeaderView( + text: headerText, + alignment: .left + ) + .padding(.horizontal, insets: safeAreaInsets) + .accessibility(addTraits: .isHeader) + } + + sectionContentView(section.content) + .padding(.horizontal, Layout.contentSidePadding) + .background(Color(.systemBackground)) + .addingTopAndBottomDividers() + + if let footerText = section.footerText { + Text(footerText) + .padding(.horizontal, Layout.contentSidePadding) + .font(.footnote) + .foregroundColor(.gray) + } + } + } + + @ViewBuilder + func sectionContentView(_ content: BookingDetailsViewModel.SectionContent) -> some View { + switch content { + case .header(let content): + headerView(with: content) + case .appointmentDetails(let content): + appointmentDetailsView(with: content) + default: + EmptyView() + } + } + + 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) + HStack { + ForEach(headerContent.status, id: \.self) { status in + Text(status.labelText) + .font(.caption) + .padding(4) + .background(status.labelColor) + .cornerRadius(4) + } + } + .padding(.top, Layout.headerBadgesAdditionalTopPadding) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 6) + } + + func appointmentDetailsView(with content: BookingDetailsViewModel.AppointmentDetailsContent) -> some View { + VStack(alignment: .leading, spacing: 0) { + ForEach(content.rows) { row in + TitleAndTextFieldRow( + 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, + ) + + if row.id != content.rows.last?.id { + Divider() + .padding(.trailing, -Layout.contentSidePadding) + } + } + } + } +} + +#if DEBUG +struct BookingDetailsView_Previews: PreviewProvider { + static var previews: some View { + let now = Date() + let hourFromNow = now.addingTimeInterval(3600) + let sampleBooking = Booking( + siteID: 1, + bookingID: 123, + allDay: false, + cost: "70.00", + customerID: 456, + dateCreated: now, + dateModified: now, + endDate: hourFromNow, + googleCalendarEventID: nil, + orderID: 789, + orderItemID: 101, + parentID: 0, + productID: 112, + resourceID: 113, + startDate: now, + statusKey: "paid", + localTimezone: "America/New_York" + ) + let viewModel = BookingDetailsViewModel(booking: sampleBooking) + return BookingDetailsView(viewModel) + } +} +#endif diff --git a/WooCommerce/Classes/ViewRelated/Coupons/Add and Edit Coupons/AddEditCoupon.swift b/WooCommerce/Classes/ViewRelated/Coupons/Add and Edit Coupons/AddEditCoupon.swift index d037bb13ed8..9f646a2b880 100644 --- a/WooCommerce/Classes/ViewRelated/Coupons/Add and Edit Coupons/AddEditCoupon.swift +++ b/WooCommerce/Classes/ViewRelated/Coupons/Add and Edit Coupons/AddEditCoupon.swift @@ -106,7 +106,8 @@ struct AddEditCoupon: View { editable: true, fieldAlignment: .leading, keyboardType: .decimalPad, - contentColor: viewModel.amountFieldColor, + titleColor: viewModel.amountFieldColor, + valueColor: viewModel.amountFieldColor, inputFormatter: CouponAmountInputFormatter()) { beginningEditing in if !beginningEditing { viewModel.validatePercentageAmountInput(withWarning: true) diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TitleAndTextFieldRow.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TitleAndTextFieldRow.swift index cf932a8e814..462da6c8231 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TitleAndTextFieldRow.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TitleAndTextFieldRow.swift @@ -12,7 +12,10 @@ struct TitleAndTextFieldRow: View { private let editable: Bool private let fieldAlignment: TextAlignment private let inputFormatter: UnitInputFormatter? - private let contentColor: Color + private let titleColor: Color + private let titleFont: Font + private let valueColor: Color + private let valueFont: Font private let minHeight: CGFloat private let horizontalPadding: CGFloat @@ -33,7 +36,10 @@ struct TitleAndTextFieldRow: View { fieldAlignment: TextAlignment = .trailing, keyboardType: UIKeyboardType = .default, autocapitalization: TextInputAutocapitalization = .sentences, - contentColor: Color = Color(.label), + titleColor: Color = Color(.label), + titleFont: Font = .body, + valueColor: Color = Color(.label), + valueFont: Font = .body, inputFormatter: UnitInputFormatter? = nil, minHeight: CGFloat = Constants.height, horizontalPadding: CGFloat = Constants.padding, @@ -47,7 +53,10 @@ struct TitleAndTextFieldRow: View { self.fieldAlignment = fieldAlignment self.keyboardType = keyboardType self.autocapitalization = autocapitalization - self.contentColor = contentColor + self.titleColor = titleColor + self.titleFont = titleFont + self.valueColor = valueColor + self.valueFont = valueFont self.inputFormatter = inputFormatter self.minHeight = minHeight self.horizontalPadding = horizontalPadding @@ -57,15 +66,15 @@ struct TitleAndTextFieldRow: View { var body: some View { AdaptiveStack(horizontalAlignment: .leading, spacing: Constants.spacing) { Text(title) - .foregroundColor(contentColor) - .bodyStyle() + .foregroundColor(titleColor) .lineLimit(1) + .font(titleFont) .fixedSize() .modifier(MaxWidthModifier()) .frame(width: titleWidth, alignment: .leading) HStack { TextField(placeholder, text: $text, onEditingChanged: onEditingChanged ?? { _ in }) - .foregroundColor(contentColor) + .foregroundColor(valueColor) .onChange(of: text) { _, newValue in text = formatText(newValue) } @@ -73,13 +82,15 @@ struct TitleAndTextFieldRow: View { text = formatText(text) } .multilineTextAlignment(fieldAlignment) - .font(.body) + .font(valueFont) .keyboardType(keyboardType) .disabled(!editable) .textInputAutocapitalization(autocapitalization) if let symbol = symbol { Text(symbol) .bodyStyle() + .font(valueFont) + .foregroundColor(valueColor) } } } diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 7b6ef7bb5a6..054acb658e9 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1243,12 +1243,19 @@ 26FE09E124DB8FA000B9BDF5 /* SurveyCoordinatorControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26FE09E024DB8FA000B9BDF5 /* SurveyCoordinatorControllerTests.swift */; }; 26FFC50C2BED7C5A0067B3A4 /* WatchDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B249702BEC801400730730 /* WatchDependencies.swift */; }; 26FFC50D2BED7C5B0067B3A4 /* WatchDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B249702BEC801400730730 /* WatchDependencies.swift */; }; + 2D05D19F2E82D1A8004111FD /* BookingDetailsViewModel+Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05D19E2E82D1A3004111FD /* BookingDetailsViewModel+Section.swift */; }; + 2D05D1A22E82D235004111FD /* HeaderContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05D1A12E82D233004111FD /* HeaderContent.swift */; }; + 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 */; }; 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 */; }; 2D880B492DFB2F3F00A6FB2C /* OptionalBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D880B482DFB2F3D00A6FB2C /* OptionalBinding.swift */; }; 2D88C1112DF883C300A6FB2C /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D88C1102DF883BD00A6FB2C /* AttributedString+Helpers.swift */; }; 2DA63E042E69B6D400B0CB28 /* ApplicationPasswordsExperimentAvailabilityCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA63E032E69B6D200B0CB28 /* ApplicationPasswordsExperimentAvailabilityCheckerTests.swift */; }; + 2DAC25202E82A02C008521AF /* BookingDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC251F2E82A02C008521AF /* BookingDetailsViewModel.swift */; }; + 2DAC2C992E82A185008521AF /* BookingDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC2C972E82A185008521AF /* BookingDetailsView.swift */; }; 2DB877522E25466C0001B175 /* ShippingItemRowAccessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB877512E25466B0001B175 /* ShippingItemRowAccessibility.swift */; }; 2DB88DA42E27DD8D0001B175 /* MarkOrderAsReadUseCase+Woo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB88DA32E27DD790001B175 /* MarkOrderAsReadUseCase+Woo.swift */; }; 2DB891662E27F0830001B175 /* Address+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB891652E27F07E0001B175 /* Address+Shared.swift */; }; @@ -4432,12 +4439,19 @@ 26FE09E024DB8FA000B9BDF5 /* SurveyCoordinatorControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyCoordinatorControllerTests.swift; sourceTree = ""; }; 26FFD32628C6A0A4002E5E5E /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 26FFD32928C6A0F4002E5E5E /* UIImage+Widgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Widgets.swift"; sourceTree = ""; }; + 2D05D19E2E82D1A3004111FD /* BookingDetailsViewModel+Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BookingDetailsViewModel+Section.swift"; sourceTree = ""; }; + 2D05D1A02E82D1EF004111FD /* BookingDetailsViewModel+SectionContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BookingDetailsViewModel+SectionContent.swift"; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; 2D880B482DFB2F3D00A6FB2C /* OptionalBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalBinding.swift; sourceTree = ""; }; 2D88C1102DF883BD00A6FB2C /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = ""; }; 2DA63E032E69B6D200B0CB28 /* ApplicationPasswordsExperimentAvailabilityCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordsExperimentAvailabilityCheckerTests.swift; sourceTree = ""; }; + 2DAC251F2E82A02C008521AF /* BookingDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookingDetailsViewModel.swift; sourceTree = ""; }; + 2DAC2C972E82A185008521AF /* BookingDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookingDetailsView.swift; sourceTree = ""; }; 2DB877512E25466B0001B175 /* ShippingItemRowAccessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingItemRowAccessibility.swift; sourceTree = ""; }; 2DB88DA32E27DD790001B175 /* MarkOrderAsReadUseCase+Woo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MarkOrderAsReadUseCase+Woo.swift"; sourceTree = ""; }; 2DB891652E27F07E0001B175 /* Address+Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Address+Shared.swift"; sourceTree = ""; }; @@ -9102,6 +9116,35 @@ path = CIAB; sourceTree = ""; }; + 2DAC251E2E829FF9008521AF /* Booking Details */ = { + isa = PBXGroup; + children = ( + 2D05D1A12E82D233004111FD /* HeaderContent.swift */, + 2D05D1A32E82D25F004111FD /* AppointmentDetailsContent.swift */, + 2DAC251F2E82A02C008521AF /* BookingDetailsViewModel.swift */, + 2D05D1A62E82D49D004111FD /* BookingDetailsViewModel+Status.swift */, + 2D05D19E2E82D1A3004111FD /* BookingDetailsViewModel+Section.swift */, + 2D05D1A02E82D1EF004111FD /* BookingDetailsViewModel+SectionContent.swift */, + ); + path = "Booking Details"; + sourceTree = ""; + }; + 2DAC2C952E82A15C008521AF /* Bookings */ = { + isa = PBXGroup; + children = ( + 2DAC2C962E82A169008521AF /* Booking Details */, + ); + path = Bookings; + sourceTree = ""; + }; + 2DAC2C962E82A169008521AF /* Booking Details */ = { + isa = PBXGroup; + children = ( + 2DAC2C972E82A185008521AF /* BookingDetailsView.swift */, + ); + path = "Booking Details"; + sourceTree = ""; + }; 2DCB54F82E6AE8C900621F90 /* CIAB */ = { isa = PBXGroup; children = ( @@ -10875,6 +10918,7 @@ B56DB3EF2049C06D00D4AA8E /* ViewRelated */ = { isa = PBXGroup; children = ( + 2DAC2C952E82A15C008521AF /* Bookings */, 68B3BA242D91473D0000B2F2 /* AI Settings */, B626C7192876599B0083820C /* Custom Fields */, 86023FA82B15CA8D00A28F07 /* Themes */, @@ -12523,6 +12567,7 @@ CE85535B209B5B6A00938BDC /* ViewModels */ = { isa = PBXGroup; children = ( + 2DAC251E2E829FF9008521AF /* Booking Details */, 68709D3E2A2EE2C000A7FA6C /* InAppPurchases */, 02E3B62B290631A5007E0F13 /* Authentication */, D41C9F2A26D9A04A00993558 /* WhatsNew */, @@ -15714,6 +15759,7 @@ D89CFF3A25B43BBB000E4683 /* WrongAccountErrorViewModel.swift in Sources */, 02B2829227C4808D004A332A /* InfiniteScrollIndicator.swift in Sources */, 03E471DC29424EC9001A58AD /* CardPresentModalTapToPaySuccessWithoutEmail.swift in Sources */, + 2DAC25202E82A02C008521AF /* BookingDetailsViewModel.swift in Sources */, 45C91CFE25E55A1200FD8812 /* ShippingLabelAddressTopBannerFactory.swift in Sources */, B5DBF3CB20E149CC00B53AED /* AuthenticatedState.swift in Sources */, DEFE13C32DF1553E005B3D39 /* UPSTermsView.swift in Sources */, @@ -15845,6 +15891,8 @@ CC254F3226C2BCCF005F3C82 /* ShippingLabelAddNewPackageViewModel.swift in Sources */, 0202B6922387AB0C00F3EBE0 /* WooTab+Tag.swift in Sources */, CE29FEF22C009867007679C2 /* OrderShippingSection.swift in Sources */, + 2D05D1A42E82D266004111FD /* AppointmentDetailsContent.swift in Sources */, + 2D05D19F2E82D1A8004111FD /* BookingDetailsViewModel+Section.swift in Sources */, D8736B5A22F07D7100A14A29 /* MainTabViewModel.swift in Sources */, 02619858256B53DD00E321E9 /* AggregatedShippingLabelOrderItems.swift in Sources */, 260520F22B83B1B7005D5D59 /* ConnectivityToolViewModel.swift in Sources */, @@ -16372,6 +16420,7 @@ 010F7D8D2E7A8447002B02EA /* ProductImageThumbnail+Extensions.swift in Sources */, 26A630FE253F63C300CBC3B1 /* RefundableOrderItem.swift in Sources */, CEE006052077D1280079161F /* SummaryTableViewCell.swift in Sources */, + 2D05D1A72E82D49F004111FD /* BookingDetailsViewModel+Status.swift in Sources */, DEE215342D1297CD004A11F3 /* UserDefaults+EditStoreList.swift in Sources */, CE63024E2BAC664900E3325C /* EmailView.swift in Sources */, DE4B3B5826A7041800EEF2D8 /* EdgeInsets+Woo.swift in Sources */, @@ -16655,6 +16704,7 @@ B57C744E20F56E3800EEFC87 /* UITableViewCell+Helpers.swift in Sources */, 0295355B245ADF8100BDC42B /* FilterType+Products.swift in Sources */, 02CA63DA23D1ADD100BBF148 /* CameraCaptureCoordinator.swift in Sources */, + 2D05D1A52E82D3F6004111FD /* BookingDetailsViewModel+SectionContent.swift in Sources */, 260C31602524ECA900157BC2 /* IssueRefundViewController.swift in Sources */, 4521397027FF53E400964ED3 /* CouponExpiryDateView.swift in Sources */, EE4C45812C36E769001A3D94 /* ViewPackagePhoto.swift in Sources */, @@ -17005,6 +17055,7 @@ 74EC34A5225FE21F004BBC2E /* ProductLoaderViewController.swift in Sources */, CE8CCD43239AC06E009DBD22 /* RefundDetailsViewController.swift in Sources */, 869C2AA42C91791B00DDEE13 /* AztecEditorView.swift in Sources */, + 2DAC2C992E82A185008521AF /* BookingDetailsView.swift in Sources */, B560D68C2195BD1E0027BB7E /* NoteDetailsCommentTableViewCell.swift in Sources */, DEA88F502AA9D0100037273B /* AddEditProductCategoryViewModel.swift in Sources */, 451C77712404518600413F73 /* ProductSettingsRows.swift in Sources */, @@ -17022,6 +17073,7 @@ B541B21A2189F3A2008FE7C1 /* StringFormatter.swift in Sources */, 0250192B2BDF3757009493A5 /* PriceFieldFormatter.swift in Sources */, 0279F0DF252DC12D0098D7DE /* ProductLoaderViewControllerModel+Init.swift in Sources */, + 2D05D1A22E82D235004111FD /* HeaderContent.swift in Sources */, 26B9875D273C6A830090E8CA /* SimplePaymentsNoteViewModel.swift in Sources */, CE6302482BAB60AE00E3325C /* CustomerDetailViewModel.swift in Sources */, DEF36DEA2898D3CF00178AC2 /* AuthenticatedWebViewModel.swift in Sources */,