Implement unified network layer with APILayer and migrate iOS ViewModels
Major architectural improvements: - Created APILayer as single entry point for all network operations - Integrated cache-first reads with automatic cache updates on mutations - Migrated all shared Kotlin ViewModels to use APILayer instead of direct API calls - Migrated iOS ViewModels to wrap shared Kotlin ViewModels with StateFlow observation - Replaced LookupsManager with DataCache for centralized lookup data management - Added password reset methods to AuthViewModel - Added task completion and update methods to APILayer - Added residence user management methods to APILayer iOS specific changes: - Updated LoginViewModel, RegisterViewModel, ProfileViewModel to use shared AuthViewModel - Updated ContractorViewModel, DocumentViewModel to use shared ViewModels - Updated ResidenceViewModel to use shared ViewModel and APILayer - Updated TaskViewModel to wrap shared ViewModel with callback-based interface - Migrated PasswordResetViewModel and VerifyEmailViewModel to shared AuthViewModel - Migrated AllTasksView, CompleteTaskView, EditTaskView to use APILayer - Migrated ManageUsersView, ResidenceDetailView to use APILayer - Migrated JoinResidenceView to use async/await pattern with APILayer - Removed LookupsManager.swift in favor of DataCache - Fixed PushNotificationManager @MainActor issue - Converted all direct API calls to use async/await with proper error handling Benefits: - Reduced code duplication between iOS and Android - Consistent error handling across platforms - Automatic cache management for better performance - Centralized network layer for easier testing and maintenance - Net reduction of ~700 lines of code through shared logic 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -10,239 +10,6 @@ struct AddTaskView: View {
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AddTaskView(residenceId: 1, isPresented: .constant(true))
|
||||
}
|
||||
|
||||
// Deprecated: For reference only
|
||||
@available(*, deprecated, message: "Use TaskFormView instead")
|
||||
private struct OldAddTaskView: 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 = ""
|
||||
|
||||
enum Field {
|
||||
case title, description, intervalDays, estimatedCost
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
if lookupsManager.isLoading {
|
||||
VStack(spacing: 16) {
|
||||
ProgressView()
|
||||
Text("Loading lookup data...")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} else {
|
||||
Form {
|
||||
Section(header: Text("Task Details")) {
|
||||
TextField("Title", text: $title)
|
||||
.focused($focusedField, equals: .title)
|
||||
|
||||
if !titleError.isEmpty {
|
||||
Text(titleError)
|
||||
.font(.caption)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
|
||||
TextField("Description (optional)", text: $description, axis: .vertical)
|
||||
.lineLimit(3...6)
|
||||
.focused($focusedField, equals: .description)
|
||||
}
|
||||
|
||||
Section(header: Text("Category")) {
|
||||
Picker("Category", selection: $selectedCategory) {
|
||||
Text("Select Category").tag(nil as TaskCategory?)
|
||||
ForEach(lookupsManager.taskCategories, id: \.id) { category in
|
||||
Text(category.name.capitalized).tag(category as TaskCategory?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Scheduling")) {
|
||||
Picker("Frequency", selection: $selectedFrequency) {
|
||||
Text("Select Frequency").tag(nil as TaskFrequency?)
|
||||
ForEach(lookupsManager.taskFrequencies, id: \.id) { frequency in
|
||||
Text(frequency.displayName).tag(frequency as TaskFrequency?)
|
||||
}
|
||||
}
|
||||
|
||||
if selectedFrequency?.name != "once" {
|
||||
TextField("Custom Interval (days, optional)", text: $intervalDays)
|
||||
.keyboardType(.numberPad)
|
||||
.focused($focusedField, equals: .intervalDays)
|
||||
}
|
||||
|
||||
DatePicker("Due Date", selection: $dueDate, displayedComponents: .date)
|
||||
}
|
||||
|
||||
Section(header: Text("Priority & Status")) {
|
||||
Picker("Priority", selection: $selectedPriority) {
|
||||
Text("Select Priority").tag(nil as TaskPriority?)
|
||||
ForEach(lookupsManager.taskPriorities, id: \.id) { priority in
|
||||
Text(priority.displayName).tag(priority as TaskPriority?)
|
||||
}
|
||||
}
|
||||
|
||||
Picker("Status", selection: $selectedStatus) {
|
||||
Text("Select Status").tag(nil as TaskStatus?)
|
||||
ForEach(lookupsManager.taskStatuses, id: \.id) { status in
|
||||
Text(status.displayName).tag(status as TaskStatus?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Cost")) {
|
||||
TextField("Estimated Cost (optional)", text: $estimatedCost)
|
||||
.keyboardType(.decimalPad)
|
||||
.focused($focusedField, equals: .estimatedCost)
|
||||
}
|
||||
|
||||
if let errorMessage = viewModel.errorMessage {
|
||||
Section {
|
||||
Text(errorMessage)
|
||||
.foregroundColor(.red)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Add Task")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
isPresented = false
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
submitForm()
|
||||
}
|
||||
.disabled(viewModel.isLoading)
|
||||
}
|
||||
}
|
||||
.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: selectedStatus.map { KotlinInt(value: $0.id) },
|
||||
dueDate: dueDateString,
|
||||
estimatedCost: estimatedCost.isEmpty ? nil : estimatedCost,
|
||||
archived: false
|
||||
)
|
||||
|
||||
viewModel.createTask(request: request) { success in
|
||||
if success {
|
||||
// View will dismiss automatically via onChange
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
AddTaskView(residenceId: 1, isPresented: .constant(true))
|
||||
}
|
||||
|
||||
@@ -13,259 +13,3 @@ struct AddTaskWithResidenceView: View {
|
||||
#Preview {
|
||||
AddTaskWithResidenceView(isPresented: .constant(true), residences: [])
|
||||
}
|
||||
|
||||
// Deprecated: For reference only
|
||||
@available(*, deprecated, message: "Use TaskFormView instead")
|
||||
private struct OldAddTaskWithResidenceView: View {
|
||||
@Binding var isPresented: Bool
|
||||
let residences: [Residence]
|
||||
@StateObject private var viewModel = TaskViewModel()
|
||||
@StateObject private var lookupsManager = LookupsManager.shared
|
||||
@FocusState private var focusedField: Field?
|
||||
|
||||
// Form fields
|
||||
@State private var selectedResidence: Residence?
|
||||
@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 = ""
|
||||
@State private var residenceError: String = ""
|
||||
|
||||
enum Field {
|
||||
case title, description, intervalDays, estimatedCost
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
if lookupsManager.isLoading {
|
||||
VStack(spacing: 16) {
|
||||
ProgressView()
|
||||
Text("Loading...")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} else {
|
||||
Form {
|
||||
Section(header: Text("Property")) {
|
||||
Picker("Property", selection: $selectedResidence) {
|
||||
Text("Select Property").tag(nil as Residence?)
|
||||
ForEach(residences, id: \.id) { residence in
|
||||
Text(residence.name).tag(residence as Residence?)
|
||||
}
|
||||
}
|
||||
|
||||
if !residenceError.isEmpty {
|
||||
Text(residenceError)
|
||||
.font(.caption)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Task Details")) {
|
||||
TextField("Title", text: $title)
|
||||
.focused($focusedField, equals: .title)
|
||||
|
||||
if !titleError.isEmpty {
|
||||
Text(titleError)
|
||||
.font(.caption)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
|
||||
TextField("Description (optional)", text: $description, axis: .vertical)
|
||||
.lineLimit(3...6)
|
||||
.focused($focusedField, equals: .description)
|
||||
}
|
||||
|
||||
Section(header: Text("Category")) {
|
||||
Picker("Category", selection: $selectedCategory) {
|
||||
Text("Select Category").tag(nil as TaskCategory?)
|
||||
ForEach(lookupsManager.taskCategories, id: \.id) { category in
|
||||
Text(category.name.capitalized).tag(category as TaskCategory?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Scheduling")) {
|
||||
Picker("Frequency", selection: $selectedFrequency) {
|
||||
Text("Select Frequency").tag(nil as TaskFrequency?)
|
||||
ForEach(lookupsManager.taskFrequencies, id: \.id) { frequency in
|
||||
Text(frequency.displayName).tag(frequency as TaskFrequency?)
|
||||
}
|
||||
}
|
||||
|
||||
if selectedFrequency?.name != "once" {
|
||||
TextField("Custom Interval (days, optional)", text: $intervalDays)
|
||||
.keyboardType(.numberPad)
|
||||
.focused($focusedField, equals: .intervalDays)
|
||||
}
|
||||
|
||||
DatePicker("Due Date", selection: $dueDate, displayedComponents: .date)
|
||||
}
|
||||
|
||||
Section(header: Text("Priority & Status")) {
|
||||
Picker("Priority", selection: $selectedPriority) {
|
||||
Text("Select Priority").tag(nil as TaskPriority?)
|
||||
ForEach(lookupsManager.taskPriorities, id: \.id) { priority in
|
||||
Text(priority.displayName).tag(priority as TaskPriority?)
|
||||
}
|
||||
}
|
||||
|
||||
Picker("Status", selection: $selectedStatus) {
|
||||
Text("Select Status").tag(nil as TaskStatus?)
|
||||
ForEach(lookupsManager.taskStatuses, id: \.id) { status in
|
||||
Text(status.displayName).tag(status as TaskStatus?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Cost")) {
|
||||
TextField("Estimated Cost (optional)", text: $estimatedCost)
|
||||
.keyboardType(.decimalPad)
|
||||
.focused($focusedField, equals: .estimatedCost)
|
||||
}
|
||||
|
||||
if let errorMessage = viewModel.errorMessage {
|
||||
Section {
|
||||
Text(errorMessage)
|
||||
.foregroundColor(.red)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Add Task")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
isPresented = false
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
submitForm()
|
||||
}
|
||||
.disabled(viewModel.isLoading)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
setDefaults()
|
||||
}
|
||||
.onChange(of: viewModel.taskCreated) { created in
|
||||
if created {
|
||||
isPresented = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setDefaults() {
|
||||
if selectedResidence == nil && !residences.isEmpty {
|
||||
selectedResidence = residences.first
|
||||
}
|
||||
|
||||
if selectedCategory == nil && !lookupsManager.taskCategories.isEmpty {
|
||||
selectedCategory = lookupsManager.taskCategories.first
|
||||
}
|
||||
|
||||
if selectedFrequency == nil && !lookupsManager.taskFrequencies.isEmpty {
|
||||
selectedFrequency = lookupsManager.taskFrequencies.first { $0.name == "once" } ?? lookupsManager.taskFrequencies.first
|
||||
}
|
||||
|
||||
if selectedPriority == nil && !lookupsManager.taskPriorities.isEmpty {
|
||||
selectedPriority = lookupsManager.taskPriorities.first { $0.name == "medium" } ?? lookupsManager.taskPriorities.first
|
||||
}
|
||||
|
||||
if selectedStatus == nil && !lookupsManager.taskStatuses.isEmpty {
|
||||
selectedStatus = lookupsManager.taskStatuses.first { $0.name == "pending" } ?? lookupsManager.taskStatuses.first
|
||||
}
|
||||
}
|
||||
|
||||
private func validateForm() -> Bool {
|
||||
var isValid = true
|
||||
|
||||
if selectedResidence == nil {
|
||||
residenceError = "Property is required"
|
||||
isValid = false
|
||||
} else {
|
||||
residenceError = ""
|
||||
}
|
||||
|
||||
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 residence = selectedResidence,
|
||||
let category = selectedCategory,
|
||||
let frequency = selectedFrequency,
|
||||
let priority = selectedPriority,
|
||||
let status = selectedStatus else {
|
||||
return
|
||||
}
|
||||
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd"
|
||||
let dueDateString = dateFormatter.string(from: dueDate)
|
||||
|
||||
let request = TaskCreateRequest(
|
||||
residence: Int32(residence.id),
|
||||
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: selectedStatus.map { KotlinInt(value: $0.id) },
|
||||
dueDate: dueDateString,
|
||||
estimatedCost: estimatedCost.isEmpty ? nil : estimatedCost,
|
||||
archived: false
|
||||
)
|
||||
|
||||
viewModel.createTask(request: request) { success in
|
||||
if success {
|
||||
// View will dismiss automatically via onChange
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AddTaskWithResidenceView(isPresented: .constant(true), residences: [])
|
||||
}
|
||||
|
||||
@@ -179,22 +179,32 @@ struct AllTasksView: View {
|
||||
}
|
||||
|
||||
private func loadAllTasks() {
|
||||
guard let token = TokenStorage.shared.getToken() else { return }
|
||||
|
||||
guard TokenStorage.shared.getToken() != nil 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<TaskColumnsResponse> {
|
||||
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
|
||||
|
||||
Task {
|
||||
do {
|
||||
let result = try await APILayer.shared.getTasks(forceRefresh: false)
|
||||
await MainActor.run {
|
||||
if let success = result as? ApiResultSuccess<TaskColumnsResponse> {
|
||||
self.tasksResponse = success.data
|
||||
self.isLoadingTasks = false
|
||||
self.tasksError = nil
|
||||
} else if let error = result as? ApiResultError {
|
||||
self.tasksError = error.message
|
||||
self.isLoadingTasks = false
|
||||
} else {
|
||||
self.tasksError = "Failed to load tasks"
|
||||
self.isLoadingTasks = false
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.tasksError = error.localizedDescription
|
||||
self.isLoadingTasks = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,15 +282,14 @@ struct CompleteTaskView: View {
|
||||
}
|
||||
|
||||
private func handleComplete() {
|
||||
isSubmitting = true
|
||||
|
||||
guard let token = TokenStorage.shared.getToken() else {
|
||||
guard TokenStorage.shared.getToken() != nil else {
|
||||
errorMessage = "Not authenticated"
|
||||
showError = true
|
||||
isSubmitting = false
|
||||
return
|
||||
}
|
||||
|
||||
isSubmitting = true
|
||||
|
||||
// Get current date in ISO format
|
||||
let dateFormatter = ISO8601DateFormatter()
|
||||
let currentDate = dateFormatter.string(from: Date())
|
||||
@@ -310,48 +309,52 @@ struct CompleteTaskView: View {
|
||||
rating: KotlinInt(int: Int32(rating))
|
||||
)
|
||||
|
||||
let completionApi = TaskCompletionApi(client: ApiClient_iosKt.createHttpClient())
|
||||
Task {
|
||||
do {
|
||||
let result: ApiResult<TaskCompletion>
|
||||
|
||||
// If there are images, upload with images
|
||||
if !selectedImages.isEmpty {
|
||||
// Compress images to meet size requirements
|
||||
let imageDataArray = ImageCompression.compressImages(selectedImages)
|
||||
let imageByteArrays = imageDataArray.map { KotlinByteArray(data: $0) }
|
||||
let fileNames = (0..<imageDataArray.count).map { "image_\($0).jpg" }
|
||||
// If there are images, upload with images
|
||||
if !selectedImages.isEmpty {
|
||||
// Compress images to meet size requirements
|
||||
let imageDataArray = ImageCompression.compressImages(selectedImages)
|
||||
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)
|
||||
result = try await APILayer.shared.createTaskCompletionWithImages(
|
||||
request: request,
|
||||
images: imageByteArrays,
|
||||
imageFileNames: fileNames
|
||||
)
|
||||
} else {
|
||||
// Upload without images
|
||||
result = try await APILayer.shared.createTaskCompletion(request: request)
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
if result is ApiResultSuccess<TaskCompletion> {
|
||||
self.isSubmitting = false
|
||||
self.dismiss()
|
||||
self.onComplete()
|
||||
} else if let errorResult = result as? ApiResultError {
|
||||
self.errorMessage = errorResult.message
|
||||
self.showError = true
|
||||
self.isSubmitting = false
|
||||
} else {
|
||||
self.errorMessage = "Failed to complete task"
|
||||
self.showError = true
|
||||
self.isSubmitting = false
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.errorMessage = error.localizedDescription
|
||||
self.showError = true
|
||||
self.isSubmitting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleCompletionResult(result: ApiResult<TaskCompletion>?, error: Error?) {
|
||||
DispatchQueue.main.async {
|
||||
if result is ApiResultSuccess<TaskCompletion> {
|
||||
self.isSubmitting = false
|
||||
self.dismiss()
|
||||
self.onComplete()
|
||||
} else if let errorResult = result as? ApiResultError {
|
||||
self.errorMessage = errorResult.message
|
||||
self.showError = true
|
||||
self.isSubmitting = false
|
||||
} else if let error = error {
|
||||
self.errorMessage = error.localizedDescription
|
||||
self.showError = true
|
||||
self.isSubmitting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper extension to convert Data to KotlinByteArray
|
||||
|
||||
@@ -6,7 +6,6 @@ struct EditTaskView: View {
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
@StateObject private var viewModel = TaskViewModel()
|
||||
@StateObject private var lookupsManager = LookupsManager.shared
|
||||
|
||||
@State private var title: String
|
||||
@State private var description: String
|
||||
@@ -20,6 +19,12 @@ struct EditTaskView: View {
|
||||
@State private var showAlert = false
|
||||
@State private var alertMessage = ""
|
||||
|
||||
// Lookups from DataCache
|
||||
@State private var taskCategories: [TaskCategory] = []
|
||||
@State private var taskFrequencies: [TaskFrequency] = []
|
||||
@State private var taskPriorities: [TaskPriority] = []
|
||||
@State private var taskStatuses: [TaskStatus] = []
|
||||
|
||||
init(task: TaskDetail, isPresented: Binding<Bool>) {
|
||||
self.task = task
|
||||
self._isPresented = isPresented
|
||||
@@ -47,7 +52,7 @@ struct EditTaskView: View {
|
||||
|
||||
Section(header: Text("Category")) {
|
||||
Picker("Category", selection: $selectedCategory) {
|
||||
ForEach(lookupsManager.taskCategories, id: \.id) { category in
|
||||
ForEach(taskCategories, id: \.id) { category in
|
||||
Text(category.name.capitalized).tag(category as TaskCategory?)
|
||||
}
|
||||
}
|
||||
@@ -55,7 +60,7 @@ struct EditTaskView: View {
|
||||
|
||||
Section(header: Text("Scheduling")) {
|
||||
Picker("Frequency", selection: $selectedFrequency) {
|
||||
ForEach(lookupsManager.taskFrequencies, id: \.id) { frequency in
|
||||
ForEach(taskFrequencies, id: \.id) { frequency in
|
||||
Text(frequency.name.capitalized).tag(frequency as TaskFrequency?)
|
||||
}
|
||||
}
|
||||
@@ -66,13 +71,13 @@ struct EditTaskView: View {
|
||||
|
||||
Section(header: Text("Priority & Status")) {
|
||||
Picker("Priority", selection: $selectedPriority) {
|
||||
ForEach(lookupsManager.taskPriorities, id: \.id) { priority in
|
||||
ForEach(taskPriorities, id: \.id) { priority in
|
||||
Text(priority.name.capitalized).tag(priority as TaskPriority?)
|
||||
}
|
||||
}
|
||||
|
||||
Picker("Status", selection: $selectedStatus) {
|
||||
ForEach(lookupsManager.taskStatuses, id: \.id) { status in
|
||||
ForEach(taskStatuses, id: \.id) { status in
|
||||
Text(status.name.capitalized).tag(status as TaskStatus?)
|
||||
}
|
||||
}
|
||||
@@ -120,6 +125,20 @@ struct EditTaskView: View {
|
||||
showAlert = true
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
loadLookups()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadLookups() {
|
||||
Task {
|
||||
await MainActor.run {
|
||||
self.taskCategories = DataCache.shared.taskCategories.value as! [TaskCategory]
|
||||
self.taskFrequencies = DataCache.shared.taskFrequencies.value as! [TaskFrequency]
|
||||
self.taskPriorities = DataCache.shared.taskPriorities.value as! [TaskPriority]
|
||||
self.taskStatuses = DataCache.shared.taskStatuses.value as! [TaskStatus]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,19 @@ struct TaskFormView: View {
|
||||
let residences: [Residence]?
|
||||
@Binding var isPresented: Bool
|
||||
@StateObject private var viewModel = TaskViewModel()
|
||||
@StateObject private var lookupsManager = LookupsManager.shared
|
||||
@FocusState private var focusedField: Field?
|
||||
|
||||
private var needsResidenceSelection: Bool {
|
||||
residenceId == nil
|
||||
}
|
||||
|
||||
// Lookups from DataCache
|
||||
@State private var taskCategories: [TaskCategory] = []
|
||||
@State private var taskFrequencies: [TaskFrequency] = []
|
||||
@State private var taskPriorities: [TaskPriority] = []
|
||||
@State private var taskStatuses: [TaskStatus] = []
|
||||
@State private var isLoadingLookups: Bool = false
|
||||
|
||||
// Form fields
|
||||
@State private var selectedResidence: Residence?
|
||||
@State private var title: String = ""
|
||||
@@ -35,7 +41,7 @@ struct TaskFormView: View {
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
if lookupsManager.isLoading {
|
||||
if isLoadingLookups {
|
||||
VStack(spacing: 16) {
|
||||
ProgressView()
|
||||
Text("Loading...")
|
||||
@@ -79,7 +85,7 @@ struct TaskFormView: View {
|
||||
Section(header: Text("Category")) {
|
||||
Picker("Category", selection: $selectedCategory) {
|
||||
Text("Select Category").tag(nil as TaskCategory?)
|
||||
ForEach(lookupsManager.taskCategories, id: \.id) { category in
|
||||
ForEach(taskCategories, id: \.id) { category in
|
||||
Text(category.name.capitalized).tag(category as TaskCategory?)
|
||||
}
|
||||
}
|
||||
@@ -88,7 +94,7 @@ struct TaskFormView: View {
|
||||
Section(header: Text("Scheduling")) {
|
||||
Picker("Frequency", selection: $selectedFrequency) {
|
||||
Text("Select Frequency").tag(nil as TaskFrequency?)
|
||||
ForEach(lookupsManager.taskFrequencies, id: \.id) { frequency in
|
||||
ForEach(taskFrequencies, id: \.id) { frequency in
|
||||
Text(frequency.displayName).tag(frequency as TaskFrequency?)
|
||||
}
|
||||
}
|
||||
@@ -105,14 +111,14 @@ struct TaskFormView: View {
|
||||
Section(header: Text("Priority & Status")) {
|
||||
Picker("Priority", selection: $selectedPriority) {
|
||||
Text("Select Priority").tag(nil as TaskPriority?)
|
||||
ForEach(lookupsManager.taskPriorities, id: \.id) { priority in
|
||||
ForEach(taskPriorities, id: \.id) { priority in
|
||||
Text(priority.displayName).tag(priority as TaskPriority?)
|
||||
}
|
||||
}
|
||||
|
||||
Picker("Status", selection: $selectedStatus) {
|
||||
Text("Select Status").tag(nil as TaskStatus?)
|
||||
ForEach(lookupsManager.taskStatuses, id: \.id) { status in
|
||||
ForEach(taskStatuses, id: \.id) { status in
|
||||
Text(status.displayName).tag(status as TaskStatus?)
|
||||
}
|
||||
}
|
||||
@@ -149,7 +155,7 @@ struct TaskFormView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
setDefaults()
|
||||
loadLookups()
|
||||
}
|
||||
.onChange(of: viewModel.taskCreated) { created in
|
||||
if created {
|
||||
@@ -160,25 +166,42 @@ struct TaskFormView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func loadLookups() {
|
||||
Task {
|
||||
isLoadingLookups = true
|
||||
|
||||
// Load all lookups from DataCache
|
||||
await MainActor.run {
|
||||
self.taskCategories = DataCache.shared.taskCategories.value as! [TaskCategory]
|
||||
self.taskFrequencies = DataCache.shared.taskFrequencies.value as! [TaskFrequency]
|
||||
self.taskPriorities = DataCache.shared.taskPriorities.value as! [TaskPriority]
|
||||
self.taskStatuses = DataCache.shared.taskStatuses.value as! [TaskStatus]
|
||||
self.isLoadingLookups = false
|
||||
}
|
||||
|
||||
setDefaults()
|
||||
}
|
||||
}
|
||||
|
||||
private func setDefaults() {
|
||||
// Set default values if not already set
|
||||
if selectedCategory == nil && !lookupsManager.taskCategories.isEmpty {
|
||||
selectedCategory = lookupsManager.taskCategories.first
|
||||
if selectedCategory == nil && !taskCategories.isEmpty {
|
||||
selectedCategory = taskCategories.first
|
||||
}
|
||||
|
||||
if selectedFrequency == nil && !lookupsManager.taskFrequencies.isEmpty {
|
||||
if selectedFrequency == nil && !taskFrequencies.isEmpty {
|
||||
// Default to "once"
|
||||
selectedFrequency = lookupsManager.taskFrequencies.first { $0.name == "once" } ?? lookupsManager.taskFrequencies.first
|
||||
selectedFrequency = taskFrequencies.first { $0.name == "once" } ?? taskFrequencies.first
|
||||
}
|
||||
|
||||
if selectedPriority == nil && !lookupsManager.taskPriorities.isEmpty {
|
||||
if selectedPriority == nil && !taskPriorities.isEmpty {
|
||||
// Default to "medium"
|
||||
selectedPriority = lookupsManager.taskPriorities.first { $0.name == "medium" } ?? lookupsManager.taskPriorities.first
|
||||
selectedPriority = taskPriorities.first { $0.name == "medium" } ?? taskPriorities.first
|
||||
}
|
||||
|
||||
if selectedStatus == nil && !lookupsManager.taskStatuses.isEmpty {
|
||||
if selectedStatus == nil && !taskStatuses.isEmpty {
|
||||
// Default to "pending"
|
||||
selectedStatus = lookupsManager.taskStatuses.first { $0.name == "pending" } ?? lookupsManager.taskStatuses.first
|
||||
selectedStatus = taskStatuses.first { $0.name == "pending" } ?? taskStatuses.first
|
||||
}
|
||||
|
||||
// Set default residence if provided
|
||||
|
||||
@@ -16,124 +16,160 @@ class TaskViewModel: ObservableObject {
|
||||
@Published var taskUnarchived: Bool = false
|
||||
|
||||
// MARK: - Private Properties
|
||||
private let taskApi: TaskApi
|
||||
private let tokenStorage: TokenStorage
|
||||
private let sharedViewModel: ComposeApp.TaskViewModel
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: - Initialization
|
||||
init() {
|
||||
self.taskApi = TaskApi(client: ApiClient_iosKt.createHttpClient())
|
||||
self.tokenStorage = TokenStorage.shared
|
||||
self.sharedViewModel = ComposeApp.TaskViewModel()
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
sharedViewModel.createNewTask(request: request)
|
||||
|
||||
func updateTask(id: Int32, request: TaskCreateRequest, completion: @escaping (Bool) -> Void) {
|
||||
guard let token = tokenStorage.getToken() else {
|
||||
errorMessage = "Not authenticated"
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
taskUpdated = false
|
||||
|
||||
taskApi.updateTask(token: token, id: id, request: request) { result, error in
|
||||
if result is ApiResultSuccess<CustomTask> {
|
||||
self.isLoading = false
|
||||
self.taskUpdated = 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)
|
||||
// Observe the state
|
||||
Task {
|
||||
for await state in sharedViewModel.taskAddNewCustomTaskState {
|
||||
if state is ApiResultLoading {
|
||||
await MainActor.run {
|
||||
self.isLoading = true
|
||||
}
|
||||
} else if let success = state as? ApiResultSuccess<CustomTask> {
|
||||
await MainActor.run {
|
||||
self.isLoading = false
|
||||
self.taskCreated = true
|
||||
}
|
||||
sharedViewModel.resetAddTaskState()
|
||||
completion(true)
|
||||
break
|
||||
} else if let error = state as? ApiResultError {
|
||||
await MainActor.run {
|
||||
self.errorMessage = error.message
|
||||
self.isLoading = false
|
||||
}
|
||||
sharedViewModel.resetAddTaskState()
|
||||
completion(false)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cancelTask(id: Int32, completion: @escaping (Bool) -> Void) {
|
||||
guard let token = tokenStorage.getToken() else {
|
||||
errorMessage = "Not authenticated"
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
taskCancelled = false
|
||||
|
||||
taskApi.cancelTask(token: token, id: id) { result, error in
|
||||
if result is ApiResultSuccess<TaskCancelResponse> {
|
||||
sharedViewModel.cancelTask(taskId: id) { success in
|
||||
Task { @MainActor in
|
||||
self.isLoading = false
|
||||
self.taskCancelled = 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)
|
||||
if success.boolValue {
|
||||
self.taskCancelled = true
|
||||
completion(true)
|
||||
} else {
|
||||
self.errorMessage = "Failed to cancel task"
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func uncancelTask(id: Int32, completion: @escaping (Bool) -> Void) {
|
||||
guard let token = tokenStorage.getToken() else {
|
||||
errorMessage = "Not authenticated"
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
taskUncancelled = false
|
||||
|
||||
taskApi.uncancelTask(token: token, id: id) { result, error in
|
||||
if result is ApiResultSuccess<TaskCancelResponse> {
|
||||
sharedViewModel.uncancelTask(taskId: id) { success in
|
||||
Task { @MainActor in
|
||||
self.isLoading = false
|
||||
self.taskUncancelled = true
|
||||
completion(true)
|
||||
} else if let errorResult = result as? ApiResultError {
|
||||
self.errorMessage = errorResult.message
|
||||
if success.boolValue {
|
||||
self.taskUncancelled = true
|
||||
completion(true)
|
||||
} else {
|
||||
self.errorMessage = "Failed to uncancel task"
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func markInProgress(id: Int32, completion: @escaping (Bool) -> Void) {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
taskMarkedInProgress = false
|
||||
|
||||
sharedViewModel.markInProgress(taskId: id) { success in
|
||||
Task { @MainActor in
|
||||
self.isLoading = false
|
||||
completion(false)
|
||||
} else if let error = error {
|
||||
self.errorMessage = error.localizedDescription
|
||||
if success.boolValue {
|
||||
self.taskMarkedInProgress = true
|
||||
completion(true)
|
||||
} else {
|
||||
self.errorMessage = "Failed to mark task in progress"
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func archiveTask(id: Int32, completion: @escaping (Bool) -> Void) {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
taskArchived = false
|
||||
|
||||
sharedViewModel.archiveTask(taskId: id) { success in
|
||||
Task { @MainActor in
|
||||
self.isLoading = false
|
||||
completion(false)
|
||||
if success.boolValue {
|
||||
self.taskArchived = true
|
||||
completion(true)
|
||||
} else {
|
||||
self.errorMessage = "Failed to archive task"
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func unarchiveTask(id: Int32, completion: @escaping (Bool) -> Void) {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
taskUnarchived = false
|
||||
|
||||
sharedViewModel.unarchiveTask(taskId: id) { success in
|
||||
Task { @MainActor in
|
||||
self.isLoading = false
|
||||
if success.boolValue {
|
||||
self.taskUnarchived = true
|
||||
completion(true)
|
||||
} else {
|
||||
self.errorMessage = "Failed to unarchive task"
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateTask(id: Int32, request: TaskCreateRequest, completion: @escaping (Bool) -> Void) {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
taskUpdated = false
|
||||
|
||||
sharedViewModel.updateTask(taskId: id, request: request) { success in
|
||||
Task { @MainActor in
|
||||
self.isLoading = false
|
||||
if success.boolValue {
|
||||
self.taskUpdated = true
|
||||
completion(true)
|
||||
} else {
|
||||
self.errorMessage = "Failed to update task"
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,135 +178,6 @@ 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 archiveTask(id: Int32, completion: @escaping (Bool) -> Void) {
|
||||
guard let token = tokenStorage.getToken() else {
|
||||
errorMessage = "Not authenticated"
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
taskArchived = false
|
||||
|
||||
taskApi.archiveTask(token: token, id: id) { result, error in
|
||||
if result is ApiResultSuccess<TaskCancelResponse> {
|
||||
self.isLoading = false
|
||||
self.taskArchived = 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 unarchiveTask(id: Int32, completion: @escaping (Bool) -> Void) {
|
||||
guard let token = tokenStorage.getToken() else {
|
||||
errorMessage = "Not authenticated"
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
taskUnarchived = false
|
||||
|
||||
taskApi.unarchiveTask(token: token, id: id) { result, error in
|
||||
if result is ApiResultSuccess<TaskCancelResponse> {
|
||||
self.isLoading = false
|
||||
self.taskUnarchived = 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,
|
||||
contractor: nil,
|
||||
completedByName: nil,
|
||||
completedByPhone: nil,
|
||||
completedByEmail: nil,
|
||||
companyName: 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
|
||||
|
||||
Reference in New Issue
Block a user