diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index b58b603..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 72bf4c7..c2d908a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,8 +3,10 @@ plugins { } android { - namespace = "com.campusaula.edbole.kanban_clone_android" - compileSdk = 36 + namespace = "com.campusaula.edbole.KanbanCloneAndroid" + compileSdk { + version = release(36) + } defaultConfig { applicationId = "com.campusaula.edbole.KanbanCloneAndroid" @@ -37,19 +39,6 @@ dependencies { implementation(libs.material) implementation(libs.androidx.activity) implementation(libs.androidx.constraintlayout) - - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") - - implementation("com.squareup.okhttp3:okhttp:4.11.0") - implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") - implementation("com.squareup.retrofit2:retrofit:2.9.0") - implementation("com.squareup.retrofit2:converter-gson:2.9.0") - - implementation("com.auth0.android:jwtdecode:2.0.1") - implementation(libs.androidx.recyclerview) - testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/androidTest/java/com/campusaula/edbole/kanban_clone_android/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/campusaula/edbole/KanbanCloneAndroid/ExampleInstrumentedTest.kt similarity index 92% rename from app/src/androidTest/java/com/campusaula/edbole/kanban_clone_android/ExampleInstrumentedTest.kt rename to app/src/androidTest/java/com/campusaula/edbole/KanbanCloneAndroid/ExampleInstrumentedTest.kt index d6c0fca..1548188 100644 --- a/app/src/androidTest/java/com/campusaula/edbole/kanban_clone_android/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/campusaula/edbole/KanbanCloneAndroid/ExampleInstrumentedTest.kt @@ -1,4 +1,4 @@ -package com.campusaula.edbole.kanban_clone_android +package com.campusaula.edbole.KanbanCloneAndroid import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d5f2638..26642ca 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,9 +2,6 @@ - - - + android:theme="@style/Theme.KanbanCloneAndroid"> - - - - - - - diff --git a/app/src/main/java/com/campusaula/edbole/KanbanCloneAndroid/MainActivity.kt b/app/src/main/java/com/campusaula/edbole/KanbanCloneAndroid/MainActivity.kt new file mode 100644 index 0000000..4a9ba34 --- /dev/null +++ b/app/src/main/java/com/campusaula/edbole/KanbanCloneAndroid/MainActivity.kt @@ -0,0 +1,20 @@ +package com.campusaula.edbole.KanbanCloneAndroid + +import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContentView(R.layout.activity_main) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/kanban/Auth.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/kanban/Auth.kt deleted file mode 100644 index 63af700..0000000 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/kanban/Auth.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.campusaula.edbole.kanban_clone_android.kanban - -data class LoginResponse( - val message: String?, - val user: LoginUser? -) - -data class LoginUser( - val id: String?, - val name: String?, - val email: String? -) - -// Error response from the API (e.g. 401 Unauthorized) -data class ErrorResponse( - val detail: String? -) diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/kanban/Project.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/kanban/Project.kt deleted file mode 100644 index 70c8c21..0000000 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/kanban/Project.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.campusaula.edbole.kanban_clone_android.kanban - -import com.google.gson.annotations.SerializedName - -class Project{ - val id: Int = 0 - val name: String = "" - val description: String = "" - val users: List = emptyList() - val tasks: List = emptyList() - - - override fun toString(): String { - return "Project(id=$id, name='$name', description='$description', users=$users, tasks=$tasks)" - } -} - -data class ProjectBase( - @SerializedName("id") - val id : Int, - @SerializedName("name") - val name : String, - @SerializedName("description") - val description : String -) - -data class ProjectCreate( - @SerializedName("name") - val name : String, - @SerializedName("description") - val description : String -) diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/kanban/Task.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/kanban/Task.kt deleted file mode 100644 index 87f99c5..0000000 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/kanban/Task.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.campusaula.edbole.kanban_clone_android.kanban - -import com.google.gson.annotations.SerializedName - -enum class TaskStatus { - @SerializedName("pending") - PENDING, - @SerializedName("in_progress") - IN_PROGRESS, - @SerializedName("completed") - COMPLETED, - @SerializedName("failed") - FAILED, - @SerializedName("stashed") - STASHED -} - -class Task { - val id: Int = 0 - val title: String = "" - val description: String = "" - var status: TaskStatus = TaskStatus.PENDING - val project: Project? = null -} - -data class TaskBase( - @SerializedName("id") - val id : Int, - - @SerializedName("title") - val title : String, - - @SerializedName("description") - val description : String, - - @SerializedName("status") - val status: TaskStatus -) - -data class TaskUpdate( - @SerializedName("title") - val title : String?, - - @SerializedName("description") - val description : String?, - - @SerializedName("status") - val status: TaskStatus? -) diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/kanban/User.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/kanban/User.kt deleted file mode 100644 index 3c4496c..0000000 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/kanban/User.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.campusaula.edbole.kanban_clone_android.kanban - -import com.google.gson.annotations.SerializedName - -class User { - val id: Int = 0 - val name : String = "" - val email: String = "" - val password: String = "" - val projects: List = emptyList() -} - -data class UserBase ( - @SerializedName("id") - val id: Int, - @SerializedName("name") - val name: String, - @SerializedName("email") - val email: String -) - -data class ProjectUser( - @SerializedName("id") - val id: Int, - @SerializedName("name") - val name: String, - @SerializedName("email") - val email: String, - @SerializedName("projects") - val projects: List -) - -data class UserLogin ( - @SerializedName("email") - val email: String, - @SerializedName("password") - val password: String -) - -data class UserCreate ( - @SerializedName("name") - val name: String, - @SerializedName("email") - val email: String, - @SerializedName("password") - val password: String -) - -data class UserUpdatePassword( - @SerializedName("password") - val password: String, - @SerializedName("new_password") - val newPassword: String -) - diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/network/ApiService.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/network/ApiService.kt deleted file mode 100644 index 296b6a1..0000000 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/network/ApiService.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.campusaula.edbole.kanban_clone_android.network - -import com.campusaula.edbole.kanban_clone_android.kanban.* -import retrofit2.Response -import retrofit2.http.* - -interface ApiService { - @GET("/ping") - suspend fun ping(): Response - - @POST("auth/login/") - suspend fun login(@Body userLogin: UserLogin): Response - - @GET("me/logout/") - suspend fun logout(): Response - - @DELETE("me/delete-me/") - suspend fun deleteMe(): Response - - @GET("me/") - suspend fun getMe(): Response - - @GET("users/{user_id}/") - suspend fun getUserById(@Path("user_id") userId: Int): Response - - @GET("users/{user_id}/projects/") - suspend fun getUserProjectsByUserId(@Path("user_id") userId: Int): Response> - - @POST("users/") - suspend fun createUser(@Body userLogin: UserCreate): Response - - // Projects endpoints - - @GET("projects/") - suspend fun getAllProjects(): Response> - - @GET("projects/{project_id}/") - suspend fun getProjectById(@Path("project_id") projectId: Int): Response - - @GET("projects/{project_id}/users/") - suspend fun getProjectUsers(@Path("project_id") projectId: Int): Response> - - @POST("projects/{project_id}/users/") - suspend fun addProjectCollaborator( - @Path("project_id") projectId: Int, - @Body email: Map - ): Response - - @DELETE("projects/{project_id}/users/{user_id}/") - suspend fun removeProjectCollaborator( - @Path("project_id") projectId: Int, - @Path("user_id") userId: Int - ): Response - - @POST("projects/") - suspend fun createProject(@Body projectCreate: ProjectCreate): Response - - @PUT("projects/{project_id}/") - suspend fun updateProject(@Path("project_id") projectId: Int, @Body projectCreate: ProjectCreate): Response - - @DELETE("projects/{project_id}/") - suspend fun deleteProject(@Path("project_id") projectId: Int): Response - - // Tasks endpoints - - @GET("projects/{project_id}/tasks/") - suspend fun getProjectTasks(@Path("project_id") projectId: Int): Response> - - @POST("projects/{project_id}/tasks/") - suspend fun createTask(@Path("project_id") projectId: Int, @Body taskBase: TaskBase): Response - - @PUT("projects/{project_id}/tasks/{task_id}/") - suspend fun updateProjectTask( - @Path("project_id") projectId: Int, - @Path("task_id") taskId: Int, - @Body taskUpdate: TaskUpdate - ): Response - - @DELETE("projects/{project_id}/tasks/{task_id}/") - suspend fun deleteProjectTask( - @Path("project_id") projectId: Int, - @Path("task_id") taskId: Int - ): Response - -} diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/network/AuthCookieJar.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/network/AuthCookieJar.kt deleted file mode 100644 index f9ee3f4..0000000 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/network/AuthCookieJar.kt +++ /dev/null @@ -1,186 +0,0 @@ -package com.campusaula.edbole.kanban_clone_android.network - -import android.content.Context -import okhttp3.Cookie -import okhttp3.CookieJar -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import com.google.gson.Gson -import androidx.core.content.edit -import java.util.concurrent.ConcurrentHashMap -import android.util.Base64 -import com.auth0.android.jwt.JWT - - -private data class StoredCookie( - val name: String, - val value: String, - val expiresAt: Long, - val domain: String, - val path: String, - val secure: Boolean, - val httpOnly: Boolean, - val hostOnly: Boolean, - val iat: Long? = null, - val exp: Long? = null -) - -class AuthCookieJar( - context: Context, - private val authCookieNames: Set = setOf("access_token", "session", "auth", "auth_token", "JSESSIONID") -) : CookieJar { - - private val prefs = context.applicationContext.getSharedPreferences("auth_cookie_prefs", Context.MODE_PRIVATE) - private val lock = Any() - private val gson = Gson() - private val knownHosts = ConcurrentHashMap.newKeySet() - - override fun saveFromResponse(url: HttpUrl, cookies: List) { - if (cookies.isEmpty()) return - val hostKey = url.host - synchronized(lock) { - val existing = prefs.getStringSet(hostKey, emptySet())?.toMutableSet() ?: mutableSetOf() - val now = System.currentTimeMillis() - for (cookie in cookies) { - if (cookie.expiresAt <= now) continue - existing.removeIf { it.startsWith("${'$'}{cookie.name}|${'$'}{cookie.path}|") } - - // default stored expiresAt comes from cookie.expiresAt (already in ms) - var storedExpires = cookie.expiresAt - var iatVal: Long? = null - var expVal: Long? = null - - // if this is an auth cookie, try to decode JWT payload to obtain exp/iat - if (cookie.name in authCookieNames) { - try { - val (expSec, iatSec) = decodeJwtExpIat(cookie.value) - if (expSec != null) { - expVal = expSec - // convert to millis - storedExpires = expSec * 1000L - } - if (iatSec != null) { - iatVal = iatSec - } - } catch (_: Exception) { - // ignore, keep cookie.expiresAt - } - } - - val stored = StoredCookie( - name = cookie.name, - value = cookie.value, - expiresAt = storedExpires, - domain = cookie.domain, - path = cookie.path, - secure = cookie.secure, - httpOnly = cookie.httpOnly, - hostOnly = cookie.hostOnly, - iat = iatVal, - exp = expVal - ) - // serialize to json explicitly and use it - val json = gson.toJson(stored) - existing.add("${cookie.name}|${cookie.path}|${json}") - } - prefs.edit { putStringSet(hostKey, existing) } - knownHosts.add(hostKey) - } - } - - override fun loadForRequest(url: HttpUrl): List { - val hostKey = url.host - val result = ArrayList() - val now = System.currentTimeMillis() - synchronized(lock) { - val set = prefs.getStringSet(hostKey, emptySet()) ?: emptySet() - val newSet = mutableSetOf() - for (s in set) { - try { - val jsonStart = s.indexOf('{') - val json = if (jsonStart >= 0) s.substring(jsonStart) else s - val stored = gson.fromJson(json, StoredCookie::class.java) - if (stored.expiresAt <= now) continue - val builder = Cookie.Builder() - .name(stored.name) - .value(stored.value) - .expiresAt(stored.expiresAt) - .path(stored.path) - if (stored.hostOnly) builder.hostOnlyDomain(stored.domain) else builder.domain(stored.domain) - if (stored.secure) builder.secure() - if (stored.httpOnly) builder.httpOnly() - val cookie = builder.build() - if (cookie.matches(url)) { - result.add(cookie) - } - newSet.add(s) - } catch (_: Exception) { - // skip malformed - } - } - prefs.edit { putStringSet(hostKey, newSet) } - if (newSet.isNotEmpty()) knownHosts.add(hostKey) - } - return result - } - - fun saveFromSetCookieHeader(setCookieHeader: String, requestUrl: String) { - val url = requestUrl.toHttpUrlOrNull() ?: return - val lines = setCookieHeader.split('\n').map { it.trim() }.filter { it.isNotEmpty() } - val parsed = mutableListOf() - for (line in lines) { - Cookie.parse(url, line)?.let { parsed.add(it) } - } - if (parsed.isNotEmpty()) saveFromResponse(url, parsed) - } - - fun getCookieHeaderForUrl(urlString: String): String? { - val url = urlString.toHttpUrlOrNull() ?: return null - val cookies = loadForRequest(url) - if (cookies.isEmpty()) return null - return cookies.joinToString("; ") { "${'$'}{it.name}=${'$'}{it.value}" } - } - - fun getAuthCookieForUrl(urlString: String): String? { - val url = urlString.toHttpUrlOrNull() ?: return null - val host = url.host - val set = prefs.getStringSet(host, emptySet()) ?: return null - val now = System.currentTimeMillis() - for (s in set) { - try { - val jsonStart = s.indexOf('{') - val json = if (jsonStart >= 0) s.substring(jsonStart) else s - val stored = gson.fromJson(json, StoredCookie::class.java) - if (stored.expiresAt <= now) continue - if (stored.name in authCookieNames) return stored.value - } catch (_: Exception) { - } - } - return null - } - - /** Remove all cookies stored for the given host. */ - fun clearCookiesForHost(host: String) { - synchronized(lock) { - prefs.edit { remove(host) } - knownHosts.remove(host) - } - } - - private fun decodeJwtExpIat(token: String): Pair { - return try { - val jwt = JWT(token) - // expiresAt / issuedAt -> java.util.Date? - val expSec = jwt.expiresAt?.time?.div(1000) // segundos desde epoch - val iatSec = jwt.issuedAt?.time?.div(1000) - Pair(expSec, iatSec) - } catch (e: Exception) { - Pair(null, null) - } - } - - private fun padBase64(b64: String): String { - val rem = b64.length % 4 - return if (rem == 0) b64 else b64 + "=".repeat(4 - rem) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/network/RetrofitInstance.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/network/RetrofitInstance.kt deleted file mode 100644 index c0c778e..0000000 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/network/RetrofitInstance.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.campusaula.edbole.kanban_clone_android.network - -import android.content.Context -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory -import java.util.concurrent.TimeUnit - -object RetrofitInstance { - private const val BASE_URL = "http://10.0.2.2:8000/" - - @Volatile - private var retrofit: Retrofit? = null - private var cookieJar: AuthCookieJar? = null - - private val logging = HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY - } - - fun getRetrofit(context: Context): Retrofit { - return retrofit ?: synchronized(this) { - retrofit ?: buildRetrofit(context.applicationContext).also { retrofit = it } - } - } - - private fun buildRetrofit(context: Context): Retrofit { - cookieJar = AuthCookieJar(context) - - val client = OkHttpClient.Builder() - .cookieJar(cookieJar!!) - .addInterceptor(logging) - .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .build() - - return Retrofit.Builder() - .baseUrl(BASE_URL) - .client(client) - .addConverterFactory(GsonConverterFactory.create()) - .build() - } - - /** Helper: obtiene el valor de la cookie de autenticación para una URL. */ - fun getAuthCookieForUrl(url: String): String? { - return cookieJar?.getAuthCookieForUrl(url) - } - - fun getCookieHeaderForUrl(url: String): String? { - return cookieJar?.getCookieHeaderForUrl(url) - } - - fun clearCookiesForHost(host: String) { - cookieJar?.clearCookiesForHost(host) - } - - fun cookieJarInstance(): AuthCookieJar? = cookieJar -} diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/CollaboratorAddActivity.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/CollaboratorAddActivity.kt deleted file mode 100644 index e68042e..0000000 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/CollaboratorAddActivity.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.campusaula.edbole.kanban_clone_android.ui - -import android.content.Intent -import android.os.Bundle -import android.util.Log -import android.util.Patterns -import android.widget.Button -import android.widget.EditText -import android.widget.Toast -import androidx.activity.enableEdgeToEdge -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.lifecycle.lifecycleScope -import com.campusaula.edbole.kanban_clone_android.R -import com.campusaula.edbole.kanban_clone_android.network.ApiService -import com.campusaula.edbole.kanban_clone_android.network.RetrofitInstance -import com.google.android.material.floatingactionbutton.FloatingActionButton -import kotlinx.coroutines.launch - -class CollaboratorAddActivity : AppCompatActivity() { - - private lateinit var api: ApiService - - private lateinit var returnActionButton: FloatingActionButton - private lateinit var collaboratorEmailInput: EditText - private lateinit var addCollaboratorButton: Button - - private var projectId: Int = -1 - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContentView(R.layout.activity_collaborator_add) - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> - val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) - insets - } - - api = RetrofitInstance.getRetrofit(applicationContext).create(ApiService::class.java) - - // Get project ID from intent - projectId = intent.getIntExtra("project_id", -1) - - if (projectId == -1) { - Toast.makeText(this, "Error: Invalid project ID", Toast.LENGTH_SHORT).show() - finish() - return - } - - // Initialize views - returnActionButton = findViewById(R.id.returnActionButton) - collaboratorEmailInput = findViewById(R.id.collaboratorEmailInput) - addCollaboratorButton = findViewById(R.id.addCollaboratorButton) - - // Set up button listeners - returnActionButton.setOnClickListener { - finish() - - val intent = Intent(this@CollaboratorAddActivity, ProjectDetailActivity::class.java) - intent.putExtra("project_id", projectId) - startActivity(intent) - - } - - addCollaboratorButton.setOnClickListener { - addCollaborator() - } - } - - private fun addCollaborator() { - val user_email = collaboratorEmailInput.text.toString().trim() - - // Validate email - if (user_email.isEmpty()) { - Toast.makeText(this, "Email cannot be empty", Toast.LENGTH_SHORT).show() - return - } - - if (!Patterns.EMAIL_ADDRESS.matcher(user_email).matches()) { - Toast.makeText(this, "Please enter a valid email address", Toast.LENGTH_SHORT).show() - return - } - - lifecycleScope.launch { - try { - Log.d("CollaboratorAddActivity", "Adding collaborator: $user_email") - val emailBody = mapOf("user_email" to user_email) - val response = api.addProjectCollaborator(projectId, emailBody) - - if (response.isSuccessful) { - Log.d("CollaboratorAddActivity", "Collaborator added successfully") - Toast.makeText( - this@CollaboratorAddActivity, - "Collaborator added successfully", - Toast.LENGTH_SHORT - ).show() - setResult(RESULT_OK) - finish() - - // Reopen ProjectDetailActivity to show the new collaborator - val intent = Intent(this@CollaboratorAddActivity, ProjectDetailActivity::class.java) - intent.putExtra("project_id", projectId) - startActivity(intent) - } else { - val errorBody = response.errorBody()?.string() - Log.e("CollaboratorAddActivity", "Error adding collaborator: $errorBody") - Toast.makeText( - this@CollaboratorAddActivity, - "Error adding collaborator: ${response.code()}", - Toast.LENGTH_SHORT - ).show() - } - } catch (e: Exception) { - Log.e("CollaboratorAddActivity", "Exception adding collaborator: ${e.message}") - Toast.makeText( - this@CollaboratorAddActivity, - "Failed to add collaborator: ${e.message}", - Toast.LENGTH_SHORT - ).show() - } - } - } -} - diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/CreateProjectActivity.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/CreateProjectActivity.kt deleted file mode 100644 index 521add0..0000000 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/CreateProjectActivity.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.campusaula.edbole.kanban_clone_android.ui - -import android.content.Intent -import android.os.Bundle -import android.util.Log -import android.widget.Button -import android.widget.EditText -import android.widget.Toast -import androidx.activity.enableEdgeToEdge -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.lifecycle.lifecycleScope -import com.campusaula.edbole.kanban_clone_android.R -import com.campusaula.edbole.kanban_clone_android.kanban.ProjectCreate -import com.campusaula.edbole.kanban_clone_android.network.ApiService -import com.campusaula.edbole.kanban_clone_android.network.RetrofitInstance -import com.google.android.material.floatingactionbutton.FloatingActionButton -import kotlinx.coroutines.launch - - -class CreateProjectActivity : AppCompatActivity() { - - private lateinit var api: ApiService - - private lateinit var returnActionButton : FloatingActionButton - private lateinit var newProjectName : EditText - private lateinit var newProjectDescription : EditText - private lateinit var newProjectCreateButton : Button - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContentView(R.layout.activity_create_project) - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> - val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) - insets - } - - api = RetrofitInstance.getRetrofit(applicationContext).create(ApiService::class.java) - - returnActionButton = findViewById(R.id.returnActionButton) - returnActionButton.setOnClickListener { - finish() - - val intent = Intent(this@CreateProjectActivity, MainActivity::class.java) - startActivity(intent) - } - - newProjectName = findViewById(R.id.newProjectName) - newProjectDescription = findViewById(R.id.newProjectDescription) - - newProjectCreateButton = findViewById(R.id.newProjectCreateButton) - newProjectCreateButton.setOnClickListener { - val projectName = newProjectName.text.toString().trim() - val projectDescription = newProjectDescription.text.toString().trim() - - if (projectName.isEmpty()) { - Toast.makeText(this, "Project name cannot be empty", Toast.LENGTH_SHORT).show() - return@setOnClickListener - } - - lifecycleScope.launch { - try { - Log.d("CreateProjectActivity", "Creating project: $projectName") - val projectCreate = ProjectCreate(projectName, projectDescription) - val response = api.createProject(projectCreate) - - if (response.isSuccessful) { - Log.d("CreateProjectActivity", "Project created successfully: ${response.body()}") - Toast.makeText( - this@CreateProjectActivity, - "Project created successfully", - Toast.LENGTH_SHORT - ).show() - finish() - - // Volver a MainActivity para ver el nuevo proyecto - val intent = Intent(this@CreateProjectActivity, MainActivity::class.java) - startActivity(intent) - } else { - val errorBody = response.errorBody()?.string() - Log.e("CreateProjectActivity", "Error creating project: $errorBody") - Toast.makeText( - this@CreateProjectActivity, - "Error creating project: ${response.code()}", - Toast.LENGTH_SHORT - ).show() - } - } catch (e: Exception) { - Log.e("CreateProjectActivity", "Exception creating project: ${e.message}") - Toast.makeText( - this@CreateProjectActivity, - "Failed to create project: ${e.message}", - Toast.LENGTH_SHORT - ).show() - } - } - } - - } -} \ No newline at end of file diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/LoginActivity.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/LoginActivity.kt deleted file mode 100644 index 8b08d11..0000000 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/LoginActivity.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.campusaula.edbole.kanban_clone_android.ui - -import android.content.Intent -import android.os.Bundle -import android.widget.Toast -import androidx.activity.enableEdgeToEdge -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.AppCompatButton -import androidx.appcompat.widget.AppCompatEditText -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.lifecycle.lifecycleScope -import com.campusaula.edbole.kanban_clone_android.R -import com.campusaula.edbole.kanban_clone_android.network.ApiService -import com.campusaula.edbole.kanban_clone_android.network.RetrofitInstance -import com.campusaula.edbole.kanban_clone_android.kanban.ErrorResponse -import com.campusaula.edbole.kanban_clone_android.kanban.UserLogin -import com.google.gson.Gson -import kotlinx.coroutines.launch -import retrofit2.Retrofit - -class LoginActivity : AppCompatActivity() { - - private lateinit var emailInput : AppCompatEditText - private lateinit var passwordInput : AppCompatEditText - - private lateinit var loginButton : AppCompatButton - private lateinit var logonButton : AppCompatButton - - private lateinit var retrofit : Retrofit - private lateinit var api : ApiService - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContentView(R.layout.activity_login) - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> - val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) - insets - } - - retrofit = RetrofitInstance.getRetrofit(applicationContext) - api = retrofit.create(ApiService::class.java) - - emailInput = findViewById(R.id.emailInput) - passwordInput = findViewById(R.id.passwordInput) - loginButton = findViewById(R.id.loginButton) - logonButton = findViewById(R.id.logonButton) - - loginButton.setOnClickListener { - val email = emailInput.text.toString() - val password = passwordInput.text.toString() - - if (email.isEmpty() && password.isEmpty()) { - Toast.makeText(this, "Please enter email and password", Toast.LENGTH_SHORT).show() - return@setOnClickListener - } - - lifecycleScope.launch{ - try { - val loginResponse = api.login( - UserLogin( - email = email, - password = password - ) - ) - - val baseUrl = retrofit.baseUrl().toString() - val baseHost = retrofit.baseUrl().host - - if (loginResponse.isSuccessful) { - // Después del login exitoso OkHttp/CookieJar habrá guardado las cookies. - val authValue = RetrofitInstance.getAuthCookieForUrl(baseUrl) - if (authValue != null) { - Toast.makeText(this@LoginActivity, "Auth cookie guardada", Toast.LENGTH_SHORT).show() - } else { - Toast.makeText(this@LoginActivity, "Login OK pero no se encontró cookie de auth", Toast.LENGTH_SHORT).show() - } - // Navegar a MainActivity - val intent = Intent(this@LoginActivity, MainActivity::class.java) - startActivity(intent) - finish() - } else { - if (loginResponse.code() == 401) { - // parse error body if possible - val errBody = loginResponse.errorBody()?.string() - val gson = Gson() - val errMsg = try { - val err = gson.fromJson(errBody, ErrorResponse::class.java) - err.detail ?: "Unauthorized" - } catch (_: Exception) { - errBody ?: "Unauthorized" - } - // clear stored cookies for base host - RetrofitInstance.clearCookiesForHost(baseHost) - - Toast.makeText(this@LoginActivity, "Login failed (401): $errMsg", Toast.LENGTH_SHORT).show() - } else { - Toast.makeText(this@LoginActivity, "Login failed: ${loginResponse.code()}", Toast.LENGTH_SHORT).show() - } - } - - } catch (ex: Exception){ - Toast.makeText(this@LoginActivity, "Login failed: ${ex.message}", Toast.LENGTH_SHORT).show() - } - - } - } - } -} diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/MainActivity.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/MainActivity.kt deleted file mode 100644 index 2dcfa11..0000000 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/MainActivity.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.campusaula.edbole.kanban_clone_android.ui - -import android.content.Intent -import android.os.Bundle -import android.widget.Button -import android.widget.TextView -import android.widget.Toast -import androidx.activity.enableEdgeToEdge -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.RecyclerView -import com.campusaula.edbole.kanban_clone_android.R -import com.campusaula.edbole.kanban_clone_android.kanban.Project -import com.campusaula.edbole.kanban_clone_android.network.ApiService -import com.campusaula.edbole.kanban_clone_android.network.RetrofitInstance -import com.campusaula.edbole.kanban_clone_android.ui.adapters.ProjectItemAdapter -import com.google.android.material.floatingactionbutton.FloatingActionButton -import kotlinx.coroutines.launch - -class MainActivity : AppCompatActivity() { - - private lateinit var api: ApiService - private lateinit var projectList : List - - private lateinit var loggedInAs: TextView - private lateinit var logoutButton: Button - private lateinit var addProjectActionButton: FloatingActionButton - private lateinit var projectsRecyclerView: RecyclerView - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContentView(R.layout.activity_main) - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> - val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) - insets - } - - api = RetrofitInstance.getRetrofit(applicationContext).create(ApiService::class.java) - projectList = emptyList() - - /* Activity components */ - loggedInAs = findViewById(R.id.loggedInAs) - logoutButton = findViewById(R.id.logoutButton) - addProjectActionButton = findViewById(R.id.addProjectActionButton) - projectsRecyclerView = findViewById(R.id.projectsRecyclerView) - projectsRecyclerView.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this) - val adapter = ProjectItemAdapter(projectList) { project -> - val intent = Intent(this, ProjectDetailActivity::class.java) - intent.putExtra("project_id", project.id) - startActivity(intent) - } - projectsRecyclerView.adapter = adapter - - addProjectActionButton.setOnClickListener { - val intent = Intent(this, CreateProjectActivity::class.java) - startActivity(intent) - } - - /* Getting the logged-in user info */ - lifecycleScope.launch{ - - val getMe = api.getMe() - if (getMe.isSuccessful){ - val user = getMe.body() - loggedInAs.text = "Logged in as: ${user?.name}" - projectList = api.getAllProjects().body()!! - adapter.submitList(projectList) - } else { - val intent = Intent(this@MainActivity, LoginActivity::class.java) - startActivity(intent) - } - - } - - logoutButton.setOnClickListener { - lifecycleScope.launch { - val logoutResponse = api.logout() - if (logoutResponse.isSuccessful) { - // Clear cookies for the API host - RetrofitInstance.clearCookiesForHost( - "10.0.2.2" - ) - - // Navigate back to the login screen - val intent = - Intent(this@MainActivity, LoginActivity::class.java) - startActivity(intent) - finish() // Optional: close the MainActivity so it's removed from the back stack - } else { - Toast.makeText( - this@MainActivity, - "Logout failed", - Toast.LENGTH_SHORT - ).show() - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/ProjectDetailActivity.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/ProjectDetailActivity.kt deleted file mode 100644 index 4db305c..0000000 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/ProjectDetailActivity.kt +++ /dev/null @@ -1,255 +0,0 @@ -package com.campusaula.edbole.kanban_clone_android.ui - -import android.content.Intent -import android.health.connect.datatypes.units.Percentage -import android.os.Bundle -import android.util.Log -import android.widget.Button -import android.widget.TextView -import androidx.activity.enableEdgeToEdge -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.RecyclerView -import com.campusaula.edbole.kanban_clone_android.R -import com.campusaula.edbole.kanban_clone_android.kanban.Project -import com.campusaula.edbole.kanban_clone_android.kanban.Task -import com.campusaula.edbole.kanban_clone_android.kanban.TaskStatus -import com.campusaula.edbole.kanban_clone_android.network.ApiService -import com.campusaula.edbole.kanban_clone_android.network.RetrofitInstance -import com.campusaula.edbole.kanban_clone_android.ui.adapters.ProjectCollaboratorAdapter -import com.campusaula.edbole.kanban_clone_android.ui.adapters.ProjectTaskAdapter -import com.google.android.material.floatingactionbutton.FloatingActionButton -import kotlinx.coroutines.launch - -class ProjectDetailActivity : AppCompatActivity() { - - private lateinit var api: ApiService - - private lateinit var returnActionButton: FloatingActionButton - private lateinit var addTaskButton: Button - private lateinit var addCollaboratorButton: Button - private lateinit var editProjectButton: Button - private lateinit var deleteProjectButton: Button - - private lateinit var taskListRecycler: RecyclerView - private lateinit var collaboratorListRecycler: RecyclerView - private lateinit var collaboratorListAdapter: ProjectCollaboratorAdapter - private lateinit var taskListAdapter: ProjectTaskAdapter - - - private lateinit var projectTitleText : TextView - private lateinit var projectDescriptionText : TextView - private lateinit var completedPercentageText: TextView - - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContentView(R.layout.activity_project_detail) - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> - val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) - insets - } - - - api = RetrofitInstance.getRetrofit(applicationContext).create(ApiService::class.java) - val projectId = intent.getIntExtra("project_id", -1) - - projectTitleText = findViewById(R.id.projectTitleText) - projectDescriptionText = findViewById(R.id.projectDescriptionText) - completedPercentageText = findViewById(R.id.completedPercentageText) - - returnActionButton = findViewById(R.id.returnActionButton) - returnActionButton.setOnClickListener { finish() } - - addTaskButton = findViewById(R.id.addTaskButton) - addTaskButton.setOnClickListener { - val intent = Intent(this, TaskAddActivity::class.java) - intent.putExtra("project_id", projectId) - startActivity(intent) - finish() - } - - addCollaboratorButton = findViewById(R.id.addCollaboratorButton) - addCollaboratorButton.setOnClickListener { - val intent = Intent(this, CollaboratorAddActivity::class.java) - intent.putExtra("project_id", projectId) - startActivity(intent) - finish() - } - - taskListRecycler = findViewById(R.id.taskListRecycler) - taskListRecycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this) - taskListAdapter = ProjectTaskAdapter(emptyList(), api, projectId, { - updateCompletionRate() - }, { - finish() - }) - taskListRecycler.adapter = taskListAdapter - - collaboratorListRecycler = findViewById(R.id.collaboratorListRecycler) - collaboratorListRecycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this) - collaboratorListAdapter = ProjectCollaboratorAdapter(emptyList(), api, projectId) { - updateCollaboratorList() - } - collaboratorListRecycler.adapter = collaboratorListAdapter - - // Danger Zone buttons - editProjectButton = findViewById(R.id.editProjectButton) - editProjectButton.setOnClickListener { - val intent = Intent(this, ProjectEditActivity::class.java) - intent.putExtra("project_id", projectId) - intent.putExtra("project_name", projectTitleText.text.toString()) - intent.putExtra("project_description", projectDescriptionText.text.toString()) - startActivity(intent) - finish() - } - - deleteProjectButton = findViewById(R.id.deleteProjectButton) - deleteProjectButton.setOnClickListener { - deleteProject(projectId) - } - - if (projectId > 0) { - Log.d("ProjectDetailActivity", "Received project ID: $projectId") - lifecycleScope.launch { - try { - val projectResponse = api.getProjectById(projectId) - - if (projectResponse.isSuccessful && projectResponse.body() != null) { - Log.d("ProjectDetailActivity", "Fetched project: ${projectResponse.body()!!.name}") - val project = projectResponse.body()!! - - - Log.d("ProjectDetailActivity", "Displaying project details for: $project") - - projectTitleText.text = project.name - projectDescriptionText.text = project.description - - var percentageFinished = 0.0; - val collaborators = project.users - val tasks: List = project.tasks - val totalTasks: Int = tasks.size - - var completedTasks = 0 - for (task in tasks) { - if (task.status == TaskStatus.COMPLETED) { - completedTasks++ - } - } - - percentageFinished = if (totalTasks > 0) (completedTasks.toDouble() / totalTasks.toDouble()) * 100 else 0.0 - completedPercentageText.text = "Completed: ${"%.2f".format(percentageFinished)}%" - - taskListAdapter.submitList(tasks.toMutableList()) - collaboratorListAdapter.submitList(collaborators.toMutableList()) - - - } else { - Log.e("ProjectDetailActivity", "Failed to fetch project: ${projectResponse.code()} - ${projectResponse.message()}") - finish() - } - } catch (e: Exception) { - Log.e("ProjectDetailActivity", "Error fetching project", e) - finish() - } - } - } - else { - Log.e("ProjectDetailActivity", "No project ID found in intent") - finish() - } - - - - } - - private fun updateCompletionRate() { - lifecycleScope.launch { - try { - val projectId = intent.getIntExtra("project_id", -1) - val projectResponse = api.getProjectById(projectId) - - if (projectResponse.isSuccessful && projectResponse.body() != null) { - val project = projectResponse.body()!! - val tasks: List = project.tasks - val totalTasks: Int = tasks.size - - var completedTasks = 0 - for (task in tasks) { - if (task.status == TaskStatus.COMPLETED) { - completedTasks++ - } - } - - val percentageFinished = if (totalTasks > 0) (completedTasks.toDouble() / totalTasks.toDouble()) * 100 else 0.0 - completedPercentageText.text = "Completed: ${"%.2f".format(percentageFinished)}%" - - // Actualizar la lista de tareas también - taskListAdapter.submitList(tasks.toMutableList()) - } - } catch (e: Exception) { - Log.e("ProjectDetailActivity", "Error updating completion rate", e) - } - } - } - - private fun updateCollaboratorList() { - lifecycleScope.launch { - try { - val projectId = intent.getIntExtra("project_id", -1) - val projectResponse = api.getProjectById(projectId) - - if (projectResponse.isSuccessful && projectResponse.body() != null) { - val project = projectResponse.body()!! - val collaborators = project.users - - collaboratorListAdapter.submitList(collaborators.toMutableList()) - } - } catch (e: Exception) { - Log.e("ProjectDetailActivity", "Error updating collaborator list", e) - } - } - } - - private fun deleteProject(projectId: Int) { - lifecycleScope.launch { - try { - Log.d("ProjectDetailActivity", "Deleting project: $projectId") - val response = api.deleteProject(projectId) - - if (response.isSuccessful) { - Log.d("ProjectDetailActivity", "Project deleted successfully") - android.widget.Toast.makeText( - this@ProjectDetailActivity, - "Project deleted successfully", - android.widget.Toast.LENGTH_SHORT - ).show() - finish() - - // Volver a MainActivity - val intent = Intent(this@ProjectDetailActivity, MainActivity::class.java) - startActivity(intent) - } else { - val errorBody = response.errorBody()?.string() - Log.e("ProjectDetailActivity", "Error deleting project: $errorBody") - android.widget.Toast.makeText( - this@ProjectDetailActivity, - "Error deleting project: ${response.code()}", - android.widget.Toast.LENGTH_SHORT - ).show() - } - } catch (e: Exception) { - Log.e("ProjectDetailActivity", "Exception deleting project: ${e.message}") - android.widget.Toast.makeText( - this@ProjectDetailActivity, - "Failed to delete project: ${e.message}", - android.widget.Toast.LENGTH_SHORT - ).show() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/ProjectEditActivity.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/ProjectEditActivity.kt deleted file mode 100644 index 0948826..0000000 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/ProjectEditActivity.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.campusaula.edbole.kanban_clone_android.ui - -import android.content.Intent -import android.os.Bundle -import android.util.Log -import android.widget.Button -import android.widget.EditText -import android.widget.Toast -import androidx.activity.enableEdgeToEdge -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.lifecycle.lifecycleScope -import com.campusaula.edbole.kanban_clone_android.R -import com.campusaula.edbole.kanban_clone_android.kanban.ProjectCreate -import com.campusaula.edbole.kanban_clone_android.network.ApiService -import com.campusaula.edbole.kanban_clone_android.network.RetrofitInstance -import com.google.android.material.floatingactionbutton.FloatingActionButton -import kotlinx.coroutines.launch - -class ProjectEditActivity : AppCompatActivity() { - - private lateinit var api: ApiService - - private lateinit var returnActionButton: FloatingActionButton - private lateinit var projectNameInput: EditText - private lateinit var projectDescriptionInput: EditText - private lateinit var saveProjectButton: Button - - private var projectId: Int = -1 - private var currentName: String = "" - private var currentDescription: String = "" - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContentView(R.layout.activity_project_edit) - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> - val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) - insets - } - - api = RetrofitInstance.getRetrofit(applicationContext).create(ApiService::class.java) - - // Get project data from intent - projectId = intent.getIntExtra("project_id", -1) - currentName = intent.getStringExtra("project_name") ?: "" - currentDescription = intent.getStringExtra("project_description") ?: "" - - if (projectId == -1) { - Toast.makeText(this, "Error: Invalid project ID", Toast.LENGTH_SHORT).show() - finish() - return - } - - // Initialize views - returnActionButton = findViewById(R.id.returnActionButton) - projectNameInput = findViewById(R.id.projectNameInput) - projectDescriptionInput = findViewById(R.id.projectDescriptionInput) - saveProjectButton = findViewById(R.id.saveProjectButton) - - // Populate fields with current project data - projectNameInput.setText(currentName) - projectDescriptionInput.setText(currentDescription) - - // Set up button listeners - returnActionButton.setOnClickListener { - finish() - - val intent = Intent(this@ProjectEditActivity, ProjectDetailActivity::class.java) - intent.putExtra("project_id", projectId) - startActivity(intent) - } - - saveProjectButton.setOnClickListener { - saveProject() - } - } - - private fun saveProject() { - val newName = projectNameInput.text.toString().trim() - val newDescription = projectDescriptionInput.text.toString().trim() - - if (newName.isEmpty()) { - Toast.makeText(this, "Project name cannot be empty", Toast.LENGTH_SHORT).show() - return - } - - lifecycleScope.launch { - try { - Log.d("ProjectEditActivity", "Updating project: $projectId") - val projectCreate = ProjectCreate( - name = newName, - description = newDescription - ) - - val response = api.updateProject(projectId, projectCreate) - - if (response.isSuccessful) { - Log.d("ProjectEditActivity", "Project updated successfully") - Toast.makeText( - this@ProjectEditActivity, - "Project updated successfully", - Toast.LENGTH_SHORT - ).show() - setResult(RESULT_OK) - finish() - - // Reopen ProjectDetailActivity to show the updated project - val intent = Intent(this@ProjectEditActivity, ProjectDetailActivity::class.java) - intent.putExtra("project_id", projectId) - startActivity(intent) - } else { - val errorBody = response.errorBody()?.string() - Log.e("ProjectEditActivity", "Error updating project: $errorBody") - Toast.makeText( - this@ProjectEditActivity, - "Error updating project: ${response.code()}", - Toast.LENGTH_SHORT - ).show() - } - } catch (e: Exception) { - Log.e("ProjectEditActivity", "Exception updating project: ${e.message}") - Toast.makeText( - this@ProjectEditActivity, - "Failed to update project: ${e.message}", - Toast.LENGTH_SHORT - ).show() - } - } - } -} - diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/TaskAddActivity.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/TaskAddActivity.kt deleted file mode 100644 index 37d764c..0000000 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/TaskAddActivity.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.campusaula.edbole.kanban_clone_android.ui - -import android.content.Intent -import android.os.Bundle -import android.util.Log -import android.widget.Button -import android.widget.EditText -import android.widget.Spinner -import android.widget.Toast -import androidx.activity.enableEdgeToEdge -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.lifecycle.lifecycleScope -import com.campusaula.edbole.kanban_clone_android.R -import com.campusaula.edbole.kanban_clone_android.kanban.TaskBase -import com.campusaula.edbole.kanban_clone_android.kanban.TaskStatus -import com.campusaula.edbole.kanban_clone_android.network.ApiService -import com.campusaula.edbole.kanban_clone_android.network.RetrofitInstance -import com.google.android.material.floatingactionbutton.FloatingActionButton -import kotlinx.coroutines.launch - -class TaskAddActivity : AppCompatActivity() { - - private lateinit var api: ApiService - - private lateinit var returnActionButton: FloatingActionButton - private lateinit var taskTitleInput: EditText - private lateinit var taskDescriptionInput: EditText - private lateinit var taskStatusSpinner: Spinner - private lateinit var createTaskButton: Button - - private var projectId: Int = -1 - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContentView(R.layout.activity_task_add) - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> - val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) - insets - } - - api = RetrofitInstance.getRetrofit(applicationContext).create(ApiService::class.java) - - // Get project ID from intent - projectId = intent.getIntExtra("project_id", -1) - - if (projectId == -1) { - Toast.makeText(this, "Error: Invalid project ID", Toast.LENGTH_SHORT).show() - finish() - return - } - - // Initialize views - returnActionButton = findViewById(R.id.returnActionButton) - taskTitleInput = findViewById(R.id.taskTitleInput) - taskDescriptionInput = findViewById(R.id.taskDescriptionInput) - taskStatusSpinner = findViewById(R.id.taskStatusSpinner) - createTaskButton = findViewById(R.id.createTaskButton) - - // Set default status to PENDING (index 0) - taskStatusSpinner.setSelection(0) - - // Set up button listeners - returnActionButton.setOnClickListener { - finish() - - val intent = Intent(this@TaskAddActivity, ProjectDetailActivity::class.java) - intent.putExtra("project_id", projectId) - startActivity(intent) - } - - createTaskButton.setOnClickListener { - createTask() - } - } - - private fun createTask() { - val title = taskTitleInput.text.toString().trim() - val description = taskDescriptionInput.text.toString().trim() - val status = TaskStatus.entries[taskStatusSpinner.selectedItemPosition] - - if (title.isEmpty()) { - Toast.makeText(this, "Title cannot be empty", Toast.LENGTH_SHORT).show() - return - } - - lifecycleScope.launch { - try { - Log.d("TaskAddActivity", "Creating task: $title") - val taskBase = TaskBase( - id = 0, // ID will be assigned by the server - title = title, - description = description, - status = status - ) - - val response = api.createTask(projectId, taskBase) - - if (response.isSuccessful) { - Log.d("TaskAddActivity", "Task created successfully") - Toast.makeText( - this@TaskAddActivity, - "Task created successfully", - Toast.LENGTH_SHORT - ).show() - setResult(RESULT_OK) - finish() - - // Reopen ProjectDetailActivity to show the new task - val intent = Intent(this@TaskAddActivity, ProjectDetailActivity::class.java) - intent.putExtra("project_id", projectId) - startActivity(intent) - } else { - val errorBody = response.errorBody()?.string() - Log.e("TaskAddActivity", "Error creating task: $errorBody") - Toast.makeText( - this@TaskAddActivity, - "Error creating task: ${response.code()}", - Toast.LENGTH_SHORT - ).show() - } - } catch (e: Exception) { - Log.e("TaskAddActivity", "Exception creating task: ${e.message}") - Toast.makeText( - this@TaskAddActivity, - "Failed to create task: ${e.message}", - Toast.LENGTH_SHORT - ).show() - } - } - } -} - diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/TaskEditActivity.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/TaskEditActivity.kt deleted file mode 100644 index 51aa741..0000000 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/TaskEditActivity.kt +++ /dev/null @@ -1,189 +0,0 @@ -package com.campusaula.edbole.kanban_clone_android.ui - -import android.content.Intent -import android.os.Bundle -import android.util.Log -import android.widget.Button -import android.widget.EditText -import android.widget.Spinner -import android.widget.Toast -import androidx.activity.enableEdgeToEdge -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.lifecycle.lifecycleScope -import com.campusaula.edbole.kanban_clone_android.R -import com.campusaula.edbole.kanban_clone_android.kanban.TaskStatus -import com.campusaula.edbole.kanban_clone_android.kanban.TaskUpdate -import com.campusaula.edbole.kanban_clone_android.network.ApiService -import com.campusaula.edbole.kanban_clone_android.network.RetrofitInstance -import com.google.android.material.floatingactionbutton.FloatingActionButton -import kotlinx.coroutines.launch - -class TaskEditActivity : AppCompatActivity() { - - private lateinit var api: ApiService - - private lateinit var returnActionButton: FloatingActionButton - private lateinit var taskTitleInput: EditText - private lateinit var taskDescriptionInput: EditText - private lateinit var taskStatusSpinner: Spinner - private lateinit var saveTaskButton: Button - private lateinit var deleteTaskButton: Button - - private var projectId: Int = -1 - private var taskId: Int = -1 - private var currentTitle: String = "" - private var currentDescription: String = "" - private var currentStatus: TaskStatus = TaskStatus.PENDING - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContentView(R.layout.activity_task_edit) - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> - val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) - insets - } - - api = RetrofitInstance.getRetrofit(applicationContext).create(ApiService::class.java) - - // Get task and project IDs from intent - projectId = intent.getIntExtra("project_id", -1) - taskId = intent.getIntExtra("task_id", -1) - currentTitle = intent.getStringExtra("task_title") ?: "" - currentDescription = intent.getStringExtra("task_description") ?: "" - val statusString = intent.getStringExtra("task_status") ?: "PENDING" - currentStatus = TaskStatus.valueOf(statusString) - - if (projectId == -1 || taskId == -1) { - Toast.makeText(this, "Error: Invalid task or project ID", Toast.LENGTH_SHORT).show() - finish() - } - - // Initialize views - returnActionButton = findViewById(R.id.returnActionButton) - taskTitleInput = findViewById(R.id.taskTitleInput) - taskDescriptionInput = findViewById(R.id.taskDescriptionInput) - taskStatusSpinner = findViewById(R.id.taskStatusSpinner) - saveTaskButton = findViewById(R.id.saveTaskButton) - deleteTaskButton = findViewById(R.id.deleteTaskButton) - - // Populate fields with current task data - taskTitleInput.setText(currentTitle) - taskDescriptionInput.setText(currentDescription) - taskStatusSpinner.setSelection(TaskStatus.entries.indexOf(currentStatus)) - - // Set up button listeners - returnActionButton.setOnClickListener { - finish() - - val intent = Intent(this@TaskEditActivity, ProjectDetailActivity::class.java) - intent.putExtra("project_id", projectId) - startActivity(intent) - } - - saveTaskButton.setOnClickListener { - saveTask() - } - - deleteTaskButton.setOnClickListener { - deleteTask() - } - } - - private fun saveTask() { - val newTitle = taskTitleInput.text.toString().trim() - val newDescription = taskDescriptionInput.text.toString().trim() - val newStatus = TaskStatus.entries[taskStatusSpinner.selectedItemPosition] - - if (newTitle.isEmpty()) { - Toast.makeText(this, "Title cannot be empty", Toast.LENGTH_SHORT).show() - return - } - - lifecycleScope.launch { - try { - Log.d("TaskEditActivity", "Updating task: $taskId") - val taskUpdate = TaskUpdate( - title = if (newTitle != currentTitle) newTitle else null, - description = if (newDescription != currentDescription) newDescription else null, - status = if (newStatus != currentStatus) newStatus else null - ) - - val response = api.updateProjectTask(projectId, taskId, taskUpdate) - - if (response.isSuccessful) { - Log.d("TaskEditActivity", "Task updated successfully") - Toast.makeText( - this@TaskEditActivity, - "Task updated successfully", - Toast.LENGTH_SHORT - ).show() - setResult(RESULT_OK) - finish() - val intent = Intent(this@TaskEditActivity, ProjectDetailActivity::class.java) - intent.putExtra("project_id", projectId) - startActivity(intent) - } else { - val errorBody = response.errorBody()?.string() - Log.e("TaskEditActivity", "Error updating task: $errorBody") - Toast.makeText( - this@TaskEditActivity, - "Error updating task: ${response.code()}", - Toast.LENGTH_SHORT - ).show() - } - } catch (e: Exception) { - Log.e("TaskEditActivity", "Exception updating task: ${e.message}") - Toast.makeText( - this@TaskEditActivity, - "Failed to update task: ${e.message}", - Toast.LENGTH_SHORT - ).show() - } - } - } - - private fun deleteTask() { - lifecycleScope.launch { - try { - Log.d("TaskEditActivity", "Deleting task: $taskId") - val response = api.deleteProjectTask(projectId, taskId) - - if (response.isSuccessful) { - Log.d("TaskEditActivity", "Task deleted successfully") - Toast.makeText( - this@TaskEditActivity, - "Task deleted successfully", - Toast.LENGTH_SHORT - ).show() - setResult(RESULT_OK) - finish() - - // Reopen ProjectDetailActivity after deleting the task - val intent = Intent(this@TaskEditActivity, ProjectDetailActivity::class.java) - intent.putExtra("project_id", projectId) - startActivity(intent) - } else { - val errorBody = response.errorBody()?.string() - Log.e("TaskEditActivity", "Error deleting task: $errorBody") - Toast.makeText( - this@TaskEditActivity, - "Error deleting task: ${response.code()}", - Toast.LENGTH_SHORT - ).show() - } - } catch (e: Exception) { - Log.e("TaskEditActivity", "Exception deleting task: ${e.message}") - Toast.makeText( - this@TaskEditActivity, - "Failed to delete task: ${e.message}", - Toast.LENGTH_SHORT - ).show() - } - } - } -} - diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/adapters/ProjectCollaboratorAdapter.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/adapters/ProjectCollaboratorAdapter.kt deleted file mode 100644 index 96dbde5..0000000 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/adapters/ProjectCollaboratorAdapter.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.campusaula.edbole.kanban_clone_android.ui.adapters - -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import android.widget.TextView -import android.widget.Toast -import androidx.recyclerview.widget.RecyclerView -import com.campusaula.edbole.kanban_clone_android.R -import com.campusaula.edbole.kanban_clone_android.kanban.User -import com.campusaula.edbole.kanban_clone_android.network.ApiService -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class ProjectCollaboratorAdapter( - private var collaborators: List, - private val apiService: ApiService, - private val projectId: Int, - private val onCollaboratorRemoved: () -> Unit = {} -) : RecyclerView.Adapter() { - - private val adapterScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - - fun submitList(newList: List) { - collaborators = newList.toMutableList() - notifyDataSetChanged() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context).inflate(R.layout.item_collaborator, parent, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(collaborators[position]) - } - - override fun getItemCount(): Int = collaborators.size - - fun onDestroy() { - adapterScope.cancel("Adapter destroyed") - } - - inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - private val collaboratorNameText: TextView = itemView.findViewById(R.id.collaboratorNameText) - private val collaboratorEmailText: TextView = itemView.findViewById(R.id.collaboratorEmailText) - private val removeCollaboratorButton: Button = itemView.findViewById(R.id.removeCollaboratorButton) - - fun bind(collaborator: User) { - collaboratorNameText.text = collaborator.name - collaboratorEmailText.text = collaborator.email - - removeCollaboratorButton.setOnClickListener { - adapterScope.launch { - try { - Log.d("ProjectCollaboratorAdapter", "Removing collaborator: ${collaborator.id}") - val response = apiService.removeProjectCollaborator(projectId, collaborator.id) - - if (response.isSuccessful) { - Log.d("ProjectCollaboratorAdapter", "Collaborator removed successfully: ${collaborator.id}") - withContext(Dispatchers.Main) { - Toast.makeText( - itemView.context, - "Collaborator removed: ${collaborator.name}", - Toast.LENGTH_SHORT - ).show() - onCollaboratorRemoved() - } - } else { - val errorBody = response.errorBody()?.string() - Log.e("ProjectCollaboratorAdapter", "Error removing collaborator: $errorBody") - withContext(Dispatchers.Main) { - Toast.makeText( - itemView.context, - "Error removing collaborator: ${response.code()}", - Toast.LENGTH_SHORT - ).show() - } - } - } catch (e: Exception) { - Log.e("ProjectCollaboratorAdapter", "Exception removing collaborator: ${e.message}") - withContext(Dispatchers.Main) { - Toast.makeText( - itemView.context, - "Failed to remove collaborator: ${e.message}", - Toast.LENGTH_SHORT - ).show() - } - } - } - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/adapters/ProjectItemAdapter.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/adapters/ProjectItemAdapter.kt deleted file mode 100644 index b2b3219..0000000 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/adapters/ProjectItemAdapter.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.campusaula.edbole.kanban_clone_android.ui.adapters - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import com.campusaula.edbole.kanban_clone_android.R -import com.campusaula.edbole.kanban_clone_android.kanban.Project - -class ProjectItemAdapter( - private var items: List, - private val onItemClick: ((Project) -> Unit)? = null -) : RecyclerView.Adapter() { - - fun submitList(newList: List) { - items = newList - notifyDataSetChanged() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_project, parent, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val project = items[position] - holder.bind(project) - holder.itemView.setOnClickListener { onItemClick?.invoke(project) } - } - - override fun getItemCount(): Int = items.size - - class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - private val nameTextView: TextView = itemView.findViewById(R.id.projectName) - private val descriptionTextView: TextView = itemView.findViewById(R.id.projectDescription) - - fun bind(project: Project) { - nameTextView.text = project.name - descriptionTextView.text = project.description - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/adapters/ProjectTaskAdapter.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/adapters/ProjectTaskAdapter.kt deleted file mode 100644 index ddb16db..0000000 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/adapters/ProjectTaskAdapter.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.campusaula.edbole.kanban_clone_android.ui.adapters - -import android.content.Intent -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.Button -import android.widget.Spinner -import android.widget.TextView -import android.widget.Toast -import androidx.recyclerview.widget.RecyclerView -import com.campusaula.edbole.kanban_clone_android.R -import com.campusaula.edbole.kanban_clone_android.kanban.Task -import com.campusaula.edbole.kanban_clone_android.kanban.TaskStatus -import com.campusaula.edbole.kanban_clone_android.kanban.TaskUpdate -import com.campusaula.edbole.kanban_clone_android.network.ApiService -import com.campusaula.edbole.kanban_clone_android.ui.TaskEditActivity -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class ProjectTaskAdapter( - private var tasks: List, - private val apiService: ApiService, - private val projectId: Int, - private val onTaskUpdated: () -> Unit = {}, - private val onEditTask: () -> Unit = {} -) : RecyclerView.Adapter() { - - private val adapterScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context).inflate(R.layout.item_task, parent, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(tasks[position]) - } - - override fun getItemCount(): Int = tasks.size - - fun submitList(newList: List) { - tasks = newList - notifyDataSetChanged() - } - - // Cancelar el CoroutineScope cuando el adaptador ya no sea necesario - fun onDestroy() { - adapterScope.cancel() - } - - inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - private val taskTitleText: TextView = itemView.findViewById(R.id.taskTitleText) - private val taskDescriptionText: TextView = itemView.findViewById(R.id.taskDescriptionText) - private val editTaskButton: Button = itemView.findViewById(R.id.editTaskButton) - private val taskStatusPicker: Spinner = itemView.findViewById(R.id.taskStatusDropdown) - - fun bind(task: Task) { - taskTitleText.text = task.title - taskDescriptionText.text = task.description - - editTaskButton.setOnClickListener { - val intent = Intent(itemView.context, TaskEditActivity::class.java) - intent.putExtra("project_id", projectId) - intent.putExtra("task_id", task.id) - intent.putExtra("task_title", task.title) - intent.putExtra("task_description", task.description) - intent.putExtra("task_status", task.status.name) - itemView.context.startActivity(intent) - onEditTask() // Cerrar la actividad - } - - taskStatusPicker.setSelection(TaskStatus.entries.indexOf(task.status)) - - taskStatusPicker.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) { - val selectedStatus: TaskStatus = TaskStatus.entries[position] - task.status = selectedStatus - - adapterScope.launch { - try { - Log.d("ProjectTaskAdapter", "Sending PUT request for task: ${task.id}") - Log.d("ProjectTaskAdapter", "Project ID: $projectId") - Log.d("ProjectTaskAdapter", "TaskUpdate data: ${TaskUpdate(null, null, task.status)}") - - val response = apiService.updateProjectTask( - projectId, - task.id, - TaskUpdate(null, null, task.status) - ) - - if (response.isSuccessful) { - Log.d("ProjectTaskAdapter", "Task updated successfully: ${task.id}") - withContext(Dispatchers.Main) { - onTaskUpdated() - notifyDataSetChanged() - } - } else { - val errorBody = response.errorBody()?.string() - Log.e("ProjectTaskAdapter", "Error response: $errorBody") - withContext(Dispatchers.Main) { - Toast.makeText( - itemView.context, - "Error updating task: ${response.code()} - $errorBody", - Toast.LENGTH_SHORT - ).show() - } - } - } catch (e: Exception) { - Log.e("ProjectTaskAdapter", "Exception updating task: ${e.message}") - withContext(Dispatchers.Main) { - Toast.makeText( - itemView.context, - "Failed to update task: ${e.message}", - Toast.LENGTH_SHORT - ).show() - } - } - } - } - - override fun onNothingSelected(parent: AdapterView<*>?) { - // No action needed - } - } - - } - } -} diff --git a/app/src/main/res/layout/activity_collaborator_add.xml b/app/src/main/res/layout/activity_collaborator_add.xml deleted file mode 100644 index b31ddce..0000000 --- a/app/src/main/res/layout/activity_collaborator_add.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - -