This commit is contained in:
Trey t
2025-11-05 10:38:46 -06:00
parent 025fcf677a
commit 2be3a5a3a8
23 changed files with 2837 additions and 124 deletions

View File

@@ -21,6 +21,11 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
7A237E53D5D71D9D6A361E29 /* Configuration */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = Configuration;
sourceTree = "<group>";
};
E822E6B231E7783DE992578C /* iosApp */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
@@ -29,11 +34,6 @@
path = iosApp;
sourceTree = "<group>";
};
7A237E53D5D71D9D6A361E29 /* Configuration */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = Configuration;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
@@ -167,6 +167,92 @@
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
0248CABA5A5197845F2E5C26 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = arm64;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = "${TEAM_ID}";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iosApp/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
468E4A6C96BEEFB382150D37 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReferenceAnchor = 7A237E53D5D71D9D6A361E29 /* Configuration */;
baseConfigurationReferenceRelativePath = Config.xcconfig;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
80B0F01D77D413305F161C14 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReferenceAnchor = 7A237E53D5D71D9D6A361E29 /* Configuration */;
@@ -232,64 +318,6 @@
};
name = Debug;
};
468E4A6C96BEEFB382150D37 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReferenceAnchor = 7A237E53D5D71D9D6A361E29 /* Configuration */;
baseConfigurationReferenceRelativePath = Config.xcconfig;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
E767E942685C7832D51FF978 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -318,46 +346,9 @@
};
name = Debug;
};
0248CABA5A5197845F2E5C26 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = arm64;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = "${TEAM_ID}";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iosApp/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
F25B3A5CCAC6BFCC21CD4636 /* Build configuration list for PBXProject "iosApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
80B0F01D77D413305F161C14 /* Debug */,
468E4A6C96BEEFB382150D37 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
293B4412461C9407D900D07D /* Build configuration list for PBXNativeTarget "iosApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@@ -367,7 +358,16 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
F25B3A5CCAC6BFCC21CD4636 /* Build configuration list for PBXProject "iosApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
80B0F01D77D413305F161C14 /* Debug */,
468E4A6C96BEEFB382150D37 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 6A3E1D84F9F1A2FD92A75A6C /* Project object */;
}
}

View File

@@ -0,0 +1,413 @@
import SwiftUI
import ComposeApp
struct AddResidenceView: View {
@Binding var isPresented: Bool
@StateObject private var viewModel = ResidenceViewModel()
@StateObject private var lookupsManager = LookupsManager.shared
@FocusState private var focusedField: Field?
// Form fields
@State private var name: String = ""
@State private var selectedPropertyType: ResidenceType?
@State private var streetAddress: String = ""
@State private var apartmentUnit: String = ""
@State private var city: String = ""
@State private var stateProvince: String = ""
@State private var postalCode: String = ""
@State private var country: String = "USA"
@State private var bedrooms: String = ""
@State private var bathrooms: String = ""
@State private var squareFootage: String = ""
@State private var lotSize: String = ""
@State private var yearBuilt: String = ""
@State private var description: String = ""
@State private var isPrimary: Bool = false
// Validation errors
@State private var nameError: String = ""
@State private var streetAddressError: String = ""
@State private var cityError: String = ""
@State private var stateProvinceError: String = ""
@State private var postalCodeError: String = ""
// Picker state
@State private var showPropertyTypePicker = false
enum Field {
case name, streetAddress, apartmentUnit, city, stateProvince, postalCode, country
case bedrooms, bathrooms, squareFootage, lotSize, yearBuilt, description
}
var body: some View {
NavigationView {
ZStack {
Color(.systemGroupedBackground)
.ignoresSafeArea()
ScrollView {
VStack(spacing: 24) {
// Required Information Section
VStack(alignment: .leading, spacing: 16) {
Text("Required Information")
.font(.headline)
.foregroundColor(.blue)
FormTextField(
label: "Property Name",
text: $name,
error: nameError,
placeholder: "My Home",
focusedField: $focusedField,
field: .name
)
// Property Type Picker
VStack(alignment: .leading, spacing: 8) {
Text("Property Type")
.font(.subheadline)
.foregroundColor(.secondary)
Button(action: {
showPropertyTypePicker = true
}) {
HStack {
Text(selectedPropertyType?.name ?? "Select Type")
.foregroundColor(selectedPropertyType == nil ? .gray : .primary)
Spacer()
Image(systemName: "chevron.down")
.foregroundColor(.gray)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(8)
}
}
FormTextField(
label: "Street Address",
text: $streetAddress,
error: streetAddressError,
placeholder: "123 Main St",
focusedField: $focusedField,
field: .streetAddress
)
FormTextField(
label: "Apartment/Unit (Optional)",
text: $apartmentUnit,
error: "",
placeholder: "Apt 4B",
focusedField: $focusedField,
field: .apartmentUnit
)
FormTextField(
label: "City",
text: $city,
error: cityError,
placeholder: "San Francisco",
focusedField: $focusedField,
field: .city
)
FormTextField(
label: "State/Province",
text: $stateProvince,
error: stateProvinceError,
placeholder: "CA",
focusedField: $focusedField,
field: .stateProvince
)
FormTextField(
label: "Postal Code",
text: $postalCode,
error: postalCodeError,
placeholder: "94102",
focusedField: $focusedField,
field: .postalCode
)
FormTextField(
label: "Country",
text: $country,
error: "",
placeholder: "USA",
focusedField: $focusedField,
field: .country
)
}
// Optional Information Section
VStack(alignment: .leading, spacing: 16) {
Text("Optional Information")
.font(.headline)
.foregroundColor(.blue)
HStack(spacing: 12) {
FormTextField(
label: "Bedrooms",
text: $bedrooms,
error: "",
placeholder: "3",
focusedField: $focusedField,
field: .bedrooms,
keyboardType: .numberPad
)
FormTextField(
label: "Bathrooms",
text: $bathrooms,
error: "",
placeholder: "2.5",
focusedField: $focusedField,
field: .bathrooms,
keyboardType: .decimalPad
)
}
FormTextField(
label: "Square Footage",
text: $squareFootage,
error: "",
placeholder: "1800",
focusedField: $focusedField,
field: .squareFootage,
keyboardType: .numberPad
)
FormTextField(
label: "Lot Size (acres)",
text: $lotSize,
error: "",
placeholder: "0.25",
focusedField: $focusedField,
field: .lotSize,
keyboardType: .decimalPad
)
FormTextField(
label: "Year Built",
text: $yearBuilt,
error: "",
placeholder: "2010",
focusedField: $focusedField,
field: .yearBuilt,
keyboardType: .numberPad
)
VStack(alignment: .leading, spacing: 8) {
Text("Description")
.font(.subheadline)
.foregroundColor(.secondary)
TextEditor(text: $description)
.frame(height: 100)
.padding(8)
.background(Color(.systemBackground))
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.gray.opacity(0.3), lineWidth: 1)
)
}
Toggle("Primary Residence", isOn: $isPrimary)
.font(.subheadline)
}
// Submit Button
Button(action: submitForm) {
HStack {
if viewModel.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
} else {
Text("Add Property")
.fontWeight(.semibold)
}
}
.frame(maxWidth: .infinity)
.padding()
.background(viewModel.isLoading ? Color.gray : Color.blue)
.foregroundColor(.white)
.cornerRadius(12)
}
.disabled(viewModel.isLoading)
}
.padding()
}
}
.navigationTitle("Add Residence")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
isPresented = false
}
}
}
.sheet(isPresented: $showPropertyTypePicker) {
PropertyTypePickerView(
propertyTypes: lookupsManager.residenceTypes,
selectedType: $selectedPropertyType,
isPresented: $showPropertyTypePicker
)
}
.onAppear {
setDefaults()
}
}
}
private func setDefaults() {
// Set default property type if not already set
if selectedPropertyType == nil && !lookupsManager.residenceTypes.isEmpty {
selectedPropertyType = lookupsManager.residenceTypes.first
}
}
private func validateForm() -> Bool {
var isValid = true
if name.isEmpty {
nameError = "Name is required"
isValid = false
} else {
nameError = ""
}
if streetAddress.isEmpty {
streetAddressError = "Street address is required"
isValid = false
} else {
streetAddressError = ""
}
if city.isEmpty {
cityError = "City is required"
isValid = false
} else {
cityError = ""
}
if stateProvince.isEmpty {
stateProvinceError = "State/Province is required"
isValid = false
} else {
stateProvinceError = ""
}
if postalCode.isEmpty {
postalCodeError = "Postal code is required"
isValid = false
} else {
postalCodeError = ""
}
return isValid
}
private func submitForm() {
guard validateForm() else { return }
guard let propertyType = selectedPropertyType else {
// Show error
return
}
let request = ResidenceCreateRequest(
name: name,
propertyType: Int32(propertyType.id),
streetAddress: streetAddress,
apartmentUnit: apartmentUnit.isEmpty ? nil : apartmentUnit,
city: city,
stateProvince: stateProvince,
postalCode: postalCode,
country: country,
bedrooms: Int32(bedrooms) as? KotlinInt,
bathrooms: Float(bathrooms) as? KotlinFloat,
squareFootage: Int32(squareFootage) as? KotlinInt,
lotSize: Float(lotSize) as? KotlinFloat,
yearBuilt: Int32(yearBuilt) as? KotlinInt,
description: description.isEmpty ? nil : description,
purchaseDate: nil,
purchasePrice: nil,
isPrimary: isPrimary
)
viewModel.createResidence(request: request) { success in
if success {
isPresented = false
}
}
}
}
struct FormTextField: View {
let label: String
@Binding var text: String
let error: String
let placeholder: String
var focusedField: FocusState<AddResidenceView.Field?>.Binding
let field: AddResidenceView.Field
var keyboardType: UIKeyboardType = .default
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(label)
.font(.subheadline)
.foregroundColor(.secondary)
TextField(placeholder, text: $text)
.textFieldStyle(.roundedBorder)
.keyboardType(keyboardType)
.focused(focusedField, equals: field)
if !error.isEmpty {
Text(error)
.font(.caption)
.foregroundColor(.red)
}
}
}
}
struct PropertyTypePickerView: View {
let propertyTypes: [ResidenceType]
@Binding var selectedType: ResidenceType?
@Binding var isPresented: Bool
var body: some View {
NavigationView {
List(propertyTypes, id: \.id) { type in
Button(action: {
selectedType = type
isPresented = false
}) {
HStack {
Text(type.name)
Spacer()
if selectedType?.id == type.id {
Image(systemName: "checkmark")
.foregroundColor(.blue)
}
}
}
}
.navigationTitle("Select Property Type")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
isPresented = false
}
}
}
}
}
}
#Preview {
AddResidenceView(isPresented: .constant(true))
}

