From b63375babf89cecf3b2bb4a6240da7d10a62d5e4 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Tue, 23 Sep 2025 14:44:59 +0300 Subject: [PATCH 01/15] Draft booking details view and view model --- .../BookingDetailsViewModel.swift | 109 +++++++++ .../Booking Details/BookingDetailsView.swift | 228 ++++++++++++++++++ .../BookingDetailsViewController.swift | 28 +++ .../WooCommerce.xcodeproj/project.pbxproj | 36 +++ 4 files changed, 401 insertions(+) create mode 100644 WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift create mode 100644 WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift create mode 100644 WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsViewController.swift diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift new file mode 100644 index 00000000000..43633f9fc4e --- /dev/null +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -0,0 +1,109 @@ +import Foundation +import WooFoundation +import struct Networking.Booking + +extension BookingDetailsViewModel { + enum Status { + case booked, paid + } +} + +private extension BookingDetailsViewModel { + static let dateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "dd/MM/yyyy, hh:mm a" + return dateFormatter + }() + + static let appointmentDateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EEEE, dd MMMM yyyy" + return dateFormatter + }() + + static let appointmentTimeFrameFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "hh:mm a" + return dateFormatter + }() +} + +final class BookingDetailsViewModel: ObservableObject { + + init(booking: Booking) { + bookingDate = Self.dateFormatter.string(from: booking.startDate) + + // This will be assigned later + serviceName = "Women's Haircut" + customerName = "Margarita Nikolaevna" + service = "Women's Haircut" + status = [.paid, .booked] + quantity = 1 + servicesCost = "$62.68" + tax = "$7.32" + + appointmentDate = Self.appointmentDateFormatter.string(from: booking.startDate) + appointmentTimeFrame = [ + Self.appointmentTimeFrameFormatter.string(from: booking.startDate), + Self.appointmentTimeFrameFormatter.string(from: booking.endDate) + ].joined(separator: " - ") + durationMinutes = Int(booking.endDate.timeIntervalSince(booking.startDate) / 60) + + cost = booking.cost + total = booking.cost + paid = booking.cost + } + + // MARK: - Header Properties + let bookingDate: String + let serviceName: String + let customerName: String + let status: [Status] + + // MARK: - Appointment Details + let appointmentDate: String + let appointmentTimeFrame: String + let service: String + let quantity: Int + let durationMinutes: Int + let cost: String + + // MARK: - Payment Details + let servicesCost: String + let tax: String + let total: String + let paid: String + + // MARK: - Customer Details + var customerEmail: String { + // This will be fetched from the customer details later + return "margarita.n@mail.com" + } + + var customerPhone: String { + // This will be fetched from the customer details later + return "+1742582943798" + } + + var billingAddress: String { + // This will be fetched from the customer details later + return "238 Willow Creek Drive\nMontgomery\nAL 36109" + } + + // MARK: - Actions + func rescheduleBooking() { + // Placeholder for reschedule logic + } + + func cancelBooking() { + // Placeholder for cancel logic + } + + func markAsPaid() { + // Placeholder for mark as paid logic + } + + func viewOrder() { + // Placeholder for view order logic + } +} 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..d29b5686f2c --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -0,0 +1,228 @@ +import SwiftUI +import WooFoundation +import Networking + +struct BookingDetailsView: View { + @ObservedObject var viewModel: BookingDetailsViewModel + + enum Layout { + static let contentSidePadding: CGFloat = 16 + static let headerContentVerticalPadding: CGFloat = 6 + static let headerBadgesAdditionalTopPadding: CGFloat = 4 + } + + enum TextFont { + static let headerBodyText = Font.body.weight(.medium) + } + + enum ColorConstants { + static let bookingStatusLabel: Color = .gray + } + + var body: some View { + RefreshablePlainList(action: { + print("Refresh triggered") + }) { + VStack(alignment: .leading) { + headerView + .padding(.horizontal) + + Divider() +// +// // Appointment Details +// appointmentDetailsSectionView +// .padding(.horizontal) +// +// VStack(spacing: 12) { +// Button(action: { +// viewModel.rescheduleBooking() +// }) { +// Text("Reschedule") +// .frame(maxWidth: .infinity) +// .padding() +// .background(Color.white) +// .border(Color.gray, width: 1) +// .cornerRadius(8) +// } +// +// Button(action: { +// viewModel.cancelBooking() +// }) { +// Text("Cancel booking") +// .frame(maxWidth: .infinity) +// .padding() +// .background(Color.white) +// .border(Color.gray, width: 1) +// .cornerRadius(8) +// } +// } +// .padding(.horizontal) +// +// Divider() +// +// // Payment Details +// VStack(alignment: .leading, spacing: 16) { +// Text("PAYMENT") +// .font(.caption) +// .foregroundColor(.gray) +// +// DetailRow(title: "Services", value: viewModel.servicesCost) +// DetailRow(title: "Tax", value: viewModel.tax) +// DetailRow(title: "Total", value: viewModel.total, isBold: true) +// DetailRow(title: "Paid", value: viewModel.paid, isBold: true) +// } +// .padding(.horizontal) +// +// VStack(spacing: 12) { +// Button(action: { +// viewModel.markAsPaid() +// }) { +// Text("Mark as paid") +// .frame(maxWidth: .infinity) +// .padding() +// .background(Color.accentColor) +// .foregroundColor(.white) +// .cornerRadius(8) +// } +// +// Button(action: { +// viewModel.viewOrder() +// }) { +// Text("View order") +// .frame(maxWidth: .infinity) +// .padding() +// .background(Color.white) +// .border(Color.gray, width: 1) +// .cornerRadius(8) +// } +// } +// .padding(.horizontal) +// +// Divider() +// +// // Customer Details +// VStack(alignment: .leading, spacing: 16) { +// Text("CUSTOMER") +// .font(.caption) +// .foregroundColor(.gray) +// +// Text(viewModel.customerName).font(.headline) +// Text(viewModel.customerEmail) +// Text(viewModel.customerPhone) +// +// Text("Billing address").font(.headline).padding(.top) +// Text(viewModel.billingAddress) +// } +// .padding(.horizontal) + } + .padding(.vertical) + } + .navigationBarTitleDisplayMode(.inline) + .background(Color(uiColor: .listBackground)) + } +} + +struct DetailRow: View { + let title: String + let value: String + var isBold: Bool = false + + var body: some View { + HStack { + Text(title) + Spacer() + Text(value) + .fontWeight(isBold ? .bold : .regular) + } + } +} + +private extension BookingDetailsView { + var headerView: some View { + VStack(alignment: .leading, spacing: Layout.headerContentVerticalPadding) { + Text(viewModel.bookingDate) + .font(.caption) + .foregroundColor(.secondary) + Text(viewModel.serviceName) + .font(TextFont.headerBodyText) + Text(viewModel.customerName) + .font(TextFont.headerBodyText) + .foregroundColor(.secondary) + HStack { + ForEach(viewModel.status, id: \.self) { status in + Text(status.labelText) + .font(.caption) + .padding(4) + .background(status.labelColor) + .cornerRadius(4) + } + } + .padding(.top, Layout.headerBadgesAdditionalTopPadding) + } + } + +// var appointmentDetailsSectionView: some View { +// VStack(alignment: .leading, spacing: 16) { +// Text("APPOINTMENT DETAILS") +// .font(.caption) +// .foregroundColor(.gray) +// +// DetailRow(title: "Date", value: viewModel.appointmentDate) +// DetailRow(title: "Time", value: viewModel.appointmentTime) +// DetailRow(title: "Service", value: viewModel.service) +// DetailRow(title: "Quantity", value: "\(viewModel.quantity)") +// DetailRow(title: "Duration", value: viewModel.duration) +// DetailRow(title: "Cost", value: viewModel.cost) +// } +// } +} + +extension BookingDetailsViewModel.Status { + var labelText: String { + switch self { + case .booked: + return "Booked" + case .paid: + return "Paid" + } + } + + var labelColor: Color { + switch self { + case .booked: + return Color(UIColor.systemGray6) + case .paid: + return Color(UIColor.systemGray6) + } + } +} + +#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: viewModel) + } +} +#endif diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsViewController.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsViewController.swift new file mode 100644 index 00000000000..d23ceff0358 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsViewController.swift @@ -0,0 +1,28 @@ +import UIKit +import SwiftUI +import WooFoundation + +final class BookingDetailsViewController: UIHostingController { + + private let viewModel: BookingDetailsViewModel + + init(viewModel: BookingDetailsViewModel) { + self.viewModel = viewModel + super.init(rootView: BookingDetailsView(viewModel: viewModel)) + } + + @MainActor + required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + configureNavigationBar() + } + + private func configureNavigationBar() { + navigationItem.title = NSLocalizedString("Booking Details", comment: "Booking details screen title") + navigationItem.largeTitleDisplayMode = .never + } +} diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 7b6ef7bb5a6..36eb8916f9d 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1249,6 +1249,9 @@ 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 */; }; + 2DAC2C9A2E82A185008521AF /* BookingDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC2C982E82A185008521AF /* BookingDetailsViewController.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 */; }; @@ -4438,6 +4441,9 @@ 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 = ""; }; + 2DAC2C982E82A185008521AF /* BookingDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookingDetailsViewController.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 +9108,31 @@ path = CIAB; sourceTree = ""; }; + 2DAC251E2E829FF9008521AF /* Booking Details */ = { + isa = PBXGroup; + children = ( + 2DAC251F2E82A02C008521AF /* BookingDetailsViewModel.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 */, + 2DAC2C982E82A185008521AF /* BookingDetailsViewController.swift */, + ); + path = "Booking Details"; + sourceTree = ""; + }; 2DCB54F82E6AE8C900621F90 /* CIAB */ = { isa = PBXGroup; children = ( @@ -10875,6 +10906,7 @@ B56DB3EF2049C06D00D4AA8E /* ViewRelated */ = { isa = PBXGroup; children = ( + 2DAC2C952E82A15C008521AF /* Bookings */, 68B3BA242D91473D0000B2F2 /* AI Settings */, B626C7192876599B0083820C /* Custom Fields */, 86023FA82B15CA8D00A28F07 /* Themes */, @@ -12523,6 +12555,7 @@ CE85535B209B5B6A00938BDC /* ViewModels */ = { isa = PBXGroup; children = ( + 2DAC251E2E829FF9008521AF /* Booking Details */, 68709D3E2A2EE2C000A7FA6C /* InAppPurchases */, 02E3B62B290631A5007E0F13 /* Authentication */, D41C9F2A26D9A04A00993558 /* WhatsNew */, @@ -15714,6 +15747,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 */, @@ -17005,6 +17039,8 @@ 74EC34A5225FE21F004BBC2E /* ProductLoaderViewController.swift in Sources */, CE8CCD43239AC06E009DBD22 /* RefundDetailsViewController.swift in Sources */, 869C2AA42C91791B00DDEE13 /* AztecEditorView.swift in Sources */, + 2DAC2C992E82A185008521AF /* BookingDetailsView.swift in Sources */, + 2DAC2C9A2E82A185008521AF /* BookingDetailsViewController.swift in Sources */, B560D68C2195BD1E0027BB7E /* NoteDetailsCommentTableViewCell.swift in Sources */, DEA88F502AA9D0100037273B /* AddEditProductCategoryViewModel.swift in Sources */, 451C77712404518600413F73 /* ProductSettingsRows.swift in Sources */, From 4be259b17225f27da8a75677ada11a3da3e2f739 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Tue, 23 Sep 2025 16:00:56 +0300 Subject: [PATCH 02/15] Draft sectioned approach --- .../BookingDetailsViewModel.swift | 234 ++++++++++++------ .../Booking Details/BookingDetailsView.swift | 104 ++++---- 2 files changed, 213 insertions(+), 125 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index 43633f9fc4e..b215276133d 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -8,102 +8,172 @@ extension BookingDetailsViewModel { } } -private extension BookingDetailsViewModel { - static let dateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "dd/MM/yyyy, hh:mm a" - return dateFormatter - }() - - static let appointmentDateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "EEEE, dd MMMM yyyy" - return dateFormatter - }() - - static let appointmentTimeFrameFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "hh:mm a" - return dateFormatter - }() -} - -final class BookingDetailsViewModel: ObservableObject { - - init(booking: Booking) { - bookingDate = Self.dateFormatter.string(from: booking.startDate) +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 + } + } - // This will be assigned later - serviceName = "Women's Haircut" - customerName = "Margarita Nikolaevna" - service = "Women's Haircut" - status = [.paid, .booked] - quantity = 1 - servicesCost = "$62.68" - tax = "$7.32" - - appointmentDate = Self.appointmentDateFormatter.string(from: booking.startDate) - appointmentTimeFrame = [ - Self.appointmentTimeFrameFormatter.string(from: booking.startDate), - Self.appointmentTimeFrameFormatter.string(from: booking.endDate) - ].joined(separator: " - ") - durationMinutes = Int(booking.endDate.timeIntervalSince(booking.startDate) / 60) - - cost = booking.cost - total = booking.cost - paid = booking.cost + enum SectionContent: Identifiable { + var id: String { + switch self { + case .header: + return "header" + case .appointmentDetails: + return "appointmentDetails" + } + } + + case header(HeaderContent) + case appointmentDetails(AppointmentDetailsContent) +// case attendance(AttendanceContent) +// case payment(PaymentContent) +// case customer(CustomerContent) +// case teamMember(TeamMemberContent) } +} - // MARK: - Header Properties - let bookingDate: String - let serviceName: String - let customerName: String - let status: [Status] - - // MARK: - Appointment Details - let appointmentDate: String - let appointmentTimeFrame: String - let service: String - let quantity: Int - let durationMinutes: Int - let cost: String - - // MARK: - Payment Details - let servicesCost: String - let tax: String - let total: String - let paid: String - - // MARK: - Customer Details - var customerEmail: String { - // This will be fetched from the customer details later - return "margarita.n@mail.com" +extension BookingDetailsViewModel { + struct HeaderContent: Hashable { + static let dateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "dd/MM/yyyy, hh:mm a" + return dateFormatter + }() + + let bookingDate: String + let serviceName: String + let customerName: String + let status: [Status] + + init(_ booking: Booking) { + bookingDate = Self.dateFormatter.string(from: booking.startDate) + serviceName = "Women's Haircut" + customerName = "Margarita Nikolaevna" + status = [.paid, .booked] + } } - var customerPhone: String { - // This will be fetched from the customer details later - return "+1742582943798" + struct AppointmentDetailsContent { + static let appointmentDateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EEEE, dd MMMM yyyy" + return dateFormatter + }() + + static let appointmentTimeFrameFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "hh:mm a" + return dateFormatter + }() + + let rows: [Row] + + struct Row: Identifiable { + let title: String + let value: String + + var id: String { + return title + } + } + + init(_ booking: Booking) { + let durationMinutes = Int(booking.endDate.timeIntervalSince(booking.startDate) / 60) + let appointmentDate = Self.appointmentDateFormatter.string(from: booking.startDate) + let appointmentTimeFrame = [ + Self.appointmentTimeFrameFormatter.string(from: booking.startDate), + Self.appointmentTimeFrameFormatter.string(from: booking.endDate) + ].joined(separator: " - ") + + rows = [ + Row(title: "Date", value: appointmentDate), + Row(title: "Time", value: appointmentTimeFrame), + Row(title: "Service", value: "Women's Haircut"), + Row(title: "Quantity", value: "1"), + Row(title: "Duration", value: String(durationMinutes)), + Row(title: "Cost", value: booking.cost) + ] + } } - var billingAddress: String { - // This will be fetched from the customer details later - return "238 Willow Creek Drive\nMontgomery\nAL 36109" + struct AttendanceContent { + } - // MARK: - Actions - func rescheduleBooking() { - // Placeholder for reschedule logic + struct PaymentContent { + } - func cancelBooking() { - // Placeholder for cancel logic + struct CustomerContent { + } - func markAsPaid() { - // Placeholder for mark as paid logic + struct TeamMemberContent { + } +} + + +final class BookingDetailsViewModel: ObservableObject { +// // MARK: - Payment Details +// let servicesCost: String +// let tax: String +// let total: String +// let paid: String +// +// // MARK: - Customer Details +// var customerEmail: String { +// // This will be fetched from the customer details later +// return "margarita.n@mail.com" +// } +// +// var customerPhone: String { +// // This will be fetched from the customer details later +// return "+1742582943798" +// } +// +// var billingAddress: String { +// // This will be fetched from the customer details later +// return "238 Willow Creek Drive\nMontgomery\nAL 36109" +// } + + let sections: [Section] - func viewOrder() { - // Placeholder for view order logic + init(booking: Booking) { + let headerSection = Section.init( + content: .header(HeaderContent(booking)) + ) + + let appointmentDetailsSection = Section( + headerText: "Appointment Details".uppercased(), + content: .appointmentDetails(AppointmentDetailsContent(booking)) + ) + + sections = [ + headerSection, + appointmentDetailsSection + ] + + // This will be assigned later +// servicesCost = "$62.68" +// tax = "$7.32" +// total = booking.cost +// paid = booking.cost } } diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index d29b5686f2c..b5effd24111 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -19,15 +19,73 @@ struct BookingDetailsView: View { static let bookingStatusLabel: Color = .gray } + func sectionView(with section: BookingDetailsViewModel.Section) -> some View { + VStack(alignment: .leading, spacing: Layout.headerContentVerticalPadding) { + if let headerText = section.headerText { + Text(headerText) + .font(.caption) + .foregroundColor(.gray) + } + + switch section.content { + case .header(let content): + headerView(with: content) + case .appointmentDetails(let content): + appointmentDetailsView(with: content) + } + + if let footerText = section.footerText { + Text(footerText) + .font(.caption) + .foregroundColor(.gray) + } + } + } + + 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.headerBodyText) + Text(headerContent.customerName) + .font(TextFont.headerBodyText) + .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) + } + } + + func appointmentDetailsView(with content: BookingDetailsViewModel.AppointmentDetailsContent) -> some View { + VStack(alignment: .leading, spacing: 16) { + ForEach(content.rows) { row in + DetailRow(title: row.title, value: row.value) + Divider() + } + } + } + var body: some View { RefreshablePlainList(action: { print("Refresh triggered") }) { VStack(alignment: .leading) { - headerView - .padding(.horizontal) + ForEach(viewModel.sections) { section in + sectionView(with: section) + .padding(.horizontal) + + Divider() + } - Divider() // // // Appointment Details // appointmentDetailsSectionView @@ -137,46 +195,6 @@ struct DetailRow: View { } } -private extension BookingDetailsView { - var headerView: some View { - VStack(alignment: .leading, spacing: Layout.headerContentVerticalPadding) { - Text(viewModel.bookingDate) - .font(.caption) - .foregroundColor(.secondary) - Text(viewModel.serviceName) - .font(TextFont.headerBodyText) - Text(viewModel.customerName) - .font(TextFont.headerBodyText) - .foregroundColor(.secondary) - HStack { - ForEach(viewModel.status, id: \.self) { status in - Text(status.labelText) - .font(.caption) - .padding(4) - .background(status.labelColor) - .cornerRadius(4) - } - } - .padding(.top, Layout.headerBadgesAdditionalTopPadding) - } - } - -// var appointmentDetailsSectionView: some View { -// VStack(alignment: .leading, spacing: 16) { -// Text("APPOINTMENT DETAILS") -// .font(.caption) -// .foregroundColor(.gray) -// -// DetailRow(title: "Date", value: viewModel.appointmentDate) -// DetailRow(title: "Time", value: viewModel.appointmentTime) -// DetailRow(title: "Service", value: viewModel.service) -// DetailRow(title: "Quantity", value: "\(viewModel.quantity)") -// DetailRow(title: "Duration", value: viewModel.duration) -// DetailRow(title: "Cost", value: viewModel.cost) -// } -// } -} - extension BookingDetailsViewModel.Status { var labelText: String { switch self { From 989a6708e083da9137ef2c501288178defbe16ae Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Tue, 23 Sep 2025 16:16:47 +0300 Subject: [PATCH 03/15] Split new model extensions by separate files --- .../AppointmentDetailsContent.swift | 47 ++++++ .../BookingDetailsViewModel+Section.swift | 23 +++ ...okingDetailsViewModel+SectionContent.swift | 31 ++++ .../BookingDetailsViewModel+Status.swift | 28 ++++ .../BookingDetailsViewModel.swift | 142 ------------------ .../Booking Details/HeaderContent.swift | 24 +++ .../Booking Details/BookingDetailsView.swift | 109 +------------- .../WooCommerce.xcodeproj/project.pbxproj | 20 +++ 8 files changed, 175 insertions(+), 249 deletions(-) create mode 100644 WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift create mode 100644 WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Section.swift create mode 100644 WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+SectionContent.swift create mode 100644 WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Status.swift create mode 100644 WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift diff --git a/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift new file mode 100644 index 00000000000..195aad6d1e0 --- /dev/null +++ b/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift @@ -0,0 +1,47 @@ +import Foundation +import struct Networking.Booking + +extension BookingDetailsViewModel { + struct AppointmentDetailsContent { + static let appointmentDateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EEEE, dd MMMM yyyy" + return dateFormatter + }() + + static let appointmentTimeFrameFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "hh:mm a" + return dateFormatter + }() + + 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 = Self.appointmentDateFormatter.string(from: booking.startDate) + let appointmentTimeFrame = [ + Self.appointmentTimeFrameFormatter.string(from: booking.startDate), + Self.appointmentTimeFrameFormatter.string(from: booking.endDate) + ].joined(separator: " - ") + + rows = [ + Row(title: "Date", value: appointmentDate), + Row(title: "Time", value: appointmentTimeFrame), + Row(title: "Service", value: "Women's Haircut"), + Row(title: "Quantity", value: "1"), + Row(title: "Duration", value: String(durationMinutes)), + Row(title: "Cost", value: booking.cost) + ] + } + } +} 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..ee8d384fdc2 --- /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 "ttendance" + 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..36970b2bf42 --- /dev/null +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Status.swift @@ -0,0 +1,28 @@ +import Foundation +import SwiftUI + +extension BookingDetailsViewModel { + enum Status { + case booked, paid + } +} + +extension BookingDetailsViewModel.Status { + var labelText: String { + switch self { + case .booked: + return "Booked" + case .paid: + return "Paid" + } + } + + var labelColor: Color { + switch self { + case .booked: + return Color(UIColor.systemGray6) + case .paid: + return Color(UIColor.systemGray6) + } + } +} diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index b215276133d..c4869b0abd5 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -3,156 +3,20 @@ import WooFoundation import struct Networking.Booking extension BookingDetailsViewModel { - enum Status { - case booked, paid - } -} - -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 - } - } - - enum SectionContent: Identifiable { - var id: String { - switch self { - case .header: - return "header" - case .appointmentDetails: - return "appointmentDetails" - } - } - - case header(HeaderContent) - case appointmentDetails(AppointmentDetailsContent) -// case attendance(AttendanceContent) -// case payment(PaymentContent) -// case customer(CustomerContent) -// case teamMember(TeamMemberContent) - } -} - -extension BookingDetailsViewModel { - struct HeaderContent: Hashable { - static let dateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "dd/MM/yyyy, hh:mm a" - return dateFormatter - }() - - let bookingDate: String - let serviceName: String - let customerName: String - let status: [Status] - - init(_ booking: Booking) { - bookingDate = Self.dateFormatter.string(from: booking.startDate) - serviceName = "Women's Haircut" - customerName = "Margarita Nikolaevna" - status = [.paid, .booked] - } - } - - struct AppointmentDetailsContent { - static let appointmentDateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "EEEE, dd MMMM yyyy" - return dateFormatter - }() - - static let appointmentTimeFrameFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "hh:mm a" - return dateFormatter - }() - - let rows: [Row] - - struct Row: Identifiable { - let title: String - let value: String - - var id: String { - return title - } - } - - init(_ booking: Booking) { - let durationMinutes = Int(booking.endDate.timeIntervalSince(booking.startDate) / 60) - let appointmentDate = Self.appointmentDateFormatter.string(from: booking.startDate) - let appointmentTimeFrame = [ - Self.appointmentTimeFrameFormatter.string(from: booking.startDate), - Self.appointmentTimeFrameFormatter.string(from: booking.endDate) - ].joined(separator: " - ") - - rows = [ - Row(title: "Date", value: appointmentDate), - Row(title: "Time", value: appointmentTimeFrame), - Row(title: "Service", value: "Women's Haircut"), - Row(title: "Quantity", value: "1"), - Row(title: "Duration", value: String(durationMinutes)), - Row(title: "Cost", value: booking.cost) - ] - } - } - struct AttendanceContent { - } struct PaymentContent { - } struct CustomerContent { - } struct TeamMemberContent { - } } - final class BookingDetailsViewModel: ObservableObject { -// // MARK: - Payment Details -// let servicesCost: String -// let tax: String -// let total: String -// let paid: String -// -// // MARK: - Customer Details -// var customerEmail: String { -// // This will be fetched from the customer details later -// return "margarita.n@mail.com" -// } -// -// var customerPhone: String { -// // This will be fetched from the customer details later -// return "+1742582943798" -// } -// -// var billingAddress: String { -// // This will be fetched from the customer details later -// return "238 Willow Creek Drive\nMontgomery\nAL 36109" -// } - let sections: [Section] init(booking: Booking) { @@ -169,11 +33,5 @@ final class BookingDetailsViewModel: ObservableObject { headerSection, appointmentDetailsSection ] - - // This will be assigned later -// servicesCost = "$62.68" -// tax = "$7.32" -// total = booking.cost -// paid = booking.cost } } diff --git a/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift new file mode 100644 index 00000000000..d486d699c4a --- /dev/null +++ b/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift @@ -0,0 +1,24 @@ +import Foundation +import struct Networking.Booking + +extension BookingDetailsViewModel { + struct HeaderContent: Hashable { + static let dateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "dd/MM/yyyy, hh:mm a" + return dateFormatter + }() + + let bookingDate: String + let serviceName: String + let customerName: String + let status: [Status] + + init(_ booking: Booking) { + bookingDate = Self.dateFormatter.string(from: booking.startDate) + 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 index b5effd24111..fbb20eaec5c 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -32,6 +32,8 @@ struct BookingDetailsView: View { headerView(with: content) case .appointmentDetails(let content): appointmentDetailsView(with: content) + default: + EmptyView() } if let footerText = section.footerText { @@ -85,93 +87,6 @@ struct BookingDetailsView: View { Divider() } - -// -// // Appointment Details -// appointmentDetailsSectionView -// .padding(.horizontal) -// -// VStack(spacing: 12) { -// Button(action: { -// viewModel.rescheduleBooking() -// }) { -// Text("Reschedule") -// .frame(maxWidth: .infinity) -// .padding() -// .background(Color.white) -// .border(Color.gray, width: 1) -// .cornerRadius(8) -// } -// -// Button(action: { -// viewModel.cancelBooking() -// }) { -// Text("Cancel booking") -// .frame(maxWidth: .infinity) -// .padding() -// .background(Color.white) -// .border(Color.gray, width: 1) -// .cornerRadius(8) -// } -// } -// .padding(.horizontal) -// -// Divider() -// -// // Payment Details -// VStack(alignment: .leading, spacing: 16) { -// Text("PAYMENT") -// .font(.caption) -// .foregroundColor(.gray) -// -// DetailRow(title: "Services", value: viewModel.servicesCost) -// DetailRow(title: "Tax", value: viewModel.tax) -// DetailRow(title: "Total", value: viewModel.total, isBold: true) -// DetailRow(title: "Paid", value: viewModel.paid, isBold: true) -// } -// .padding(.horizontal) -// -// VStack(spacing: 12) { -// Button(action: { -// viewModel.markAsPaid() -// }) { -// Text("Mark as paid") -// .frame(maxWidth: .infinity) -// .padding() -// .background(Color.accentColor) -// .foregroundColor(.white) -// .cornerRadius(8) -// } -// -// Button(action: { -// viewModel.viewOrder() -// }) { -// Text("View order") -// .frame(maxWidth: .infinity) -// .padding() -// .background(Color.white) -// .border(Color.gray, width: 1) -// .cornerRadius(8) -// } -// } -// .padding(.horizontal) -// -// Divider() -// -// // Customer Details -// VStack(alignment: .leading, spacing: 16) { -// Text("CUSTOMER") -// .font(.caption) -// .foregroundColor(.gray) -// -// Text(viewModel.customerName).font(.headline) -// Text(viewModel.customerEmail) -// Text(viewModel.customerPhone) -// -// Text("Billing address").font(.headline).padding(.top) -// Text(viewModel.billingAddress) -// } -// .padding(.horizontal) } .padding(.vertical) } @@ -195,26 +110,6 @@ struct DetailRow: View { } } -extension BookingDetailsViewModel.Status { - var labelText: String { - switch self { - case .booked: - return "Booked" - case .paid: - return "Paid" - } - } - - var labelColor: Color { - switch self { - case .booked: - return Color(UIColor.systemGray6) - case .paid: - return Color(UIColor.systemGray6) - } - } -} - #if DEBUG struct BookingDetailsView_Previews: PreviewProvider { static var previews: some View { diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 36eb8916f9d..4e0c13a860b 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1243,6 +1243,11 @@ 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 */; }; @@ -4435,6 +4440,11 @@ 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 = ""; }; @@ -9111,7 +9121,12 @@ 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 = ""; @@ -15879,6 +15894,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 */, @@ -16406,6 +16423,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 */, @@ -16689,6 +16707,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 */, @@ -17058,6 +17077,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 */, From d83625f5355674ca14b65afe15558517019afd34 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Tue, 23 Sep 2025 18:53:03 +0300 Subject: [PATCH 04/15] Update layout to look like a grouped table --- .../Booking Details/BookingDetailsView.swift | 112 +++++++++++------- .../BookingDetailsViewController.swift | 2 +- 2 files changed, 73 insertions(+), 41 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index fbb20eaec5c..33713eeed50 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -3,56 +3,99 @@ import WooFoundation import Networking struct BookingDetailsView: View { - @ObservedObject var viewModel: BookingDetailsViewModel + @ObservedObject private var viewModel: BookingDetailsViewModel - enum Layout { + private enum Layout { static let contentSidePadding: CGFloat = 16 static let headerContentVerticalPadding: CGFloat = 6 static let headerBadgesAdditionalTopPadding: CGFloat = 4 + static let appointmentDetailsRowVerticalPadding: CGFloat = 6 } - enum TextFont { - static let headerBodyText = Font.body.weight(.medium) + fileprivate enum TextFont { + static var bodyMedium: Font { + Font.body.weight(.medium) + } + + static var bodyRegular: Font { + Font.body.weight(.regular) + } + } + + private enum ColorConstants { + static var bookingStatusLabel: Color { + return .gray + } } - enum ColorConstants { - static let bookingStatusLabel: Color = .gray + init(_ viewModel: BookingDetailsViewModel) { + self.viewModel = viewModel } + var body: some View { + RefreshablePlainList(action: { + print("Refresh triggered") + }) { + VStack(alignment: .leading, spacing: .zero) { + ForEach(viewModel.sections) { section in + sectionView(with: section) + Divider() + } + } + } + .navigationBarTitleDisplayMode(.inline) + .background(Color(uiColor: .systemGroupedBackground)) + } +} + +private extension BookingDetailsView { func sectionView(with section: BookingDetailsViewModel.Section) -> some View { - VStack(alignment: .leading, spacing: Layout.headerContentVerticalPadding) { + VStack(alignment: .leading, spacing: 0) { if let headerText = section.headerText { Text(headerText) - .font(.caption) + .font(.footnote) .foregroundColor(.gray) + .padding(.vertical) + .padding(.horizontal, Layout.contentSidePadding) + Divider() } - switch section.content { - case .header(let content): - headerView(with: content) - case .appointmentDetails(let content): - appointmentDetailsView(with: content) - default: - EmptyView() - } + sectionContentView(section.content) + .padding(.horizontal, Layout.contentSidePadding) + .padding(.vertical, 10) + .background(Color(uiColor: .listBackground)) if let footerText = section.footerText { + Divider() Text(footerText) - .font(.caption) + .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.headerBodyText) + .font(TextFont.bodyMedium) Text(headerContent.customerName) - .font(TextFont.headerBodyText) + .font(TextFont.bodyMedium) .foregroundColor(.secondary) HStack { ForEach(headerContent.status, id: \.self) { status in @@ -65,47 +108,36 @@ struct BookingDetailsView: View { } .padding(.top, Layout.headerBadgesAdditionalTopPadding) } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 6) } func appointmentDetailsView(with content: BookingDetailsViewModel.AppointmentDetailsContent) -> some View { - VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading) { ForEach(content.rows) { row in DetailRow(title: row.title, value: row.value) - Divider() - } - } - } - - var body: some View { - RefreshablePlainList(action: { - print("Refresh triggered") - }) { - VStack(alignment: .leading) { - ForEach(viewModel.sections) { section in - sectionView(with: section) - .padding(.horizontal) + .padding(.vertical, Layout.appointmentDetailsRowVerticalPadding) + if row.id != content.rows.last?.id { Divider() + .padding(.trailing, -Layout.contentSidePadding) } } - .padding(.vertical) } - .navigationBarTitleDisplayMode(.inline) - .background(Color(uiColor: .listBackground)) } } struct DetailRow: View { let title: String let value: String - var isBold: Bool = false - var body: some View { HStack { Text(title) + .font(BookingDetailsView.TextFont.bodyMedium) Spacer() Text(value) - .fontWeight(isBold ? .bold : .regular) + .font(BookingDetailsView.TextFont.bodyRegular) + .foregroundColor(.secondary) } } } @@ -135,7 +167,7 @@ struct BookingDetailsView_Previews: PreviewProvider { localTimezone: "America/New_York" ) let viewModel = BookingDetailsViewModel(booking: sampleBooking) - return BookingDetailsView(viewModel: viewModel) + return BookingDetailsView(viewModel) } } #endif diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsViewController.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsViewController.swift index d23ceff0358..0f4eb752e5f 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsViewController.swift @@ -8,7 +8,7 @@ final class BookingDetailsViewController: UIHostingController Date: Thu, 25 Sep 2025 15:51:50 +0300 Subject: [PATCH 05/15] Use TitleAndTextFieldRow and ListHeaderView for content --- .../Booking Details/BookingDetailsView.swift | 46 ++++++++----------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 33713eeed50..6f32aa9f55d 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -3,6 +3,8 @@ import WooFoundation import Networking struct BookingDetailsView: View { + @Environment(\.safeAreaInsets) var safeAreaInsets: EdgeInsets + @ObservedObject private var viewModel: BookingDetailsViewModel private enum Layout { @@ -39,7 +41,6 @@ struct BookingDetailsView: View { VStack(alignment: .leading, spacing: .zero) { ForEach(viewModel.sections) { section in sectionView(with: section) - Divider() } } } @@ -52,21 +53,20 @@ private extension BookingDetailsView { func sectionView(with section: BookingDetailsViewModel.Section) -> some View { VStack(alignment: .leading, spacing: 0) { if let headerText = section.headerText { - Text(headerText) - .font(.footnote) - .foregroundColor(.gray) - .padding(.vertical) - .padding(.horizontal, Layout.contentSidePadding) - Divider() + ListHeaderView( + text: headerText, + alignment: .left + ) + .padding(.horizontal, insets: safeAreaInsets) + .accessibility(addTraits: .isHeader) } sectionContentView(section.content) .padding(.horizontal, Layout.contentSidePadding) - .padding(.vertical, 10) .background(Color(uiColor: .listBackground)) + .addingTopAndBottomDividers() if let footerText = section.footerText { - Divider() Text(footerText) .padding(.horizontal, Layout.contentSidePadding) .font(.footnote) @@ -113,10 +113,17 @@ private extension BookingDetailsView { } func appointmentDetailsView(with content: BookingDetailsViewModel.AppointmentDetailsContent) -> some View { - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 0) { ForEach(content.rows) { row in - DetailRow(title: row.title, value: row.value) - .padding(.vertical, Layout.appointmentDetailsRowVerticalPadding) + TitleAndTextFieldRow( + title: row.title, + placeholder: String(), + text: .constant(row.value), + fieldAlignment: .trailing, + keyboardType: .default, + minHeight: 44, + horizontalPadding: 0 // Parent section padding is added elsewhere, + ) if row.id != content.rows.last?.id { Divider() @@ -127,21 +134,6 @@ private extension BookingDetailsView { } } -struct DetailRow: View { - let title: String - let value: String - var body: some View { - HStack { - Text(title) - .font(BookingDetailsView.TextFont.bodyMedium) - Spacer() - Text(value) - .font(BookingDetailsView.TextFont.bodyRegular) - .foregroundColor(.secondary) - } - } -} - #if DEBUG struct BookingDetailsView_Previews: PreviewProvider { static var previews: some View { From 4aed3d83405ef12032ced49ca291807295ae9346 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Thu, 25 Sep 2025 16:13:21 +0300 Subject: [PATCH 06/15] Update TitleAndTextFieldRow to have configurable fonts and colors --- .../Add and Edit Coupons/AddEditCoupon.swift | 3 ++- .../TitleAndTextFieldRow.swift | 25 +++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) 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) } } } From 0336bd10115f03f3858de30e78011428dfd65692 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Thu, 25 Sep 2025 16:13:37 +0300 Subject: [PATCH 07/15] Apple separate colors for title and value --- .../Bookings/Booking Details/BookingDetailsView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 6f32aa9f55d..bfe506a2ab2 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -121,7 +121,9 @@ private extension BookingDetailsView { text: .constant(row.value), fieldAlignment: .trailing, keyboardType: .default, - minHeight: 44, + titleFont: BookingDetailsView.TextFont.bodyMedium, + valueColor: .secondary, + valueFont: BookingDetailsView.TextFont.bodyRegular, horizontalPadding: 0 // Parent section padding is added elsewhere, ) From 8fa80f4890639fc495ab8f187d29ff9152a1c37a Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Thu, 25 Sep 2025 16:20:13 +0300 Subject: [PATCH 08/15] Fix typo --- .../BookingDetailsViewModel+SectionContent.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+SectionContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+SectionContent.swift index ee8d384fdc2..78e1e8992ff 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+SectionContent.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+SectionContent.swift @@ -19,7 +19,7 @@ extension BookingDetailsViewModel.SectionContent: Identifiable { case .appointmentDetails: return "appointmentDetails" case .attendance: - return "ttendance" + return "attendance" case .payment: return "payment" case .customer: From 407000d7e99f9f9ac1a9d5944c004965f1848921 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Thu, 25 Sep 2025 18:22:37 +0300 Subject: [PATCH 09/15] Delete unused code --- .../Booking Details/BookingDetailsViewModel.swift | 1 - .../Bookings/Booking Details/BookingDetailsView.swift | 7 ------- .../Booking Details/BookingDetailsViewController.swift | 2 +- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index c4869b0abd5..7c92b2832a3 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -1,5 +1,4 @@ import Foundation -import WooFoundation import struct Networking.Booking extension BookingDetailsViewModel { diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index bfe506a2ab2..43c1ebc161a 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -11,7 +11,6 @@ struct BookingDetailsView: View { static let contentSidePadding: CGFloat = 16 static let headerContentVerticalPadding: CGFloat = 6 static let headerBadgesAdditionalTopPadding: CGFloat = 4 - static let appointmentDetailsRowVerticalPadding: CGFloat = 6 } fileprivate enum TextFont { @@ -24,12 +23,6 @@ struct BookingDetailsView: View { } } - private enum ColorConstants { - static var bookingStatusLabel: Color { - return .gray - } - } - init(_ viewModel: BookingDetailsViewModel) { self.viewModel = viewModel } diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsViewController.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsViewController.swift index 0f4eb752e5f..16d4233f7af 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsViewController.swift @@ -1,7 +1,7 @@ import UIKit import SwiftUI -import WooFoundation +/// periphery: ignore - will be used after Booking list is ready final class BookingDetailsViewController: UIHostingController { private let viewModel: BookingDetailsViewModel From d53a9a4d36836a99d585939e4cdd4d3c01b99df9 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Thu, 25 Sep 2025 23:52:07 +0300 Subject: [PATCH 10/15] Update background color --- .../Bookings/Booking Details/BookingDetailsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 43c1ebc161a..410c09f24fb 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -56,7 +56,7 @@ private extension BookingDetailsView { sectionContentView(section.content) .padding(.horizontal, Layout.contentSidePadding) - .background(Color(uiColor: .listBackground)) + .background(Color(.systemBackground)) .addingTopAndBottomDividers() if let footerText = section.footerText { From 7b451747d88589b154f226e9eca5f011cc0b3ab8 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Fri, 26 Sep 2025 12:37:51 +0300 Subject: [PATCH 11/15] Delete hosting view controller --- .../BookingDetailsViewController.swift | 28 ------------------- .../WooCommerce.xcodeproj/project.pbxproj | 4 --- 2 files changed, 32 deletions(-) delete mode 100644 WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsViewController.swift diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsViewController.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsViewController.swift deleted file mode 100644 index 16d4233f7af..00000000000 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsViewController.swift +++ /dev/null @@ -1,28 +0,0 @@ -import UIKit -import SwiftUI - -/// periphery: ignore - will be used after Booking list is ready -final class BookingDetailsViewController: UIHostingController { - - private let viewModel: BookingDetailsViewModel - - init(viewModel: BookingDetailsViewModel) { - self.viewModel = viewModel - super.init(rootView: BookingDetailsView(viewModel)) - } - - @MainActor - required dynamic init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - configureNavigationBar() - } - - private func configureNavigationBar() { - navigationItem.title = NSLocalizedString("Booking Details", comment: "Booking details screen title") - navigationItem.largeTitleDisplayMode = .never - } -} diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 4e0c13a860b..054acb658e9 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1256,7 +1256,6 @@ 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 */; }; - 2DAC2C9A2E82A185008521AF /* BookingDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC2C982E82A185008521AF /* BookingDetailsViewController.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 */; }; @@ -4453,7 +4452,6 @@ 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 = ""; }; - 2DAC2C982E82A185008521AF /* BookingDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookingDetailsViewController.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 = ""; }; @@ -9143,7 +9141,6 @@ isa = PBXGroup; children = ( 2DAC2C972E82A185008521AF /* BookingDetailsView.swift */, - 2DAC2C982E82A185008521AF /* BookingDetailsViewController.swift */, ); path = "Booking Details"; sourceTree = ""; @@ -17059,7 +17056,6 @@ CE8CCD43239AC06E009DBD22 /* RefundDetailsViewController.swift in Sources */, 869C2AA42C91791B00DDEE13 /* AztecEditorView.swift in Sources */, 2DAC2C992E82A185008521AF /* BookingDetailsView.swift in Sources */, - 2DAC2C9A2E82A185008521AF /* BookingDetailsViewController.swift in Sources */, B560D68C2195BD1E0027BB7E /* NoteDetailsCommentTableViewCell.swift in Sources */, DEA88F502AA9D0100037273B /* AddEditProductCategoryViewModel.swift in Sources */, 451C77712404518600413F73 /* ProductSettingsRows.swift in Sources */, From 1220ac5eb1f1a4b3b10cb5df308634bc70e10753 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Fri, 26 Sep 2025 12:43:01 +0300 Subject: [PATCH 12/15] Simplify date formatting --- .../AppointmentDetailsContent.swift | 18 +++--------------- .../Booking Details/HeaderContent.swift | 2 +- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift index 195aad6d1e0..3b376e47746 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift @@ -3,18 +3,6 @@ import struct Networking.Booking extension BookingDetailsViewModel { struct AppointmentDetailsContent { - static let appointmentDateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "EEEE, dd MMMM yyyy" - return dateFormatter - }() - - static let appointmentTimeFrameFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "hh:mm a" - return dateFormatter - }() - struct Row: Identifiable { let title: String let value: String @@ -28,10 +16,10 @@ extension BookingDetailsViewModel { init(_ booking: Booking) { let durationMinutes = Int(booking.endDate.timeIntervalSince(booking.startDate) / 60) - let appointmentDate = Self.appointmentDateFormatter.string(from: booking.startDate) + let appointmentDate = booking.startDate.formatted(date: .numeric, time: .omitted) let appointmentTimeFrame = [ - Self.appointmentTimeFrameFormatter.string(from: booking.startDate), - Self.appointmentTimeFrameFormatter.string(from: booking.endDate) + booking.startDate.formatted(date: .omitted, time: .shortened), + booking.endDate.formatted(date: .omitted, time: .shortened) ].joined(separator: " - ") rows = [ diff --git a/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift index d486d699c4a..d4f42a545ba 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift @@ -15,7 +15,7 @@ extension BookingDetailsViewModel { let status: [Status] init(_ booking: Booking) { - bookingDate = Self.dateFormatter.string(from: booking.startDate) + bookingDate = booking.startDate.formatted(date: .numeric, time: .omitted) serviceName = "Women's Haircut" customerName = "Margarita Nikolaevna" status = [.paid, .booked] From aa85786c90c9d8cd664d2497368513192e848f58 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Fri, 26 Sep 2025 13:01:33 +0300 Subject: [PATCH 13/15] Simplify pull-to-refresh --- .../Bookings/Booking Details/BookingDetailsView.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 410c09f24fb..cc88f61b384 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -28,15 +28,16 @@ struct BookingDetailsView: View { } var body: some View { - RefreshablePlainList(action: { - print("Refresh triggered") - }) { + ScrollView { VStack(alignment: .leading, spacing: .zero) { ForEach(viewModel.sections) { section in sectionView(with: section) } } } + .refreshable { + print("Refresh triggered") + } .navigationBarTitleDisplayMode(.inline) .background(Color(uiColor: .systemGroupedBackground)) } From 9a194875828e5cd7cd19f322a2fcc5b38dbde966 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Fri, 26 Sep 2025 13:32:37 +0300 Subject: [PATCH 14/15] Add localizations --- .../AppointmentDetailsContent.swift | 52 ++++++++++++++++--- .../BookingDetailsViewModel+Status.swift | 20 ++++++- .../BookingDetailsViewModel.swift | 12 ++++- 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift index 3b376e47746..14971aee79f 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift @@ -23,13 +23,53 @@ extension BookingDetailsViewModel { ].joined(separator: " - ") rows = [ - Row(title: "Date", value: appointmentDate), - Row(title: "Time", value: appointmentTimeFrame), - Row(title: "Service", value: "Women's Haircut"), - Row(title: "Quantity", value: "1"), - Row(title: "Duration", value: String(durationMinutes)), - Row(title: "Cost", value: booking.cost) + 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+Status.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Status.swift index 36970b2bf42..71552a555d3 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Status.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Status.swift @@ -11,9 +11,9 @@ extension BookingDetailsViewModel.Status { var labelText: String { switch self { case .booked: - return "Booked" + return Localization.bookingStatusBooked case .paid: - return "Paid" + return Localization.bookingStatusPaid } } @@ -26,3 +26,19 @@ extension BookingDetailsViewModel.Status { } } } + +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 index 7c92b2832a3..25e7997df4e 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -24,7 +24,7 @@ final class BookingDetailsViewModel: ObservableObject { ) let appointmentDetailsSection = Section( - headerText: "Appointment Details".uppercased(), + headerText: Localization.appointmentDetailsSectionHeaderTitle.uppercased(), content: .appointmentDetails(AppointmentDetailsContent(booking)) ) @@ -34,3 +34,13 @@ final class BookingDetailsViewModel: ObservableObject { ] } } + +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." + ) + } +} From 93d2f679960b91085a1a3446d8553efeff8653e1 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Fri, 26 Sep 2025 13:36:39 +0300 Subject: [PATCH 15/15] Delete unused code --- .../Classes/ViewModels/Booking Details/HeaderContent.swift | 6 ------ .../Bookings/Booking Details/BookingDetailsView.swift | 1 - 2 files changed, 7 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift index d4f42a545ba..9e7a9c2ee66 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift @@ -3,12 +3,6 @@ import struct Networking.Booking extension BookingDetailsViewModel { struct HeaderContent: Hashable { - static let dateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "dd/MM/yyyy, hh:mm a" - return dateFormatter - }() - let bookingDate: String let serviceName: String let customerName: String diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index cc88f61b384..eef19b175a2 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -1,5 +1,4 @@ import SwiftUI -import WooFoundation import Networking struct BookingDetailsView: View {