Files
JQC_iOS_App/JanitorialQC/Views/Auth/LoginView.swift
T

154 lines
6.5 KiB
Swift

// 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)
}
}
}
private func signIn() async {
focusedField = nil
await auth.login(username: username, password: password)
}
}
#Preview {
LoginView()
.environmentObject(AuthManager.shared)
}