View File

@@ -8,14 +8,39 @@ struct ComposeView: UIViewControllerRepresentable {
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
struct ContentView: View {
var body: some View {
ComposeView()
CustomView()
.ignoresSafeArea()
}
}
struct CustomView: View {
var body: some View {
Text("Custom view")
.task {
await ViewModel().somethingRandom()
}
}
}
class ViewModel {
func somethingRandom() async {
TokenStorage().initialize(manager: TokenManager.init())
// TokenStorage.initialize(TokenManager.getInstance())
let api = ResidenceApi(client: ApiClient_iosKt.createHttpClient())
api.deleteResidence(token: "token", id: 32) { result, error in
if let error = error {
print("Interop error: \(error)")
return
}
guard let result = result else { return }
}
}
}

View File

@@ -0,0 +1,165 @@
import SwiftUI
import ComposeApp
struct HomeScreenView: View {
@StateObject private var viewModel = ResidenceViewModel()
@StateObject private var loginViewModel = LoginViewModel()
var body: some View {
NavigationView {
ZStack {
Color(.systemGroupedBackground)
.ignoresSafeArea()
if viewModel.isLoading {
ProgressView()
} else {
ScrollView {
VStack(spacing: 20) {
// Overview Card
if let summary = viewModel.residenceSummary {
OverviewCard(summary: summary.summary)
}
// Navigation Cards
VStack(spacing: 16) {
NavigationLink(destination: ResidencesListView()) {
HomeNavigationCard(
icon: "house.fill",
title: "Residences",
subtitle: "Manage your properties"
)
}
NavigationLink(destination: Text("Tasks (Coming Soon)")) {
HomeNavigationCard(
icon: "checkmark.circle.fill",
title: "Tasks",
subtitle: "View and manage tasks"
)
}
}
.padding(.horizontal)
}
.padding(.vertical)
}
}
}
.navigationTitle("MyCrib")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
loginViewModel.logout()
}) {
Image(systemName: "rectangle.portrait.and.arrow.right")
}
}
}
.onAppear {
viewModel.loadResidenceSummary()
}
}
}
}
struct OverviewCard: View {
let summary: OverallSummary
var body: some View {
VStack(spacing: 16) {
HStack {
Image(systemName: "chart.bar.fill")
.font(.title3)
Text("Overview")
.font(.title2)
.fontWeight(.bold)
Spacer()
}
HStack(spacing: 40) {
StatView(
icon: "house.fill",
value: "\(summary.totalResidences)",
label: "Properties"
)
StatView(
icon: "list.bullet",
value: "\(summary.totalTasks)",
label: "Total Tasks"
)
StatView(
icon: "clock.fill",
value: "\(summary.totalPending)",
label: "Pending"
)
}
}
.padding(20)
.background(Color.blue.opacity(0.1))
.cornerRadius(16)
.padding(.horizontal)
}
}
struct StatView: View {
let icon: String
let value: String
let label: String
var body: some View {
VStack(spacing: 8) {
Image(systemName: icon)
.font(.title2)
.foregroundColor(.blue)
Text(value)
.font(.title)
.fontWeight(.bold)
Text(label)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
struct HomeNavigationCard: View {
let icon: String
let title: String
let subtitle: String
var body: some View {
HStack(spacing: 16) {
Image(systemName: icon)
.font(.system(size: 36))
.foregroundColor(.blue)
.frame(width: 60)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.title3)
.fontWeight(.semibold)
.foregroundColor(.primary)
Text(subtitle)
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
}
.padding(20)
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
}
}
#Preview {
HomeScreenView()
}

View File

@@ -0,0 +1,145 @@
import SwiftUI
struct LoginView: View {
@StateObject private var viewModel = LoginViewModel()
@FocusState private var focusedField: Field?
@State private var showingRegister = false
enum Field {
case username, password
}
var body: some View {
NavigationView {
ZStack {
// Background
Color(.systemGroupedBackground)
.ignoresSafeArea()
ScrollView {
VStack(spacing: 24) {
// Logo or App Name
VStack(spacing: 8) {
Image(systemName: "house.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 80, height: 80)
.foregroundColor(.blue)
Text("MyCrib")
.font(.largeTitle)
.fontWeight(.bold)
}
.padding(.top, 60)
.padding(.bottom, 20)
// Login Form
VStack(spacing: 16) {
// Username Field
VStack(alignment: .leading, spacing: 8) {
Text("Username")
.font(.subheadline)
.foregroundColor(.secondary)
TextField("Enter your username", text: $viewModel.username)
.textFieldStyle(.roundedBorder)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.focused($focusedField, equals: .username)
.submitLabel(.next)
.onSubmit {
focusedField = .password
}
}
// Password Field
VStack(alignment: .leading, spacing: 8) {
Text("Password")
.font(.subheadline)
.foregroundColor(.secondary)
SecureField("Enter your password", text: $viewModel.password)
.textFieldStyle(.roundedBorder)
.focused($focusedField, equals: .password)
.submitLabel(.go)
.onSubmit {
viewModel.login()
}
}
// Error Message
if let errorMessage = viewModel.errorMessage {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.red)
Text(errorMessage)
.font(.caption)
.foregroundColor(.red)
Spacer()
Button(action: viewModel.clearError) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.red)
}
}
.padding()
.background(Color.red.opacity(0.1))
.cornerRadius(8)
}
// Login Button
Button(action: viewModel.login) {
HStack {
if viewModel.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
} else {
Text("Login")
.fontWeight(.semibold)
}
}
.frame(maxWidth: .infinity)
.padding()
.background(viewModel.isLoading ? Color.gray : Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
.disabled(viewModel.isLoading)
// Forgot Password / Sign Up Links
HStack(spacing: 4) {
Text("Don't have an account?")
.font(.caption)
.foregroundColor(.secondary)
Button("Sign Up") {
showingRegister = true
}
.font(.caption)
.fontWeight(.semibold)
}
}
.padding(.horizontal, 24)
Spacer()
}
}
}
.navigationTitle("Welcome Back")
.navigationBarTitleDisplayMode(.inline)
.fullScreenCover(isPresented: $viewModel.isAuthenticated) {
MainTabView()
}
.sheet(isPresented: $showingRegister) {
RegisterView()
}
}
}
}
// MARK: - Preview
#Preview {
LoginView()
}

