Jul 26 - Update scheduled inspection settings

This commit is contained in:
2026-07-26 14:56:39 -04:00
parent 1921f95faf
commit 5999871f77
6 changed files with 37 additions and 4 deletions
+5
View File
@@ -238,6 +238,11 @@ actor APIClient {
if let score = inspection.overallScore { body["overall_score"] = score } if let score = inspection.overallScore { body["overall_score"] = score }
if let areaId = inspection.areaServerId { body["area_id"] = areaId } if let areaId = inspection.areaServerId { body["area_id"] = areaId }
if let parentId = inspection.parentServerId { body["parent_inspection_id"] = parentId } if let parentId = inspection.parentServerId { body["parent_inspection_id"] = parentId }
// Links the submission back to the schedule it was started from so the
// server fulfils it (clears the banner) and badges it as "Scheduled".
if let schedId = inspection.scheduledInspectionServerId {
body["scheduled_inspection_id"] = schedId
}
if !inspection.inspectorNotes.isEmpty { body["notes"] = inspection.inspectorNotes } if !inspection.inspectorNotes.isEmpty { body["notes"] = inspection.inspectorNotes }
if let lat = inspection.submitLatitude { body["submit_latitude"] = lat } if let lat = inspection.submitLatitude { body["submit_latitude"] = lat }
if let lng = inspection.submitLongitude { body["submit_longitude"] = lng } if let lng = inspection.submitLongitude { body["submit_longitude"] = lng }
+8 -2
View File
@@ -193,7 +193,7 @@ PendingPhoto.self, SyncQueueEntry.self
| `LocalFacility` | Read-only cached facility reference | `serverId` (`@Attribute(.unique)`), `name`, `projectId`, `projectName`, `areas` (cascade) | | `LocalFacility` | Read-only cached facility reference | `serverId` (`@Attribute(.unique)`), `name`, `projectId`, `projectName`, `areas` (cascade) |
| `LocalArea` | Read-only cached area reference | `serverId`, `facilityServerId`, `name`, `areaType` | | `LocalArea` | Read-only cached area reference | `serverId`, `facilityServerId`, `name`, `areaType` |
| `LocalTemplate` | Cached template + raw JSON schema | `serverId`, `formSchemaJSON`, `formSchema` (computed) | | `LocalTemplate` | Cached template + raw JSON schema | `serverId`, `formSchemaJSON`, `formSchema` (computed) |
| `LocalInspection` | Inspector-authored inspection record | `localId` (UUID, unique), `serverId`, `status`, `syncStatus`, `formDataJSON`, `followUpRequired`, `parentLocalId`, `parentServerId`, `submitLatitude` (Double?), `submitLongitude` (Double?) | | `LocalInspection` | Inspector-authored inspection record | `localId` (UUID, unique), `serverId`, `status`, `syncStatus`, `formDataJSON`, `followUpRequired`, `parentLocalId`, `parentServerId`, `scheduledInspectionServerId` (Int?, inline default — links the submission to the schedule it fulfils), `submitLatitude` (Double?), `submitLongitude` (Double?) |
| `LocalIssue` | Issue record | `localId` (UUID, unique), `serverId`, `inspectionLocalId` (`""` for standalone/server-pulled), `facilityServerId`, `severity`, `syncStatus`, `photoLocalPathsJSON`, `photoServerPathsJSON`, **handler fields** (`handlerType`, `handlerLabel`, `facilityHandler*`, `vendor*` — all optional, synced from server, inspector-editable) | | `LocalIssue` | Issue record | `localId` (UUID, unique), `serverId`, `inspectionLocalId` (`""` for standalone/server-pulled), `facilityServerId`, `severity`, `syncStatus`, `photoLocalPathsJSON`, `photoServerPathsJSON`, **handler fields** (`handlerType`, `handlerLabel`, `facilityHandler*`, `vendor*` — all optional, synced from server, inspector-editable) |
| `LocalScheduledInspection` | Read-only cached scheduled/recurring assignment (phase36) | `serverId` (`@Attribute(.unique)`, **no default** — rule 63), `facilityServerId`, `facilityName`, `templateServerId`, `templateName`, `inspectorId`, `frequency`, `frequencyLabel`, `dueDateString` (sort key), `isOverdue`, `nextDue` (computed). Pulled by `pullScheduledInspections()`; `init(from:)`/`update(from:)` like `LocalFacility` | | `LocalScheduledInspection` | Read-only cached scheduled/recurring assignment (phase36) | `serverId` (`@Attribute(.unique)`, **no default** — rule 63), `facilityServerId`, `facilityName`, `templateServerId`, `templateName`, `inspectorId`, `frequency`, `frequencyLabel`, `dueDateString` (sort key), `isOverdue`, `nextDue` (computed). Pulled by `pullScheduledInspections()`; `init(from:)`/`update(from:)` like `LocalFacility` |
| `PendingPhoto` | Photo awaiting upload | `localId`, `localFilePath`, `serverPath`, `uploadStatus`, `entityType` (`"issue"` or `"inspection"`), `fieldId` | | `PendingPhoto` | Photo awaiting upload | `localId`, `localFilePath`, `serverPath`, `uploadStatus`, `entityType` (`"issue"` or `"inspection"`), `fieldId` |
@@ -381,7 +381,13 @@ Contract → Facility cascade pickers (same as `StandaloneIssueView`). `onChange
### Scheduled inspections (phase36, July 2026) ### Scheduled inspections (phase36, July 2026)
`ScheduledInspectionsView.swift` holds `ScheduledRow` + `ScheduledInspectionsCard`. The card renders on the Dashboard (`DashboardStatsView`); `MyInspectionsView` renders its own inline "Scheduled" `List` section reusing `ScheduledRow`. Both query `LocalScheduledInspection` (sorted by `dueDateString`), self-hide when empty, and tap-to-Start opens `StartInspectionView(preFillTemplateId:preFillFacilityId:)`. Data is pulled read-only by `pullScheduledInspections()` (see rules 6364 for the model + cover pitfalls hit while building it). `ScheduledInspectionsView.swift` holds `ScheduledRow` + `ScheduledInspectionsCard`. The card renders on the Dashboard (`DashboardStatsView`); `MyInspectionsView` renders its own inline "Scheduled" `List` section reusing `ScheduledRow`. Both query `LocalScheduledInspection` (sorted by `dueDateString`), self-hide when empty, and tap-to-Start opens `StartInspectionView(preFillTemplateId:preFillFacilityId:preFillScheduleId:)`. Data is pulled read-only by `pullScheduledInspections()` (see rules 6364 for the model + cover pitfalls hit while building it).
**Fulfilling the schedule (July 2026 fix).** `preFillScheduleId` is the schedule's `serverId`; `startInspection()` copies it onto `LocalInspection.scheduledInspectionServerId`, and `submitInspection()` sends it as **`scheduled_inspection_id`**. The server then fulfils the schedule (one-time → deactivated, recurring → rolled forward) in the same commit as the inspection.
Without it — the original bug — the schedule was never fulfilled: the banner stayed on the inspector's Dashboard and My Inspections, it stayed on the web dashboard for admin/director, and the web inspection list showed no "Scheduled" badge. **Both** Start call sites must pass `preFillScheduleId` (`ScheduledInspectionsCard` and the `MyInspectionsView` inline section); re-inspection launches correctly leave it nil.
No SyncManager change was needed: `pullScheduledInspections()` already runs after `processInspectionQueue()` in the same `triggerSync()` pass and deletes rows the server no longer returns, so the section clears on the same sync that submits the inspection.
### Inspection-start presentation (July 2026) ### Inspection-start presentation (July 2026)
+11
View File
@@ -51,6 +51,16 @@ final class LocalInspection {
/// followUpRequired badge without relying on parentServerId being non-nil. /// followUpRequired badge without relying on parentServerId being non-nil.
var parentLocalId: String? var parentLocalId: String?
// Scheduled inspection link
/// Server ID of the ScheduledInspection this inspection was started from,
/// set when the inspector taps Start on a scheduled row. Sent as
/// `scheduled_inspection_id` on submit so the server can fulfil the
/// schedule (deactivate a one-time / roll a recurring one forward) and
/// flag the inspection as "Scheduled" in the web list. Nil for ad-hoc work.
///
/// Declared with an inline default so existing stores migrate lightweight.
var scheduledInspectionServerId: Int? = nil
// GPS (captured at submit time via CoreLocation) // GPS (captured at submit time via CoreLocation)
/// Device latitude at the moment the inspector tapped Submit. Nil if /// Device latitude at the moment the inspector tapped Submit. Nil if
/// location permission was denied or a fix could not be obtained in time. /// location permission was denied or a fix could not be obtained in time.
@@ -89,6 +99,7 @@ final class LocalInspection {
self.followUpNote = nil self.followUpNote = nil
self.parentServerId = nil self.parentServerId = nil
self.parentLocalId = nil self.parentLocalId = nil
self.scheduledInspectionServerId = nil
self.submitLatitude = nil self.submitLatitude = nil
self.submitLongitude = nil self.submitLongitude = nil
self.pendingPhotos = [] self.pendingPhotos = []
@@ -75,7 +75,8 @@ struct MyInspectionsView: View {
.fullScreenCover(item: $scheduledStartTarget) { s in .fullScreenCover(item: $scheduledStartTarget) { s in
StartInspectionView( StartInspectionView(
preFillTemplateId: s.templateServerId, preFillTemplateId: s.templateServerId,
preFillFacilityId: s.facilityServerId preFillFacilityId: s.facilityServerId,
preFillScheduleId: s.serverId
) )
} }
} }
@@ -105,7 +105,8 @@ struct ScheduledInspectionsCard: View {
.fullScreenCover(item: $startTarget) { s in .fullScreenCover(item: $startTarget) { s in
StartInspectionView( StartInspectionView(
preFillTemplateId: s.templateServerId, preFillTemplateId: s.templateServerId,
preFillFacilityId: s.facilityServerId preFillFacilityId: s.facilityServerId,
preFillScheduleId: s.serverId
) )
} }
} }
@@ -37,6 +37,13 @@ struct StartInspectionView: View {
var parentServerId: Int? = nil var parentServerId: Int? = nil
var parentLocalId: String? = nil var parentLocalId: 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
// Derived lists // Derived lists
/// Unique contracts (projectId, projectName) sorted by name. /// Unique contracts (projectId, projectName) sorted by name.
@@ -266,6 +273,8 @@ struct StartInspectionView: View {
// Link to parent if this is a re-inspection // Link to parent if this is a re-inspection
inspection.parentServerId = parentServerId inspection.parentServerId = parentServerId
inspection.parentLocalId = parentLocalId inspection.parentLocalId = parentLocalId
// Link to the schedule if launched from a scheduled row
inspection.scheduledInspectionServerId = preFillScheduleId
// Pre-fill from parent (mirrors web app behaviour) // Pre-fill from parent (mirrors web app behaviour)
// Copy non-scoring field values from the parent inspection so the // Copy non-scoring field values from the parent inspection so the