Skip to content

Commit 8b00c93

Browse files
committed
Draft booking details view and view model
1 parent 0e16d10 commit 8b00c93

File tree

4 files changed

+401
-2
lines changed

4 files changed

+401
-2
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import Foundation
2+
import WooFoundation
3+
import struct Networking.Booking
4+
5+
extension BookingDetailsViewModel {
6+
enum Status {
7+
case booked, paid
8+
}
9+
}
10+
11+
private extension BookingDetailsViewModel {
12+
static let dateFormatter = {
13+
let dateFormatter = DateFormatter()
14+
dateFormatter.dateFormat = "dd/MM/yyyy, hh:mm a"
15+
return dateFormatter
16+
}()
17+
18+
static let appointmentDateFormatter = {
19+
let dateFormatter = DateFormatter()
20+
dateFormatter.dateFormat = "EEEE, dd MMMM yyyy"
21+
return dateFormatter
22+
}()
23+
24+
static let appointmentTimeFrameFormatter = {
25+
let dateFormatter = DateFormatter()
26+
dateFormatter.dateFormat = "hh:mm a"
27+
return dateFormatter
28+
}()
29+
}
30+
31+
final class BookingDetailsViewModel: ObservableObject {
32+
33+
init(booking: Booking) {
34+
bookingDate = Self.dateFormatter.string(from: booking.startDate)
35+
36+
// This will be assigned later
37+
serviceName = "Women's Haircut"
38+
customerName = "Margarita Nikolaevna"
39+
service = "Women's Haircut"
40+
status = [.paid, .booked]
41+
quantity = 1
42+
servicesCost = "$62.68"
43+
tax = "$7.32"
44+
45+
appointmentDate = Self.appointmentDateFormatter.string(from: booking.startDate)
46+
appointmentTimeFrame = [
47+
Self.appointmentTimeFrameFormatter.string(from: booking.startDate),
48+
Self.appointmentTimeFrameFormatter.string(from: booking.endDate)
49+
].joined(separator: " - ")
50+
durationMinutes = Int(booking.endDate.timeIntervalSince(booking.startDate) / 60)
51+
52+
cost = booking.cost
53+
total = booking.cost
54+
paid = booking.cost
55+
}
56+
57+
// MARK: - Header Properties
58+
let bookingDate: String
59+
let serviceName: String
60+
let customerName: String
61+
let status: [Status]
62+
63+
// MARK: - Appointment Details
64+
let appointmentDate: String
65+
let appointmentTimeFrame: String
66+
let service: String
67+
let quantity: Int
68+
let durationMinutes: Int
69+
let cost: String
70+
71+
// MARK: - Payment Details
72+
let servicesCost: String
73+
let tax: String
74+
let total: String
75+
let paid: String
76+
77+
// MARK: - Customer Details
78+
var customerEmail: String {
79+
// This will be fetched from the customer details later
80+
81+
}
82+
83+
var customerPhone: String {
84+
// This will be fetched from the customer details later
85+
return "+1742582943798"
86+
}
87+
88+
var billingAddress: String {
89+
// This will be fetched from the customer details later
90+
return "238 Willow Creek Drive\nMontgomery\nAL 36109"
91+
}
92+
93+
// MARK: - Actions
94+
func rescheduleBooking() {
95+
// Placeholder for reschedule logic
96+
}
97+
98+
func cancelBooking() {
99+
// Placeholder for cancel logic
100+
}
101+
102+
func markAsPaid() {
103+
// Placeholder for mark as paid logic
104+
}
105+
106+
func viewOrder() {
107+
// Placeholder for view order logic
108+
}
109+
}
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import SwiftUI
2+
import WooFoundation
3+
import Networking
4+
5+
struct BookingDetailsView: View {
6+
@ObservedObject var viewModel: BookingDetailsViewModel
7+
8+
enum Layout {
9+
static let contentSidePadding: CGFloat = 16
10+
static let headerContentVerticalPadding: CGFloat = 6
11+
static let headerBadgesAdditionalTopPadding: CGFloat = 4
12+
}
13+
14+
enum TextFont {
15+
static let headerBodyText = Font.body.weight(.medium)
16+
}
17+
18+
enum ColorConstants {
19+
static let bookingStatusLabel: Color = .gray
20+
}
21+
22+
var body: some View {
23+
RefreshablePlainList(action: {
24+
print("Refresh triggered")
25+
}) {
26+
VStack(alignment: .leading) {
27+
headerView
28+
.padding(.horizontal)
29+
30+
Divider()
31+
//
32+
// // Appointment Details
33+
// appointmentDetailsSectionView
34+
// .padding(.horizontal)
35+
//
36+
// VStack(spacing: 12) {
37+
// Button(action: {
38+
// viewModel.rescheduleBooking()
39+
// }) {
40+
// Text("Reschedule")
41+
// .frame(maxWidth: .infinity)
42+
// .padding()
43+
// .background(Color.white)
44+
// .border(Color.gray, width: 1)
45+
// .cornerRadius(8)
46+
// }
47+
//
48+
// Button(action: {
49+
// viewModel.cancelBooking()
50+
// }) {
51+
// Text("Cancel booking")
52+
// .frame(maxWidth: .infinity)
53+
// .padding()
54+
// .background(Color.white)
55+
// .border(Color.gray, width: 1)
56+
// .cornerRadius(8)
57+
// }
58+
// }
59+
// .padding(.horizontal)
60+
//
61+
// Divider()
62+
//
63+
// // Payment Details
64+
// VStack(alignment: .leading, spacing: 16) {
65+
// Text("PAYMENT")
66+
// .font(.caption)
67+
// .foregroundColor(.gray)
68+
//
69+
// DetailRow(title: "Services", value: viewModel.servicesCost)
70+
// DetailRow(title: "Tax", value: viewModel.tax)
71+
// DetailRow(title: "Total", value: viewModel.total, isBold: true)
72+
// DetailRow(title: "Paid", value: viewModel.paid, isBold: true)
73+
// }
74+
// .padding(.horizontal)
75+
//
76+
// VStack(spacing: 12) {
77+
// Button(action: {
78+
// viewModel.markAsPaid()
79+
// }) {
80+
// Text("Mark as paid")
81+
// .frame(maxWidth: .infinity)
82+
// .padding()
83+
// .background(Color.accentColor)
84+
// .foregroundColor(.white)
85+
// .cornerRadius(8)
86+
// }
87+
//
88+
// Button(action: {
89+
// viewModel.viewOrder()
90+
// }) {
91+
// Text("View order")
92+
// .frame(maxWidth: .infinity)
93+
// .padding()
94+
// .background(Color.white)
95+
// .border(Color.gray, width: 1)
96+
// .cornerRadius(8)
97+
// }
98+
// }
99+
// .padding(.horizontal)
100+
//
101+
// Divider()
102+
//
103+
// // Customer Details
104+
// VStack(alignment: .leading, spacing: 16) {
105+
// Text("CUSTOMER")
106+
// .font(.caption)
107+
// .foregroundColor(.gray)
108+
//
109+
// Text(viewModel.customerName).font(.headline)
110+
// Text(viewModel.customerEmail)
111+
// Text(viewModel.customerPhone)
112+
//
113+
// Text("Billing address").font(.headline).padding(.top)
114+
// Text(viewModel.billingAddress)
115+
// }
116+
// .padding(.horizontal)
117+
}
118+
.padding(.vertical)
119+
}
120+
.navigationBarTitleDisplayMode(.inline)
121+
.background(Color(uiColor: .listBackground))
122+
}
123+
}
124+
125+
struct DetailRow: View {
126+
let title: String
127+
let value: String
128+
var isBold: Bool = false
129+
130+
var body: some View {
131+
HStack {
132+
Text(title)
133+
Spacer()
134+
Text(value)
135+
.fontWeight(isBold ? .bold : .regular)
136+
}
137+
}
138+
}
139+
140+
private extension BookingDetailsView {
141+
var headerView: some View {
142+
VStack(alignment: .leading, spacing: Layout.headerContentVerticalPadding) {
143+
Text(viewModel.bookingDate)
144+
.font(.caption)
145+
.foregroundColor(.secondary)
146+
Text(viewModel.serviceName)
147+
.font(TextFont.headerBodyText)
148+
Text(viewModel.customerName)
149+
.font(TextFont.headerBodyText)
150+
.foregroundColor(.secondary)
151+
HStack {
152+
ForEach(viewModel.status, id: \.self) { status in
153+
Text(status.labelText)
154+
.font(.caption)
155+
.padding(4)
156+
.background(status.labelColor)
157+
.cornerRadius(4)
158+
}
159+
}
160+
.padding(.top, Layout.headerBadgesAdditionalTopPadding)
161+
}
162+
}
163+
164+
// var appointmentDetailsSectionView: some View {
165+
// VStack(alignment: .leading, spacing: 16) {
166+
// Text("APPOINTMENT DETAILS")
167+
// .font(.caption)
168+
// .foregroundColor(.gray)
169+
//
170+
// DetailRow(title: "Date", value: viewModel.appointmentDate)
171+
// DetailRow(title: "Time", value: viewModel.appointmentTime)
172+
// DetailRow(title: "Service", value: viewModel.service)
173+
// DetailRow(title: "Quantity", value: "\(viewModel.quantity)")
174+
// DetailRow(title: "Duration", value: viewModel.duration)
175+
// DetailRow(title: "Cost", value: viewModel.cost)
176+
// }
177+
// }
178+
}
179+
180+
extension BookingDetailsViewModel.Status {
181+
var labelText: String {
182+
switch self {
183+
case .booked:
184+
return "Booked"
185+
case .paid:
186+
return "Paid"
187+
}
188+
}
189+
190+
var labelColor: Color {
191+
switch self {
192+
case .booked:
193+
return Color(UIColor.systemGray6)
194+
case .paid:
195+
return Color(UIColor.systemGray6)
196+
}
197+
}
198+
}
199+
200+
#if DEBUG
201+
struct BookingDetailsView_Previews: PreviewProvider {
202+
static var previews: some View {
203+
let now = Date()
204+
let hourFromNow = now.addingTimeInterval(3600)
205+
let sampleBooking = Booking(
206+
siteID: 1,
207+
bookingID: 123,
208+
allDay: false,
209+
cost: "70.00",
210+
customerID: 456,
211+
dateCreated: now,
212+
dateModified: now,
213+
endDate: hourFromNow,
214+
googleCalendarEventID: nil,
215+
orderID: 789,
216+
orderItemID: 101,
217+
parentID: 0,
218+
productID: 112,
219+
resourceID: 113,
220+
startDate: now,
221+
statusKey: "paid",
222+
localTimezone: "America/New_York"
223+
)
224+
let viewModel = BookingDetailsViewModel(booking: sampleBooking)
225+
return BookingDetailsView(viewModel: viewModel)
226+
}
227+
}
228+
#endif
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import UIKit
2+
import SwiftUI
3+
import WooFoundation
4+
5+
final class BookingDetailsViewController: UIHostingController<BookingDetailsView> {
6+
7+
private let viewModel: BookingDetailsViewModel
8+
9+
init(viewModel: BookingDetailsViewModel) {
10+
self.viewModel = viewModel
11+
super.init(rootView: BookingDetailsView(viewModel: viewModel))
12+
}
13+
14+
@MainActor
15+
required dynamic init?(coder aDecoder: NSCoder) {
16+
fatalError("init(coder:) has not been implemented")
17+
}
18+
19+
override func viewDidLoad() {
20+
super.viewDidLoad()
21+
configureNavigationBar()
22+
}
23+
24+
private func configureNavigationBar() {
25+
navigationItem.title = NSLocalizedString("Booking Details", comment: "Booking details screen title")
26+
navigationItem.largeTitleDisplayMode = .never
27+
}
28+
}

0 commit comments

Comments
 (0)