View File

@@ -0,0 +1,140 @@
import Foundation
import ComposeApp
import Combine
@MainActor
class LoginViewModel: ObservableObject {
// MARK: - Published Properties
@Published var username: String = ""
@Published var password: String = ""
@Published var isLoading: Bool = false
@Published var errorMessage: String?
@Published var isAuthenticated: Bool = false
// MARK: - Private Properties
private let authApi: AuthApi
private let tokenStorage: TokenStorage
// MARK: - Initialization
init() {
self.authApi = AuthApi(client: ApiClient_iosKt.createHttpClient())
self.tokenStorage = TokenStorage()
// Initialize TokenStorage with platform-specific manager
self.tokenStorage.initialize(manager: TokenManager.init())
// Check if user is already logged in
checkAuthenticationStatus()
}
// MARK: - Public Methods
func login() {
guard !username.isEmpty else {
errorMessage = "Username is required"
return
}
guard !password.isEmpty else {
errorMessage = "Password is required"
return
}
isLoading = true
errorMessage = nil
let loginRequest = LoginRequest(username: username, password: password)
do {
// Call the KMM AuthApi login method
authApi.login(request: loginRequest) { result, error in
if let successResult = result as? ApiResultSuccess<AuthResponse> {
self.handleSuccess(results: successResult)
return
}
if let error = error {
self.handleError(error: error)
return
}
self.isLoading = false
self.isAuthenticated = false
print("uknown error")
}
}
}
@MainActor
func handleError(error: any Error) {
self.isLoading = false
self.isAuthenticated = false
print(error)
}
@MainActor
func handleSuccess(results: ApiResultSuccess<AuthResponse>) {
if let token = results.data?.token,
let user = results.data?.user {
self.tokenStorage.saveToken(token: token)
// Initialize lookups repository after successful login
LookupsManager.shared.initialize()
// Update authentication state
self.isAuthenticated = true
self.isLoading = false
print("Login successful! Token: token")
print("User: \(user.username)")
}
}
func logout() {
let token = tokenStorage.getToken()
if let token = token {
// Call logout API
authApi.logout(token: token) { _, _ in
// Ignore result, clear token anyway
}
}
// Clear token from storage
tokenStorage.clearToken()
// Clear lookups data on logout
LookupsManager.shared.clear()
// Reset state
isAuthenticated = false
username = ""
password = ""
errorMessage = nil
}
func clearError() {
errorMessage = nil
}
// MARK: - Private Methods
private func checkAuthenticationStatus() {
isAuthenticated = tokenStorage.hasToken()
// If already authenticated, initialize lookups
if isAuthenticated {
LookupsManager.shared.initialize()
}
}
}
// MARK: - Error Types
enum LoginError: LocalizedError {
case unknownError
var errorDescription: String? {
switch self {
case .unknownError:
return "An unknown error occurred"
}
}
}

View File

@@ -0,0 +1,87 @@
import Foundation
import ComposeApp
import Combine
@MainActor
class LookupsManager: ObservableObject {
static let shared = LookupsManager()
// Published properties for SwiftUI
@Published var residenceTypes: [ResidenceType] = []
@Published var taskCategories: [TaskCategory] = []
@Published var taskFrequencies: [TaskFrequency] = []
@Published var taskPriorities: [TaskPriority] = []
@Published var taskStatuses: [TaskStatus] = []
@Published var isLoading: Bool = false
@Published var isInitialized: Bool = false
private let repository = LookupsRepository.shared
private init() {
// Start observing the repository flows
startObserving()
}
private func startObserving() {
// Observe residence types
Task {
for await types in repository.residenceTypes.residenceTypesAsyncSequence {
self.residenceTypes = types
}
}
// Observe task categories
Task {
for await categories in repository.taskCategories.taskCategoriesAsyncSequence {
self.taskCategories = categories
}
}
// Observe task frequencies
Task {
for await frequencies in repository.taskFrequencies.taskFrequenciesAsyncSequence {
self.taskFrequencies = frequencies
}
}
// Observe task priorities
Task {
for await priorities in repository.taskPriorities.taskPrioritiesAsyncSequence {
self.taskPriorities = priorities
}
}
// Observe task statuses
Task {
for await statuses in repository.taskStatuses.taskStatusesAsyncSequence {
self.taskStatuses = statuses
}
}
// Observe loading state
Task {
for await loading in repository.isLoading.boolAsyncSequence {
self.isLoading = loading
}
}
// Observe initialized state
Task {
for await initialized in repository.isInitialized.boolAsyncSequence {
self.isInitialized = initialized
}
}
}
func initialize() {
repository.initialize()
}
func refresh() {
repository.refresh()
}
func clear() {
repository.clear()
}
}

View File

