338 lines
14 KiB
Swift
338 lines
14 KiB
Swift
// 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<T: Decodable & Sendable>: 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, 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 }
|
|
}
|