diff --git a/mobile.xcodeproj/project.pbxproj b/mobile.xcodeproj/project.pbxproj index bbbbec0..03aa000 100644 --- a/mobile.xcodeproj/project.pbxproj +++ b/mobile.xcodeproj/project.pbxproj @@ -45,6 +45,7 @@ 227E9C992AABBB4100049CAB /* JobManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227E9C982AABBB4100049CAB /* JobManager.swift */; }; 227E9C9B2AABBB5500049CAB /* ApplicationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227E9C9A2AABBB5500049CAB /* ApplicationManager.swift */; }; 227E9C9D2AABECBA00049CAB /* OwnApplicationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227E9C9C2AABECBA00049CAB /* OwnApplicationButton.swift */; }; + 228800CE2AB10C4300AE9A4B /* FileFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228800CD2AB10C4300AE9A4B /* FileFormatter.swift */; }; 228E0CEB2AADFF8100AEF8A2 /* JobsMapAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228E0CEA2AADFF8100AEF8A2 /* JobsMapAnnotation.swift */; }; 22B8041B2AA890BC008D7108 /* URLImage in Frameworks */ = {isa = PBXBuildFile; productRef = 22B8041A2AA890BC008D7108 /* URLImage */; }; 22B8041D2AA891ED008D7108 /* JobDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22B8041C2AA891ED008D7108 /* JobDetail.swift */; }; @@ -148,6 +149,7 @@ 227E9C982AABBB4100049CAB /* JobManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobManager.swift; sourceTree = ""; }; 227E9C9A2AABBB5500049CAB /* ApplicationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationManager.swift; sourceTree = ""; }; 227E9C9C2AABECBA00049CAB /* OwnApplicationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnApplicationButton.swift; sourceTree = ""; }; + 228800CD2AB10C4300AE9A4B /* FileFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileFormatter.swift; sourceTree = ""; }; 228E0CEA2AADFF8100AEF8A2 /* JobsMapAnnotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobsMapAnnotation.swift; sourceTree = ""; }; 22B8041C2AA891ED008D7108 /* JobDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobDetail.swift; sourceTree = ""; }; 22B8041E2AA8F813008D7108 /* APIHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIHandler.swift; sourceTree = ""; }; @@ -369,6 +371,7 @@ isa = PBXGroup; children = ( 227E9C902AAB9A5500049CAB /* DateFormatter.swift */, + 228800CD2AB10C4300AE9A4B /* FileFormatter.swift */, 22D927872AA7FAE90017678C /* HtmlView.swift */, 22430F972AAA5CEC00747827 /* ImagePicker.swift */, 22D927822AA7B4FE0017678C /* JobListView.swift */, @@ -634,6 +637,7 @@ 227E9C9B2AABBB5500049CAB /* ApplicationManager.swift in Sources */, 2216EA832A8D762400C970DD /* ExploreView.swift in Sources */, 22B8046D2AA91D54008D7108 /* OwnApplicationsView.swift in Sources */, + 228800CE2AB10C4300AE9A4B /* FileFormatter.swift in Sources */, 22B8046F2AA92781008D7108 /* AccountInfo.swift in Sources */, 22430FA22AAAB52900747827 /* AppInfoView.swift in Sources */, 22B8041D2AA891ED008D7108 /* JobDetail.swift in Sources */, diff --git a/mobile.xcodeproj/project.xcworkspace/xcuserdata/cb.xcuserdatad/UserInterfaceState.xcuserstate b/mobile.xcodeproj/project.xcworkspace/xcuserdata/cb.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..68495f9 Binary files /dev/null and b/mobile.xcodeproj/project.xcworkspace/xcuserdata/cb.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/mobile/Controllers/ApplicationManager.swift b/mobile/Controllers/ApplicationManager.swift index e578eb7..3affd56 100644 --- a/mobile/Controllers/ApplicationManager.swift +++ b/mobile/Controllers/ApplicationManager.swift @@ -136,17 +136,18 @@ class ApplicationManager: ObservableObject { } } - func submitApplication(iteration: Int, jobId: Int, userId: Int, message: String, cv: Data?) { + func submitApplication(iteration: Int, jobId: Int, userId: Int, message: String, cv: Data?, format: [String]?, completion: @escaping () -> Void) { print("Iteration \(iteration)") let application = Application(jobId: jobId, userId: userId, createdAt: "", updatedAt: "", status: "0", applicationText: message, applicationDocuments: nil, response: nil) if let accessToken = authenticationManager.getAccessToken() { - APIManager.createApplication(accessToken: accessToken, application: application, cv: cv) { tokenResponse in + APIManager.createApplication(accessToken: accessToken, application: application, cv: cv, format: format) { tokenResponse in switch tokenResponse { case .success(let apiResponse): DispatchQueue.main.async { print("case .success \(apiResponse)") self.errorHandlingManager.errorMessage = nil self.ownApplications.append(application) + completion() } case .failure(let error): DispatchQueue.main.async { @@ -158,18 +159,21 @@ class ApplicationManager: ObservableObject { // Refresh the access token and retry the request self.authenticationManager.requestAccessToken() { accessTokenSuccess in if accessTokenSuccess{ - self.submitApplication(iteration: 1, jobId: jobId, userId: userId, message: message, cv: cv) + self.submitApplication(iteration: 1, jobId: jobId, userId: userId, message: message, cv: cv, format: format, completion: completion) } else { self.errorHandlingManager.errorMessage = error.localizedDescription + completion() } } } else { print("case .else") self.errorHandlingManager.errorMessage = error.localizedDescription + completion() } } else { self.authenticationManager.isAuthenticated = false self.errorHandlingManager.errorMessage = "Tokens expired. Log in to refresh tokens." + completion() } } } diff --git a/mobile/Controllers/ErrorHandlingManager.swift b/mobile/Controllers/ErrorHandlingManager.swift index 58d4f91..4c2d467 100644 --- a/mobile/Controllers/ErrorHandlingManager.swift +++ b/mobile/Controllers/ErrorHandlingManager.swift @@ -26,6 +26,7 @@ enum APIError: Error { case forbidden // 403 case notFound // 404 case noContent(String) + case fileFormatError(String) // TODO: Add other error cases as needed @@ -57,6 +58,8 @@ enum APIError: Error { return "Not found. This page does not exist." case .noContent(let cause): return "No \(cause) found" + case .fileFormatError(let message): + return "File Format Error: \(message)" } } } diff --git a/mobile/Networking/APIManager.swift b/mobile/Networking/APIManager.swift index 05b75e3..3edc99f 100644 --- a/mobile/Networking/APIManager.swift +++ b/mobile/Networking/APIManager.swift @@ -167,9 +167,9 @@ class APIManager { /// - Parameters: /// - accessToken: The user's access token for authentication. /// - completion: A closure that receives a `Result` with a `APIResponse` or an `APIError`. - static func createApplication(accessToken: String, application: Application, cv: Data?, completion: @escaping (Result) -> Void) { - if let data = cv { - ApplicationHandler.createAttachmentApplication(accessToken: accessToken, application: application, attachment: data, completion: completion) + static func createApplication(accessToken: String, application: Application, cv: Data?, format: [String]?, completion: @escaping (Result) -> Void) { + if let data = cv, let format = format { + ApplicationHandler.createAttachmentApplication(accessToken: accessToken, application: application, attachment: data, format: format, completion: completion) } else { ApplicationHandler.createNormalApplication(accessToken: accessToken, application: application, completion: completion) } diff --git a/mobile/Networking/ApplicationHandler.swift b/mobile/Networking/ApplicationHandler.swift index b9885c4..6c219ed 100644 --- a/mobile/Networking/ApplicationHandler.swift +++ b/mobile/Networking/ApplicationHandler.swift @@ -103,7 +103,7 @@ class ApplicationHandler { } } - static func createAttachmentApplication(accessToken: String, application: Application, attachment: Data, completion: @escaping (Result) -> Void) { + static func createAttachmentApplication(accessToken: String, application: Application, attachment: Data, format: [String], completion: @escaping (Result) -> Void) { print("Started creating application with: \naccess_token: \(accessToken)\njobId: \(application.jobId)\nuserId: \(application.userId)") // Create a unique boundary string @@ -133,14 +133,16 @@ class ApplicationHandler { body.append("Content-Disposition: form-data; name=\"application_text\"\r\n\r\n".data(using: .utf8)!) body.append("\(application.applicationText)\r\n".data(using: .utf8)!) - body.append("--\(boundary)\r\n".data(using: .utf8)!) - body.append("Content-Disposition: form-data; name=\"application_attachment\"; filename=\"\(application.jobId)_\(application.userId)_cv.pdf\"\r\n".data(using: .utf8)!) - body.append("Content-Type: application/pdf\r\n\r\n".data(using: .utf8)!) - - // Append the file data here - body.append(attachment) + // Update the "Content-Type" and filename based on the specified format + for formatItem in format { + body.append("--\(boundary)\r\n".data(using: .utf8)!) + let filename = "\(application.jobId)_\(application.userId)_cv\(formatItem)" + body.append("Content-Disposition: form-data; name=\"application_attachment\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!) + body.append("Content-Type: application/\(formatItem)\r\n\r\n".data(using: .utf8)!) + body.append(attachment) + body.append("\r\n".data(using: .utf8)!) + } - body.append("\r\n".data(using: .utf8)!) body.append("--\(boundary)--\r\n".data(using: .utf8)!) request.httpBody = body diff --git a/mobile/Views/Components/ApplicationPopup.swift b/mobile/Views/Components/ApplicationPopup.swift index 6e9e96c..36f703b 100644 --- a/mobile/Views/Components/ApplicationPopup.swift +++ b/mobile/Views/Components/ApplicationPopup.swift @@ -16,6 +16,7 @@ struct ApplicationPopup: View { @Binding var isVisible: Bool @Binding var message: String @State var cvData: Data? + @State var fileName: String = "" @State private var isLoading = false @State private var isPickingDocument = false var job: Job @@ -60,11 +61,11 @@ struct ApplicationPopup: View { .cornerRadius(10) .overlay( RoundedRectangle(cornerRadius: 10) - .foregroundColor(Color("SuccessColor")) + .foregroundColor(Color("SecondaryColor")) .border(Color("FgColor"), width: 3) .padding(.horizontal, 10.0) .overlay( - Text("UPLOAD CV [\(String(describing: job.allowedCvFormat))]") + Text(fileName == "" ? "UPLOAD CV \(job.allowedCvFormat.joined(separator: ", "))" : fileName) .font(.headline) .fontWeight(.black) .foregroundColor(Color.white) @@ -73,25 +74,29 @@ struct ApplicationPopup: View { } .padding() .cornerRadius(10) - .fileImporter(isPresented: $isPickingDocument, allowedContentTypes: [.pdf], onCompletion: handleDocumentSelection) + .fileImporter(isPresented: $isPickingDocument, allowedContentTypes: FileFormatter.toUTType(allowedCvFormat: job.allowedCvFormat), onCompletion: handleDocumentSelection) } Button(action: { - applicationManager.submitApplication(iteration: 0, jobId: job.jobId, userId: authenticationManager.current.userId, message: message, cv: cvData) - isVisible = false - }) { + isLoading = true + DispatchQueue.main.async { + applicationManager.submitApplication(iteration: 0, jobId: job.jobId, userId: authenticationManager.current.userId, message: message, cv: cvData, format: job.allowedCvFormat) { + isLoading = false + isVisible = false + } + } }) { RoundedRectangle(cornerRadius: 10) .foregroundColor(Color("FeedBgColor")) .cornerRadius(10) .overlay( RoundedRectangle(cornerRadius: 10) - .foregroundColor(Color("SuccessColor")) + .foregroundColor(cvData != nil || !job.cvRequired ? Color("SuccessColor") : Color.gray) .border(Color("FgColor"), width: 3) .padding(.horizontal, 10.0) .overlay( Text("APPLY") - .font(/*@START_MENU_TOKEN@*/.title/*@END_MENU_TOKEN@*/) + .font(.title) .fontWeight(.black) .foregroundColor(Color.white) ) @@ -99,24 +104,25 @@ struct ApplicationPopup: View { } .padding() .cornerRadius(10) - + .disabled(cvData == nil && job.cvRequired) } ) ) } } - private func handleDocumentSelection(result: Result) { - if case .success(let url) = result { + func handleDocumentSelection(result: Result) { + switch result { + case .success(let selectedURL): do { - cvData = try Data(contentsOf: url) + cvData = try Data(contentsOf: selectedURL) } catch { - // Handle error while reading file data print("Error reading file data: \(error)") } - } else if case .failure(let error) = result { - // Handle document picker error - print("Document picker error: \(error)") + fileName = selectedURL.lastPathComponent + case .failure(let error): + print("File Selection Error: \(error.localizedDescription)") } } + } diff --git a/mobile/Views/Shared/FileFormatter.swift b/mobile/Views/Shared/FileFormatter.swift new file mode 100644 index 0000000..11fcefa --- /dev/null +++ b/mobile/Views/Shared/FileFormatter.swift @@ -0,0 +1,49 @@ +// +// FileFormatter.swift +// mobile +// +// Created by cb on 12.09.23. +// + +import Foundation +import UniformTypeIdentifiers + +struct FileFormatter { + + static func toUTType(allowedCvFormat: [String]) -> [UTType] { + let recognizedFileExtensions: [String: UTType] = [ + ".pdf": .pdf, + ".xml": .xml, + ".txt": .plainText, + ".docx": UTType("org.openxmlformats.wordprocessingml.document")! + ] + + var allowedCvTypes: [UTType] = [] + + for fileExtension in allowedCvFormat { + let lowercasedExtension = fileExtension.lowercased() + + if let utType = recognizedFileExtensions[lowercasedExtension] { + allowedCvTypes.append(utType) + } else { + // Handle custom or unsupported file extensions here + print("Custom or unsupported file extension: \(fileExtension)") + // Instead of fatalError, you can choose to handle it differently + } + } + print("STRINGTypes: \(allowedCvFormat)") + print("UTTypes: \(allowedCvTypes)") + return allowedCvTypes + } + + func handleDocumentSelection(fileName: String, result: Result, completion: @escaping (Result) -> Void) { + switch result { + case .success(let selectedURL): + return completion(.success(selectedURL.lastPathComponent)) + + case .failure(let error): + print("File Selection Error: \(error.localizedDescription)") + return completion(.failure(APIError.fileFormatError(error.localizedDescription))) + } + } +}