// 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 fullName: String // empty string when not set; never nil let email: String let role: String let createdAt: String? /// Returns full_name when set, otherwise falls back to username. /// Mirrors User.display_name on the server. var displayName: String { fullName.isEmpty ? username : fullName } 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) fullName = (try? c.decode(String.self, forKey: .fullName)) ?? "" 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, fullName, 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 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) description = try c.decode(String.self, forKey: .description) frequency = try c.decode(String.self, forKey: .frequency) // Default true — backwards compatible if server omits the field isActive = (try? c.decode(Bool.self, forKey: .isActive)) ?? true } private enum CodingKeys: String, CodingKey { case id, name, description, frequency, isActive } } 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? let inspectorNotes: String // Form responses and schema — included in every history response so the // detail view works without a local SwiftData copy (e.g. after reinstall). let formDataRaw: [String: JSONValue] let formSchemaRaw: [[String: JSONValue]] /// {field_id: absolute_url} for image fields (presigned R2 / absolute static). let formMedia: [String: String] // ── Follow-up / re-inspection ───────────────────────────────────────── let followUpRequired: Bool let followUpNote: String? let parentInspectionId: Int? /// Form field values as [fieldId: stringValue] for the grid renderer. var formValues: [String: String] { var result: [String: String] = [:] for (k, v) in formDataRaw { switch v { case .string(let s): result[k] = s case .int(let n): result[k] = String(n) case .double(let d): result[k] = String(d) case .bool(let b): result[k] = b ? "true" : "false" case .array(let a): result[k] = a.map { "\($0.anyValue)" }.joined(separator: ", ") case .null: result[k] = "" case .object: result[k] = "" } } return result } /// Form schema as [[String: Any]] for ReadOnlyGridFormView. var formSchema: [[String: Any]] { formSchemaRaw.map { dict in dict.mapValues { $0.anyValue } } } /// {relative_path: absolute_url} for image fields, derived by joining /// formMedia (fieldId -> url) with the form values (fieldId -> path). Lets /// the image renderer resolve a presigned URL from just the stored path. var mediaURLByPath: [String: String] { var out: [String: String] = [:] let values = formValues for (fid, url) in formMedia { if let path = values[fid], !path.isEmpty { out[path] = url } } return out } var inspectionDateParsed: Date? { guard let str = inspectionDate else { return nil } // Server sends "yyyy-MM-dd'T'HH:mm:ss" with no timezone suffix. // ISO8601DateFormatter() requires a timezone by default and returns nil // for timezone-less strings — use the shared DateFormatter instead. return SyncManager.isoFormatter.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) inspectorNotes = (try? c.decode(String.self, forKey: .inspectorNotes)) ?? "" formDataRaw = (try? c.decode([String: JSONValue].self, forKey: .formData)) ?? [:] formMedia = (try? c.decode([String: String].self, forKey: .formMedia)) ?? [:] formSchemaRaw = (try? c.decode([[String: JSONValue]].self, forKey: .formSchema)) ?? [] 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, inspectorNotes case formData, formSchema case formMedia case followUpRequired, followUpNote, parentInspectionId } // Explicit Hashable — formDataRaw/formSchemaRaw contain JSONValue which // has no Hashable conformance; identity is determined by server id alone. static func == (lhs: APIInspectionSummary, rhs: APIInspectionSummary) -> Bool { lhs.id == rhs.id } func hash(into hasher: inout Hasher) { hasher.combine(id) } } 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? // Phase A — resolution details from web let resultNotes: String? let verifiedAt: String? let verificationNote: String? let reportedByName: String? // Resolution photos uploaded via web or mobile resolve flow let resultPhotos: [String] let resultPhotoUrls: [String] // absolute display URLs (presigned R2 / static) // Phase E — area and assignee context let areaName: String? let assignedToName: String? // Handler ("Handled By", phase35) let handlerType: String? let handlerLabel: String? let facilityHandlerName: String? let facilityHandlerContact: String? let facilityHandlerNotes: String? let vendorName: String? let vendorContact: String? let vendorNotes: 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) resultNotes = try? c.decode(String.self, forKey: .resultNotes) verifiedAt = try? c.decode(String.self, forKey: .verifiedAt) verificationNote = try? c.decode(String.self, forKey: .verificationNote) reportedByName = try? c.decode(String.self, forKey: .reportedByName) resultPhotos = (try? c.decode([String].self, forKey: .resultPhotos)) ?? [] resultPhotoUrls = (try? c.decode([String].self, forKey: .resultPhotoUrls)) ?? [] areaName = try? c.decode(String.self, forKey: .areaName) assignedToName = try? c.decode(String.self, forKey: .assignedToName) handlerType = try? c.decode(String.self, forKey: .handlerType) handlerLabel = try? c.decode(String.self, forKey: .handlerLabel) facilityHandlerName = try? c.decode(String.self, forKey: .facilityHandlerName) facilityHandlerContact = try? c.decode(String.self, forKey: .facilityHandlerContact) facilityHandlerNotes = try? c.decode(String.self, forKey: .facilityHandlerNotes) vendorName = try? c.decode(String.self, forKey: .vendorName) vendorContact = try? c.decode(String.self, forKey: .vendorContact) vendorNotes = try? c.decode(String.self, forKey: .vendorNotes) } private enum CodingKeys: String, CodingKey { case id, status, severity, description, assignedTo case facilityId, facilityName, reportedAt, resolvedAt case resultNotes, verifiedAt, verificationNote, reportedByName, resultPhotos case resultPhotoUrls case areaName, assignedToName case handlerType, handlerLabel case facilityHandlerName, facilityHandlerContact, facilityHandlerNotes case vendorName, vendorContact, vendorNotes } } 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 } } struct APIIssueHandlerUpdate: Decodable, Sendable { let issueId: Int let handlerType: String nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) issueId = try c.decode(Int.self, forKey: .issueId) handlerType = try c.decode(String.self, forKey: .handlerType) } private enum CodingKeys: String, CodingKey { case issueId, handlerType } } // ── 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 evidence photo let mobilePhotoPaths: [String] // extra evidence photos from iPad let resultPhotos: [String] // resolution photos added via web // Absolute display URLs (presigned R2 / absolute static). photoUrls order // mirrors the evidence merge: [photoPath] + mobilePhotoPaths. let photoUrls: [String] let resultPhotoUrls: [String] // Phase A — resolution details from web let resultNotes: String? let verifiedAt: String? let verificationNote: String? let reportedByName: String? // Phase E — area and assignee context let areaName: String? let assignedToName: String? // Handler ("Handled By", phase35) — who resolves the issue let handlerType: String? // "internal" | "facility" | "vendor" let handlerLabel: String? // human-readable label let facilityHandlerName: String? let facilityHandlerContact: String? let facilityHandlerNotes: String? let vendorName: String? let vendorContact: String? let vendorNotes: 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) mobileLocalId = try? c.decode(String.self, forKey: .mobileLocalId) photoPath = try? c.decode(String.self, forKey: .photoPath) mobilePhotoPaths = (try? c.decode([String].self, forKey: .mobilePhotoPaths)) ?? [] resultPhotos = (try? c.decode([String].self, forKey: .resultPhotos)) ?? [] photoUrls = (try? c.decode([String].self, forKey: .photoUrls)) ?? [] resultPhotoUrls = (try? c.decode([String].self, forKey: .resultPhotoUrls)) ?? [] resultNotes = try? c.decode(String.self, forKey: .resultNotes) verifiedAt = try? c.decode(String.self, forKey: .verifiedAt) verificationNote = try? c.decode(String.self, forKey: .verificationNote) reportedByName = try? c.decode(String.self, forKey: .reportedByName) areaName = try? c.decode(String.self, forKey: .areaName) assignedToName = try? c.decode(String.self, forKey: .assignedToName) handlerType = try? c.decode(String.self, forKey: .handlerType) handlerLabel = try? c.decode(String.self, forKey: .handlerLabel) facilityHandlerName = try? c.decode(String.self, forKey: .facilityHandlerName) facilityHandlerContact = try? c.decode(String.self, forKey: .facilityHandlerContact) facilityHandlerNotes = try? c.decode(String.self, forKey: .facilityHandlerNotes) vendorName = try? c.decode(String.self, forKey: .vendorName) vendorContact = try? c.decode(String.self, forKey: .vendorContact) vendorNotes = try? c.decode(String.self, forKey: .vendorNotes) } private enum CodingKeys: String, CodingKey { case id, status, severity, description, assignedTo case facilityId, facilityName, reportedAt, mobileLocalId case photoPath, mobilePhotoPaths, resultPhotos case photoUrls, resultPhotoUrls case resultNotes, verifiedAt, verificationNote, reportedByName case areaName, assignedToName case handlerType, handlerLabel case facilityHandlerName, facilityHandlerContact, facilityHandlerNotes case vendorName, vendorContact, vendorNotes } } 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 } } // ── Scheduled Inspections (phase36 — planned/recurring assignments) ─────────── struct APIScheduledInspection: Decodable, Identifiable, Sendable { let id: Int let facilityId: Int let facilityName: String? let templateId: Int let templateName: String? let inspectorId: Int? let frequency: String let frequencyLabel: String? let nextDueDate: String? // ISO date "YYYY-MM-DD" let isOverdue: Bool let notes: String? /// Set when this schedule is a planned follow-up of a specific inspection /// (phase45, "Schedule Follow-up"). Carried onto the inspection started /// from it so the run lands as a linked re-inspection. Nil for an ordinary /// schedule, and on servers older than phase45. let parentInspectionId: Int? 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) facilityName = try? c.decode(String.self, forKey: .facilityName) templateId = try c.decode(Int.self, forKey: .templateId) templateName = try? c.decode(String.self, forKey: .templateName) inspectorId = try? c.decode(Int.self, forKey: .inspectorId) frequency = (try? c.decode(String.self, forKey: .frequency)) ?? "once" frequencyLabel = try? c.decode(String.self, forKey: .frequencyLabel) nextDueDate = try? c.decode(String.self, forKey: .nextDueDate) isOverdue = (try? c.decode(Bool.self, forKey: .isOverdue)) ?? false notes = try? c.decode(String.self, forKey: .notes) parentInspectionId = try? c.decode(Int.self, forKey: .parentInspectionId) } private enum CodingKeys: String, CodingKey { case id, facilityId, facilityName, templateId, templateName case inspectorId, frequency, frequencyLabel, nextDueDate, isOverdue, notes case parentInspectionId } } struct APIScheduledInspectionsResponseData: Decodable, Sendable { let scheduled: [APIScheduledInspection] let total: Int let limit: Int let offset: Int nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) scheduled = try c.decode([APIScheduledInspection].self, forKey: .scheduled) 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 scheduled, total, limit, offset } } /// Response of `POST /api/v1/scheduled-inspections/follow-up` (phase45). /// `created` is false when the server re-dated an existing follow-up for the /// same parent instead of adding a second one. struct APIScheduledFollowUpResponseData: Decodable, Sendable { let scheduled: APIScheduledInspection let created: Bool nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) scheduled = try c.decode(APIScheduledInspection.self, forKey: .scheduled) created = (try? c.decode(Bool.self, forKey: .created)) ?? true } private enum CodingKeys: String, CodingKey { case scheduled, created } } // ── Dashboard Stats (Phase B) ───────────────────────────────────────────────── struct APIDashboardStats: Decodable, Sendable { let todayInspections: Int let completedToday: Int let openIssues: Int let avgScore30d: Double? let pendingFollowups: Int let slaBreached: Int let slaAtRisk: Int // Phase E — severity breakdown of open issues let severityCritical: Int let severityHigh: Int let severityMedium: Int let severityLow: Int nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) todayInspections = (try? c.decode(Int.self, forKey: .todayInspections)) ?? 0 completedToday = (try? c.decode(Int.self, forKey: .completedToday)) ?? 0 openIssues = (try? c.decode(Int.self, forKey: .openIssues)) ?? 0 avgScore30d = try? c.decode(Double.self, forKey: .avgScore30d) pendingFollowups = (try? c.decode(Int.self, forKey: .pendingFollowups)) ?? 0 slaBreached = (try? c.decode(Int.self, forKey: .slaBreached)) ?? 0 slaAtRisk = (try? c.decode(Int.self, forKey: .slaAtRisk)) ?? 0 // Decode from nested severity_breakdown dict if let breakdown = try? c.decode([String: Int].self, forKey: .severityBreakdown) { severityCritical = breakdown["critical"] ?? 0 severityHigh = breakdown["high"] ?? 0 severityMedium = breakdown["medium"] ?? 0 severityLow = breakdown["low"] ?? 0 } else { severityCritical = 0 severityHigh = 0 severityMedium = 0 severityLow = 0 } } private enum CodingKeys: String, CodingKey { case todayInspections, completedToday, openIssues case avgScore30d, pendingFollowups, slaBreached, slaAtRisk case severityBreakdown } } // ── Issue Comments (Phase D) ────────────────────────────────────────────────── struct APIIssueComment: Decodable, Identifiable, Sendable { let id: Int let issueId: Int let authorName: String let authorRole: String let statusAtTime: String let body: String let createdAt: String var createdAtDate: Date? { SyncManager.isoFormatter.date(from: createdAt) } nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(Int.self, forKey: .id) issueId = try c.decode(Int.self, forKey: .issueId) authorName = (try? c.decode(String.self, forKey: .authorName)) ?? "Unknown" authorRole = (try? c.decode(String.self, forKey: .authorRole)) ?? "" statusAtTime = (try? c.decode(String.self, forKey: .statusAtTime)) ?? "" body = try c.decode(String.self, forKey: .body) createdAt = try c.decode(String.self, forKey: .createdAt) } private enum CodingKeys: String, CodingKey { case id, issueId, authorName, authorRole, statusAtTime, body, createdAt } } struct APIIssueCommentsResponseData: Decodable, Sendable { let issueId: Int let comments: [APIIssueComment] let count: Int nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) issueId = try c.decode(Int.self, forKey: .issueId) comments = try c.decode([APIIssueComment].self, forKey: .comments) count = try c.decode(Int.self, forKey: .count) } private enum CodingKeys: String, CodingKey { case issueId, comments, count } } struct APIAddCommentResponseData: Decodable, Sendable { let commentId: Int nonisolated init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) commentId = try c.decode(Int.self, forKey: .commentId) } private enum CodingKeys: String, CodingKey { case commentId } }