This commit is contained in:
Trey t
2025-11-06 09:25:21 -06:00
parent e272e45689
commit e24d1d8559
29 changed files with 1806 additions and 103 deletions

View File

@@ -31,11 +31,11 @@ struct HomeScreenView: View {
)
}
NavigationLink(destination: Text("Tasks (Coming Soon)")) {
NavigationLink(destination: AllTasksView()) {
HomeNavigationCard(
icon: "checkmark.circle.fill",
title: "Tasks",
subtitle: "View and manage tasks"
subtitle: "View and manage all tasks"
)
}
}

View File

@@ -6,6 +6,7 @@ struct LoginView: View {
@State private var showingRegister = false
@State private var showMainTab = false
@State private var showVerification = false
@State private var isPasswordVisible = false
enum Field {
case username, password
@@ -42,13 +43,40 @@ struct LoginView: View {
.onSubmit {
focusedField = .password
}
SecureField("Password", text: $viewModel.password)
.focused($focusedField, equals: .password)
.submitLabel(.go)
.onSubmit {
viewModel.login()
.onChange(of: viewModel.username) { _, _ in
viewModel.clearError()
}
HStack {
if isPasswordVisible {
TextField("Password", text: $viewModel.password)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.focused($focusedField, equals: .password)
.submitLabel(.go)
.onSubmit {
viewModel.login()
}
} else {
SecureField("Password", text: $viewModel.password)
.focused($focusedField, equals: .password)
.submitLabel(.go)
.onSubmit {
viewModel.login()
}
}
Button(action: {
isPasswordVisible.toggle()
}) {
Image(systemName: isPasswordVisible ? "eye.slash.fill" : "eye.fill")
.foregroundColor(.secondary)
}
.buttonStyle(.plain)
}
.onChange(of: viewModel.password) { _, _ in
viewModel.clearError()
}
} header: {
Text("Account Information")
}
@@ -97,8 +125,6 @@ struct LoginView: View {
}
.listRowBackground(Color.clear)
}
.navigationTitle("Welcome Back")
.navigationBarTitleDisplayMode(.large)
.onChange(of: viewModel.isAuthenticated) { _, isAuth in
if isAuth {
print("isAuthenticated changed to true, isVerified = \(viewModel.isVerified)")

View File

@@ -20,11 +20,8 @@ class LoginViewModel: ObservableObject {
// 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())
self.tokenStorage = TokenStorage.shared
// Check if user is already logged in
checkAuthenticationStatus()
}
@@ -53,15 +50,21 @@ class LoginViewModel: ObservableObject {
self.handleSuccess(results: successResult)
return
}
if let errorResult = result as? ApiResultError {
self.handleApiError(errorResult: errorResult)
return
}
if let error = error {
self.handleError(error: error)
return
}
self.isLoading = false
self.isAuthenticated = false
print("uknown error")
self.errorMessage = "Login failed. Please try again."
print("unknown error")
}
}
}
@@ -70,9 +73,25 @@ class LoginViewModel: ObservableObject {
func handleError(error: any Error) {
self.isLoading = false
self.isAuthenticated = false
self.errorMessage = error.localizedDescription
print(error)
}
@MainActor
func handleApiError(errorResult: ApiResultError) {
self.isLoading = false
self.isAuthenticated = false
// Check for specific error codes
if errorResult.code?.intValue == 401 || errorResult.code?.intValue == 400 {
self.errorMessage = "Invalid username or password"
} else {
self.errorMessage = errorResult.message
}
print("API Error: \(errorResult.message)")
}
@MainActor
func handleSuccess(results: ApiResultSuccess<AuthResponse>) {
if let token = results.data?.token,
@@ -133,12 +152,37 @@ class LoginViewModel: ObservableObject {
// MARK: - Private Methods
private func checkAuthenticationStatus() {
isAuthenticated = tokenStorage.hasToken()
guard let token = tokenStorage.getToken() else {
isAuthenticated = false
isVerified = false
return
}
// If already authenticated, initialize lookups
if isAuthenticated {
// Fetch current user to check verification status
authApi.getCurrentUser(token: token) { result, error in
if let successResult = result as? ApiResultSuccess<User> {
self.handleAuthCheck(user: successResult.data!)
} else {
// Token invalid or expired, clear it
self.tokenStorage.clearToken()
self.isAuthenticated = false
self.isVerified = false
}
}
}
@MainActor
private func handleAuthCheck(user: User) {
self.currentUser = user
self.isVerified = user.verified
self.isAuthenticated = true
// Initialize lookups if verified
if user.verified {
LookupsManager.shared.initialize()
}
print("Auth check - User: \(user.username), Verified: \(user.verified)")
}
}

View File

@@ -14,11 +14,13 @@ struct MainTabView: View {
}
.tag(0)
Text("Tasks (Coming Soon)")
.tabItem {
Label("Tasks", systemImage: "checkmark.circle.fill")
}
.tag(1)
NavigationView {
AllTasksView()
}
.tabItem {
Label("Tasks", systemImage: "checkmark.circle.fill")
}
.tag(1)
NavigationView {
ProfileTabView()

View File

@@ -20,10 +20,7 @@ class ProfileViewModel: ObservableObject {
// 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())
self.tokenStorage = TokenStorage.shared
// Load current user data
loadCurrentUser()

View File

@@ -121,7 +121,7 @@ struct RegisterView: View {
},
onLogout: {
// Logout and return to login screen
TokenManager().clearToken()
TokenStorage.shared.clearToken()
LookupsManager.shared.clear()
dismiss()
}

View File

@@ -20,8 +20,7 @@ class RegisterViewModel: ObservableObject {
// MARK: - Initialization
init() {
self.authApi = AuthApi(client: ApiClient_iosKt.createHttpClient())
self.tokenStorage = TokenStorage()
self.tokenStorage.initialize(manager: TokenManager.init())
self.tokenStorage = TokenStorage.shared
}
// MARK: - Public Methods

View File

@@ -14,6 +14,8 @@ struct ResidenceDetailView: View {
@State private var selectedTaskForEdit: TaskDetail?
@State private var showInProgressTasks = false
@State private var showDoneTasks = false
@State private var showCompleteTask = false
@State private var selectedTaskForComplete: TaskDetail?
var body: some View {
ZStack {
@@ -53,6 +55,17 @@ struct ResidenceDetailView: View {
taskViewModel.uncancelTask(id: task.id) { _ in
loadResidenceTasks()
}
},
onMarkInProgress: { task in
taskViewModel.markInProgress(id: task.id) { success in
if success {
loadResidenceTasks()
}
}
},
onCompleteTask: { task in
selectedTaskForComplete = task
showCompleteTask = true
}
)
.padding(.horizontal)
@@ -102,6 +115,13 @@ struct ResidenceDetailView: View {
EditTaskView(task: task, isPresented: $showEditTask)
}
}
.sheet(isPresented: $showCompleteTask) {
if let task = selectedTaskForComplete {
CompleteTaskView(task: task, isPresented: $showCompleteTask) {
loadResidenceTasks()
}
}
}
.onChange(of: showAddTask) { isShowing in
if !isShowing {
loadResidenceTasks()
@@ -128,7 +148,7 @@ struct ResidenceDetailView: View {
}
private func loadResidenceTasks() {
guard let token = TokenStorage().getToken() else { return }
guard let token = TokenStorage.shared.getToken() else { return }
isLoadingTasks = true
tasksError = nil

View File

@@ -18,8 +18,7 @@ class ResidenceViewModel: ObservableObject {
// MARK: - Initialization
init() {
self.residenceApi = ResidenceApi(client: ApiClient_iosKt.createHttpClient())
self.tokenStorage = TokenStorage()
self.tokenStorage.initialize(manager: TokenManager.init())
self.tokenStorage = TokenStorage.shared
}
// MARK: - Public Methods

View File

@@ -6,6 +6,8 @@ struct TaskCard: View {
let onEdit: () -> Void
let onCancel: (() -> Void)?
let onUncancel: (() -> Void)?
let onMarkInProgress: (() -> Void)?
let onComplete: (() -> Void)?
var body: some View {
VStack(alignment: .leading, spacing: 12) {
@@ -57,21 +59,39 @@ struct TaskCard: View {
}
if task.showCompletedButton {
Button(action: {}) {
HStack {
Image(systemName: "checkmark.circle.fill")
.resizable()
.frame(width: 20, height: 20)
Text("Complete Task")
.font(.title3.weight(.semibold))
VStack(spacing: 8) {
if let onMarkInProgress = onMarkInProgress, task.status?.name != "in_progress" {
Button(action: onMarkInProgress) {
HStack {
Image(systemName: "play.circle.fill")
.resizable()
.frame(width: 18, height: 18)
Text("In Progress")
.font(.subheadline.weight(.semibold))
}
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
.tint(.orange)
}
if task.showCompletedButton, let onComplete = onComplete {
Button(action: onComplete) {
HStack {
Image(systemName: "checkmark.circle.fill")
.resizable()
.frame(width: 18, height: 18)
Text("Complete")
.font(.subheadline.weight(.semibold))
}
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
}
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.cornerRadius(12)
}
HStack(spacing: 8) {
VStack(spacing: 8) {
Button(action: onEdit) {
Label("Edit", systemImage: "pencil")
.font(.subheadline)
@@ -139,7 +159,9 @@ struct TaskCard: View {
),
onEdit: {},
onCancel: {},
onUncancel: nil
onUncancel: nil,
onMarkInProgress: {},
onComplete: {}
)
}
.padding()

View File

@@ -8,6 +8,8 @@ struct TasksSection: View {
let onEditTask: (TaskDetail) -> Void
let onCancelTask: (TaskDetail) -> Void
let onUncancelTask: (TaskDetail) -> Void
let onMarkInProgress: (TaskDetail) -> Void
let onCompleteTask: (TaskDetail) -> Void
var body: some View {
VStack(alignment: .leading, spacing: 12) {
@@ -34,7 +36,9 @@ struct TasksSection: View {
task: task,
onEdit: { onEditTask(task) },
onCancel: { onCancelTask(task) },
onUncancel: nil
onUncancel: nil,
onMarkInProgress: { onMarkInProgress(task) },
onComplete: { onCompleteTask(task) }
)
}
@@ -64,7 +68,9 @@ struct TasksSection: View {
task: task,
onEdit: { onEditTask(task) },
onCancel: { onCancelTask(task) },
onUncancel: nil
onUncancel: nil,
onMarkInProgress: nil,
onComplete: { onCompleteTask(task) }
)
}
}
@@ -97,7 +103,9 @@ struct TasksSection: View {
task: task,
onEdit: { onEditTask(task) },
onCancel: nil,
onUncancel: nil
onUncancel: nil,
onMarkInProgress: nil,
onComplete: nil
)
}
}
@@ -166,7 +174,9 @@ struct TasksSection: View {
showDoneTasks: .constant(true),
onEditTask: { _ in },
onCancelTask: { _ in },
onUncancelTask: { _ in }
onUncancelTask: { _ in },
onMarkInProgress: { _ in },
onCompleteTask: { _ in }
)
.padding()
}

View File

@@ -0,0 +1,267 @@
import SwiftUI
import ComposeApp
struct AllTasksView: View {
@StateObject private var taskViewModel = TaskViewModel()
@State private var tasksResponse: AllTasksResponse?
@State private var isLoadingTasks = false
@State private var tasksError: String?
@State private var showAddTask = false
@State private var showEditTask = false
@State private var selectedTaskForEdit: TaskDetail?
@State private var showInProgressTasks = false
@State private var showDoneTasks = false
@State private var showCompleteTask = false
@State private var selectedTaskForComplete: TaskDetail?
var body: some View {
ZStack {
Color(.systemGroupedBackground)
.ignoresSafeArea()
if isLoadingTasks {
ProgressView()
} else if let error = tasksError {
ErrorView(message: error) {
loadAllTasks()
}
} else if let tasksResponse = tasksResponse {
ScrollView {
VStack(spacing: 16) {
// Header Card
VStack(spacing: 12) {
Image(systemName: "checklist")
.font(.system(size: 48))
.foregroundStyle(.blue.gradient)
Text("All Tasks")
.font(.title)
.fontWeight(.bold)
Text("Tasks across all your properties")
.font(.subheadline)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
.padding(.horizontal)
.padding(.top)
// Tasks Section
AllTasksSectionView(
tasksResponse: tasksResponse,
showInProgressTasks: $showInProgressTasks,
showDoneTasks: $showDoneTasks,
onEditTask: { task in
selectedTaskForEdit = task
showEditTask = true
},
onCancelTask: { task in
taskViewModel.cancelTask(id: task.id) { _ in
loadAllTasks()
}
},
onUncancelTask: { task in
taskViewModel.uncancelTask(id: task.id) { _ in
loadAllTasks()
}
},
onMarkInProgress: { task in
taskViewModel.markInProgress(id: task.id) { success in
if success {
loadAllTasks()
}
}
},
onCompleteTask: { task in
selectedTaskForComplete = task
showCompleteTask = true
}
)
.padding(.horizontal)
}
.padding(.bottom)
}
}
}
.navigationTitle("All Tasks")
.navigationBarTitleDisplayMode(.inline)
.sheet(isPresented: $showEditTask) {
if let task = selectedTaskForEdit {
EditTaskView(task: task, isPresented: $showEditTask)
}
}
.sheet(isPresented: $showCompleteTask) {
if let task = selectedTaskForComplete {
CompleteTaskView(task: task, isPresented: $showCompleteTask) {
loadAllTasks()
}
}
}
.onChange(of: showEditTask) { isShowing in
if !isShowing {
loadAllTasks()
}
}
.onAppear {
loadAllTasks()
}
}
private func loadAllTasks() {
guard let token = TokenStorage.shared.getToken() else { return }
isLoadingTasks = true
tasksError = nil
let taskApi = TaskApi(client: ApiClient_iosKt.createHttpClient())
taskApi.getTasks(token: token, days: 30) { result, error in
if let successResult = result as? ApiResultSuccess<AllTasksResponse> {
self.tasksResponse = successResult.data
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 AllTasksSectionView: View {
let tasksResponse: AllTasksResponse
@Binding var showInProgressTasks: Bool
@Binding var showDoneTasks: Bool
let onEditTask: (TaskDetail) -> Void
let onCancelTask: (TaskDetail) -> Void
let onUncancelTask: (TaskDetail) -> Void
let onMarkInProgress: (TaskDetail) -> Void
let onCompleteTask: (TaskDetail) -> Void
var body: some View {
VStack(alignment: .leading, spacing: 12) {
// Task summary pills
HStack(spacing: 8) {
TaskPill(
count: Int32(tasksResponse.summary.upcoming),
label: "Upcoming",
color: .blue
)
TaskPill(
count: Int32(tasksResponse.summary.inProgress),
label: "In Progress",
color: .orange
)
TaskPill(
count: Int32(tasksResponse.summary.done),
label: "Done",
color: .green
)
}
.padding(.bottom, 4)
// Upcoming tasks
if !tasksResponse.upcomingTasks.isEmpty {
VStack(alignment: .leading, spacing: 8) {
Label("Upcoming (\(tasksResponse.upcomingTasks.count))", systemImage: "calendar")
.font(.headline)
.foregroundColor(.blue)
ForEach(tasksResponse.upcomingTasks, id: \.id) { task in
TaskCard(
task: task,
onEdit: { onEditTask(task) },
onCancel: { onCancelTask(task) },
onUncancel: { onUncancelTask(task) },
onMarkInProgress: { onMarkInProgress(task) },
onComplete: { onCompleteTask(task) }
)
}
}
}
// In Progress section (collapsible)
if !tasksResponse.inProgressTasks.isEmpty {
VStack(alignment: .leading, spacing: 8) {
HStack {
Label("In Progress (\(tasksResponse.inProgressTasks.count))", systemImage: "play.circle")
.font(.headline)
.foregroundColor(.orange)
Spacer()
Image(systemName: showInProgressTasks ? "chevron.up" : "chevron.down")
.foregroundColor(.secondary)
}
.contentShape(Rectangle())
.onTapGesture {
withAnimation {
showInProgressTasks.toggle()
}
}
if showInProgressTasks {
ForEach(tasksResponse.inProgressTasks, id: \.id) { task in
TaskCard(
task: task,
onEdit: { onEditTask(task) },
onCancel: { onCancelTask(task) },
onUncancel: { onUncancelTask(task) },
onMarkInProgress: nil,
onComplete: { onCompleteTask(task) }
)
}
}
}
}
// Done section (collapsible)
if !tasksResponse.doneTasks.isEmpty {
VStack(alignment: .leading, spacing: 8) {
HStack {
Label("Done (\(tasksResponse.doneTasks.count))", systemImage: "checkmark.circle")
.font(.headline)
.foregroundColor(.green)
Spacer()
Image(systemName: showDoneTasks ? "chevron.up" : "chevron.down")
.foregroundColor(.secondary)
}
.contentShape(Rectangle())
.onTapGesture {
withAnimation {
showDoneTasks.toggle()
}
}
if showDoneTasks {
ForEach(tasksResponse.doneTasks, id: \.id) { task in
TaskCard(
task: task,
onEdit: { onEditTask(task) },
onCancel: nil,
onUncancel: nil,
onMarkInProgress: nil,
onComplete: nil
)
}
}
}
}
}
}
}
#Preview {
NavigationView {
AllTasksView()
}
}

View File

@@ -0,0 +1,300 @@
import SwiftUI
import PhotosUI
import ComposeApp
struct CompleteTaskView: View {
let task: TaskDetail
@Binding var isPresented: Bool
let onComplete: () -> Void
@StateObject private var taskViewModel = TaskViewModel()
@State private var completedByName: String = ""
@State private var actualCost: String = ""
@State private var notes: String = ""
@State private var rating: Int = 3
@State private var selectedItems: [PhotosPickerItem] = []
@State private var selectedImages: [UIImage] = []
@State private var isSubmitting: Bool = false
@State private var showError: Bool = false
@State private var errorMessage: String = ""
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 20) {
// Task Info Header
VStack(alignment: .leading, spacing: 8) {
Text(task.title)
.font(.title2)
.fontWeight(.bold)
Text(task.category.name.capitalized)
.font(.subheadline)
.foregroundColor(.secondary)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Color.blue.opacity(0.1))
.cornerRadius(8)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
// Completed By
VStack(alignment: .leading, spacing: 8) {
Text("Completed By (Optional)")
.font(.subheadline)
.foregroundColor(.secondary)
TextField("Enter name or leave blank", text: $completedByName)
.textFieldStyle(.roundedBorder)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
// Actual Cost
VStack(alignment: .leading, spacing: 8) {
Text("Actual Cost (Optional)")
.font(.subheadline)
.foregroundColor(.secondary)
HStack {
Text("$")
.foregroundColor(.secondary)
TextField("0.00", text: $actualCost)
.keyboardType(.decimalPad)
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(8)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
// Notes
VStack(alignment: .leading, spacing: 8) {
Text("Notes (Optional)")
.font(.subheadline)
.foregroundColor(.secondary)
TextEditor(text: $notes)
.frame(minHeight: 100)
.padding(8)
.background(Color(.systemGray6))
.cornerRadius(8)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
// Rating
VStack(alignment: .leading, spacing: 12) {
Text("Rating")
.font(.subheadline)
.foregroundColor(.secondary)
HStack(spacing: 16) {
ForEach(1...5, id: \.self) { star in
Image(systemName: star <= rating ? "star.fill" : "star")
.font(.title2)
.foregroundColor(star <= rating ? .yellow : .gray)
.onTapGesture {
rating = star
}
}
}
Text("\(rating) out of 5")
.font(.caption)
.foregroundColor(.secondary)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
// Image Picker
VStack(alignment: .leading, spacing: 12) {
Text("Add Images (up to 5)")
.font(.subheadline)
.foregroundColor(.secondary)
PhotosPicker(
selection: $selectedItems,
maxSelectionCount: 5,
matching: .images
) {
HStack {
Image(systemName: "photo.on.rectangle.angled")
Text("Select Images")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue.opacity(0.1))
.foregroundColor(.blue)
.cornerRadius(8)
}
.onChange(of: selectedItems) { newItems in
Task {
selectedImages = []
for item in newItems {
if let data = try? await item.loadTransferable(type: Data.self),
let image = UIImage(data: data) {
selectedImages.append(image)
}
}
}
}
// Display selected images
if !selectedImages.isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(selectedImages.indices, id: \.self) { index in
ZStack(alignment: .topTrailing) {
Image(uiImage: selectedImages[index])
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipShape(RoundedRectangle(cornerRadius: 8))
Button(action: {
selectedImages.remove(at: index)
selectedItems.remove(at: index)
}) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.white)
.background(Circle().fill(Color.black.opacity(0.6)))
}
.padding(4)
}
}
}
}
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
// Complete Button
Button(action: handleComplete) {
HStack {
if isSubmitting {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
} else {
Image(systemName: "checkmark.circle.fill")
Text("Complete Task")
.fontWeight(.semibold)
}
}
.frame(maxWidth: .infinity)
.padding()
.background(isSubmitting ? Color.gray : Color.green)
.foregroundColor(.white)
.cornerRadius(12)
}
.disabled(isSubmitting)
.padding()
}
.padding()
}
.background(Color(.systemGroupedBackground))
.navigationTitle("Complete Task")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
isPresented = false
}
}
}
.alert("Error", isPresented: $showError) {
Button("OK") {
showError = false
}
} message: {
Text(errorMessage)
}
}
}
private func handleComplete() {
isSubmitting = true
guard let token = TokenStorage.shared.getToken() else {
errorMessage = "Not authenticated"
showError = true
isSubmitting = false
return
}
// Get current date in ISO format
let dateFormatter = ISO8601DateFormatter()
let currentDate = dateFormatter.string(from: Date())
// Create request
let request = TaskCompletionCreateRequest(
task: task.id,
completedByUser: nil,
completedByName: completedByName.isEmpty ? nil : completedByName,
completionDate: currentDate,
actualCost: actualCost.isEmpty ? nil : actualCost,
notes: notes.isEmpty ? nil : notes,
rating: KotlinInt(int: Int32(rating))
)
let completionApi = TaskCompletionApi(client: ApiClient_iosKt.createHttpClient())
// If there are images, upload with images
if !selectedImages.isEmpty {
let imageDataArray = selectedImages.compactMap { $0.jpegData(compressionQuality: 0.8) }
let imageByteArrays = imageDataArray.map { KotlinByteArray(data: $0) }
let fileNames = (0..<imageDataArray.count).map { "image_\($0).jpg" }
completionApi.createCompletionWithImages(
token: token,
request: request,
images: imageByteArrays,
imageFileNames: fileNames
) { result, error in
handleCompletionResult(result: result, error: error)
}
} else {
// Upload without images
completionApi.createCompletion(token: token, request: request) { result, error in
handleCompletionResult(result: result, error: error)
}
}
}
private func handleCompletionResult(result: ApiResult<TaskCompletion>?, error: Error?) {
if result is ApiResultSuccess<TaskCompletion> {
isSubmitting = false
isPresented = false
onComplete()
} else if let errorResult = result as? ApiResultError {
errorMessage = errorResult.message
showError = true
isSubmitting = false
} else if let error = error {
errorMessage = error.localizedDescription
showError = true
isSubmitting = false
}
}
}
// Helper extension to convert Data to KotlinByteArray
extension KotlinByteArray {
convenience init(data: Data) {
let array = [UInt8](data)
self.init(size: Int32(array.count))
for (index, byte) in array.enumerated() {
self.set(index: Int32(index), value: Int8(bitPattern: byte))
}
}
}

View File

@@ -11,6 +11,7 @@ class TaskViewModel: ObservableObject {
@Published var taskUpdated: Bool = false
@Published var taskCancelled: Bool = false
@Published var taskUncancelled: Bool = false
@Published var taskMarkedInProgress: Bool = false
// MARK: - Private Properties
private let taskApi: TaskApi
@@ -19,8 +20,7 @@ class TaskViewModel: ObservableObject {
// MARK: - Initialization
init() {
self.taskApi = TaskApi(client: ApiClient_iosKt.createHttpClient())
self.tokenStorage = TokenStorage()
self.tokenStorage.initialize(manager: TokenManager.init())
self.tokenStorage = TokenStorage.shared
}
// MARK: - Public Methods
@@ -140,11 +140,81 @@ class TaskViewModel: ObservableObject {
errorMessage = nil
}
func markInProgress(id: Int32, completion: @escaping (Bool) -> Void) {
guard let token = tokenStorage.getToken() else {
errorMessage = "Not authenticated"
completion(false)
return
}
isLoading = true
errorMessage = nil
taskMarkedInProgress = false
taskApi.markInProgress(token: token, id: id) { result, error in
if result is ApiResultSuccess<TaskCancelResponse> {
self.isLoading = false
self.taskMarkedInProgress = 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 completeTask(taskId: Int32, completion: @escaping (Bool) -> Void) {
guard let token = tokenStorage.getToken() else {
errorMessage = "Not authenticated"
completion(false)
return
}
isLoading = true
errorMessage = nil
// Get current date in ISO format
let dateFormatter = ISO8601DateFormatter()
let currentDate = dateFormatter.string(from: Date())
let request = TaskCompletionCreateRequest(
task: taskId,
completedByUser: nil,
completedByName: nil,
completionDate: currentDate,
actualCost: nil,
notes: nil,
rating: nil
)
let completionApi = TaskCompletionApi(client: ApiClient_iosKt.createHttpClient())
completionApi.createCompletion(token: token, request: request) { result, error in
if result is ApiResultSuccess<TaskCompletion> {
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 resetState() {
taskCreated = false
taskUpdated = false
taskCancelled = false
taskUncancelled = false
taskMarkedInProgress = false
errorMessage = nil
}
}

View File

@@ -17,8 +17,7 @@ class VerifyEmailViewModel: ObservableObject {
// MARK: - Initialization
init() {
self.authApi = AuthApi(client: ApiClient_iosKt.createHttpClient())
self.tokenStorage = TokenStorage()
self.tokenStorage.initialize(manager: TokenManager.init())
self.tokenStorage = TokenStorage.shared
}
// MARK: - Public Methods

View File

@@ -1,7 +1,13 @@
import SwiftUI
import ComposeApp
@main
struct iOSApp: App {
init() {
// Initialize TokenStorage once at app startup
TokenStorage.shared.initialize(manager: TokenManager())
}
var body: some Scene {
WindowGroup {
LoginView()