// API/APIModels.swift // ------------------- // All types in this file are pure value types with no actor isolation. // AnyDecodable uses a JSONValue enum (not `Any`) so it is fully Sendable // without @unchecked and causes no actor-isolation warnings. import Foundation // ── JSONValue — replaces AnyDecodable ──────────────────────────────────────── // // Using `Any` as a stored property is not Sendable, which causes the Swift // compiler to defensively infer @MainActor on any struct containing it. // A typed enum avoids this entirely: every case is a concrete Sendable type. enum JSONValue: Decodable, Sendable { case string(String) case int(Int) case double(Double) case bool(Bool) case array([JSONValue]) case object([String: JSONValue]) case null nonisolated init(from decoder: any Decoder) throws { let c = try decoder.singleValueContainer() // Order matters: Bool before Int (Bool is Int-decodable on some platforms) if let v = try? c.decode(Bool.self) { self = .bool(v); return } if let v = try? c.decode(Int.self) { self = .int(v); return } if let v = try? c.decode(Double.self) { self = .double(v); return } if let v = try? c.decode(String.self) { self = .string(v); return } if let v = try? c.decode([JSONValue].self) { self = .array(v); return } if let v = try? c.decode([String: JSONValue].self) { self = .object(v); return } self = .null } // Convert to Any for compatibility with existing formData/formSchema code var anyValue: Any { switch self { case .string(let v): return v case .int(let v): return v case .double(let v): return v case .bool(let v): return v case .null: return NSNull() case .array(let v): return v.map(\.anyValue) case .object(let v): return v.mapValues(\.anyValue) } } } // Typealias so existing code that references AnyDecodable still compiles typealias AnyDecodable = JSONValue // ── API Envelope ────────────────────────────────────────────────────────────── struct APIResponse: Decodable, Sendable { let ok: Bool let data: T? let error: String? nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) ok = try c.decode(Bool.self, forKey: .ok) data = try? c.decode(T.self, forKey: .data) error = try? c.decode(String.self, forKey: .error) } private enum CodingKeys: String, CodingKey { case ok, data, error } } // ── Auth ────────────────────────────────────────────────────────────────────── struct APIUser: Decodable, Sendable { let id: Int let username: String let email: String let role: String let createdAt: String? var displayName: String { username } nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(Int.self, forKey: .id) username = try c.decode(String.self, forKey: .username) email = try c.decode(String.self, forKey: .email) role = try c.decode(String.self, forKey: .role) createdAt = try? c.decode(String.self, forKey: .createdAt) } private enum CodingKeys: String, CodingKey { case id, username, email, role, createdAt } } struct LoginResponseData: Decodable, Sendable { let accessToken: String let refreshToken: String let tokenType: String let expiresIn: Int let user: APIUser nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) accessToken = try c.decode(String.self, forKey: .accessToken) refreshToken = try c.decode(String.self, forKey: .refreshToken) tokenType = try c.decode(String.self, forKey: .tokenType) expiresIn = try c.decode(Int.self, forKey: .expiresIn) user = try c.decode(APIUser.self, forKey: .user) } private enum CodingKeys: String, CodingKey { case accessToken, refreshToken, tokenType, expiresIn, user } } struct RefreshResponseData: Decodable, Sendable { let accessToken: String let refreshToken: String let tokenType: String let expiresIn: Int nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) accessToken = try c.decode(String.self, forKey: .accessToken) refreshToken = try c.decode(String.self, forKey: .refreshToken) tokenType = try c.decode(String.self, forKey: .tokenType) expiresIn = try c.decode(Int.self, forKey: .expiresIn) } private enum CodingKeys: String, CodingKey { case accessToken, refreshToken, tokenType, expiresIn } } struct MeResponseData: Decodable, Sendable { let user: APIUser nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) user = try c.decode(APIUser.self, forKey: .user) } private enum CodingKeys: String, CodingKey { case user } } // ── Facilities ──────────────────────────────────────────────────────────────── struct APIFacility: Decodable, Identifiable, Sendable { let id: Int let name: String let address: String let contactPerson: String let contactPhone: String let projectId: Int? let projectName: String? let isActive: Bool nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(Int.self, forKey: .id) name = try c.decode(String.self, forKey: .name) address = try c.decode(String.self, forKey: .address) contactPerson = try c.decode(String.self, forKey: .contactPerson) contactPhone = try c.decode(String.self, forKey: .contactPhone) projectId = try? c.decode(Int.self, forKey: .projectId) projectName = try? c.decode(String.self, forKey: .projectName) isActive = try c.decode(Bool.self, forKey: .isActive) } private enum CodingKeys: String, CodingKey { case id, name, address, contactPerson, contactPhone case projectId, projectName, isActive } } struct FacilitiesResponseData: Decodable, Sendable { let facilities: [APIFacility] let count: Int nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) facilities = try c.decode([APIFacility].self, forKey: .facilities) count = try c.decode(Int.self, forKey: .count) } private enum CodingKeys: String, CodingKey { case facilities, count } } struct APIArea: Decodable, Identifiable, Sendable { let id: Int let facilityId: Int let name: String let areaType: String nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(Int.self, forKey: .id) facilityId = try c.decode(Int.self, forKey: .facilityId) name = try c.decode(String.self, forKey: .name) areaType = try c.decode(String.self, forKey: .areaType) } private enum CodingKeys: String, CodingKey { case id, facilityId, name, areaType } } struct AreasResponseData: Decodable, Sendable { let facilityId: Int let areas: [APIArea] let count: Int nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) facilityId = try c.decode(Int.self, forKey: .facilityId) areas = try c.decode([APIArea].self, forKey: .areas) count = try c.decode(Int.self, forKey: .count) } private enum CodingKeys: String, CodingKey { case facilityId, areas, count } } // ── Templates ───────────────────────────────────────────────────────────────── struct APITemplateSummary: Decodable, Identifiable, Sendable { let id: Int let name: String let description: String let frequency: String nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(Int.self, forKey: .id) name = try c.decode(String.self, forKey: .name) description = try c.decode(String.self, forKey: .description) frequency = try c.decode(String.self, forKey: .frequency) } private enum CodingKeys: String, CodingKey { case id, name, description, frequency } } struct TemplatesResponseData: Decodable, Sendable { let templates: [APITemplateSummary] let count: Int nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) templates = try c.decode([APITemplateSummary].self, forKey: .templates) count = try c.decode(Int.self, forKey: .count) } private enum CodingKeys: String, CodingKey { case templates, count } } struct APITemplate: Decodable, Identifiable, Sendable { let id: Int let name: String let description: String let frequency: String // Use [[String: JSONValue]] instead of [[String: AnyDecodable]] — fully Sendable let formSchema: [[String: JSONValue]] nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(Int.self, forKey: .id) name = try c.decode(String.self, forKey: .name) description = try c.decode(String.self, forKey: .description) frequency = try c.decode(String.self, forKey: .frequency) formSchema = try c.decode([[String: JSONValue]].self, forKey: .formSchema) } private enum CodingKeys: String, CodingKey { case id, name, description, frequency, formSchema } } struct TemplateDetailResponseData: Decodable, Sendable { let template: APITemplate nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) template = try c.decode(APITemplate.self, forKey: .template) } private enum CodingKeys: String, CodingKey { case template } } // ── Inspections ─────────────────────────────────────────────────────────────── struct APIInspectionSummary: Decodable, Identifiable, Hashable, Sendable { let id: Int let templateId: Int let templateName: String let facilityId: Int let facilityName: String let areaId: Int? let areaName: String? let status: String let overallScore: Double? let inspectionDate: String? let completedAt: String? let mobileLocalId: String? // ── Follow-up / re-inspection ───────────────────────────────────────── let followUpRequired: Bool let followUpNote: String? let parentInspectionId: Int? var inspectionDateParsed: Date? { guard let str = inspectionDate else { return nil } return ISO8601DateFormatter().date(from: str) } nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(Int.self, forKey: .id) templateId = try c.decode(Int.self, forKey: .templateId) templateName = try c.decode(String.self, forKey: .templateName) facilityId = try c.decode(Int.self, forKey: .facilityId) facilityName = try c.decode(String.self, forKey: .facilityName) areaId = try? c.decode(Int.self, forKey: .areaId) areaName = try? c.decode(String.self, forKey: .areaName) status = try c.decode(String.self, forKey: .status) overallScore = try? c.decode(Double.self, forKey: .overallScore) inspectionDate = try? c.decode(String.self, forKey: .inspectionDate) completedAt = try? c.decode(String.self, forKey: .completedAt) mobileLocalId = try? c.decode(String.self, forKey: .mobileLocalId) followUpRequired = (try? c.decode(Bool.self, forKey: .followUpRequired)) ?? false followUpNote = try? c.decode(String.self, forKey: .followUpNote) parentInspectionId = try? c.decode(Int.self, forKey: .parentInspectionId) } private enum CodingKeys: String, CodingKey { case id, templateId, templateName, facilityId, facilityName case areaId, areaName, status, overallScore case inspectionDate, completedAt, mobileLocalId case followUpRequired, followUpNote, parentInspectionId } } struct InspectionHistoryResponseData: Decodable, Sendable { let inspections: [APIInspectionSummary] let total: Int let limit: Int let offset: Int nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) inspections = try c.decode([APIInspectionSummary].self, forKey: .inspections) total = try c.decode(Int.self, forKey: .total) limit = try c.decode(Int.self, forKey: .limit) offset = try c.decode(Int.self, forKey: .offset) } private enum CodingKeys: String, CodingKey { case inspections, total, limit, offset } } // ── Issue Detail ────────────────────────────────────────────────────────────── struct APIIssueDetail: Decodable, Sendable { let id: Int let status: String let severity: String let description: String let assignedTo: Int? let facilityId: Int? let facilityName: String? let reportedAt: String? let resolvedAt: String? nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(Int.self, forKey: .id) status = try c.decode(String.self, forKey: .status) severity = try c.decode(String.self, forKey: .severity) description = try c.decode(String.self, forKey: .description) assignedTo = try? c.decode(Int.self, forKey: .assignedTo) facilityId = try? c.decode(Int.self, forKey: .facilityId) facilityName = try? c.decode(String.self, forKey: .facilityName) reportedAt = try? c.decode(String.self, forKey: .reportedAt) resolvedAt = try? c.decode(String.self, forKey: .resolvedAt) } private enum CodingKeys: String, CodingKey { case id, status, severity, description, assignedTo case facilityId, facilityName, reportedAt, resolvedAt } } struct APIIssueStatusUpdate: Decodable, Sendable { let issueId: Int let status: String nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) issueId = try c.decode(Int.self, forKey: .issueId) status = try c.decode(String.self, forKey: .status) } private enum CodingKeys: String, CodingKey { case issueId, status } } // ── Notification polling (Phase C) ──────────────────────────────────────────── struct APINotification: Decodable, Identifiable, Sendable { let id: Int let title: String let body: String let eventType: String? let issueId: Int? let createdAt: String nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(Int.self, forKey: .id) title = try c.decode(String.self, forKey: .title) body = try c.decode(String.self, forKey: .body) eventType = try? c.decode(String.self, forKey: .eventType) issueId = try? c.decode(Int.self, forKey: .issueId) createdAt = try c.decode(String.self, forKey: .createdAt) } private enum CodingKeys: String, CodingKey { case id, title, body, eventType, issueId, createdAt } } struct APINotificationsResponseData: Decodable, Sendable { let notifications: [APINotification] let count: Int nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) notifications = try c.decode([APINotification].self, forKey: .notifications) count = try c.decode(Int.self, forKey: .count) } private enum CodingKeys: String, CodingKey { case notifications, count } } struct APIMarkReadResponseData: Decodable, Sendable { let marked: Int nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) marked = try c.decode(Int.self, forKey: .marked) } private enum CodingKeys: String, CodingKey { case marked } } // ── Assigned issues list (Phase C) ──────────────────────────────────────────── struct APIAssignedIssue: Decodable, Identifiable, Sendable { let id: Int let status: String let severity: String let description: String let assignedTo: Int? let facilityId: Int? let facilityName: String? let reportedAt: String? let mobileLocalId: String? let photoPath: String? // primary issue photo (relative server path) let resultPhotos: [String] // resolution photos (relative server paths) nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(Int.self, forKey: .id) status = try c.decode(String.self, forKey: .status) severity = try c.decode(String.self, forKey: .severity) description = try c.decode(String.self, forKey: .description) assignedTo = try? c.decode(Int.self, forKey: .assignedTo) facilityId = try? c.decode(Int.self, forKey: .facilityId) facilityName = try? c.decode(String.self, forKey: .facilityName) reportedAt = try? c.decode(String.self, forKey: .reportedAt) mobileLocalId = try? c.decode(String.self, forKey: .mobileLocalId) photoPath = try? c.decode(String.self, forKey: .photoPath) resultPhotos = (try? c.decode([String].self, forKey: .resultPhotos)) ?? [] } private enum CodingKeys: String, CodingKey { case id, status, severity, description, assignedTo case facilityId, facilityName, reportedAt, mobileLocalId case photoPath, resultPhotos } } struct APIAssignedIssuesResponseData: Decodable, Sendable { let issues: [APIAssignedIssue] let total: Int let limit: Int let offset: Int nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) issues = try c.decode([APIAssignedIssue].self, forKey: .issues) total = try c.decode(Int.self, forKey: .total) limit = try c.decode(Int.self, forKey: .limit) offset = try c.decode(Int.self, forKey: .offset) } private enum CodingKeys: String, CodingKey { case issues, total, limit, offset } }