@@ -0,0 +1,100 @@
import SwiftUI
struct MainTabView: View {
@StateObject private var loginViewModel = LoginViewModel()
@State private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
NavigationView {
ResidencesListView()
}
.tabItem {
Label("Residences", systemImage: "house.fill")
}
.tag(0)
Text("Tasks (Coming Soon)")
.tabItem {
Label("Tasks", systemImage: "checkmark.circle.fill")
}
.tag(1)
ProfileView()
.tabItem {
Label("Profile", systemImage: "person.fill")
}
.tag(2)
}
}
}
struct ProfileView: View {
@StateObject private var loginViewModel = LoginViewModel()
@Environment(\.dismiss) var dismiss
var body: some View {
NavigationView {
List {
Section {
HStack {
Image(systemName: "person.circle.fill")
.resizable()
.frame(width: 60, height: 60)
.foregroundColor(.blue)
VStack(alignment: .leading, spacing: 4) {
Text("User Profile")
.font(.headline)
Text("Manage your account")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding(.vertical, 8)
}
Section("Settings") {
NavigationLink(destination: Text("Account Settings")) {
Label("Account Settings", systemImage: "gear")
}
NavigationLink(destination: Text("Notifications")) {
Label("Notifications", systemImage: "bell")
}
NavigationLink(destination: Text("Privacy")) {
Label("Privacy", systemImage: "lock.shield")
}
}
Section {
Button(action: {
loginViewModel.logout()
}) {
Label("Log Out", systemImage: "rectangle.portrait.and.arrow.right")
.foregroundColor(.red)
}
}
Section {
VStack(alignment: .leading, spacing: 4) {
Text("MyCrib")
.font(.caption)
.fontWeight(.semibold)
Text("Version 1.0.0")
.font(.caption2)
.foregroundColor(.secondary)
}
}
}
.navigationTitle("Profile")
}
}
}
#Preview {
MainTabView()
}

View File

@@ -0,0 +1,171 @@
import SwiftUI
struct RegisterView: View {
@StateObject private var viewModel = RegisterViewModel()
@Environment(\.dismiss) var dismiss
@FocusState private var focusedField: Field?
enum Field {
case username, email, password, confirmPassword
}
var body: some View {
NavigationView {
ZStack {
Color(.systemGroupedBackground)
.ignoresSafeArea()
ScrollView {
VStack(spacing: 24) {
// Icon and Title
VStack(spacing: 12) {
Image(systemName: "person.badge.plus")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 64, height: 64)
.foregroundColor(.blue)
Text("Join MyCrib")
.font(.largeTitle)
.fontWeight(.bold)
Text("Start managing your properties today")
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.top, 40)
.padding(.bottom, 20)
// Registration Form
VStack(spacing: 16) {
// Username Field
VStack(alignment: .leading, spacing: 8) {
Text("Username")
.font(.subheadline)
.foregroundColor(.secondary)
TextField("Enter your username", text: $viewModel.username)
.textFieldStyle(.roundedBorder)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.focused($focusedField, equals: .username)
.submitLabel(.next)
.onSubmit {
focusedField = .email
}
}
// Email Field
VStack(alignment: .leading, spacing: 8) {
Text("Email")
.font(.subheadline)
.foregroundColor(.secondary)
TextField("Enter your email", text: $viewModel.email)
.textFieldStyle(.roundedBorder)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.emailAddress)
.focused($focusedField, equals: .email)
.submitLabel(.next)
.onSubmit {
focusedField = .password
}
}
// Password Field
VStack(alignment: .leading, spacing: 8) {
Text("Password")
.font(.subheadline)
.foregroundColor(.secondary)
SecureField("Enter your password", text: $viewModel.password)
.textFieldStyle(.roundedBorder)
.focused($focusedField, equals: .password)
.submitLabel(.next)
.onSubmit {
focusedField = .confirmPassword
}
}
// Confirm Password Field
VStack(alignment: .leading, spacing: 8) {
Text("Confirm Password")
.font(.subheadline)
.foregroundColor(.secondary)
SecureField("Confirm your password", text: $viewModel.confirmPassword)
.textFieldStyle(.roundedBorder)
.focused($focusedField, equals: .confirmPassword)
.submitLabel(.go)
.onSubmit {
viewModel.register()
}
}
// Error Message
if let errorMessage = viewModel.errorMessage {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.red)
Text(errorMessage)
.font(.caption)
.foregroundColor(.red)
Spacer()
Button(action: viewModel.clearError) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.red)
}
}
.padding()
.background(Color.red.opacity(0.1))
.cornerRadius(8)
}
// Register Button
Button(action: viewModel.register) {
HStack {
if viewModel.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
} else {
Text("Create Account")
.fontWeight(.semibold)
}
}
.frame(maxWidth: .infinity)
.padding()
.background(viewModel.isLoading ? Color.gray : Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
.disabled(viewModel.isLoading)
}
.padding(.horizontal, 24)
Spacer()
}
}
}
.navigationTitle("Create Account")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
}
.fullScreenCover(isPresented: $viewModel.isRegistered) {
MainTabView()
}
}
}
}
#Preview {
RegisterView()
}

View File

@@ -0,0 +1,105 @@
import Foundation
import ComposeApp
import Combine
@MainActor
class RegisterViewModel: ObservableObject {
// MARK: - Published Properties
@Published var username: String = ""
@Published var email: String = ""
@Published var password: String = ""
@Published var confirmPassword: String = ""
@Published var isLoading: Bool = false
@Published var errorMessage: String?
@Published var isRegistered: Bool = false
// MARK: - Private Properties
private let authApi: AuthApi
private let tokenStorage: TokenStorage
// MARK: - Initialization
init() {
self.authApi = AuthApi(client: ApiClient_iosKt.createHttpClient())
self.tokenStorage = TokenStorage()
self.tokenStorage.initialize(manager: TokenManager.init())
}
// MARK: - Public Methods
func register() {
// Validation
guard !username.isEmpty else {
errorMessage = "Username is required"
return
}
guard !email.isEmpty else {
errorMessage = "Email is required"
return
}
guard !password.isEmpty else {
errorMessage = "Password is required"
return
}
guard password == confirmPassword else {
errorMessage = "Passwords do not match"
return
}
isLoading = true
errorMessage = nil
let registerRequest = RegisterRequest(
username: username,
email: email,
password: password,
firstName: nil,
lastName: nil
)
authApi.register(request: registerRequest) { result, error in
if let successResult = result as? ApiResultSuccess<AuthResponse> {
self.handleSuccess(results: successResult)
return
}
if let error = error {
self.handleError(error: error)
return
}
self.isLoading = false
print("Unknown error during registration")
}
}
@MainActor
func handleError(error: any Error) {
self.isLoading = false
self.errorMessage = error.localizedDescription
print(error)
}
@MainActor
func handleSuccess(results: ApiResultSuccess<AuthResponse>) {
if let token = results.data?.token,
let user = results.data?.user {
self.tokenStorage.saveToken(token: token)
// Initialize lookups repository after successful registration
LookupsManager.shared.initialize()
// Update registration state
self.isRegistered = true
self.isLoading = false
print("Registration successful! Token saved")
print("User: \(user.username)")
}
}
func clearError() {
errorMessage = nil
}
}

View File

