05/29 Update functionalities

This commit is contained in:
Nguyen Ngo
2026-05-29 17:06:04 -04:00
parent 7d05c3862d
commit e4261778c8
5 changed files with 74 additions and 28 deletions
+4 -4
View File
@@ -398,7 +398,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = SB7DNYC9TY; DEVELOPMENT_TEAM = SB7DNYC9TY;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -417,7 +417,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.1; MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = com.ltservicesinc.JanitorialQC; PRODUCT_BUNDLE_IDENTIFIER = com.ltservicesinc.JanitorialQC;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -440,7 +440,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = SB7DNYC9TY; DEVELOPMENT_TEAM = SB7DNYC9TY;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -459,7 +459,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.1; MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = com.ltservicesinc.JanitorialQC; PRODUCT_BUNDLE_IDENTIFIER = com.ltservicesinc.JanitorialQC;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
+18
View File
@@ -7,9 +7,27 @@ import SwiftData
import BackgroundTasks import BackgroundTasks
import UserNotifications 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 @main
struct JanitorialQCApp: App { struct JanitorialQCApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject private var auth = AuthManager.shared @StateObject private var auth = AuthManager.shared
@StateObject private var sync = SyncManager.shared @StateObject private var sync = SyncManager.shared
+7
View File
@@ -139,6 +139,13 @@ struct LoginView: View {
.padding(.horizontal, 32) .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 { private func signIn() async {
@@ -1877,10 +1877,13 @@ struct SettingsView: View {
Section { Section {
Button(role: .destructive) { Button(role: .destructive) {
Task { Task {
// Clear server-pulled issues before logging out so stale // Do NOT clear server-pulled data on a plain logout
// records from a previous server/domain don't persist into // the user is logging out of the same server, so cached
// the next session. Device-created pending issues are preserved. // facilities, issues, and templates are still valid on
clearServerPulledData() // 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() sync.resetNotificationPoller()
await auth.logout() await auth.logout()
} }
@@ -488,9 +488,13 @@ struct GridFormView: View {
"table": 80, "table": 80,
] ]
// Width captured via PreferenceKey updates on rotation & split-screen. // Width captured via PreferenceKey updates on rotation, split-screen,
// Default 952 = 1000 max-width 2×24 outer padding (safe iPad landscape floor). // and sheet presentation.
@State private var containerWidth: CGFloat = 952 // 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 { var body: some View {
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
@@ -514,15 +518,21 @@ struct GridFormView: View {
} }
) )
// Field overlays // Field overlays only rendered after width is measured
let cellW = computedCellW // containerWidth == 0 means the PreferenceKey has not fired yet
let cellH = cellW * Self.cellAspect // (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 ForEach(schema.indices, id: \.self) { idx in
let field = schema[idx] let field = schema[idx]
let ftype = field["type"] as? String ?? "text" let ftype = field["type"] as? String ?? "text"
if !["button_submit", "button_print", "button_email"].contains(ftype) { if !["button_submit", "button_print", "button_email"].contains(ftype) {
gridCell(field: field, cellW: cellW, cellH: cellH) 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 // Height is always derived from the same arithmetic as cell offsets
// the ScrollView measures this frame and can never be wrong. // 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 // Derived cell width from current containerWidth
@@ -992,26 +1005,31 @@ struct CellPassFail: View {
} }
var body: some 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) { HStack(spacing: 6) {
ForEach(options, id: \.self) { opt in ForEach(options, id: \.self) { opt in
let isPass = isPassOption(opt) let isPass = isPassOption(opt)
let isActive = value == opt let isActive = value == opt
let baseColor: Color = isPass ? .green : .red let color: Color = isPass ? .green : .red
Button { Button {
value = isActive ? "" : opt value = isActive ? "" : opt
} label: { } label: {
Text(opt) Text(opt)
.font(.system(size: 13, weight: .semibold)) .font(.system(size: 13, weight: .semibold))
.padding(.horizontal, 14) .lineLimit(1)
.minimumScaleFactor(0.75)
.padding(.horizontal, 10)
.padding(.vertical, 6) .padding(.vertical, 6)
.background(isActive ? baseColor : Color.clear) .frame(maxWidth: .infinity)
.foregroundStyle(isActive ? .white : baseColor) .background(isActive ? color : Color.clear)
.foregroundStyle(isActive ? .white : color)
.clipShape(Capsule()) .clipShape(Capsule())
.overlay(Capsule().stroke(baseColor, lineWidth: 2)) .overlay(Capsule().stroke(color, lineWidth: 2))
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
Spacer()
} }
} }
} }