131 lines
5.2 KiB
Swift
131 lines
5.2 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 = ""
|
|
@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) {
|
|
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)
|
|
}
|