@@ -0,0 +1,400 @@
import SwiftUI
import ComposeApp
struct ResidenceDetailView: View {
let residenceId: Int32
@StateObject private var viewModel = ResidenceViewModel()
@State private var residenceWithTasks: ResidenceWithTasks?
@State private var isLoadingTasks = false
@State private var tasksError: String?
@State private var showAddTask = false
var body: some View {
ZStack {
Color(.systemGroupedBackground)
.ignoresSafeArea()
if viewModel.isLoading {
ProgressView()
} else if let error = viewModel.errorMessage {
ErrorView(message: error) {
loadResidenceData()
}
} else if let residence = viewModel.selectedResidence {
ScrollView {
VStack(spacing: 16) {
// Property Header Card
PropertyHeaderCard(residence: residence)
.padding(.horizontal)
.padding(.top)
// Tasks Section
if let residenceWithTasks = residenceWithTasks {
TasksSection(residenceWithTasks: residenceWithTasks)
.padding(.horizontal)
} else if isLoadingTasks {
ProgressView("Loading tasks...")
} else if let tasksError = tasksError {
Text("Error loading tasks: \(tasksError)")
.foregroundColor(.red)
.padding()
}
}
.padding(.bottom)
}
}
}
.navigationTitle("Property Details")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showAddTask = true
}) {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showAddTask) {
AddTaskView(residenceId: residenceId, isPresented: $showAddTask)
}
.onChange(of: showAddTask) { isShowing in
if !isShowing {
// Refresh tasks when sheet is dismissed
loadResidenceWithTasks()
}
}
.onAppear {
loadResidenceData()
}
}
private func loadResidenceData() {
viewModel.getResidence(id: residenceId)
loadResidenceWithTasks()
}
private func loadResidenceWithTasks() {
guard let token = TokenStorage().getToken() else { return }
isLoadingTasks = true
tasksError = nil
let residenceApi = ResidenceApi(client: ApiClient_iosKt.createHttpClient())
residenceApi.getMyResidences(token: token) { result, error in
if let successResult = result as? ApiResultSuccess<MyResidencesResponse> {
if let residence = successResult.data?.residences.first(where: { $0.id == residenceId }) {
self.residenceWithTasks = residence
}
self.isLoadingTasks = false
} else if let errorResult = result as? ApiResultError {
self.tasksError = errorResult.message
self.isLoadingTasks = false
} else if let error = error {
self.tasksError = error.localizedDescription
self.isLoadingTasks = false
}
}
}
}
struct PropertyHeaderCard: View {
let residence: Residence
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
Image(systemName: "house.fill")
.font(.title2)
.foregroundColor(.blue)
VStack(alignment: .leading, spacing: 4) {
Text(residence.name)
.font(.title2)
.fontWeight(.bold)
Text(residence.propertyType)
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
}
Divider()
// Address
VStack(alignment: .leading, spacing: 4) {
Label(residence.streetAddress, systemImage: "mappin.circle.fill")
.font(.subheadline)
Text("\(residence.city), \(residence.stateProvince) \(residence.postalCode)")
.font(.subheadline)
.foregroundColor(.secondary)
if !residence.country.isEmpty {
Text(residence.country)
.font(.caption)
.foregroundColor(.secondary)
}
}
// Property Details
if let bedrooms = residence.bedrooms,
let bathrooms = residence.bathrooms {
Divider()
HStack(spacing: 24) {
PropertyDetailItem(icon: "bed.double.fill", value: "\(bedrooms)", label: "Beds")
PropertyDetailItem(icon: "shower.fill", value: String(format: "%.1f", bathrooms), label: "Baths")
if let sqft = residence.squareFootage {
PropertyDetailItem(icon: "square.fill", value: "\(sqft)", label: "Sq Ft")
}
}
}
// if !residence.description.isEmpty {
// Divider()
//
// Text(residence.)
// .font(.body)
// .foregroundColor(.secondary)
// }
}
.padding(20)
.background(Color.blue.opacity(0.1))
.cornerRadius(16)
}
}
struct PropertyDetailItem: View {
let icon: String
let value: String
let label: String
var body: some View {
VStack(spacing: 4) {
Image(systemName: icon)
.font(.caption)
.foregroundColor(.blue)
Text(value)
.font(.subheadline)
.fontWeight(.semibold)
Text(label)
.font(.caption2)
.foregroundColor(.secondary)
}
}
}
struct TasksSection: View {
let residenceWithTasks: ResidenceWithTasks
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Tasks")
.font(.title2)
.fontWeight(.bold)
Spacer()
// Task Summary Pills
HStack(spacing: 8) {
TaskPill(count: residenceWithTasks.taskSummary.total, label: "Total", color: .blue)
TaskPill(count: residenceWithTasks.taskSummary.pending, label: "Pending", color: .orange)
TaskPill(count: residenceWithTasks.taskSummary.completed, label: "Done", color: .green)
}
}
if residenceWithTasks.tasks.isEmpty {
EmptyTasksView()
} else {
ForEach(residenceWithTasks.tasks, id: \.id) { task in
TaskCard(task: task)
}
}
}
}
}
struct TaskPill: View {
let count: Int32
let label: String
let color: Color
var body: some View {
HStack(spacing: 4) {
Text("\(count)")
.font(.caption)
.fontWeight(.bold)
Text(label)
.font(.caption2)
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(color.opacity(0.2))
.foregroundColor(color)
.cornerRadius(8)
}
}
struct TaskCard: View {
let task: TaskDetail
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(task.title)
.font(.headline)
.foregroundColor(.primary)
if let status = task.status {
StatusBadge(status: status.name)
}
}
Spacer()
PriorityBadge(priority: task.priority.name)
}
if let description = task.description_, !description.isEmpty {
Text(description)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(2)
}
HStack {
Label(task.frequency.displayName, systemImage: "repeat")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
Label(formatDate(task.dueDate), systemImage: "calendar")
.font(.caption)
.foregroundColor(.secondary)
}
// Completion count
if task.completions.count > 0 {
Divider()
HStack {
Image(systemName: "checkmark.circle")
.foregroundColor(.green)
Text("Completed \(task.completions.count) time\(task.completions.count == 1 ? "" : "s")")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
.padding(16)
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: Color.black.opacity(0.05), radius: 3, x: 0, y: 2)
}
private func formatDate(_ dateString: String) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
if let date = formatter.date(from: dateString) {
formatter.dateStyle = .medium
return formatter.string(from: date)
}
return dateString
}
}
struct StatusBadge: View {
let status: String
var body: some View {
Text(formatStatus(status))
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(statusColor.opacity(0.2))
.foregroundColor(statusColor)
.cornerRadius(6)
}
private func formatStatus(_ status: String) -> String {
switch status {
case "in_progress": return "In Progress"
default: return status.capitalized
}
}
private var statusColor: Color {
switch status {
case "completed": return .green
case "in_progress": return .blue
case "pending": return .orange
case "cancelled": return .red
default: return .gray
}
}
}
struct PriorityBadge: View {
let priority: String
var body: some View {
HStack(spacing: 4) {
Image(systemName: "exclamationmark.circle.fill")
.font(.caption2)
Text(priority.capitalized)
.font(.caption)
.fontWeight(.medium)
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(priorityColor.opacity(0.2))
.foregroundColor(priorityColor)
.cornerRadius(6)
}
private var priorityColor: Color {
switch priority.lowercased() {
case "high": return .red
case "medium": return .orange
case "low": return .green
default: return .gray
}
}
}
struct EmptyTasksView: View {
var body: some View {
VStack(spacing: 12) {
Image(systemName: "checkmark.circle")
.font(.system(size: 48))
.foregroundColor(.gray.opacity(0.5))
Text("No tasks yet")
.font(.subheadline)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding(32)
.background(Color(.systemBackground))
.cornerRadius(12)
}
}
#Preview {
NavigationView {
ResidenceDetailView(residenceId: 1)
}
}

View File

@@ -0,0 +1,124 @@
import Foundation
import ComposeApp
import Combine
@MainActor
class ResidenceViewModel: ObservableObject {
// MARK: - Published Properties
@Published var residenceSummary: ResidenceSummaryResponse?
@Published var myResidences: MyResidencesResponse?
@Published var selectedResidence: Residence?
@Published var isLoading: Bool = false
@Published var errorMessage: String?
// MARK: - Private Properties
private let residenceApi: ResidenceApi
private let tokenStorage: TokenStorage
// MARK: - Initialization
init() {
self.residenceApi = ResidenceApi(client: ApiClient_iosKt.createHttpClient())
self.tokenStorage = TokenStorage()
self.tokenStorage.initialize(manager: TokenManager.init())
}
// MARK: - Public Methods
func loadResidenceSummary() {
guard let token = tokenStorage.getToken() else {
errorMessage = "Not authenticated"
return
}
isLoading = true
errorMessage = nil
residenceApi.getResidenceSummary(token: token) { result, error in
if let successResult = result as? ApiResultSuccess<ResidenceSummaryResponse> {
self.residenceSummary = successResult.data
self.isLoading = false
} else if let errorResult = result as? ApiResultError {
self.errorMessage = errorResult.message
self.isLoading = false
} else if let error = error {
self.errorMessage = error.localizedDescription
self.isLoading = false
}
}
}
func loadMyResidences() {
guard let token = tokenStorage.getToken() else {
errorMessage = "Not authenticated"
return
}
isLoading = true
errorMessage = nil
residenceApi.getMyResidences(token: token) { result, error in
if let successResult = result as? ApiResultSuccess<MyResidencesResponse> {
self.myResidences = successResult.data
self.isLoading = false
} else if let errorResult = result as? ApiResultError {
self.errorMessage = errorResult.message
self.isLoading = false
} else if let error = error {
self.errorMessage = error.localizedDescription
self.isLoading = false
}
}
}
func getResidence(id: Int32) {
guard let token = tokenStorage.getToken() else {
errorMessage = "Not authenticated"
return
}
isLoading = true
errorMessage = nil
residenceApi.getResidence(token: token, id: id) { result, error in
if let successResult = result as? ApiResultSuccess<Residence> {
self.selectedResidence = successResult.data
self.isLoading = false
} else if let errorResult = result as? ApiResultError {
self.errorMessage = errorResult.message
self.isLoading = false
} else if let error = error {
self.errorMessage = error.localizedDescription
self.isLoading = false
}
}
}
func createResidence(request: ResidenceCreateRequest, completion: @escaping (Bool) -> Void) {
guard let token = tokenStorage.getToken() else {
errorMessage = "Not authenticated"
completion(false)
return
}
isLoading = true
errorMessage = nil
residenceApi.createResidence(token: token, request: request) { result, error in
if result is ApiResultSuccess<Residence> {
self.isLoading = false
completion(true)
} else if let errorResult = result as? ApiResultError {
self.errorMessage = errorResult.message
self.isLoading = false
completion(false)
} else if let error = error {
self.errorMessage = error.localizedDescription
self.isLoading = false
completion(false)
}
}
}
func clearError() {
errorMessage = nil
}
}

View File

@@ -0,0 +1,275 @@
import SwiftUI
import ComposeApp
struct ResidencesListView: View {
@StateObject private var viewModel = ResidenceViewModel()
@State private var showingAddResidence = false
var body: some View {
ZStack {
Color(.systemGroupedBackground)
.ignoresSafeArea()
if viewModel.isLoading {
ProgressView()
} else if let error = viewModel.errorMessage {
ErrorView(message: error) {
viewModel.loadMyResidences()
}
} else if let response = viewModel.myResidences {
if response.residences.isEmpty {
EmptyResidencesView()
} else {
ScrollView {
VStack(spacing: 16) {
// Summary Card
SummaryCard(summary: response.summary)
.padding(.horizontal)
.padding(.top)
// Properties Header
HStack {
Text("Your Properties")
.font(.title2)
.fontWeight(.bold)
Spacer()
}
.padding(.horizontal)
.padding(.top, 8)
// Residences List
ForEach(response.residences, id: \.id) { residence in
NavigationLink(destination: ResidenceDetailView(residenceId: residence.id)) {
ResidenceCard(residence: residence)
}
.buttonStyle(PlainButtonStyle())
}
.padding(.horizontal)
}
.padding(.bottom)
}
}
}
}
.navigationTitle("My Properties")
.navigationBarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showingAddResidence = true
}) {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showingAddResidence) {
AddResidenceView(isPresented: $showingAddResidence)
}
.onAppear {
viewModel.loadMyResidences()
}
}
}
struct SummaryCard: View {
let summary: MyResidencesSummary
var body: some View {
VStack(spacing: 16) {
HStack {
Image(systemName: "chart.bar.doc.horizontal")
.font(.title3)
Text("Overview")
.font(.title3)
.fontWeight(.bold)
Spacer()
}
HStack(spacing: 20) {
SummaryStatView(
icon: "house.fill",
value: "\(summary.totalResidences)",
label: "Properties"
)
SummaryStatView(
icon: "list.bullet",
value: "\(summary.totalTasks)",
label: "Total Tasks"
)
}
Divider()
HStack(spacing: 20) {
SummaryStatView(
icon: "calendar",
value: "\(summary.tasksDueNextWeek)",
label: "Due This Week"
)
SummaryStatView(
icon: "calendar.badge.clock",
value: "\(summary.tasksDueNextMonth)",
label: "Due This Month"
)
}
}
.padding(20)
.background(Color.blue.opacity(0.1))
.cornerRadius(16)
}
}
struct SummaryStatView: View {
let icon: String
let value: String
let label: String
var body: some View {
VStack(spacing: 8) {
Image(systemName: icon)
.font(.title3)
.foregroundColor(.blue)
Text(value)
.font(.title2)
.fontWeight(.bold)
Text(label)
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
}
}
struct ResidenceCard: View {
let residence: ResidenceWithTasks
var body: some View {
VStack(alignment: .leading, spacing: 12) {
// Property Name and Address
VStack(alignment: .leading, spacing: 4) {
Text(residence.name)
.font(.title3)
.fontWeight(.bold)
.foregroundColor(.primary)
Text(residence.streetAddress)
.font(.subheadline)
.foregroundColor(.secondary)
Text("\(residence.city), \(residence.stateProvince)")
.font(.subheadline)
.foregroundColor(.secondary)
}
Divider()
// Task Stats
HStack(spacing: 24) {
TaskStatChip(
icon: "list.bullet",
value: "\(residence.taskSummary.total)",
label: "Tasks",
color: .blue
)
TaskStatChip(
icon: "checkmark.circle.fill",
value: "\(residence.taskSummary.completed)",
label: "Done",
color: .green
)
TaskStatChip(
icon: "clock.fill",
value: "\(residence.taskSummary.pending)",
label: "Pending",
color: .orange
)
}
}
.padding(20)
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
}
}
struct TaskStatChip: View {
let icon: String
let value: String
let label: String
let color: Color
var body: some View {
HStack(spacing: 4) {
Image(systemName: icon)
.font(.caption)
.foregroundColor(color)
Text(value)
.font(.subheadline)
.fontWeight(.bold)
.foregroundColor(color)
Text(label)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
struct EmptyResidencesView: View {
var body: some View {
VStack(spacing: 16) {
Image(systemName: "house")
.font(.system(size: 80))
.foregroundColor(.blue.opacity(0.6))
Text("No properties yet")
.font(.title2)
.fontWeight(.semibold)
Text("Add your first property to get started!")
.font(.body)
.foregroundColor(.secondary)
}
}
}
struct ErrorView: View {
let message: String
let retryAction: () -> Void
var body: some View {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 64))
.foregroundColor(.red)
Text("Error: \(message)")
.foregroundColor(.red)
.multilineTextAlignment(.center)
Button(action: retryAction) {
Text("Retry")
.padding(.horizontal, 32)
.padding(.vertical, 12)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
.padding()
}
}
#Preview {
NavigationView {
ResidencesListView()
}
}

View File

@@ -0,0 +1,71 @@
import Foundation
import ComposeApp
import Combine
// MARK: - StateFlow AsyncSequence Extension
extension Kotlinx_coroutines_coreStateFlow {
func asAsyncSequence<T>() -> AsyncStream<T> {
return AsyncStream<T> { continuation in
// Create a flow collector that bridges to Swift continuation
let collector = StateFlowCollector<T> { value in
if let typedValue = value as? T {
continuation.yield(typedValue)
}
}
// Start collecting in a Task to handle the suspend function
let task = Task {
do {
try await self.collect(collector: collector)
} catch {
// Handle cancellation or other errors
continuation.finish()
}
}
continuation.onTermination = { @Sendable _ in
task.cancel()
}
}
}
}
// Helper class to bridge Kotlin FlowCollector to Swift closure
private class StateFlowCollector<T>: Kotlinx_coroutines_coreFlowCollector {
private let onValue: (Any?) -> Void
init(onValue: @escaping (Any?) -> Void) {
self.onValue = onValue
}
func emit(value: Any?) async throws {
onValue(value)
}
}
// MARK: - Convenience AsyncSequence Extensions for specific types
extension Kotlinx_coroutines_coreStateFlow {
var residenceTypesAsyncSequence: AsyncStream<[ResidenceType]> {
return asAsyncSequence()
}
var taskCategoriesAsyncSequence: AsyncStream<[TaskCategory]> {
return asAsyncSequence()
}
var taskFrequenciesAsyncSequence: AsyncStream<[TaskFrequency]> {
return asAsyncSequence()
}
var taskPrioritiesAsyncSequence: AsyncStream<[TaskPriority]> {
return asAsyncSequence()
}
var taskStatusesAsyncSequence: AsyncStream<[TaskStatus]> {
return asAsyncSequence()
}
var boolAsyncSequence: AsyncStream<Bool> {
return asAsyncSequence()
}
}

View File

@@ -0,0 +1,432 @@
import SwiftUI
import ComposeApp
struct AddTaskView: View {
let residenceId: Int32
@Binding var isPresented: Bool
@StateObject private var viewModel = TaskViewModel()
@StateObject private var lookupsManager = LookupsManager.shared
@FocusState private var focusedField: Field?
// Form fields
@State private var title: String = ""
@State private var description: String = ""
@State private var selectedCategory: TaskCategory?
@State private var selectedFrequency: TaskFrequency?
@State private var selectedPriority: TaskPriority?
@State private var selectedStatus: TaskStatus?
@State private var dueDate: Date = Date()
@State private var intervalDays: String = ""
@State private var estimatedCost: String = ""
// Validation errors
@State private var titleError: String = ""
// Picker states
@State private var showCategoryPicker = false
@State private var showFrequencyPicker = false
@State private var showPriorityPicker = false
@State private var showStatusPicker = false
enum Field {
case title, description, intervalDays, estimatedCost
}
var body: some View {
NavigationView {
ZStack {
Color(.systemGroupedBackground)
.ignoresSafeArea()
if lookupsManager.isLoading {
VStack(spacing: 16) {
ProgressView()
Text("Loading lookup data...")
.foregroundColor(.secondary)
}
} else {
ScrollView {
VStack(spacing: 24) {
// Task Information Section
VStack(alignment: .leading, spacing: 16) {
Text("Task Information")
.font(.headline)
.foregroundColor(.blue)
// Title Field
VStack(alignment: .leading, spacing: 8) {
Text("Task Title *")
.font(.subheadline)
.foregroundColor(.secondary)
TextField("e.g., Clean gutters", text: $title)
.textFieldStyle(.roundedBorder)
.focused($focusedField, equals: .title)
if !titleError.isEmpty {
Text(titleError)
.font(.caption)
.foregroundColor(.red)
}
}
// Description Field
VStack(alignment: .leading, spacing: 8) {
Text("Description (Optional)")
.font(.subheadline)
.foregroundColor(.secondary)
TextEditor(text: $description)
.frame(height: 100)
.padding(8)
.background(Color(.systemBackground))
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.gray.opacity(0.3), lineWidth: 1)
)
}
// Category Picker
PickerField(
label: "Category *",
selectedItem: selectedCategory?.name ?? "Select Category",
showPicker: $showCategoryPicker
)
// Frequency Picker
PickerField(
label: "Frequency *",
selectedItem: selectedFrequency?.displayName ?? "Select Frequency",
showPicker: $showFrequencyPicker
)
// Interval Days (if applicable)
if selectedFrequency?.name != "once" {
VStack(alignment: .leading, spacing: 8) {
Text("Custom Interval (days, optional)")
.font(.subheadline)
.foregroundColor(.secondary)
TextField("Leave empty for default", text: $intervalDays)
.textFieldStyle(.roundedBorder)
.keyboardType(.numberPad)
.focused($focusedField, equals: .intervalDays)
}
}
// Priority Picker
PickerField(
label: "Priority *",
selectedItem: selectedPriority?.displayName ?? "Select Priority",
showPicker: $showPriorityPicker
)
// Status Picker
PickerField(
label: "Status *",
selectedItem: selectedStatus?.displayName ?? "Select Status",
showPicker: $showStatusPicker
)
// Due Date Picker
VStack(alignment: .leading, spacing: 8) {
Text("Due Date *")
.font(.subheadline)
.foregroundColor(.secondary)
DatePicker("", selection: $dueDate, displayedComponents: .date)
.datePickerStyle(.compact)
.labelsHidden()
}
// Estimated Cost Field
VStack(alignment: .leading, spacing: 8) {
Text("Estimated Cost (Optional)")
.font(.subheadline)
.foregroundColor(.secondary)
TextField("e.g., 150.00", text: $estimatedCost)
.textFieldStyle(.roundedBorder)
.keyboardType(.decimalPad)
.focused($focusedField, equals: .estimatedCost)
}
}
// Error Message
if let errorMessage = viewModel.errorMessage {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.red)
Text(errorMessage)
.font(.caption)
.foregroundColor(.red)
Spacer()
Button(action: viewModel.clearError) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.red)
}
}
.padding()
.background(Color.red.opacity(0.1))
.cornerRadius(8)
}
// Submit Button
Button(action: submitForm) {
HStack {
if viewModel.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
} else {
Text("Create Task")
.fontWeight(.semibold)
}
}
.frame(maxWidth: .infinity)
.padding()
.background(viewModel.isLoading ? Color.gray : Color.blue)
.foregroundColor(.white)
.cornerRadius(12)
}
.disabled(viewModel.isLoading)
}
.padding()
}
}
}
.navigationTitle("Add Task")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
isPresented = false
}
}
}
.sheet(isPresented: $showCategoryPicker) {
LookupPickerView(
title: "Select Category",
items: lookupsManager.taskCategories.map { LookupItem(id: $0.id, name: $0.name, displayName: $0.name) },
selectedId: selectedCategory?.id,
isPresented: $showCategoryPicker,
onSelect: { id in
selectedCategory = lookupsManager.taskCategories.first { $0.id == id }
}
)
}
.sheet(isPresented: $showFrequencyPicker) {
LookupPickerView(
title: "Select Frequency",
items: lookupsManager.taskFrequencies.map { LookupItem(id: $0.id, name: $0.name, displayName: $0.displayName) },
selectedId: selectedFrequency?.id,
isPresented: $showFrequencyPicker,
onSelect: { id in
selectedFrequency = lookupsManager.taskFrequencies.first { $0.id == id }
}
)
}
.sheet(isPresented: $showPriorityPicker) {
LookupPickerView(
title: "Select Priority",
items: lookupsManager.taskPriorities.map { LookupItem(id: $0.id, name: $0.name, displayName: $0.displayName) },
selectedId: selectedPriority?.id,
isPresented: $showPriorityPicker,
onSelect: { id in
selectedPriority = lookupsManager.taskPriorities.first { $0.id == id }
}
)
}
.sheet(isPresented: $showStatusPicker) {
LookupPickerView(
title: "Select Status",
items: lookupsManager.taskStatuses.map { LookupItem(id: $0.id, name: $0.name, displayName: $0.displayName) },
selectedId: selectedStatus?.id,
isPresented: $showStatusPicker,
onSelect: { id in
selectedStatus = lookupsManager.taskStatuses.first { $0.id == id }
}
)
}
.onAppear {
setDefaults()
}
.onChange(of: viewModel.taskCreated) { created in
if created {
isPresented = false
}
}
}
}
private func setDefaults() {
// Set default values if not already set
if selectedCategory == nil && !lookupsManager.taskCategories.isEmpty {
selectedCategory = lookupsManager.taskCategories.first
}
if selectedFrequency == nil && !lookupsManager.taskFrequencies.isEmpty {
// Default to "once"
selectedFrequency = lookupsManager.taskFrequencies.first { $0.name == "once" } ?? lookupsManager.taskFrequencies.first
}
if selectedPriority == nil && !lookupsManager.taskPriorities.isEmpty {
// Default to "medium"
selectedPriority = lookupsManager.taskPriorities.first { $0.name == "medium" } ?? lookupsManager.taskPriorities.first
}
if selectedStatus == nil && !lookupsManager.taskStatuses.isEmpty {
// Default to "pending"
selectedStatus = lookupsManager.taskStatuses.first { $0.name == "pending" } ?? lookupsManager.taskStatuses.first
}
}
private func validateForm() -> Bool {
var isValid = true
if title.isEmpty {
titleError = "Title is required"
isValid = false
} else {
titleError = ""
}
if selectedCategory == nil {
viewModel.errorMessage = "Please select a category"
isValid = false
}
if selectedFrequency == nil {
viewModel.errorMessage = "Please select a frequency"
isValid = false
}
if selectedPriority == nil {
viewModel.errorMessage = "Please select a priority"
isValid = false
}
if selectedStatus == nil {
viewModel.errorMessage = "Please select a status"
isValid = false
}
return isValid
}
private func submitForm() {
guard validateForm() else { return }
guard let category = selectedCategory,
let frequency = selectedFrequency,
let priority = selectedPriority,
let status = selectedStatus else {
return
}
// Format date as yyyy-MM-dd
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let dueDateString = dateFormatter.string(from: dueDate)
let request = TaskCreateRequest(
residence: residenceId,
title: title,
description: description.isEmpty ? nil : description,
category: Int32(category.id),
frequency: Int32(frequency.id),
intervalDays: intervalDays.isEmpty ? nil : Int32(intervalDays) as? KotlinInt,
priority: Int32(priority.id),
status: Int32(status.id),
dueDate: dueDateString,
estimatedCost: estimatedCost.isEmpty ? nil : estimatedCost
)
viewModel.createTask(request: request) { success in
if success {
// View will dismiss automatically via onChange
}
}
}
}
// MARK: - Supporting Views
struct PickerField: View {
let label: String
let selectedItem: String
@Binding var showPicker: Bool
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(label)
.font(.subheadline)
.foregroundColor(.secondary)
Button(action: {
showPicker = true
}) {
HStack {
Text(selectedItem)
.foregroundColor(selectedItem.contains("Select") ? .gray : .primary)
Spacer()
Image(systemName: "chevron.down")
.foregroundColor(.gray)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(8)
}
}
}
}
struct LookupItem: Identifiable {
let id: Int32
let name: String
let displayName: String
}
struct LookupPickerView: View {
let title: String
let items: [LookupItem]
let selectedId: Int32?
@Binding var isPresented: Bool
let onSelect: (Int32) -> Void
var body: some View {
NavigationView {
List(items) { item in
Button(action: {
onSelect(item.id)
isPresented = false
}) {
HStack {
Text(item.displayName)
.foregroundColor(.primary)
Spacer()
if selectedId == item.id {
Image(systemName: "checkmark")
.foregroundColor(.blue)
}
}
}
}
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
isPresented = false
}
}
}
}
}
}
#Preview {
AddTaskView(residenceId: 1, isPresented: .constant(true))
}

