05/29 Update functionalities
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user