// Views/Auth/LoginView.swift // -------------------------- // The login screen shown when the user is not authenticated. import SwiftUI struct LoginView: View { @EnvironmentObject private var auth: AuthManager @State private var username = "" @State private var password = "" @State private var selectedServer: ServerOption = ServerConfig.selectedOption @FocusState private var focusedField: Field? private enum Field { case username, password } var body: some View { NavigationStack { ZStack { // Background Color(.systemGroupedBackground).ignoresSafeArea() VStack(spacing: 0) { Spacer() // ── Logo / Header ────────────────────────────────────── VStack(spacing: 12) { Image(systemName: "checkmark.seal.fill") .font(.system(size: 64)) .foregroundStyle(.blue) Text("JanitorialQC Inspector") .font(.largeTitle.bold()) Text("Janitorial Quality Control") .font(.subheadline) .foregroundStyle(.secondary) } .padding(.bottom, 48) // ── Login Form ───────────────────────────────────────── VStack(spacing: 16) { // ── Server Picker ────────────────────────────────── GroupBox { VStack(alignment: .leading, spacing: 6) { Label("Server", systemImage: "server.rack") .font(.caption) .foregroundStyle(.secondary) Picker("Server", selection: $selectedServer) { ForEach(ServerOption.allCases, id: \.self) { option in Text(option.displayName).tag(option) } } .pickerStyle(.segmented) .onChange(of: selectedServer) { _, newValue in ServerConfig.select(newValue) } } .padding(.vertical, 4) .padding(.horizontal, 4) } .frame(maxWidth: 400) GroupBox { VStack(spacing: 0) { HStack { Image(systemName: "person") .foregroundStyle(.secondary) .frame(width: 24) TextField("Username", text: $username) .textInputAutocapitalization(.never) .autocorrectionDisabled() .focused($focusedField, equals: .username) .submitLabel(.next) .onSubmit { focusedField = .password } } .padding(.vertical, 12) Divider() HStack { Image(systemName: "lock") .foregroundStyle(.secondary) .frame(width: 24) SecureField("Password", text: $password) .focused($focusedField, equals: .password) .submitLabel(.go) .onSubmit { Task { await signIn() } } } .padding(.vertical, 12) } .padding(.horizontal, 4) } .frame(maxWidth: 400) // Error message if let error = auth.errorMessage { HStack { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(.red) Text(error) .foregroundStyle(.red) .font(.callout) } .padding(.horizontal) } // Sign In button Button { Task { await signIn() } } label: { HStack { if auth.isLoading { ProgressView() .tint(.white) .padding(.trailing, 4) } Text("Sign In") .fontWeight(.semibold) } .frame(maxWidth: 400) .padding(.vertical, 14) } .buttonStyle(.borderedProminent) .controlSize(.large) .disabled(auth.isLoading || username.isEmpty || password.isEmpty) } Spacer() // ── App Version ──────────────────────────────────────── Text("JanitorialQC Inspector") .font(.caption2) .foregroundStyle(.tertiary) .padding(.bottom, 24) } .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 { focusedField = nil await auth.login(username: username, password: password) } } #Preview { LoginView() .environmentObject(AuthManager.shared) }