Files

88 lines
3.6 KiB
Swift

// Models/PendingPhoto.swift
// -------------------------
// SwiftData model for photos waiting to be uploaded to the server.
// Photos are saved locally first, uploaded during sync, then the
// local path reference is replaced with the server path.
import Foundation
import SwiftData
@Model
final class PendingPhoto {
@Attribute(.unique) var localId: String
/// Absolute path in app's Documents/JQC/Photos/ directory
var localFilePath: String
/// "inspection" | "issue"
var entityType: String
/// References LocalInspection.localId or LocalIssue.localId
var entityLocalId: String
/// For inspection form image fields — the field's id string
var fieldId: String?
/// Populated after successful upload
var serverPath: String?
/// "pending" | "uploaded" | "failed"
///
/// "failed" is TERMINAL: it means every upload attempt was used up, and it
/// is what lets processInspectionQueue stop waiting and submit without this
/// photo. A *transient* error must therefore leave the row "pending" — see
/// `uploadRetryCount`. Marking "failed" on the first error is what turned a
/// single dropped connection into a permanently lost evidence photo.
var uploadStatus: String
/// Consecutive failed upload attempts. The row stays "pending" — and so
/// keeps blocking its parent's submission — until this reaches
/// `SyncManager.maxPhotoUploadAttempts`.
///
/// Non-optional with an inline default so SwiftData migrates lightweight
/// (CLAUDE.md rule 8): rows in existing stores read as 0.
var uploadRetryCount: Int = 0
var createdAt: Date
/// Why the last upload attempt failed, for diagnosis.
///
/// Nothing recorded this before: a photo could burn all five attempts and
/// cost an inspection its evidence with no trace anywhere of the reason —
/// the device showed only "failed" and the server logs only SUCCESSFUL
/// uploads. Cleared on a successful upload.
///
/// Optional so existing SwiftData stores migrate lightweight (rule 8).
var lastUploadError: String?
// ── Capture metadata (sent to the server, burned into the photo) ───────
// Recorded when the shutter fires, NOT when the upload runs — the app is
// offline-first, so a photo taken at 09:14 may not sync until 16:00 and
// the stamp must show 09:14. EXIF cannot serve as a fallback here: the
// photo is re-encoded via jpegData() on save, which strips every tag.
// Optional so existing SwiftData stores migrate without a schema step.
var capturedAt: Date?
var captureLatitude: Double?
var captureLongitude: Double?
var inspection: LocalInspection?
init(
localFilePath: String,
entityType: String,
entityLocalId: String,
fieldId: String? = nil,
capturedAt: Date? = nil,
captureLatitude: Double? = nil,
captureLongitude: Double? = nil
) {
self.localId = UUID().uuidString
self.localFilePath = localFilePath
self.entityType = entityType
self.entityLocalId = entityLocalId
self.fieldId = fieldId
self.serverPath = nil
self.uploadStatus = "pending"
self.uploadRetryCount = 0
self.createdAt = Date()
// Fall back to now when the caller has no recorded capture moment —
// still far better than the server's upload-time default.
self.capturedAt = capturedAt ?? Date()
self.captureLatitude = captureLatitude
self.captureLongitude = captureLongitude
}
}