RTSDA-iOS/Services/PocketBaseService.swift

302 lines
10 KiB
Swift

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
}
}
}