05/02 Phase C

This commit is contained in:
Nguyen Ngo
2026-05-03 08:31:25 -04:00
parent 8bdcbecd73
commit 59e698b5f7
6 changed files with 451 additions and 147 deletions
+20 -19
View File
@@ -1,7 +1,7 @@
// API/APIClient.swift
// -------------------
// Central HTTP client for all JQC API calls.
// Phase B adds: uploadPhoto(), submitInspection(), submitIssue()
// Phase C adds: fetchInspectionHistory()
import Foundation
import Combine
@@ -99,10 +99,24 @@ actor APIClient {
return try await request(endpoint, method: "POST", body: body)
}
// Inspection History (Phase C)
/// Fetch the inspector's synced inspection history from the server.
/// Returns up to `limit` records starting at `offset`.
func fetchInspectionHistory(
limit: Int = 50,
offset: Int = 0,
facilityId: Int? = nil
) async throws -> InspectionHistoryResponseData {
var endpoint = "/api/v1/inspections?limit=\(limit)&offset=\(offset)&status=completed"
if let fid = facilityId {
endpoint += "&facility_id=\(fid)"
}
return try await request(endpoint)
}
// Photo Upload (multipart/form-data)
/// Upload a local photo file to the server.
/// Returns the server_path string on success.
func uploadPhoto(localPath: String, entityType: String) async throws -> String {
guard let url = URL(string: baseURL + "/api/v1/photos/upload") else {
throw APIError.invalidURL
@@ -115,12 +129,10 @@ actor APIClient {
let boundary = "Boundary-\(UUID().uuidString)"
var body = Data()
// -- entity_type field
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"entity_type\"\r\n\r\n".data(using: .utf8)!)
body.append("\(entityType)\r\n".data(using: .utf8)!)
// -- file field
let filename = URL(fileURLWithPath: localPath).lastPathComponent
let ext = (filename as NSString).pathExtension.lowercased()
let mimeType = ext == "png" ? "image/png" : "image/jpeg"
@@ -171,8 +183,6 @@ actor APIClient {
// Submit Inspection
/// Submit a completed LocalInspection to the server.
/// Returns the server-assigned inspection ID.
func submitInspection(_ inspection: LocalInspection) async throws -> Int {
var body: [String: Any] = [
"template_id": inspection.templateServerId,
@@ -183,12 +193,8 @@ actor APIClient {
"overall_score": inspection.overallScore as Any,
]
if let areaId = inspection.areaServerId {
body["area_id"] = areaId
}
if !inspection.inspectorNotes.isEmpty {
body["notes"] = inspection.inspectorNotes
}
if let areaId = inspection.areaServerId { body["area_id"] = areaId }
if !inspection.inspectorNotes.isEmpty { body["notes"] = inspection.inspectorNotes }
let formatter = ISO8601DateFormatter()
body["inspection_date"] = formatter.string(from: inspection.inspectionDate)
@@ -207,8 +213,6 @@ actor APIClient {
// Submit Issue
/// Submit a LocalIssue to the server.
/// Returns the server-assigned issue ID.
func submitIssue(_ issue: LocalIssue) async throws -> Int {
var body: [String: Any] = [
"area_id": issue.areaServerId,
@@ -217,12 +221,9 @@ actor APIClient {
"mobile_local_id": issue.localId,
]
// Link to server inspection if already synced
// (inspection must be synced before its issues)
if let inspServerId = (issue.inspection?.serverId) {
if let inspServerId = issue.inspection?.serverId {
body["inspection_id"] = inspServerId
}
if let serverPhotoPath = issue.photoServerPath {
body["photo_path"] = serverPhotoPath
}
+31 -10
View File
@@ -40,11 +40,6 @@ struct APIUser: Decodable {
let email: String
let role: String
let createdAt: String?
// Computed display name: full name if present, otherwise username.
// The server's /auth/me endpoint returns username; display_name would
// require a separate field. For Phase A we use username as the display name
// and can extend later.
var displayName: String { username }
}
@@ -102,18 +97,44 @@ struct APITemplate: Decodable, Identifiable {
let name: String
let description: String
let frequency: String
let formSchema: [[String: AnyDecodable]] // Dynamic JSON fields
let formSchema: [[String: AnyDecodable]]
}
// Inspections (Phase C history)
struct InspectionHistoryResponseData: Decodable {
let inspections: [APIInspectionSummary]
let total: Int
let limit: Int
let offset: Int
}
struct APIInspectionSummary: Decodable, Identifiable {
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?
var inspectionDateParsed: Date? {
guard let str = inspectionDate else { return nil }
return ISO8601DateFormatter().date(from: str)
}
}
// AnyDecodable helper
// Allows decoding JSON values of unknown type (String, Int, Bool, Array, Dict)
struct AnyDecodable: Decodable {
let value: Any
init(_ value: Any) {
self.value = value
}
init(_ value: Any) { self.value = value }
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()