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_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;
+18
View File
@@ -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
+7
View File
@@ -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 {
@@ -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()
}
@@ -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,7 +518,12 @@ struct GridFormView: View {
}
)
// Field overlays
// 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
@@ -526,12 +535,16 @@ struct GridFormView: View {
}
}
}
}
.onPreferenceChange(WidthPreferenceKey.self) { width in
if width > 0 { containerWidth = width }
}
// 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 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()
}
}
}