diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a63b20c..5d7a6cb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { 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) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a595186..9bd5106 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,26 +1,32 @@ + xmlns:tools="http://schemas.android.com/tools"> + android:allowBackup="true" + android:dataExtractionRules="@xml/data_extraction_rules" + android:fullBackupContent="@xml/backup_rules" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="true" + android:theme="@style/Theme.KanbanCloneAndroid" + android:usesCleartextTraffic="true"> + android:name=".ui.CreateProjectActivity" + android:exported="false" /> + android:name=".ui.ProjectDetailActivity" + android:exported="false" /> + + 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 index f0a25b8..70c8c21 100644 --- 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 @@ -8,6 +8,11 @@ class Project{ 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( 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 index e79377c..6d9184b 100644 --- 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 @@ -11,7 +11,7 @@ interface ApiService { @POST("auth/login/") suspend fun login(@Body userLogin: UserLogin): Response - @POST("me/logout/") + @GET("me/logout/") suspend fun logout(): Response @DELETE("me/delete-me/") diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/MainActivity.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/CreateProjectActivity.kt similarity index 74% rename from app/src/main/java/com/campusaula/edbole/kanban_clone_android/MainActivity.kt rename to app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/CreateProjectActivity.kt index 87828e1..c516c8d 100644 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/MainActivity.kt +++ b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/CreateProjectActivity.kt @@ -1,16 +1,17 @@ -package com.campusaula.edbole.kanban_clone_android +package com.campusaula.edbole.kanban_clone_android.ui import android.os.Bundle import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import com.campusaula.edbole.kanban_clone_android.R -class MainActivity : AppCompatActivity() { +class CreateProjectActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() - setContentView(R.layout.activity_main) + 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) diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/LoginActivity.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/LoginActivity.kt similarity index 69% rename from app/src/main/java/com/campusaula/edbole/kanban_clone_android/LoginActivity.kt rename to app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/LoginActivity.kt index 3c4ca83..8b08d11 100644 --- a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/LoginActivity.kt +++ b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/LoginActivity.kt @@ -1,25 +1,31 @@ -package com.campusaula.edbole.kanban_clone_android +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 : androidx.appcompat.widget.AppCompatEditText - private lateinit var passwordInput : androidx.appcompat.widget.AppCompatEditText + private lateinit var emailInput : AppCompatEditText + private lateinit var passwordInput : AppCompatEditText - private lateinit var loginButton : androidx.appcompat.widget.AppCompatButton - private lateinit var logonButton : androidx.appcompat.widget.AppCompatButton + private lateinit var loginButton : AppCompatButton + private lateinit var logonButton : AppCompatButton private lateinit var retrofit : Retrofit private lateinit var api : ApiService @@ -47,14 +53,14 @@ class LoginActivity : AppCompatActivity() { val password = passwordInput.text.toString() if (email.isEmpty() && password.isEmpty()) { - android.widget.Toast.makeText(this, "Please enter email and password", android.widget.Toast.LENGTH_SHORT).show() + Toast.makeText(this, "Please enter email and password", Toast.LENGTH_SHORT).show() return@setOnClickListener } lifecycleScope.launch{ try { val loginResponse = api.login( - com.campusaula.edbole.kanban_clone_android.kanban.UserLogin( + UserLogin( email = email, password = password ) @@ -67,10 +73,14 @@ class LoginActivity : AppCompatActivity() { // Después del login exitoso OkHttp/CookieJar habrá guardado las cookies. val authValue = RetrofitInstance.getAuthCookieForUrl(baseUrl) if (authValue != null) { - android.widget.Toast.makeText(this@LoginActivity, "Auth cookie guardada", android.widget.Toast.LENGTH_SHORT).show() + Toast.makeText(this@LoginActivity, "Auth cookie guardada", Toast.LENGTH_SHORT).show() } else { - android.widget.Toast.makeText(this@LoginActivity, "Login OK pero no se encontró cookie de auth", android.widget.Toast.LENGTH_SHORT).show() + 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 @@ -85,14 +95,14 @@ class LoginActivity : AppCompatActivity() { // clear stored cookies for base host RetrofitInstance.clearCookiesForHost(baseHost) - android.widget.Toast.makeText(this@LoginActivity, "Login failed (401): $errMsg", android.widget.Toast.LENGTH_SHORT).show() + Toast.makeText(this@LoginActivity, "Login failed (401): $errMsg", Toast.LENGTH_SHORT).show() } else { - android.widget.Toast.makeText(this@LoginActivity, "Login failed: ${loginResponse.code()}", android.widget.Toast.LENGTH_SHORT).show() + Toast.makeText(this@LoginActivity, "Login failed: ${loginResponse.code()}", Toast.LENGTH_SHORT).show() } } } catch (ex: Exception){ - android.widget.Toast.makeText(this@LoginActivity, "Login failed: ${ex.message}", android.widget.Toast.LENGTH_SHORT).show() + 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 new file mode 100644 index 0000000..fe71980 --- /dev/null +++ b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/MainActivity.kt @@ -0,0 +1,101 @@ +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.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:8000" + ) + // 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 new file mode 100644 index 0000000..5790ade --- /dev/null +++ b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/ProjectDetailActivity.kt @@ -0,0 +1,92 @@ +package com.campusaula.edbole.kanban_clone_android.ui + +import android.health.connect.datatypes.units.Percentage +import android.os.Bundle +import android.util.Log +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 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.google.android.material.floatingactionbutton.FloatingActionButton +import kotlinx.coroutines.launch + +class ProjectDetailActivity : AppCompatActivity() { + + private lateinit var api: ApiService + + private lateinit var projectTitleText : TextView + private lateinit var projectDescriptionText : TextView + private lateinit var completedPercentageText: TextView + private lateinit var returnActionButton: FloatingActionButton + + 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) + projectTitleText = findViewById(R.id.projectTitleText) + projectDescriptionText = findViewById(R.id.projectDescriptionText) + completedPercentageText = findViewById(R.id.completedPercentageText) + returnActionButton = findViewById(R.id.returnActionButton) + returnActionButton.setOnClickListener { finish() } + + val projectId : Int = intent.getIntExtra("project_id", -1) + + 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 tasks: List = project.tasks + val totalTasks: Int = tasks.size + val perTaskPercentage = if (totalTasks > 0) (1.0 / totalTasks)*100 else 0.0 + + for (task in tasks) { + if (task.status == TaskStatus.COMPLETED) { + percentageFinished += perTaskPercentage + } + } + completedPercentageText.text = "Completed: ${"%.2f".format(percentageFinished * 100)}%" + + + } 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() + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/ProjectItemAdapter.kt b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/ProjectItemAdapter.kt new file mode 100644 index 0000000..53856b0 --- /dev/null +++ b/app/src/main/java/com/campusaula/edbole/kanban_clone_android/ui/ProjectItemAdapter.kt @@ -0,0 +1,44 @@ +package com.campusaula.edbole.kanban_clone_android.ui + +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 nameTv: TextView = itemView.findViewById(R.id.projectName) + private val descTv: TextView = itemView.findViewById(R.id.projectDescription) + + fun bind(project: Project) { + nameTv.text = project.id.toString() + " " + project.name + descTv.text = project.description + } + } +} diff --git a/app/src/main/res/layout/activity_create_project.xml b/app/src/main/res/layout/activity_create_project.xml new file mode 100644 index 0000000..68de28d --- /dev/null +++ b/app/src/main/res/layout/activity_create_project.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 4330ddd..ac739f4 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -5,7 +5,7 @@ android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context="com.campusaula.edbole.kanban_clone_android.LoginActivity"> + tools:context="com.campusaula.edbole.kanban_clone_android.ui.LoginActivity"> + tools:context="com.campusaula.edbole.kanban_clone_android.ui.MainActivity"> + app:layout_constraintTop_toTopOf="parent" + android:id="@+id/loggedInAs" + android:textAlignment="textStart" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintVertical_bias="0.0" + android:layout_marginTop="16dp" + android:layout_marginStart="16dp" /> + +