Merge branch 'main' of https://gitea.ngodanguyen.tech/nngo/JQC_iOS_App
This commit is contained in:
@@ -37,6 +37,41 @@ struct StartInspectionView: View {
|
||||
var parentServerId: Int? = nil
|
||||
var parentLocalId: String? = nil
|
||||
|
||||
/// The director's note explaining what the follow-up should address, set when
|
||||
/// the inspector taps a row in the FOLLOW-UP REQUESTED card. Passed as a plain
|
||||
/// String rather than read from SwiftData here, for the same reason
|
||||
/// FollowUpStartTarget exists — the cached request row is invalidated at
|
||||
/// submit while this flow is still on screen. Nil for a re-inspection the
|
||||
/// inspector started themselves from CompletedInspectionView.
|
||||
var preFillFollowUpNote: String? = nil
|
||||
|
||||
/// The flagged inspection's answers, JSON-encoded, used to pre-fill this
|
||||
/// re-inspection when the parent `LocalInspection` is not on this device.
|
||||
///
|
||||
/// The local-parent lookup in `startInspection()` covers the
|
||||
/// CompletedInspectionView path, where the inspector is re-inspecting
|
||||
/// something they just finished on this iPad. It does **not** cover a
|
||||
/// follow-up raised on the web: that parent synced long ago and is often
|
||||
/// absent locally, so the lookup found nothing and the form came up blank —
|
||||
/// where the web pre-fills it. Cached at pull time on
|
||||
/// `LocalFollowUpRequest`, so this works offline too. Nil for every other
|
||||
/// start path.
|
||||
var preFillParentFormDataJSON: String? = nil
|
||||
|
||||
// ── Scheduled inspection launch ───────────────────────────────────────
|
||||
/// Server ID of the ScheduledInspection this run fulfils, passed when the
|
||||
/// inspector taps Start on a scheduled row. Carried onto the LocalInspection
|
||||
/// so submitInspection() can send it; without it the server cannot fulfil
|
||||
/// the schedule and the "Scheduled" banner never clears.
|
||||
var preFillScheduleId: Int? = nil
|
||||
|
||||
/// Instructions the manager attached to this schedule ("Instructions" in the
|
||||
/// UI; still `notes` on the wire and in the DB). Passed as a plain String
|
||||
/// rather than read from SwiftData here, for the same reason
|
||||
/// ScheduledStartTarget exists — the cached schedule row is deleted at
|
||||
/// submit while this flow is still on screen.
|
||||
var preFillScheduleInstructions: String? = nil
|
||||
|
||||
// ── Derived lists ─────────────────────────────────────────────────────
|
||||
|
||||
/// Facilities this inspector may actually start work at.
|
||||
@@ -89,6 +124,25 @@ struct StartInspectionView: View {
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
// ── Instructions from the schedule ─────────────────────────
|
||||
// Shown first: this is the reason the inspector was sent here,
|
||||
// and it may change what they carry in with them. Repeated
|
||||
// above the form itself in ExecuteInspectionView.
|
||||
if let instructions = preFillScheduleInstructions,
|
||||
!instructions.isEmpty {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Label("Instructions", systemImage: "info.circle.fill")
|
||||
.font(.callout.bold())
|
||||
.foregroundStyle(.blue)
|
||||
Text(instructions)
|
||||
.font(.callout)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Re-inspection notice ───────────────────────────────────
|
||||
if parentServerId != nil {
|
||||
Section {
|
||||
@@ -101,6 +155,16 @@ struct StartInspectionView: View {
|
||||
Text("This will be linked to inspection #\(parentServerId!).")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
// What the director actually asked for. Shown
|
||||
// here rather than in its own section so it
|
||||
// reads as part of the request, and repeated in
|
||||
// full because the card truncates it to a line.
|
||||
if let note = preFillFollowUpNote, !note.isEmpty {
|
||||
Text(note)
|
||||
.font(.callout)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
@@ -277,23 +341,38 @@ struct StartInspectionView: View {
|
||||
// Link to parent if this is a re-inspection
|
||||
inspection.parentServerId = parentServerId
|
||||
inspection.parentLocalId = parentLocalId
|
||||
// Link to the schedule if launched from a scheduled row
|
||||
inspection.scheduledInspectionServerId = preFillScheduleId
|
||||
|
||||
// ── Pre-fill from parent (mirrors web app behaviour) ───────────────
|
||||
// Copy non-scoring field values from the parent inspection so the
|
||||
// inspector doesn't re-enter static data. Scoring fields (rating,
|
||||
// pass_fail) and media fields (image, signature) are always left blank
|
||||
// so every scoreable item must be re-evaluated fresh.
|
||||
if let parentId = parentServerId {
|
||||
let allInspections = (try? context.fetch(FetchDescriptor<LocalInspection>())) ?? []
|
||||
if let parent = allInspections.first(where: { $0.serverId == parentId }),
|
||||
!parent.formData.isEmpty {
|
||||
if parentServerId != nil {
|
||||
// Prefer the local parent (the CompletedInspectionView path, where
|
||||
// the inspector just finished it on this iPad); otherwise fall back
|
||||
// to the snapshot cached on the follow-up request. A follow-up
|
||||
// raised on the web has usually synced and been dropped locally, so
|
||||
// without the fallback this whole block silently no-opped and the
|
||||
// form came up blank — the bug this fixes.
|
||||
let parentData = resolvedParentFormData()
|
||||
|
||||
// Fetch the template schema to identify field types.
|
||||
// Split into two statements — avoids Xcode 26 #Predicate
|
||||
// ambiguity under SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor.
|
||||
let allTemplates = (try? context.fetch(FetchDescriptor<LocalTemplate>())) ?? []
|
||||
let tid = templateId
|
||||
let schema = allTemplates.first(where: { $0.serverId == tid })?.formSchema ?? []
|
||||
// Fetch the template schema to identify field types.
|
||||
// Split into two statements — avoids Xcode 26 #Predicate
|
||||
// ambiguity under SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor.
|
||||
let allTemplates = (try? context.fetch(FetchDescriptor<LocalTemplate>())) ?? []
|
||||
let tid = templateId
|
||||
let schema = allTemplates.first(where: { $0.serverId == tid })?.formSchema ?? []
|
||||
|
||||
// The schema is what identifies which fields must NOT be carried
|
||||
// over, so without it there is no safe prefill: an empty exclude set
|
||||
// would copy *everything*, including the parent's `image` paths and
|
||||
// its ratings — attaching the previous inspection's photos as this
|
||||
// one's evidence and pre-answering the scoreable items. Copy nothing
|
||||
// instead. (Unreachable in practice: the template was chosen from
|
||||
// the local picker, so it is cached — this is a guard, not a case.)
|
||||
if !parentData.isEmpty, !schema.isEmpty {
|
||||
|
||||
// Build the set of field IDs that must NOT be carried over
|
||||
let excludeTypes: Set<String> = ["rating", "pass_fail", "image", "signature"]
|
||||
@@ -306,7 +385,6 @@ struct StartInspectionView: View {
|
||||
}
|
||||
|
||||
// Copy all parent values except excluded fields
|
||||
let parentData = parent.formData
|
||||
var prefilled: [String: Any] = [:]
|
||||
for (key, value) in parentData {
|
||||
if !excludeIds.contains(key) {
|
||||
@@ -325,4 +403,34 @@ struct StartInspectionView: View {
|
||||
createdInspection = inspection
|
||||
navigateToExecution = true
|
||||
}
|
||||
|
||||
/// The parent inspection's answers to pre-fill from, or `[:]` when there are
|
||||
/// none to carry.
|
||||
///
|
||||
/// Two sources, in order:
|
||||
/// 1. The local `LocalInspection` with a matching `serverId` — the
|
||||
/// re-inspection-from-history path, where the parent is on this device
|
||||
/// and is the freshest copy.
|
||||
/// 2. `preFillParentFormDataJSON`, snapshotted from the server at pull
|
||||
/// time — the follow-up-request path, where the parent has synced and
|
||||
/// is typically no longer local.
|
||||
///
|
||||
/// Source 1 is checked first but only wins when it actually holds values, so
|
||||
/// a stray empty local shell can't shadow a good server snapshot.
|
||||
private func resolvedParentFormData() -> [String: Any] {
|
||||
if let parentId = parentServerId {
|
||||
// Fetch-all then filter in Swift — no #Predicate (CLAUDE.md rule 3).
|
||||
let allInspections = (try? context.fetch(FetchDescriptor<LocalInspection>())) ?? []
|
||||
if let parent = allInspections.first(where: { $0.serverId == parentId }) {
|
||||
let localData = parent.formData
|
||||
if !localData.isEmpty { return localData }
|
||||
}
|
||||
}
|
||||
|
||||
guard let json = preFillParentFormDataJSON,
|
||||
let data = json.data(using: .utf8),
|
||||
let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
else { return [:] }
|
||||
return dict
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user