diff --git a/WooCommerce/Classes/View Modifiers/View+Tappable.swift b/WooCommerce/Classes/View Modifiers/View+Tappable.swift new file mode 100644 index 00000000000..7d02d739f91 --- /dev/null +++ b/WooCommerce/Classes/View Modifiers/View+Tappable.swift @@ -0,0 +1,20 @@ +import SwiftUI + +private struct TappableViewModifier: ViewModifier { + let onTap: () -> Void + + func body(content: Content) -> some View { + Button { + onTap() + } label: { + content + } + .buttonStyle(.plain) + } +} + +extension View { + func tappable(_ onTap: @escaping () -> Void) -> some View { + self.modifier(TappableViewModifier(onTap: onTap)) + } +} diff --git a/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift index 14971aee79f..ce155cef0d3 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/AppointmentDetailsContent.swift @@ -25,10 +25,10 @@ extension BookingDetailsViewModel { rows = [ Row(title: Localization.appointmentDetailsDateRowTitle, value: appointmentDate), Row(title: Localization.appointmentDetailsTimeRowTitle, value: appointmentTimeFrame), - Row(title: Localization.appointmentDetailsServiceTitle, value: "Women's Haircut"), - Row(title: Localization.appointmentDetailsQuantityTitle, value: "1"), + Row(title: Localization.appointmentDetailsAssignedStaffTitle, value: "Marianne Renoir"), /// Temporarily hardcoded + Row(title: Localization.appointmentDetailsLocationTitle, value: "238 Willow Creek Drive, Montgomery ..."), /// Temporarily hardcoded Row(title: Localization.appointmentDetailsDurationTitle, value: String(durationMinutes)), - Row(title: Localization.appointmentDetailsCostTitle, value: booking.cost) + Row(title: Localization.appointmentDetailsPriceTitle, value: booking.cost) ] } } @@ -48,16 +48,16 @@ private extension BookingDetailsViewModel.AppointmentDetailsContent { comment: "Time row title in appointment details section in booking details view." ) - static let appointmentDetailsServiceTitle = NSLocalizedString( - "BookingDetailsView.appointmentDetails.serviceRow.title", - value: "Service", - comment: "Service name row title in appointment details section in booking details view." + static let appointmentDetailsAssignedStaffTitle = NSLocalizedString( + "BookingDetailsView.appointmentDetails.assignedStaff.title", + value: "Assigned staff", + comment: "Assigned staff row title in appointment details section in booking details view." ) - static let appointmentDetailsQuantityTitle = NSLocalizedString( - "BookingDetailsView.appointmentDetails.quantityRow.title", - value: "Quantity", - comment: "Quantity row title in appointment details section in booking details view." + static let appointmentDetailsLocationTitle = NSLocalizedString( + "BookingDetailsView.appointmentDetails.locationRow.title", + value: "Location", + comment: "Location row title in appointment details section in booking details view." ) static let appointmentDetailsDurationTitle = NSLocalizedString( @@ -66,10 +66,10 @@ private extension BookingDetailsViewModel.AppointmentDetailsContent { comment: "Duration row title in appointment details section in booking details view." ) - static let appointmentDetailsCostTitle = NSLocalizedString( - "BookingDetailsView.appointmentDetails.costRow.title", - value: "Cost", - comment: "Cost row title in appointment details section in booking details view." + static let appointmentDetailsPriceTitle = NSLocalizedString( + "BookingDetailsView.appointmentDetails.priceRow.title", + value: "Price", + comment: "Price row title in appointment details section in booking details view." ) } } diff --git a/WooCommerce/Classes/ViewModels/Booking Details/AttendanceContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/AttendanceContent.swift new file mode 100644 index 00000000000..33b7c270f83 --- /dev/null +++ b/WooCommerce/Classes/ViewModels/Booking Details/AttendanceContent.swift @@ -0,0 +1,9 @@ +import Foundation + +extension BookingDetailsViewModel { + struct AttendanceContent { + /// Hardcoded attendance value + /// Will be replaced with model value or binding + let value = "Booked" + } +} diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Section.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Section.swift index 451c2528a93..ece7cee70c5 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Section.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Section.swift @@ -6,18 +6,25 @@ extension BookingDetailsViewModel { return content.id } - let headerText: String? + let header: Header? let footerText: String? let content: SectionContent init( - headerText: String? = nil, + header: Header? = nil, footerText: String? = nil, content: SectionContent ) { - self.headerText = headerText + self.header = header self.footerText = footerText self.content = content } } } + +extension BookingDetailsViewModel.Section { + enum Header { + case empty + case title(String) + } +} diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+SectionContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+SectionContent.swift index 78e1e8992ff..4dda47f5a36 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+SectionContent.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+SectionContent.swift @@ -7,7 +7,7 @@ extension BookingDetailsViewModel { case attendance(AttendanceContent) case payment(PaymentContent) case customer(CustomerContent) - case teamMember(TeamMemberContent) + case bookingNotes } } @@ -24,8 +24,8 @@ extension BookingDetailsViewModel.SectionContent: Identifiable { return "payment" case .customer: return "customer" - case .teamMember: - return "teamMember" + case .bookingNotes: + return "bookingNotes" } } } diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Status.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Status.swift index 71552a555d3..91c35425009 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Status.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel+Status.swift @@ -3,7 +3,7 @@ import SwiftUI extension BookingDetailsViewModel { enum Status { - case booked, paid + case booked, paid, payAtLocation } } @@ -14,6 +14,8 @@ extension BookingDetailsViewModel.Status { return Localization.bookingStatusBooked case .paid: return Localization.bookingStatusPaid + case .payAtLocation: + return Localization.bookingStatusPayAtLocation } } @@ -23,6 +25,8 @@ extension BookingDetailsViewModel.Status { return Color(UIColor.systemGray6) case .paid: return Color(UIColor.systemGray6) + case .payAtLocation: + return Color(UIColor(hexString: "FFE365")) } } } @@ -30,15 +34,21 @@ extension BookingDetailsViewModel.Status { private extension BookingDetailsViewModel.Status { enum Localization { static let bookingStatusBooked = NSLocalizedString( - "BookingDetailsView.appointmentDetails.statusLabel.booked", + "BookingDetailsView.statusLabel.booked", value: "Booked", - comment: "Title for the 'Booked' status label in the appointment details view." + comment: "Title for the 'Booked' status label in the header view." ) static let bookingStatusPaid = NSLocalizedString( - "BookingDetailsView.appointmentDetails.statusLabel.paid", + "BookingDetailsView.statusLabel.paid", value: "Paid", - comment: "Title for the 'Paid' status label in the appointment details view." + comment: "Title for the 'Paid' status label in the header view." + ) + + static let bookingStatusPayAtLocation = NSLocalizedString( + "BookingDetailsView.statusLabel.payAtLocation", + value: "Pay at location", + comment: "Title for the 'Pay at location' status label in the header view." ) } } diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index 25e7997df4e..11c7beb62e7 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -1,20 +1,6 @@ import Foundation import struct Networking.Booking -extension BookingDetailsViewModel { - struct AttendanceContent { - } - - struct PaymentContent { - } - - struct CustomerContent { - } - - struct TeamMemberContent { - } -} - final class BookingDetailsViewModel: ObservableObject { let sections: [Section] @@ -24,13 +10,48 @@ final class BookingDetailsViewModel: ObservableObject { ) let appointmentDetailsSection = Section( - headerText: Localization.appointmentDetailsSectionHeaderTitle.uppercased(), + header: .title(Localization.appointmentDetailsSectionHeaderTitle.uppercased()), content: .appointmentDetails(AppointmentDetailsContent(booking)) ) + let attendanceSection = Section( + header: .title(Localization.attendanceSectionHeaderTitle.uppercased()), + footerText: Localization.attendanceSectionFooterText, + content: .attendance(AttendanceContent()) + ) + + let customerSection = Section( + header: .title(Localization.customerSectionHeaderTitle.uppercased()), + content: .customer( + /// Temporary hardcode + CustomerContent( + nameText: "Margarita Nikolaevna", + emailText: "margarita.n@mail.com", + phoneText: "+1 742582943798", + billingAddressText: """ + 238 Willow Creek Drive
Montgomery
AL 36109 + """ + ) + ) + ) + + let paymentSection = Section( + header: .title(Localization.paymentSectionHeaderTitle.uppercased()), + content: .payment(PaymentContent(booking: booking)) + ) + + let bookingNotes = Section( + header: .title(Localization.bookingNotesSectionHeaderTitle.uppercased()), + content: .bookingNotes + ) + sections = [ headerSection, - appointmentDetailsSection + appointmentDetailsSection, + customerSection, + attendanceSection, + paymentSection, + bookingNotes ] } } @@ -42,5 +63,35 @@ private extension BookingDetailsViewModel { value: "Appointment Details", comment: "Header title for the 'Appointment Details' section in the booking details screen." ) + + static let attendanceSectionHeaderTitle = NSLocalizedString( + "BookingDetailsView.attendance.headerTitle", + value: "Attendance", + comment: "Header title for the 'Attendance' section in the booking details screen." + ) + + static let customerSectionHeaderTitle = NSLocalizedString( + "BookingDetailsView.customer.headerTitle", + value: "Customer", + comment: "Header title for the 'Customer' section in the booking details screen." + ) + + static let attendanceSectionFooterText = NSLocalizedString( + "BookingDetailsView.attendance.footerText", + value: "Mark attendance to keep your reports accurate and spot booking trends.", + comment: "Footer text for the 'Attendance' section in the booking details screen." + ) + + static let paymentSectionHeaderTitle = NSLocalizedString( + "BookingDetailsView.payment.headerTitle", + value: "Payment", + comment: "Header title for the 'Payment' section in the booking details screen." + ) + + static let bookingNotesSectionHeaderTitle = NSLocalizedString( + "BookingDetailsView.bookingNotes.headerTitle", + value: "Booking notes", + comment: "Header title for the 'Booking notes' section in the booking details screen." + ) } } diff --git a/WooCommerce/Classes/ViewModels/Booking Details/CustomerContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/CustomerContent.swift new file mode 100644 index 00000000000..0a962538144 --- /dev/null +++ b/WooCommerce/Classes/ViewModels/Booking Details/CustomerContent.swift @@ -0,0 +1,10 @@ +import Foundation + +extension BookingDetailsViewModel { + struct CustomerContent { + let nameText: String + let emailText: String + let phoneText: String + let billingAddressText: String? + } +} diff --git a/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift index 9e7a9c2ee66..a88f1fc1380 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift @@ -4,15 +4,28 @@ import struct Networking.Booking extension BookingDetailsViewModel { struct HeaderContent: Hashable { let bookingDate: String - let serviceName: String - let customerName: String + let serviceAndCustomerLine: String let status: [Status] init(_ booking: Booking) { - bookingDate = booking.startDate.formatted(date: .numeric, time: .omitted) - serviceName = "Women's Haircut" - customerName = "Margarita Nikolaevna" - status = [.paid, .booked] + bookingDate = booking.startDate.formatted( + date: .numeric, + time: .shortened + ) + + /// Temporary hardcode + serviceAndCustomerLine = [ + "Women's Haircut", + "Margarita Nikolaevna" + ].joined(separator: Constants.dotSeparator) + + status = [.booked, .payAtLocation] } } } + +private extension BookingDetailsViewModel { + enum Constants { + static let dotSeparator: String = " • " + } +} diff --git a/WooCommerce/Classes/ViewModels/Booking Details/PaymentContent.swift b/WooCommerce/Classes/ViewModels/Booking Details/PaymentContent.swift new file mode 100644 index 00000000000..c8577638511 --- /dev/null +++ b/WooCommerce/Classes/ViewModels/Booking Details/PaymentContent.swift @@ -0,0 +1,147 @@ +import Foundation +import struct Networking.Booking + +extension BookingDetailsViewModel { + struct PaymentContent { + let amounts: [Amount] + let actions: [Action] + + init(booking: Booking) { + amounts = [ + .init(value: booking.cost, type: .service), + .init(value: "$0", type: .tax), + .init(value: "-", type: .discount), + .init(value: "$55.00", type: .total, emphasized: true), + ] + + actions = [ + .markAsPaid, + .viewOrder + ] + } + } +} + +extension BookingDetailsViewModel.PaymentContent { + struct Amount { + enum AmountType { + case service + case tax + case discount + case total + } + + let value: String + let type: AmountType + let emphasized: Bool + + var title: String { + return type.rowTitle + } + + init(value: String, type: AmountType, emphasized: Bool = false) { + self.value = value + self.type = type + self.emphasized = emphasized + } + } +} + +extension BookingDetailsViewModel.PaymentContent.Amount: Identifiable { + var id: String { + return title + } +} + +extension BookingDetailsViewModel.PaymentContent.Amount.AmountType { + var rowTitle: String { + switch self { + case .service: + return Localization.paymentServiceRowTitle + case .tax: + return Localization.paymentTaxRowTitle + case .discount: + return Localization.paymentDiscountRowTitle + case .total: + return Localization.paymentTotalRowTitle + } + } +} + +extension BookingDetailsViewModel.PaymentContent { + enum Action: String, Identifiable { + case markAsPaid + case markAsRefunded + case viewOrder + + var id: String { + return rawValue + } + } +} + +extension BookingDetailsViewModel.PaymentContent.Action { + var buttonTitle: String { + switch self { + case .markAsPaid: + return Localization.paymentMarkAsPaidButtonTitle + case .markAsRefunded: + return Localization.paymentMarkAsRefundedButtonTitle + case .viewOrder: + return Localization.paymentViewOrderButtonTitle + } + } + + var isEmphasized: Bool { + switch self { + case .markAsPaid: + return true + case .markAsRefunded, .viewOrder: + return false + } + } +} + +private enum Localization { + static let paymentServiceRowTitle = NSLocalizedString( + "BookingDetailsView.payment.serviceRow.title", + value: "Service", + comment: "Service row title in payment section in booking details view." + ) + + static let paymentTaxRowTitle = NSLocalizedString( + "BookingDetailsView.payment.taxRow.title", + value: "Tax", + comment: "Tax row title in payment section in booking details view." + ) + + static let paymentDiscountRowTitle = NSLocalizedString( + "BookingDetailsView.payment.discountRow.title", + value: "Discount", + comment: "Discount row title in payment section in booking details view." + ) + + static let paymentTotalRowTitle = NSLocalizedString( + "BookingDetailsView.payment.totalRow.title", + value: "Total", + comment: "Total row title in payment section in booking details view." + ) + + static let paymentMarkAsPaidButtonTitle = NSLocalizedString( + "BookingDetailsView.payment.markAsPaid.title", + value: "Mark as paid", + comment: "Title for 'Mark as paid' button in payment section in booking details view." + ) + + static let paymentMarkAsRefundedButtonTitle = NSLocalizedString( + "BookingDetailsView.payment.markAsRefunded.title", + value: "Mark as refunded", + comment: "Title for 'Mark as refunded' button in payment section in booking details view." + ) + + static let paymentViewOrderButtonTitle = NSLocalizedString( + "BookingDetailsView.payment.viewOrder.title", + value: "View order", + comment: "Title for 'View order' button in payment section in booking details view." + ) +} diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index eef19b175a2..7c160994206 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -8,18 +8,17 @@ struct BookingDetailsView: View { private enum Layout { static let contentSidePadding: CGFloat = 16 + static let contentVerticalPadding: CGFloat = 16 static let headerContentVerticalPadding: CGFloat = 6 static let headerBadgesAdditionalTopPadding: CGFloat = 4 + static let sectionFooterTextVerticalPadding: CGFloat = 8 + static let rowTextVerticalPadding: CGFloat = 11 } fileprivate enum TextFont { static var bodyMedium: Font { Font.body.weight(.medium) } - - static var bodyRegular: Font { - Font.body.weight(.regular) - } } init(_ viewModel: BookingDetailsViewModel) { @@ -45,9 +44,18 @@ struct BookingDetailsView: View { private extension BookingDetailsView { func sectionView(with section: BookingDetailsViewModel.Section) -> some View { VStack(alignment: .leading, spacing: 0) { - if let headerText = section.headerText { + if let header = section.header { + let text = { + switch header { + case .empty: + return "" + case .title(let text): + return text + } + }() + ListHeaderView( - text: headerText, + text: text, alignment: .left ) .padding(.horizontal, insets: safeAreaInsets) @@ -62,6 +70,7 @@ private extension BookingDetailsView { if let footerText = section.footerText { Text(footerText) .padding(.horizontal, Layout.contentSidePadding) + .padding(.vertical, Layout.sectionFooterTextVerticalPadding) .font(.footnote) .foregroundColor(.gray) } @@ -75,26 +84,31 @@ private extension BookingDetailsView { headerView(with: content) case .appointmentDetails(let content): appointmentDetailsView(with: content) - default: - EmptyView() + case .attendance(let content): + attendanceView(with: content) + case .customer(let content): + customerDetailsView(with: content) + case .payment(let content): + paymentDetailsView(with: content) + case .bookingNotes: + bookingNotesView() } } 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(.primary) + Text(headerContent.serviceAndCustomerLine) + .font(.footnote.weight(.medium)) .foregroundColor(.secondary) HStack { ForEach(headerContent.status, id: \.self) { status in Text(status.labelText) - .font(.caption) - .padding(4) + .font(.caption2) + .padding(.vertical, 4.5) + .padding(.horizontal, 8) .background(status.labelColor) .cornerRadius(4) } @@ -102,31 +116,209 @@ private extension BookingDetailsView { .padding(.top, Layout.headerBadgesAdditionalTopPadding) } .frame(maxWidth: .infinity, alignment: .leading) - .padding(.vertical, 6) + .padding(.vertical) + } + + func attendanceView(with content: BookingDetailsViewModel.AttendanceContent) -> some View { + TitleAndValueRow( + title: Localization.statusRowTitle, + value: .placeholder(content.value), + selectionStyle: .disclosure, + horizontalPadding: 0 + ) } func appointmentDetailsView(with content: BookingDetailsViewModel.AppointmentDetailsContent) -> some View { VStack(alignment: .leading, spacing: 0) { ForEach(content.rows) { row in - TitleAndTextFieldRow( + TitleAndValueRow( title: row.title, - placeholder: String(), - text: .constant(row.value), - fieldAlignment: .trailing, - keyboardType: .default, - titleFont: BookingDetailsView.TextFont.bodyMedium, - valueColor: .secondary, - valueFont: BookingDetailsView.TextFont.bodyRegular, - horizontalPadding: 0 // Parent section padding is added elsewhere, + value: .placeholder(row.value), + horizontalPadding: 0, + isMultiline: false ) - if row.id != content.rows.last?.id { - Divider() - .padding(.trailing, -Layout.contentSidePadding) + Divider() + .padding(.trailing, -Layout.contentSidePadding) + } + + Button { + /// On cancel booking button tap + } label: { + Text(Localization.cancelBooking) + } + .buttonStyle(SecondaryButtonStyle()) + .padding(.vertical, Layout.contentVerticalPadding) + } + } + + func customerDetailsView(with content: BookingDetailsViewModel.CustomerContent) -> some View { + VStack(spacing: 0) { + /// Name + HStack { + Text(content.nameText) + .rowTextStyle() + Spacer() + } + .padding(.vertical, Layout.rowTextVerticalPadding) + + Divider() + .padding(.trailing, -Layout.contentSidePadding) + + /// Email + HStack { + Text(content.emailText) + .rowTextStyle() + Spacer() + Image(systemName: "doc.on.doc") + .font(TextFont.bodyMedium) + .foregroundStyle(Color.accentColor) + } + .padding(.vertical, Layout.rowTextVerticalPadding) + .tappable { + print("On email copy") + } + + Divider() + .padding(.trailing, -Layout.contentSidePadding) + + /// Phone + HStack { + Text(content.phoneText) + .rowTextStyle() + Spacer() + Image(systemName: "ellipsis") + .font(TextFont.bodyMedium) + .foregroundStyle(Color.accentColor) + } + .padding(.vertical, Layout.rowTextVerticalPadding) + .tappable { + print("On phone ellipsis") + } + + Divider() + .padding(.trailing, -Layout.contentSidePadding) + + /// Billing address + if let billingAddressText = content.billingAddressText, !billingAddressText.isEmpty { + HStack { + VStack(alignment: .leading) { + Text(Localization.billingAddressRowTitle) + .rowTextStyle() + Text(billingAddressText) + .font(TextFont.bodyMedium) + .foregroundStyle(.secondary) + .multilineTextAlignment(.leading) + } + Spacer() } + .padding(.vertical, Layout.rowTextVerticalPadding) } } } + + func paymentDetailsView(with content: BookingDetailsViewModel.PaymentContent) -> some View { + VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 8) { + ForEach(content.amounts) { amount in + HStack { + Text(amount.title) + Spacer() + Text(amount.value) + } + .if(!amount.emphasized) { view in + view.rowTextStyle() + } + .if(amount.emphasized) { view in + view.font(.body.weight(.bold)) + } + } + } + .padding(.bottom) + + Divider() + .padding(.trailing, -Layout.contentSidePadding) + + VStack(alignment: .leading, spacing: Layout.contentVerticalPadding) { + ForEach(content.actions) { action in + Button { + /// On action tap + } label: { + Text(action.buttonTitle) + } + .if(action.isEmphasized) { + $0.buttonStyle(PrimaryButtonStyle()) + } + .if(!action.isEmphasized) { + $0.buttonStyle(SecondaryButtonStyle()) + } + } + } + .padding(.top) + } + .padding(.vertical) + } + + func bookingNotesView() -> some View { + HStack(spacing: Layout.contentSidePadding) { + Image(systemName: "plus") + .font(.title3.weight(.medium)) + Text(Localization.bookingNotesRowText) + .rowTextStyle() + Spacer() + } + .foregroundStyle(Color.accentColor) + .padding(.vertical, Layout.rowTextVerticalPadding) + .tappable { + print("On Add a note tap") + } + } +} + +private struct RowTextStyle: ViewModifier { + func body(content: Content) -> some View { + content + .font(BookingDetailsView.TextFont.bodyMedium) + .foregroundStyle(.primary) + .multilineTextAlignment(.leading) + } +} + +private extension View { + func rowTextStyle() -> some View { + self.modifier(RowTextStyle()) + } +} + +private extension BookingDetailsView { + enum Localization { + static let cancelBooking = NSLocalizedString( + "BookingDetailsView.customer.cancelBookingButton.title", + value: "Cancel booking", + comment: "'Cancel booking' button title in appointment details section in booking details view." + ) + + /// Attendance section + static let statusRowTitle = NSLocalizedString( + "BookingDetailsView.customer.status.title", + value: "Status", + comment: "'Status' row title in attendance section in booking details view." + ) + + /// Customer section + static let billingAddressRowTitle = NSLocalizedString( + "BookingDetailsView.customer.billingAddress.title", + value: "Billing address", + comment: "Billing address row title in customer section in booking details view." + ) + + /// Booking notes + static let bookingNotesRowText = NSLocalizedString( + "BookingDetailsView.bookingNotes.addANoteRow.title", + value: "Add a note", + comment: "Add a note row title in booking notes section in booking details view." + ) + } } #if DEBUG @@ -138,7 +330,7 @@ struct BookingDetailsView_Previews: PreviewProvider { siteID: 1, bookingID: 123, allDay: false, - cost: "70.00", + cost: "$70.00", customerID: 456, dateCreated: now, dateModified: now, diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TitleAndValueRow.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TitleAndValueRow.swift index 3db052bd1fa..ecf3f4091e6 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TitleAndValueRow.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TitleAndValueRow.swift @@ -18,6 +18,8 @@ struct TitleAndValueRow: View { private let bold: Bool private let selectionStyle: SelectionStyle private let isLoading: Bool + private let horizontalPadding: CGFloat + private let isMultiline: Bool private let action: () -> Void /// Static width for title label. Used to align values between different rows. @@ -46,6 +48,8 @@ struct TitleAndValueRow: View { bold: Bool = false, selectionStyle: SelectionStyle = .none, isLoading: Bool = false, + horizontalPadding: CGFloat = Constants.horizontalPadding, + isMultiline: Bool = true, action: @escaping () -> Void = {}) { self.title = title self.titleSuffixImage = titleSuffixImage @@ -55,6 +59,8 @@ struct TitleAndValueRow: View { self.bold = bold self.selectionStyle = selectionStyle self.isLoading = isLoading + self.horizontalPadding = horizontalPadding + self.isMultiline = isMultiline self.action = action } @@ -79,6 +85,8 @@ struct TitleAndValueRow: View { Text(value.text) .style(for: value, bold: bold, highlighted: false) .multilineTextAlignment(valueTextAlignment) + .lineLimit(isMultiline ? nil : 1) + .truncationMode(.tail) .frame(maxWidth: .infinity, alignment: valueFrameAlignment) .padding(.vertical, Constants.verticalPadding) .redacted(reason: isLoading ? .placeholder : []) @@ -92,7 +100,7 @@ struct TitleAndValueRow: View { }) .disabled(selectionStyle == .none) .frame(minHeight: Constants.minHeight) - .padding(.horizontal, Constants.horizontalPadding) + .padding(.horizontal, horizontalPadding) .accessibilityElement() .accessibilityLabel(Text(title)) .accessibilityValue(Text(value.text)) diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 054acb658e9..ba4e569558a 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1248,6 +1248,10 @@ 2D05D1A42E82D266004111FD /* AppointmentDetailsContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05D1A32E82D25F004111FD /* AppointmentDetailsContent.swift */; }; 2D05D1A52E82D3F6004111FD /* BookingDetailsViewModel+SectionContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05D1A02E82D1EF004111FD /* BookingDetailsViewModel+SectionContent.swift */; }; 2D05D1A72E82D49F004111FD /* BookingDetailsViewModel+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05D1A62E82D49D004111FD /* BookingDetailsViewModel+Status.swift */; }; + 2D05E80F2E86BE50004111FD /* AttendanceContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05E80E2E86BE4F004111FD /* AttendanceContent.swift */; }; + 2D05E8112E8A9905004111FD /* CustomerContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05E8102E8A98FE004111FD /* CustomerContent.swift */; }; + 2D05E8132E8AADB9004111FD /* PaymentContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05E8122E8AADB2004111FD /* PaymentContent.swift */; }; + 2D05F7102E8BE921004111FD /* View+Tappable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D05F70F2E8BE91E004111FD /* View+Tappable.swift */; }; 2D09E0D12E61BC7F005C26F3 /* ApplicationPasswordsExperimentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D09E0D02E61BC7D005C26F3 /* ApplicationPasswordsExperimentState.swift */; }; 2D09E0D52E65C9B9005C26F3 /* ApplicationPasswordsExperimentStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D09E0D42E65C9B9005C26F3 /* ApplicationPasswordsExperimentStateTests.swift */; }; 2D7A3E232E7891DB00C46401 /* CIABEligibilityCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7A3E222E7891D200C46401 /* CIABEligibilityCheckerTests.swift */; }; @@ -4444,6 +4448,10 @@ 2D05D1A12E82D233004111FD /* HeaderContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderContent.swift; sourceTree = ""; }; 2D05D1A32E82D25F004111FD /* AppointmentDetailsContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppointmentDetailsContent.swift; sourceTree = ""; }; 2D05D1A62E82D49D004111FD /* BookingDetailsViewModel+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BookingDetailsViewModel+Status.swift"; sourceTree = ""; }; + 2D05E80E2E86BE4F004111FD /* AttendanceContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttendanceContent.swift; sourceTree = ""; }; + 2D05E8102E8A98FE004111FD /* CustomerContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerContent.swift; sourceTree = ""; }; + 2D05E8122E8AADB2004111FD /* PaymentContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentContent.swift; sourceTree = ""; }; + 2D05F70F2E8BE91E004111FD /* View+Tappable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Tappable.swift"; sourceTree = ""; }; 2D09E0D02E61BC7D005C26F3 /* ApplicationPasswordsExperimentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordsExperimentState.swift; sourceTree = ""; }; 2D09E0D42E65C9B9005C26F3 /* ApplicationPasswordsExperimentStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordsExperimentStateTests.swift; sourceTree = ""; }; 2D7A3E222E7891D200C46401 /* CIABEligibilityCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIABEligibilityCheckerTests.swift; sourceTree = ""; }; @@ -8638,6 +8646,7 @@ 262A097F2628A8BF0033AD20 /* View Modifiers */ = { isa = PBXGroup; children = ( + 2D05F70F2E8BE91E004111FD /* View+Tappable.swift */, DE4B3B5526A68DD000EEF2D8 /* View+InsetPaddings.swift */, 26281775278F0B0100C836D3 /* View+NoticesModifier.swift */, AE77EA4F27A47C99006A21BD /* View+AddingDividers.swift */, @@ -9119,6 +9128,9 @@ 2DAC251E2E829FF9008521AF /* Booking Details */ = { isa = PBXGroup; children = ( + 2D05E8122E8AADB2004111FD /* PaymentContent.swift */, + 2D05E8102E8A98FE004111FD /* CustomerContent.swift */, + 2D05E80E2E86BE4F004111FD /* AttendanceContent.swift */, 2D05D1A12E82D233004111FD /* HeaderContent.swift */, 2D05D1A32E82D25F004111FD /* AppointmentDetailsContent.swift */, 2DAC251F2E82A02C008521AF /* BookingDetailsViewModel.swift */, @@ -15344,6 +15356,7 @@ 265284022624937600F91BA1 /* AddOnCrossreferenceUseCase.swift in Sources */, 02F3A6842A618CD7004CD2E8 /* WordPressMediaLibraryPickerCoordinator.swift in Sources */, DE6F99802BEE04310007B2DD /* InboxDashboardCardViewModel.swift in Sources */, + 2D05F7102E8BE921004111FD /* View+Tappable.swift in Sources */, 026D68492A0E060A00D8C22C /* LocalNotificationScheduler.swift in Sources */, 02CA63DB23D1ADD100BBF148 /* MediaPickingCoordinator.swift in Sources */, 028FA466257E021100F88A48 /* RefundShippingLabelViewModel.swift in Sources */, @@ -16256,6 +16269,7 @@ 0286B27B23C7051F003D784B /* ProductImagesCollectionViewController.swift in Sources */, E107FCE126C12B2700BAF51B /* InPersonPaymentsCountryNotSupported.swift in Sources */, 26E7EE7229301EBC00793045 /* ProductsReportCardViewModel.swift in Sources */, + 2D05E8112E8A9905004111FD /* CustomerContent.swift in Sources */, 027A2E142513124E00DA6ACB /* Keychain+Entries.swift in Sources */, B97C6E562B15E51A008A2BF2 /* UpdateProductInventoryView.swift in Sources */, 268EC45F26CEA50C00716F5C /* EditCustomerNote.swift in Sources */, @@ -16722,6 +16736,7 @@ 021BCDF82D3648CD002E9F15 /* PointOfSaleItemListFullscreenErrorView.swift in Sources */, E1C5E78226C2A971008D4C47 /* InPersonPaymentsPluginNotSetup.swift in Sources */, 022F941E257F8E820011CD94 /* BoldableTextParser.swift in Sources */, + 2D05E80F2E86BE50004111FD /* AttendanceContent.swift in Sources */, 26FE09DD24D9F3F600B9BDF5 /* LoadingView.swift in Sources */, 457151AB243B6E8000EB2DFA /* ProductSlugViewController.swift in Sources */, 02F49ADA23BF356E00FA0BFA /* TitleAndTextFieldTableViewCell.ViewModel+State.swift in Sources */, @@ -16748,6 +16763,7 @@ 4535EE7A281ADD56004212B4 /* CouponCodeInputFormatter.swift in Sources */, 0373A1312A1E3FD700731236 /* TapToPayBadgePromotionChecker.swift in Sources */, DEDB2D262845D31900CE7D35 /* CouponAllowedEmailsViewModel.swift in Sources */, + 2D05E8132E8AADB9004111FD /* PaymentContent.swift in Sources */, 035DBA45292D0164003E5125 /* CollectOrderPaymentUseCase.swift in Sources */, 0282DD96233C960C006A5FDB /* SearchResultCell.swift in Sources */, 027F83ED29B046D2002688C6 /* TopPerformersPeriodViewModel.swift in Sources */,