diff --git a/JanitorialQC.xcodeproj/project.pbxproj b/JanitorialQC.xcodeproj/project.pbxproj index 1b38ec9..d102bce 100644 --- a/JanitorialQC.xcodeproj/project.pbxproj +++ b/JanitorialQC.xcodeproj/project.pbxproj @@ -398,7 +398,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = SB7DNYC9TY; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -417,7 +417,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = com.ltservicesinc.JanitorialQC; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -440,7 +440,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = SB7DNYC9TY; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -459,7 +459,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1; + MARKETING_VERSION = 1.2; PRODUCT_BUNDLE_IDENTIFIER = com.ltservicesinc.JanitorialQC; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; diff --git a/JanitorialQC/JanitorialQCApp.swift b/JanitorialQC/JanitorialQCApp.swift index 648ab09..531d6b1 100644 --- a/JanitorialQC/JanitorialQCApp.swift +++ b/JanitorialQC/JanitorialQCApp.swift @@ -7,9 +7,27 @@ import SwiftData import BackgroundTasks import UserNotifications +// ── AppDelegate — runtime orientation lock ──────────────────────────────────── +// Info.plist must declare all 4 orientations so iPad multitasking is supported +// (App Store requirement). This delegate restricts the app to landscape-only +// at runtime by returning only the two landscape masks. +// Portrait is intentionally excluded: the grid-based inspection form is +// designed for landscape and does not adapt well to portrait on iPad. + +final class AppDelegate: NSObject, UIApplicationDelegate { + func application( + _ application: UIApplication, + supportedInterfaceOrientationsFor window: UIWindow? + ) -> UIInterfaceOrientationMask { + return [.landscapeLeft, .landscapeRight] + } +} + @main struct JanitorialQCApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + @StateObject private var auth = AuthManager.shared @StateObject private var sync = SyncManager.shared diff --git a/JanitorialQC/Views/Auth/LoginView.swift b/JanitorialQC/Views/Auth/LoginView.swift index 394988c..f064732 100644 --- a/JanitorialQC/Views/Auth/LoginView.swift +++ b/JanitorialQC/Views/Auth/LoginView.swift @@ -139,6 +139,13 @@ struct LoginView: View { .padding(.horizontal, 32) } } + // Re-read the persisted server selection each time the login screen + // appears. Without this, the picker shows a stale value when the user + // switched server in Settings and was logged out — because @State is + // initialised once and never refreshed by UserDefaults changes. + .onAppear { + selectedServer = ServerConfig.selectedOption + } } private func signIn() async { diff --git a/JanitorialQC/Views/Dashboard/DashboardView.swift b/JanitorialQC/Views/Dashboard/DashboardView.swift index b24a995..6d5612c 100644 --- a/JanitorialQC/Views/Dashboard/DashboardView.swift +++ b/JanitorialQC/Views/Dashboard/DashboardView.swift @@ -1877,10 +1877,13 @@ struct SettingsView: View { Section { Button(role: .destructive) { Task { - // Clear server-pulled issues before logging out so stale - // records from a previous server/domain don't persist into - // the next session. Device-created pending issues are preserved. - clearServerPulledData() + // Do NOT clear server-pulled data on a plain logout — + // the user is logging out of the same server, so cached + // facilities, issues, and templates are still valid on + // their next login. Clearing here leaves the issues list + // empty until a full sync succeeds, which breaks offline use. + // Server-pulled data is only cleared when switching servers + // (see the Switch & Log Out alert below). sync.resetNotificationPoller() await auth.logout() } diff --git a/JanitorialQC/Views/Dashboard/ExecuteInspectionView.swift b/JanitorialQC/Views/Dashboard/ExecuteInspectionView.swift index b841b71..fdc82b2 100644 --- a/JanitorialQC/Views/Dashboard/ExecuteInspectionView.swift +++ b/JanitorialQC/Views/Dashboard/ExecuteInspectionView.swift @@ -488,9 +488,13 @@ struct GridFormView: View { "table": 80, ] - // Width captured via PreferenceKey — updates on rotation & split-screen. - // Default 952 = 1000 max-width − 2×24 outer padding (safe iPad landscape floor). - @State private var containerWidth: CGFloat = 952 + // Width captured via PreferenceKey — updates on rotation, split-screen, + // and sheet presentation. + // 0 = unmeasured; the grid does NOT render fields until a real width arrives. + // This prevents the first-frame overflow that occurred when a sheet modal + // (narrower than full screen on iPad 10th gen) was rendered with the old + // hardcoded 952 pt fallback, causing fields to overflow the modal bounds. + @State private var containerWidth: CGFloat = 0 var body: some View { ZStack(alignment: .topLeading) { @@ -514,15 +518,21 @@ struct GridFormView: View { } ) - // ── Field overlays ───────────────────────────────────────────── - let cellW = computedCellW - let cellH = cellW * Self.cellAspect + // ── Field overlays — only rendered after width is measured ───── + // containerWidth == 0 means the PreferenceKey has not fired yet + // (first layout pass). Skipping the overlay pass on the zero frame + // prevents fields from being positioned using a stale width and + // overflowing the modal on narrow sheet presentations (iPad 10th gen). + if containerWidth > 0 { + let cellW = computedCellW + let cellH = cellW * Self.cellAspect - ForEach(schema.indices, id: \.self) { idx in - let field = schema[idx] - let ftype = field["type"] as? String ?? "text" - if !["button_submit", "button_print", "button_email"].contains(ftype) { - gridCell(field: field, cellW: cellW, cellH: cellH) + ForEach(schema.indices, id: \.self) { idx in + let field = schema[idx] + let ftype = field["type"] as? String ?? "text" + if !["button_submit", "button_print", "button_email"].contains(ftype) { + gridCell(field: field, cellW: cellW, cellH: cellH) + } } } } @@ -531,7 +541,10 @@ struct GridFormView: View { } // Height is always derived from the same arithmetic as cell offsets — // the ScrollView measures this frame and can never be wrong. - .frame(height: canvasHeight() + 2 * Self.cardPadding) + // When containerWidth is 0, canvasHeight() still returns the correct + // value (it uses computedCellW which returns 0 when containerWidth is 0), + // so the card reserves space and avoids a layout jump. + .frame(height: containerWidth > 0 ? canvasHeight() + 2 * Self.cardPadding : 0) } // ── Derived cell width from current containerWidth ──────────────────── @@ -992,26 +1005,31 @@ struct CellPassFail: View { } var body: some View { + // Each button expands equally to fill the available cell width. + // .lineLimit(1) + fixedSize prevents multi-line wrapping when + // custom option labels are long or the cell is narrow. HStack(spacing: 6) { ForEach(options, id: \.self) { opt in - let isPass = isPassOption(opt) - let isActive = value == opt - let baseColor: Color = isPass ? .green : .red + let isPass = isPassOption(opt) + let isActive = value == opt + let color: Color = isPass ? .green : .red Button { value = isActive ? "" : opt } label: { Text(opt) .font(.system(size: 13, weight: .semibold)) - .padding(.horizontal, 14) + .lineLimit(1) + .minimumScaleFactor(0.75) + .padding(.horizontal, 10) .padding(.vertical, 6) - .background(isActive ? baseColor : Color.clear) - .foregroundStyle(isActive ? .white : baseColor) + .frame(maxWidth: .infinity) + .background(isActive ? color : Color.clear) + .foregroundStyle(isActive ? .white : color) .clipShape(Capsule()) - .overlay(Capsule().stroke(baseColor, lineWidth: 2)) + .overlay(Capsule().stroke(color, lineWidth: 2)) } .buttonStyle(.plain) } - Spacer() } } }