diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8862093..3bb7360 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,6 +18,10 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + buildFeatures { + viewBinding = true + } + buildTypes { release { isMinifyEnabled = false diff --git a/app/src/main/java/com/prm/tasksboard/taskboards/entity/BoardItem.kt b/app/src/main/java/com/prm/tasksboard/taskboards/entity/BoardItem.kt index e8bc9d2..c76b9ea 100644 --- a/app/src/main/java/com/prm/tasksboard/taskboards/entity/BoardItem.kt +++ b/app/src/main/java/com/prm/tasksboard/taskboards/entity/BoardItem.kt @@ -4,7 +4,7 @@ import com.google.firebase.Timestamp import com.google.firebase.firestore.PropertyName data class BoardItem( - var boardId: String = "", + @get:PropertyName("board_id") @set:PropertyName("board_id") var boardId: String = "", @get:PropertyName("created_at") @set:PropertyName("created_at") var createdAt: Timestamp, @get:PropertyName("name") @set:PropertyName("name") var name: String, @get:PropertyName("updated_at") @set:PropertyName("updated_at") var updatedAt: Timestamp, diff --git a/app/src/main/java/com/prm/tasksboard/taskboards/entity/TaskItem.kt b/app/src/main/java/com/prm/tasksboard/taskboards/entity/TaskItem.kt new file mode 100644 index 0000000..05cc187 --- /dev/null +++ b/app/src/main/java/com/prm/tasksboard/taskboards/entity/TaskItem.kt @@ -0,0 +1,16 @@ +package com.prm.tasksboard.taskboards.entity + +import com.google.firebase.Timestamp +import com.google.firebase.firestore.PropertyName + +data class TaskItem( + @get:PropertyName("task_id") @set:PropertyName("task_id") var taskId: String = "", + @get:PropertyName("title") @set:PropertyName("title") var title: String = "", + @get:PropertyName("description") @set:PropertyName("description") var description: String = "", + @get:PropertyName("status") @set:PropertyName("status") var status: String = "", + @get:PropertyName("due_date") @set:PropertyName("due_date") var dueDate: Timestamp, + @get:PropertyName("priority") @set:PropertyName("priority") var priority: String = "", + @get:PropertyName("created_at") @set:PropertyName("created_at") var createdAt: Timestamp, +) { + constructor() : this("", "", "", "", Timestamp.now(), "", Timestamp.now()) +} \ No newline at end of file diff --git a/app/src/main/java/com/prm/tasksboard/taskboards/firestore/DatabaseHandler.kt b/app/src/main/java/com/prm/tasksboard/taskboards/firestore/DatabaseHandler.kt index 244a5c9..e797fd5 100644 --- a/app/src/main/java/com/prm/tasksboard/taskboards/firestore/DatabaseHandler.kt +++ b/app/src/main/java/com/prm/tasksboard/taskboards/firestore/DatabaseHandler.kt @@ -2,33 +2,37 @@ package com.prm.tasksboard.taskboards.firestore import android.util.Log import com.google.android.gms.tasks.Task +import com.google.firebase.Timestamp import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.QuerySnapshot import com.google.firebase.firestore.ktx.firestore import com.google.firebase.ktx.Firebase import com.prm.tasksboard.taskboards.entity.BoardItem +import com.prm.tasksboard.taskboards.entity.TaskItem class DatabaseHandler { private val db = Firebase.firestore private val loggedInUserId = FirebaseAuth.getInstance().currentUser?.uid ?: "" - fun addBoardItem(boardItem: BoardItem) { - // Firestore generates a unique ID for the new document + fun addBoardItem(boardItem: BoardItem, callback: (String) -> Unit) { val docRef = db.collection("boards").document() - boardItem.boardId = docRef.id // Assign the Firestore-generated ID to the boardItem + boardItem.boardId = docRef.id + boardItem.userId = loggedInUserId + boardItem.createdAt = Timestamp.now() // Set created_at to current time docRef.set(boardItem) .addOnSuccessListener { - Log.d("FirestoreAdd", "BoardItem successfully added with ID: ${docRef.id}") + callback(docRef.id) // Invoke the callback with the new board's ID } .addOnFailureListener { e -> - Log.w("FirestoreAdd", "Error adding BoardItem", e) + Log.w("DatabaseHandler", "Error adding BoardItem", e) } } fun getBoardItemsByUserId(): Task { return db.collection("boards") - .whereEqualTo("user_id", loggedInUserId).orderBy("created_at") + .whereEqualTo("user_id", loggedInUserId) + .orderBy("created_at") .get() .addOnSuccessListener { result -> for (document in result) { @@ -65,7 +69,7 @@ class DatabaseHandler { } fun checkFirestoreConnection() { - db.collection("boards") // Replace "known_collection" with your actual collection name + db.collection("boards") .whereEqualTo("user_id", loggedInUserId).limit(1) .get() .addOnSuccessListener { documents -> @@ -79,4 +83,77 @@ class DatabaseHandler { Log.w("FirestoreCheck", "Error connecting to Firestore: ", exception) } } -} + + fun addTaskItem(taskItem: TaskItem, boardId: String, callback: () -> Unit) { + val newTaskRef = db.collection("boards").document(boardId).collection("tasks").document() + taskItem.taskId = newTaskRef.id + newTaskRef.set(taskItem) + .addOnSuccessListener { + Log.d("FirestoreAdd", "TaskItem successfully added with ID: ${taskItem.taskId}") + callback() + } + .addOnFailureListener { e -> + Log.w("FirestoreAdd", "Error adding TaskItem", e) + } + } + + fun deleteTaskItem(boardId: String, taskId: String, callback: () -> Unit) { + db.collection("boards").document(boardId).collection("tasks").document(taskId) + .delete() + .addOnSuccessListener { + Log.d("FirestoreDelete", "Task $taskId successfully deleted!") + callback() + } + .addOnFailureListener { e -> Log.w("FirestoreDelete", "Error deleting task", e) } + } + + fun getTasksByBoardId(boardId: String, callback: (List) -> Unit) { + db.collection("boards").document(boardId).collection("tasks") + .get() + .addOnSuccessListener { documents -> + val tasks = documents.toObjects(TaskItem::class.java) + callback(tasks) + } + .addOnFailureListener { exception -> + Log.w("DatabaseHandler", "Error getting tasks by board ID", exception) + } + } + + fun getAllTasks(callback: (List) -> Unit) { + val allTasks = mutableListOf() + db.collection("boards").get().addOnSuccessListener { boardDocuments -> + val boardCount = boardDocuments.size() + var processedBoards = 0 + if (boardCount == 0) callback(allTasks) // Immediately return if no boards + + boardDocuments.forEach { boardDoc -> + db.collection("boards").document(boardDoc.id).collection("tasks").get() + .addOnSuccessListener { taskDocuments -> + val tasks = taskDocuments.toObjects(TaskItem::class.java) + allTasks.addAll(tasks) + processedBoards++ + if (processedBoards == boardCount) { + callback(allTasks) + } + } + } + } + } + + fun updateTaskItem( + boardId: String, + taskId: String, + updatedFields: Map, + callback: () -> Unit + ) { + db.collection("boards").document(boardId).collection("tasks").document(taskId) + .update(updatedFields) + .addOnSuccessListener { + Log.d("FirestoreUpdate", "Task $taskId successfully updated!") + callback() + } + .addOnFailureListener { e -> + Log.w("FirestoreUpdate", "Error updating task", e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prm/tasksboard/taskboards/view/BoardAdapter.kt b/app/src/main/java/com/prm/tasksboard/taskboards/view/BoardAdapter.kt deleted file mode 100644 index c67c6ba..0000000 --- a/app/src/main/java/com/prm/tasksboard/taskboards/view/BoardAdapter.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.prm.tasksboard.taskboards.view - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.prm.tasksboard.R -import com.prm.tasksboard.taskboards.entity.BoardItem - -class BoardAdapter(private val boardList: List) : - RecyclerView.Adapter() { - - class BoardViewHolder(view: View) : RecyclerView.ViewHolder(view) { - - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BoardViewHolder { - val view = LayoutInflater.from(parent.context).inflate(R.layout.board_item, parent, false) - return BoardViewHolder(view) - } - - override fun onBindViewHolder(holder: BoardViewHolder, position: Int) { - - } - - override fun getItemCount() = boardList.size -} diff --git a/app/src/main/java/com/prm/tasksboard/taskboards/view/BoardPagerAdapter.kt b/app/src/main/java/com/prm/tasksboard/taskboards/view/BoardPagerAdapter.kt deleted file mode 100644 index 1ae064c..0000000 --- a/app/src/main/java/com/prm/tasksboard/taskboards/view/BoardPagerAdapter.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.prm.tasksboard.taskboards.view - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import com.prm.tasksboard.R -import com.prm.tasksboard.taskboards.entity.BoardItem -import java.text.SimpleDateFormat -import java.util.Locale - -class BoardPagerAdapter(private val boardList: MutableList) : - RecyclerView.Adapter() { - - class BoardViewHolder(view: View) : RecyclerView.ViewHolder(view) { - // Define the view components of each item here - val boardCreatedAt: TextView = view.findViewById(R.id.boardCreatedAt) - val boardUpdatedAt: TextView = view.findViewById(R.id.boardUpdatedAt) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BoardViewHolder { - val view = LayoutInflater.from(parent.context).inflate(R.layout.board_item, parent, false) - return BoardViewHolder(view) - } - - override fun onBindViewHolder(holder: BoardViewHolder, position: Int) { - val boardItem = boardList[position] - val dateFormat = SimpleDateFormat("dd MMM yyyy HH:mm:ss", Locale.getDefault()) - holder.boardCreatedAt.text = holder.itemView.context.getString( - R.string.board_created_at, - dateFormat.format(boardItem.createdAt.toDate()) - ) - - holder.boardUpdatedAt.text = holder.itemView.context.getString( - R.string.board_updated_at, - dateFormat.format(boardItem.updatedAt.toDate()) - ) - } - - override fun getItemCount(): Int { - return boardList.size - } -} \ No newline at end of file diff --git a/app/src/main/java/com/prm/tasksboard/taskboards/view/GridRVAdapter.kt b/app/src/main/java/com/prm/tasksboard/taskboards/view/GridRVAdapter.kt new file mode 100644 index 0000000..f82fd6b --- /dev/null +++ b/app/src/main/java/com/prm/tasksboard/taskboards/view/GridRVAdapter.kt @@ -0,0 +1,45 @@ +package com.prm.tasksboard.taskboards.view + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.TextView +import com.prm.tasksboard.R +import com.prm.tasksboard.taskboards.entity.BoardItem + +internal class GridRVAdapter ( + private val boardList: MutableList = mutableListOf(), + private val context: Context +): + BaseAdapter() { + + private var layoutInflater:LayoutInflater? = null + private lateinit var boardName: TextView + + override fun getCount(): Int { + return boardList.size + } + + override fun getItem(p0: Int): Any?{ + return null + } + + override fun getItemId(p0: Int): Long { + return 0 + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? { + var convertView = convertView + if (layoutInflater == null){ + layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + } + if (convertView == null){ + convertView = layoutInflater!!.inflate(R.layout.grid_items, null) + } + boardName = convertView!!.findViewById(R.id.boardName) + boardName.setText(boardList.get(position).name) + return convertView + } +} \ No newline at end of file diff --git a/app/src/main/java/com/prm/tasksboard/taskboards/view/OverviewActivity.kt b/app/src/main/java/com/prm/tasksboard/taskboards/view/OverviewActivity.kt new file mode 100644 index 0000000..862a314 --- /dev/null +++ b/app/src/main/java/com/prm/tasksboard/taskboards/view/OverviewActivity.kt @@ -0,0 +1,38 @@ +package com.prm.tasksboard.taskboards.view + +import android.os.Bundle +import android.widget.GridView +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import com.prm.tasksboard.R +import com.prm.tasksboard.taskboards.entity.BoardItem +import com.prm.tasksboard.taskboards.firestore.DatabaseHandler + + +class OverviewActivity : AppCompatActivity() { + lateinit var boardGridView: GridView + private var boardList = mutableListOf() + private val dbHandler = DatabaseHandler() + val taskboardsActivity = TaskboardsActivity() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.grid_outer) + boardGridView = findViewById(R.id.idGRV) + dbHandler.getBoardItemsByUserId().addOnSuccessListener { result -> + boardList.clear() + val newItems = result.map { document -> + document.toObject(BoardItem::class.java) + } + .sortedBy { it.createdAt } + boardList.addAll(newItems) + } + val gridAdapter = GridRVAdapter(boardList, this) + boardGridView.adapter = gridAdapter + boardGridView.setOnItemClickListener { _, _, position, _ -> + Toast.makeText( applicationContext, boardList[position].name + " selected", Toast.LENGTH_SHORT).show() + taskboardsActivity.setupSelectedTabLayout(position) + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/prm/tasksboard/taskboards/view/TaskAdapter.kt b/app/src/main/java/com/prm/tasksboard/taskboards/view/TaskAdapter.kt new file mode 100644 index 0000000..78c381c --- /dev/null +++ b/app/src/main/java/com/prm/tasksboard/taskboards/view/TaskAdapter.kt @@ -0,0 +1,106 @@ +package com.prm.tasksboard.taskboards.view + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.CheckBox +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.prm.tasksboard.R +import com.prm.tasksboard.taskboards.entity.TaskItem +import com.google.firebase.Timestamp +import java.text.SimpleDateFormat +import java.util.Locale +import android.util.Log +import android.widget.PopupMenu + +class TaskAdapter( + private var tasks: List, + private val onTaskFinished: (TaskItem) -> Unit, + private val onTaskStatusChanged: (TaskItem) -> Unit, + private val onEditTask: (TaskItem) -> Unit, + private val onDeleteTask: (TaskItem) -> Unit +) : RecyclerView.Adapter() { + + inner class TaskViewHolder(view: View) : RecyclerView.ViewHolder(view) { + private val checkBox = itemView.findViewById(R.id.taskCheckBox) + private val titleView = itemView.findViewById(R.id.taskSummaryTextView) + private val expandArrowImageView = itemView.findViewById(R.id.expandArrowImageView) + private val taskDetailsLayout = itemView.findViewById(R.id.taskDetailsLayout) + private val descriptionView = itemView.findViewById(R.id.taskDescriptionTextView) + private val dueDateView = itemView.findViewById(R.id.taskDueDateTextView) + private val priorityView = itemView.findViewById(R.id.taskPriorityTextView) + private val moreOptionsImageView = itemView.findViewById(R.id.moreOptionsImageView) + + init { + moreOptionsImageView.setOnClickListener { v -> + if (adapterPosition != RecyclerView.NO_POSITION) { + showPopupMenu(v, tasks[adapterPosition]) // Pass the current task item + } + } + } + + fun bind(task: TaskItem, onTaskStatusChanged: (TaskItem) -> Unit) { + checkBox.isChecked = task.status == "finished" + titleView.text = task.title + descriptionView.text = itemView.context.getString(R.string.task_description, task.description) + dueDateView.text = itemView.context.getString(R.string.task_due_date, convertTimestampToString(task.dueDate)) + priorityView.text = itemView.context.getString(R.string.task_priority, task.priority) + + checkBox.setOnCheckedChangeListener { _, isChecked -> + val newStatus = if (isChecked) "finished" else "pending" + if (task.status != newStatus) { + task.status = newStatus + onTaskStatusChanged(task) + Log.d("TaskStatusChange", "Task ID: ${task.taskId}, New Status: $newStatus") + } + } + expandArrowImageView.setOnClickListener { + if (taskDetailsLayout.visibility == View.GONE) { + taskDetailsLayout.visibility = View.VISIBLE + expandArrowImageView.setImageResource(R.drawable.ic_arrow_down) + } else { + taskDetailsLayout.visibility = View.GONE + expandArrowImageView.setImageResource(R.drawable.ic_arrow_right) + } + } + } + + private fun showPopupMenu(view: View, task: TaskItem) { + val popup = PopupMenu(view.context, view) + popup.inflate(R.menu.task_options_menu) // Inflate your menu resource + popup.setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.edit_task -> { + onEditTask(task) // Call the lambda function for edit task + true + } + R.id.delete_task -> { + onDeleteTask(task) + true + } + else -> false + } + } + popup.show() + } + + private fun convertTimestampToString(timestamp: Timestamp): String { + val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + return sdf.format(timestamp.toDate()) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TaskViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.task_item, parent, false) + return TaskViewHolder(view) + } + + override fun onBindViewHolder(holder: TaskViewHolder, position: Int) { + holder.bind(tasks[position], onTaskStatusChanged) + } + + override fun getItemCount() = tasks.size +} diff --git a/app/src/main/java/com/prm/tasksboard/taskboards/view/TaskboardsActivity.kt b/app/src/main/java/com/prm/tasksboard/taskboards/view/TaskboardsActivity.kt index 1333d45..d8df116 100644 --- a/app/src/main/java/com/prm/tasksboard/taskboards/view/TaskboardsActivity.kt +++ b/app/src/main/java/com/prm/tasksboard/taskboards/view/TaskboardsActivity.kt @@ -1,73 +1,88 @@ package com.prm.tasksboard.taskboards.view import android.app.AlertDialog +import android.app.DatePickerDialog import android.os.Bundle import android.view.View -import android.view.ViewGroup import android.widget.Button import android.widget.EditText +import android.widget.ImageButton import android.widget.PopupMenu +import android.widget.SearchView import android.widget.TextView import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat -import androidx.viewpager2.widget.ViewPager2 +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton import com.google.android.material.tabs.TabLayout -import com.google.android.material.tabs.TabLayoutMediator import com.google.firebase.Timestamp import com.google.firebase.auth.FirebaseAuth import com.prm.tasksboard.R import com.prm.tasksboard.taskboards.entity.BoardItem +import com.prm.tasksboard.taskboards.entity.TaskItem import com.prm.tasksboard.taskboards.firestore.DatabaseHandler +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale class TaskboardsActivity : AppCompatActivity() { - private lateinit var viewPager: ViewPager2 private lateinit var tabLayout: TabLayout - private lateinit var boardPagerAdapter: BoardPagerAdapter private lateinit var addBoardButton: Button private lateinit var menuButton: MaterialButton private lateinit var emptyView: TextView - private var tabLayoutMediator: TabLayoutMediator? = null + private lateinit var recyclerView: RecyclerView + private lateinit var taskAdapter: TaskAdapter + private lateinit var searchView: SearchView private val boardList = mutableListOf() + private val tasks = mutableListOf() private val loggedInUserId = FirebaseAuth.getInstance().currentUser?.uid ?: "" private val dbHandler = DatabaseHandler() - - private fun setupTabLayoutWithViewPager() { - tabLayoutMediator?.detach() // Detach existing TabLayoutMediator if any - tabLayoutMediator = TabLayoutMediator(tabLayout, viewPager) { tab, position -> - tab.text = boardList[position].name - } - tabLayoutMediator?.attach() - } + private var currentBoardId: String? = null + private var selectedDueDate = "" + private var selectedPriority = "" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContentView(R.layout.activity_taskboards) setWindowInsetsListener() - - viewPager = findViewById(R.id.viewPager) tabLayout = findViewById(R.id.tabLayout) addBoardButton = findViewById(R.id.addBoardButton) menuButton = findViewById(R.id.menuButton) emptyView = findViewById(R.id.emptyView) - - boardPagerAdapter = BoardPagerAdapter(boardList) - viewPager.adapter = boardPagerAdapter + recyclerView = findViewById(R.id.tasksRecyclerView) + recyclerView.layoutManager = LinearLayoutManager(this) + searchView = findViewById(R.id.searchView) + setupSearchView() + taskAdapter = TaskAdapter(tasks, { taskItem -> + // Handle task finish action here + }, { taskItem -> + // Handle task status change here + }, { taskItem -> + // Handle edit task action here + showEditTaskDialog(taskItem) + }, { taskItem -> + // Handle delete task action here + dbHandler.deleteTaskItem(currentBoardId!!, taskItem.taskId) { + tasks.remove(taskItem) + taskAdapter.notifyDataSetChanged() + } + }) + recyclerView.adapter = taskAdapter dbHandler.checkFirestoreConnection() - // Ensure this is called after viewPager and tabLayout have been initialized - setupTabLayoutWithViewPager() setTabLayoutListeners() updateEmptyViewVisibility() fetchAndDisplayBoards() addBoardButton.setOnClickListener { - createNewBoard() + showAddTaskDialog() } menuButton.setOnClickListener { @@ -91,7 +106,6 @@ class TaskboardsActivity : AppCompatActivity() { deleteBoard() true } - else -> false } } @@ -99,6 +113,41 @@ class TaskboardsActivity : AppCompatActivity() { } } + private fun setupSearchView() { + findViewById(R.id.searchButton).setOnClickListener { + val isSearchViewVisible = searchView.visibility == View.VISIBLE + searchView.visibility = if (isSearchViewVisible) View.GONE else View.VISIBLE + adjustRecyclerViewTopConstraint(!isSearchViewVisible) + } + + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + query?.let { searchForTask(it) } + return true + } + + override fun onQueryTextChange(newText: String?): Boolean { + newText?.let { searchForTask(it) } + return true + } + }) + } + + private fun adjustRecyclerViewTopConstraint(isSearchViewVisible: Boolean) { + val layoutParams = recyclerView.layoutParams as ConstraintLayout.LayoutParams + layoutParams.topToBottom = if (isSearchViewVisible) R.id.searchView else R.id.tabLayout + recyclerView.layoutParams = layoutParams + } + + private fun searchForTask(query: String) { + dbHandler.getAllTasks { allTasks: List -> + val filteredTasks = allTasks.filter { it.title.contains(query, ignoreCase = true) } + tasks.clear() + tasks.addAll(filteredTasks) + taskAdapter.notifyDataSetChanged() + } + } + private fun setWindowInsetsListener() { ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) @@ -110,9 +159,11 @@ class TaskboardsActivity : AppCompatActivity() { private fun setTabLayoutListeners() { tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab?) { - val tabView = (tabLayout.getChildAt(0) as ViewGroup).getChildAt(tab?.position ?: 0) - tabView.setOnLongClickListener { - showRenameDialog(tab?.position ?: 0) + val position = tab?.position ?: return + currentBoardId = boardList[position].boardId + displayTasks(currentBoardId!!) + tab.view.setOnLongClickListener { + showRenameDialog(position) true } } @@ -137,33 +188,36 @@ class TaskboardsActivity : AppCompatActivity() { // Add it to your boardList boardList.add(newBoard) // Add it to Firestore - dbHandler.addBoardItem(newBoard) - if (boardList.size == 1) { // If this is the first board being added - viewPager.adapter = boardPagerAdapter - setupTabLayoutWithViewPager() // Re-setup TabLayout with ViewPager - } else { - boardPagerAdapter.notifyItemInserted(boardList.size - 1) + dbHandler.addBoardItem(newBoard) { newBoardId -> + boardList[boardList.size - 1].boardId = newBoardId } + setupTabLayout() updateEmptyViewVisibility() } private fun deleteBoard() { - val currentItem = viewPager.currentItem - // Delete the board from firestore - dbHandler.deleteBoardItem(boardList[viewPager.currentItem].boardId) - // Remove the board from your boardList - boardList.removeAt(viewPager.currentItem) - // Notify the adapter that the dataset has changed - boardPagerAdapter.notifyItemRemoved(viewPager.currentItem) - if (boardList.isEmpty()) { - viewPager.adapter = null - tabLayoutMediator?.detach() // Detach TabLayoutMediator if no items are left - tabLayoutMediator = null // Clear the TabLayoutMediator reference - } else { - boardPagerAdapter.notifyItemRemoved(currentItem) - setupTabLayoutWithViewPager() // Re-setup TabLayout with ViewPager + currentBoardId?.let { boardId -> + val currentItem = boardList.indexOfFirst { it.boardId == boardId } + // Delete the board from firestore + dbHandler.deleteBoardItem(boardId) + // Remove the board from your boardList + boardList.removeAt(currentItem) + // Notify the adapter that the dataset has changed + if (boardList.isEmpty()) { + clearAndHideTaskList() // Clear and hide the task list + } else { + setupTabLayout() + displayTasksForCurrentBoard() + } + updateEmptyViewVisibility() } - updateEmptyViewVisibility() + } + + private fun clearAndHideTaskList() { + // Find the RecyclerView and clear its adapter + recyclerView.adapter = null + // Optionally, update visibility or show a placeholder + emptyView.visibility = View.VISIBLE } private fun showRenameDialog(position: Int) { @@ -178,7 +232,6 @@ class TaskboardsActivity : AppCompatActivity() { val newName = input.text.toString() boardList[position].name = newName tabLayout.getTabAt(position)?.text = newName - boardPagerAdapter.notifyItemChanged(position) } builder.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() } @@ -187,18 +240,210 @@ class TaskboardsActivity : AppCompatActivity() { private fun fetchAndDisplayBoards() { dbHandler.getBoardItemsByUserId().addOnSuccessListener { result -> - val startPosition = boardList.size // Track start position before adding new items - boardList.clear() // Optional: Clear existing items if refreshing the entire list + boardList.clear() // Clear existing items if refreshing the entire list val newItems = result.map { document -> document.toObject(BoardItem::class.java) } + .sortedBy { it.createdAt } // Sort by createdAt timestamp or change it to another attribute like name boardList.addAll(newItems) - if (boardList.isNotEmpty()) { - // Notify adapter about the range of items inserted - boardPagerAdapter.notifyItemRangeInserted(startPosition, newItems.size) - setupTabLayoutWithViewPager() - } + setupTabLayout() updateEmptyViewVisibility() } } + + private fun setupTabLayout() { + tabLayout.removeAllTabs() + boardList.forEach { board -> + val tab = tabLayout.newTab().setText(board.name) + tabLayout.addTab(tab) + } + if (boardList.isNotEmpty()) { + currentBoardId = boardList[0].boardId + displayTasksForCurrentBoard() + } + } + + private fun showAddTaskDialog() { + val builder = AlertDialog.Builder(this) + builder.setTitle("Add New Task") + + val view = layoutInflater.inflate(R.layout.dialog_add_task, null) + val taskNameInput = view.findViewById(R.id.taskNameInput) + val taskDescriptionInput = view.findViewById(R.id.taskDescriptionInput) + val dueDateTextView = view.findViewById(R.id.dueDateTextView) + val priorityTextView = view.findViewById(R.id.priorityTextView) + + selectedDueDate = "" + selectedPriority = "" + + dueDateTextView.setOnClickListener { + val calendar = Calendar.getInstance() + val year = calendar.get(Calendar.YEAR) + val month = calendar.get(Calendar.MONTH) + val day = calendar.get(Calendar.DAY_OF_MONTH) + val datePickerDialog = DatePickerDialog(this, { _, yearSelected, monthOfYear, dayOfMonth -> + // Use yearSelected instead of year + val selectedDate = Calendar.getInstance() + selectedDate.set(yearSelected, monthOfYear, dayOfMonth) + val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + selectedDueDate = dateFormat.format(selectedDate.time) + dueDateTextView.text = selectedDueDate + }, year, month, day) + + datePickerDialog.datePicker.minDate = System.currentTimeMillis() - 1000 // Disallow past dates + datePickerDialog.show() + } + + priorityTextView.setOnClickListener { + val popupMenu = PopupMenu(this, priorityTextView) + popupMenu.menu.add("High") + popupMenu.menu.add("Medium") + popupMenu.menu.add("Low") + popupMenu.setOnMenuItemClickListener { item -> + selectedPriority = item.title.toString() + priorityTextView.text = selectedPriority + true + } + popupMenu.show() + } + + builder.setView(view) + + builder.setPositiveButton("OK") { _, _ -> + val taskName = taskNameInput.text.toString() + val taskDescription = taskDescriptionInput.text.toString() + addNewTask(taskName, taskDescription) + } + builder.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() } + + builder.show() + } + + private fun addNewTask(taskName: String, taskDescription: String) { + val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val dueDate = if (selectedDueDate.isNotEmpty()) { + sdf.parse(selectedDueDate)?.let { Timestamp(it) } ?: Timestamp.now() + } else { + Timestamp.now() + } + + val newTask = TaskItem( + title = taskName, + description = taskDescription, + dueDate = dueDate, + priority = selectedPriority, + createdAt = Timestamp.now() + ) + + currentBoardId?.let { boardId -> + dbHandler.addTaskItem(newTask, boardId) { + // This callback does not currently do anything. Assuming tasks is a list that holds TaskItems, + // and taskAdapter is an adapter for a RecyclerView, you might want to: + tasks.add(newTask) // Add the new task to the list + taskAdapter.notifyItemInserted(tasks.size - 1) // Notify the adapter that an item has been added + } + } + } + + private fun displayTasksForCurrentBoard() { + currentBoardId?.let { + displayTasks(it) + } + } + + private fun displayTasks(boardId: String) { + dbHandler.getTasksByBoardId(boardId) { result -> + tasks.clear() // Clear existing tasks + tasks.addAll(result) // Add all fetched tasks + taskAdapter.notifyDataSetChanged() // Notify adapter to refresh UI + } + } + + private fun showEditTaskDialog(taskItem: TaskItem) { + val builder = AlertDialog.Builder(this) + builder.setTitle("Edit Task") + + val view = layoutInflater.inflate(R.layout.dialog_add_task, null) + val taskNameInput = view.findViewById(R.id.taskNameInput) + val taskDescriptionInput = view.findViewById(R.id.taskDescriptionInput) + val dueDateTextView = view.findViewById(R.id.dueDateTextView) + val priorityTextView = view.findViewById(R.id.priorityTextView) + + // Populate dialog with task details + taskNameInput.setText(taskItem.title) + taskDescriptionInput.setText(taskItem.description) + val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + dueDateTextView.text = sdf.format(taskItem.dueDate.toDate()) + priorityTextView.text = taskItem.priority + + // Due Date Picker + dueDateTextView.setOnClickListener { + val calendar = Calendar.getInstance().apply { + time = taskItem.dueDate.toDate() + } + DatePickerDialog(this, { _, year, month, dayOfMonth -> + val newDate = Calendar.getInstance() + newDate.set(year, month, dayOfMonth) + selectedDueDate = sdf.format(newDate.time) + dueDateTextView.text = selectedDueDate + }, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)).show() + } + + // Priority Selection + priorityTextView.setOnClickListener { + val popupMenu = PopupMenu(this, priorityTextView) + popupMenu.menu.add("High") + popupMenu.menu.add("Medium") + popupMenu.menu.add("Low") + popupMenu.setOnMenuItemClickListener { item -> + selectedPriority = item.title.toString() + priorityTextView.text = selectedPriority + true + } + popupMenu.show() + } + + builder.setView(view) + + builder.setPositiveButton("OK") { _, _ -> + // Update task with new details + val updatedFields = mapOf( + "title" to taskNameInput.text.toString(), + "description" to taskDescriptionInput.text.toString(), + "due_date" to Timestamp(sdf.parse(selectedDueDate) ?: taskItem.dueDate.toDate()), + "priority" to selectedPriority + ) + currentBoardId?.let { boardId -> + dbHandler.updateTaskItem(boardId, taskItem.taskId, updatedFields) { + // Find the task in the local list and update it + val taskIndex = tasks.indexOfFirst { it.taskId == taskItem.taskId } + if (taskIndex != -1) { + tasks[taskIndex].apply { + title = taskNameInput.text.toString() + description = taskDescriptionInput.text.toString() + dueDate = Timestamp(sdf.parse(selectedDueDate) ?: dueDate.toDate()) + priority = selectedPriority + } + // Notify the adapter to refresh the item + taskAdapter.notifyItemChanged(taskIndex) + } + } + } + } + builder.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() } + + builder.show() + } + + public fun setupSelectedTabLayout(position: Int) { + tabLayout.removeAllTabs() + boardList.forEach { board -> + val tab = tabLayout.newTab().setText(board.name) + tabLayout.addTab(tab) + } + if (boardList.isNotEmpty()) { + currentBoardId = boardList[position].boardId + displayTasksForCurrentBoard() + } + } } diff --git a/app/src/main/res/drawable/ic_arrow_down.xml b/app/src/main/res/drawable/ic_arrow_down.xml new file mode 100644 index 0000000..829a681 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_down.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_right.xml b/app/src/main/res/drawable/ic_arrow_right.xml new file mode 100644 index 0000000..e0fbdde --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_right.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_more_horizontal.xml b/app/src/main/res/drawable/ic_more_horizontal.xml new file mode 100644 index 0000000..8a990fe --- /dev/null +++ b/app/src/main/res/drawable/ic_more_horizontal.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rounded_button.xml b/app/src/main/res/drawable/rounded_button.xml deleted file mode 100644 index 06ac31b..0000000 --- a/app/src/main/res/drawable/rounded_button.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_taskboards.xml b/app/src/main/res/layout/activity_taskboards.xml index df07478..0e08abc 100644 --- a/app/src/main/res/layout/activity_taskboards.xml +++ b/app/src/main/res/layout/activity_taskboards.xml @@ -15,12 +15,29 @@ app:layout_constraintTop_toTopOf="parent" app:tabMode="scrollable" /> - + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/board_item.xml b/app/src/main/res/layout/board_item.xml deleted file mode 100644 index 86c1461..0000000 --- a/app/src/main/res/layout/board_item.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_add_task.xml b/app/src/main/res/layout/dialog_add_task.xml new file mode 100644 index 0000000..f7234e1 --- /dev/null +++ b/app/src/main/res/layout/dialog_add_task.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/grid_items.xml b/app/src/main/res/layout/grid_items.xml new file mode 100644 index 0000000..78d79c1 --- /dev/null +++ b/app/src/main/res/layout/grid_items.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/grid_outer.xml b/app/src/main/res/layout/grid_outer.xml new file mode 100644 index 0000000..673d99d --- /dev/null +++ b/app/src/main/res/layout/grid_outer.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/task_item.xml b/app/src/main/res/layout/task_item.xml new file mode 100644 index 0000000..de8f9ac --- /dev/null +++ b/app/src/main/res/layout/task_item.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/bottom_navigation_menu.xml b/app/src/main/res/menu/bottom_navigation_menu.xml deleted file mode 100644 index e73b6af..0000000 --- a/app/src/main/res/menu/bottom_navigation_menu.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/task_options_menu.xml b/app/src/main/res/menu/task_options_menu.xml new file mode 100644 index 0000000..78c9dca --- /dev/null +++ b/app/src/main/res/menu/task_options_menu.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/board.png b/app/src/main/res/mipmap-hdpi/board.png new file mode 100644 index 0000000..78eb30d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/board.png differ diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..8f80834 --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,3 @@ + + #333333 + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 11e1405..ed64ee2 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -5,4 +5,5 @@ #000000 #008000 #FF0000 + #F5F5F5 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d4acc67..9ba1933 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,21 +1,11 @@ - Tasks Board - Hello blank fragment - 445591915415-1aurijt9tghpbu1j2qs64rju2a787vfk.apps.googleusercontent.com - Login - Logo of the application - Sign in with Google Tasks Board - Google icon + Tasks Board Sign in to your account - Created At: %1$s - Updated At: %1$s - Description: - Due Date: - Priority: - Status - Finished - Not Finished Tab layout for switching between boards Add task + Description: %1$s + Due date: %1$s + Priority: %1$s + Search \ No newline at end of file