import Foundation struct BulletinSection: Identifiable { let id = UUID() let title: String let content: String } struct Bulletin: Identifiable, Codable { let id: String let collectionId: String let title: String let date: Date let divineWorship: String let sabbathSchool: String let scriptureReading: String let sunset: String let pdf: String? let isActive: Bool let created: Date let updated: Date enum CodingKeys: String, CodingKey { case id case collectionId = "collectionId" case title case date case divineWorship = "divine_worship" case sabbathSchool = "sabbath_school" case scriptureReading = "scripture_reading" case sunset case pdf case isActive = "is_active" case created case updated } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: .id) collectionId = try container.decode(String.self, forKey: .collectionId) title = try container.decode(String.self, forKey: .title) date = try container.decode(Date.self, forKey: .date) divineWorship = try container.decode(String.self, forKey: .divineWorship) sabbathSchool = try container.decode(String.self, forKey: .sabbathSchool) scriptureReading = try container.decode(String.self, forKey: .scriptureReading) sunset = try container.decode(String.self, forKey: .sunset) pdf = try container.decodeIfPresent(String.self, forKey: .pdf) isActive = try container.decode(Bool.self, forKey: .isActive) created = try container.decode(Date.self, forKey: .created) updated = try container.decode(Date.self, forKey: .updated) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) try container.encode(collectionId, forKey: .collectionId) try container.encode(title, forKey: .title) try container.encode(date, forKey: .date) try container.encode(divineWorship, forKey: .divineWorship) try container.encode(sabbathSchool, forKey: .sabbathSchool) try container.encode(scriptureReading, forKey: .scriptureReading) try container.encode(sunset, forKey: .sunset) try container.encodeIfPresent(pdf, forKey: .pdf) try container.encode(isActive, forKey: .isActive) try container.encode(created, forKey: .created) try container.encode(updated, forKey: .updated) } // Computed property to get the PDF URL var pdfUrl: String { if let pdf = pdf { return "https://pocketbase.rockvilletollandsda.church/api/files/\(collectionId)/\(id)/\(pdf)" } return "" } // Computed property to get formatted content var content: String { """ Divine Worship \(divineWorship) Sabbath School \(sabbathSchool) Scripture Reading \(scriptureReading) Sunset Information \(sunset) """ } } class PocketBaseService { static let shared = PocketBaseService() let baseURL = "https://pocketbase.rockvilletollandsda.church/api/collections" private init() {} private let dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSZ" return formatter }() private let decoder: JSONDecoder = { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .custom { decoder in let container = try decoder.singleValueContainer() let dateString = try container.decode(String.self) print("Attempting to decode date string: \(dateString)") // Try ISO8601 first if let date = ISO8601DateFormatter().date(from: dateString) { print("Successfully decoded ISO8601 date") return date } // Try various date formats let formatters = [ "yyyy-MM-dd'T'HH:mm:ss.SSSZ", "yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd HH:mm:ss.SSSZ", "yyyy-MM-dd HH:mm:ssZ", "yyyy-MM-dd" ] for format in formatters { let formatter = DateFormatter() formatter.dateFormat = format if let date = formatter.date(from: dateString) { print("Successfully decoded date with format: \(format)") return date } } print("Failed to decode date string with any format") throw DecodingError.dataCorruptedError( in: container, debugDescription: "Date string '\(dateString)' does not match any expected format" ) } return decoder }() struct EventResponse: Codable { let page: Int let perPage: Int let totalItems: Int let totalPages: Int let items: [Event] enum CodingKeys: String, CodingKey { case page case perPage case totalItems case totalPages case items } } struct BulletinResponse: Codable { let page: Int let perPage: Int let totalItems: Int let totalPages: Int let items: [Bulletin] } @MainActor func fetchConfig() async throws -> Config { let recordId = "nn753t8o2t1iupd" let urlString = "\(baseURL)/config/records/\(recordId)" guard let url = URL(string: urlString) else { throw URLError(.badURL) } var request = URLRequest(url: url) request.setValue("application/json", forHTTPHeaderField: "Content-Type") let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw URLError(.badServerResponse) } guard httpResponse.statusCode == 200 else { if let errorString = String(data: data, encoding: .utf8) { print("Error fetching config: \(errorString)") } throw URLError(.badServerResponse) } let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 do { return try decoder.decode(Config.self, from: data) } catch { print("Failed to decode config: \(error)") if let decodingError = error as? DecodingError { switch decodingError { case .keyNotFound(let key, let context): print("Missing key: \(key.stringValue) in \(context.debugDescription)") case .typeMismatch(let type, let context): print("Type mismatch: expected \(type) in \(context.debugDescription)") case .valueNotFound(let type, let context): print("Value not found: expected \(type) in \(context.debugDescription)") case .dataCorrupted(let context): print("Data corrupted: \(context.debugDescription)") @unknown default: print("Unknown decoding error") } } throw error } } @MainActor func fetchEvents() async throws -> [Event] { guard let url = URL(string: "\(baseURL)/events/records?sort=start_time") else { throw URLError(.badURL) } let (data, response) = try await URLSession.shared.data(from: url) guard let httpResponse = response as? HTTPURLResponse else { throw URLError(.badServerResponse) } guard httpResponse.statusCode == 200 else { if let errorString = String(data: data, encoding: .utf8) { print("Error fetching events: \(errorString)") } throw URLError(.badServerResponse) } let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 do { let eventResponse = try decoder.decode(EventResponse.self, from: data) return eventResponse.items } catch { print("Failed to decode events: \(error)") if let decodingError = error as? DecodingError { switch decodingError { case .keyNotFound(let key, let context): print("Missing key: \(key.stringValue) in \(context.debugDescription)") case .typeMismatch(let type, let context): print("Type mismatch: expected \(type) in \(context.debugDescription)") case .valueNotFound(let type, let context): print("Value not found: expected \(type) in \(context.debugDescription)") case .dataCorrupted(let context): print("Data corrupted: \(context.debugDescription)") @unknown default: print("Unknown decoding error") } } throw error } } func fetchBulletins(activeOnly: Bool = false) async throws -> BulletinResponse { let endpoint = "\(baseURL)/bulletins/records" var components = URLComponents(string: endpoint)! var queryItems = [URLQueryItem]() if activeOnly { queryItems.append(URLQueryItem(name: "filter", value: "is_active=true")) } queryItems.append(URLQueryItem(name: "sort", value: "-date")) components.queryItems = queryItems guard let url = components.url else { throw URLError(.badURL) } let (data, response) = try await URLSession.shared.data(from: url) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw URLError(.badServerResponse) } // Debug: Print the JSON response if let jsonString = String(data: data, encoding: .utf8) { print("Received JSON response: \(jsonString)") } do { return try decoder.decode(BulletinResponse.self, from: data) } catch { print("Failed to decode bulletins: \(error)") throw error } } }