View File

@@ -0,0 +1,60 @@
import Foundation
import ComposeApp
import Combine
@MainActor
class TaskViewModel: ObservableObject {
// MARK: - Published Properties
@Published var isLoading: Bool = false
@Published var errorMessage: String?
@Published var taskCreated: Bool = false
// MARK: - Private Properties
private let taskApi: TaskApi
private let tokenStorage: TokenStorage
// MARK: - Initialization
init() {
self.taskApi = TaskApi(client: ApiClient_iosKt.createHttpClient())
self.tokenStorage = TokenStorage()
self.tokenStorage.initialize(manager: TokenManager.init())
}
// MARK: - Public Methods
func createTask(request: TaskCreateRequest, completion: @escaping (Bool) -> Void) {
guard let token = tokenStorage.getToken() else {
errorMessage = "Not authenticated"
completion(false)
return
}
isLoading = true
errorMessage = nil
taskCreated = false
taskApi.createTask(token: token, request: request) { result, error in
if result is ApiResultSuccess<TaskDetail> {
self.isLoading = false
self.taskCreated = true
completion(true)
} else if let errorResult = result as? ApiResultError {
self.errorMessage = errorResult.message
self.isLoading = false
completion(false)
} else if let error = error {
self.errorMessage = error.localizedDescription
self.isLoading = false
completion(false)
}
}
}
func clearError() {
errorMessage = nil
}
func resetState() {
taskCreated = false
errorMessage = nil
}
}

View File

@@ -4,7 +4,7 @@ import SwiftUI
struct iOSApp: App {
var body: some Scene {
WindowGroup {
ContentView()
LoginView()
}
}
}