feat: Add support for editing events

This commit is contained in:
Peter Vacho 2025-01-02 14:33:55 +01:00
parent a6579e6434
commit c1e3e09bf4
Signed by: school
GPG key ID: 8CFC3837052871B4
6 changed files with 205 additions and 58 deletions

View file

@ -32,6 +32,7 @@ import com.p_vacho.neat_calendar.api.RetrofitClient
import com.p_vacho.neat_calendar.api.models.CategoryResponse
import com.p_vacho.neat_calendar.api.models.EventRequest
import com.p_vacho.neat_calendar.api.models.EventResponse
import com.p_vacho.neat_calendar.api.models.PartialEventRequest
import com.p_vacho.neat_calendar.api.models.ValidationError
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -42,6 +43,10 @@ import java.time.LocalDateTime
import java.time.ZoneId
import kotlin.properties.Delegates
enum class EventMode {
CREATE, EDIT
}
class CreateEventActivity : AppCompatActivity() {
private lateinit var etEventTitle: EditText
private lateinit var etEventDescription: EditText
@ -59,7 +64,9 @@ class CreateEventActivity : AppCompatActivity() {
private lateinit var eventCategories: MutableList<CategoryResponse>
private lateinit var allCategories: List<CategoryResponse>
private lateinit var defaultDate: LocalDate
private var existingEvent: EventResponse? = null
private var instantEvent: Boolean = true
private var selectedColor by Delegates.notNull<Int>()
@ -76,13 +83,50 @@ class CreateEventActivity : AppCompatActivity() {
insets
}
val dateString = intent.getStringExtra("date")!!
defaultDate = LocalDate.parse(dateString)
allCategories = emptyList()
eventCategories = mutableListOf() // start off empty
initializeViews()
allCategories = emptyList() // fetched later
val mode = intent.getStringExtra("mode")!!.let { EventMode.valueOf(it) }
if (mode == EventMode.CREATE) {
val dateString = intent.getStringExtra("date")!!
defaultDate = LocalDate.parse(dateString)
eventCategories = mutableListOf() // start off empty
} else if (mode == EventMode.EDIT) {
@Suppress("DEPRECATION")
existingEvent = intent.getParcelableExtra<EventResponse>("event")!!
btnCreateEvent.setText(R.string.update_event)
// eventCategories will be filled later on, once we fetch all available categories
// as we only have the category IDs from the existingEvents.
eventCategories = mutableListOf()
defaultDate = existingEvent!!.start_time.toLocalDate()
startDateTime = existingEvent!!.start_time.toLocalDateTime()
endDateTime = existingEvent!!.end_time.toLocalDateTime()
txtStartTime.setText(formatDateTime(startDateTime))
if (endDateTime != startDateTime) {
instantEvent = false
txtEndTime.visibility = View.VISIBLE
txtEndTime.setText(formatDateTime(endDateTime))
} else {
instantEvent = true
endDateTime = null
txtEndTime.visibility = View.GONE
txtEndTime.setText("")
}
selectedColor = existingEvent!!.color.toArgb()
etEventTitle.setText(existingEvent!!.title)
etEventDescription.setText(existingEvent!!.description)
}
categoryRecyclerView.adapter = CategoryChipAdapter(eventCategories, true, ::onCategoryRemoved)
setupListeners()
updateCategoryView()
fetchCategories()
@ -105,7 +149,6 @@ class CreateEventActivity : AppCompatActivity() {
categoryRecyclerView.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
categoryRecyclerView.adapter = CategoryChipAdapter(eventCategories, true, ::onCategoryRemoved)
selectedColor = ContextCompat.getColor(this, R.color.event_indicator_color)
}
@ -150,7 +193,7 @@ class CreateEventActivity : AppCompatActivity() {
btnAddCategory.setOnClickListener { addCategory() }
btnColorPicker.setOnClickListener { openColorPickerDialog() }
btnCreateEvent.setOnClickListener { createEvent() }
btnCreateEvent.setOnClickListener { saveEvent() }
btnClose.setOnClickListener { finish() }
}
@ -167,7 +210,25 @@ class CreateEventActivity : AppCompatActivity() {
// Fetch categories from the API
val fetchedCategories = RetrofitClient.categoryService.userCategories(userId)
withContext(Dispatchers.Main) { allCategories = fetchedCategories }
withContext(Dispatchers.Main) {
allCategories = fetchedCategories
// For update mode, update event categories now that we fetched all the available ones
// as we only have the ids in the passed existingEvent.
if (existingEvent != null) {
val newEventCategories = existingEvent!!.category_ids.mapNotNull { categoryId ->
allCategories.find { it.id == categoryId }
}.toMutableList()
eventCategories.clear() // should be empty, but it doesn't hurt
eventCategories.addAll(newEventCategories)
// TODO: For some reason, the categories still aren't shown here
@Suppress("NotifyDataSetChanged")
categoryRecyclerView.adapter!!.notifyDataSetChanged()
}
}
}
}
@ -226,7 +287,7 @@ class CreateEventActivity : AppCompatActivity() {
updateCategoryView()
}
private fun createEvent() {
private fun saveEvent() {
val title = etEventTitle.text.toString()
val description = etEventDescription.text.toString()
@ -240,20 +301,35 @@ class CreateEventActivity : AppCompatActivity() {
return
}
val eventRequest = EventRequest(
title = title,
description = description,
category_ids = eventCategories.map { it.id },
start_time = startDateTime!!.atZone(ZoneId.systemDefault()).toOffsetDateTime(),
end_time = endDateTime?.atZone(ZoneId.systemDefault())?.toOffsetDateTime()
?: startDateTime!!.atZone(ZoneId.systemDefault()).toOffsetDateTime(),
color = Color.valueOf(selectedColor)
)
lifecycleScope.launch(Dispatchers.IO) {
try {
val createdEvent = RetrofitClient.eventsService.createEvent(eventRequest)
withContext(Dispatchers.Main) { handleEventCreated(createdEvent) }
var savedEvent: EventResponse
if (existingEvent == null) {
val eventRequest = EventRequest(
title = title,
description = description,
category_ids = eventCategories.map { it.id },
start_time = startDateTime!!.atZone(ZoneId.systemDefault())
.toOffsetDateTime(),
end_time = endDateTime?.atZone(ZoneId.systemDefault())?.toOffsetDateTime()
?: startDateTime!!.atZone(ZoneId.systemDefault()).toOffsetDateTime(),
color = Color.valueOf(selectedColor)
)
savedEvent = RetrofitClient.eventsService.createEvent(eventRequest)
} else {
val eventRequest = PartialEventRequest(
title = title,
description = description,
category_ids = eventCategories.map { it.id },
start_time = startDateTime!!.atZone(ZoneId.systemDefault())
.toOffsetDateTime(),
end_time = endDateTime?.atZone(ZoneId.systemDefault())?.toOffsetDateTime()
?: startDateTime!!.atZone(ZoneId.systemDefault()).toOffsetDateTime(),
color = Color.valueOf(selectedColor)
)
savedEvent = RetrofitClient.eventsService.updateEvent(existingEvent!!.id, eventRequest)
}
withContext(Dispatchers.Main) { handleEventSaved(savedEvent) }
} catch (e: HttpException) {
if (e.code() != 422) {
throw e
@ -268,7 +344,7 @@ class CreateEventActivity : AppCompatActivity() {
withContext(Dispatchers.Main) {
Toast.makeText(
this@CreateEventActivity,
"Failed to create event: $errMsg",
"Failed to save event: $errMsg",
Toast.LENGTH_SHORT
).show()
}
@ -276,13 +352,20 @@ class CreateEventActivity : AppCompatActivity() {
}
}
private fun handleEventCreated(createdEvent: EventResponse) {
Toast.makeText(this, "Event Created: ${createdEvent.title}", Toast.LENGTH_SHORT).show()
private fun handleEventSaved(createdEvent: EventResponse) {
Toast.makeText(this, "Event Saved: ${createdEvent.title}", Toast.LENGTH_SHORT).show()
val intent = Intent().apply {
putExtra("newEvent", createdEvent)
if (existingEvent == null) {
val intent = Intent().apply {
putExtra("newEvent", createdEvent)
}
setResult(RESULT_OK, intent) // Pass the event back
} else {
val intent = Intent().apply {
putExtra("updatedEvent", createdEvent)
}
setResult(RESULT_OK, intent) // Pass the event back
}
setResult(RESULT_OK, intent) // Pass the event back
finish() // Close the activity and return
}

View file

@ -1,6 +1,5 @@
package com.p_vacho.neat_calendar.activities
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.widget.ImageButton
@ -9,12 +8,17 @@ 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.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.p_vacho.neat_calendar.R
import com.p_vacho.neat_calendar.adapters.EventCardAdapter
import com.p_vacho.neat_calendar.api.RetrofitClient
import com.p_vacho.neat_calendar.api.models.EventResponse
import com.p_vacho.neat_calendar.models.CalendarDay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class DayViewActivity : AppCompatActivity() {
private lateinit var btnBack: ImageButton
@ -32,7 +36,17 @@ class DayViewActivity : AppCompatActivity() {
@Suppress("DEPRECATION")
val newEvent: EventResponse? = result.data?.getParcelableExtra("newEvent")
newEvent?.let { addNewEvent(it) }
newEvent?.let { eventCreateReply(it) }
}
}
private val editEventLauncher = registerForActivityResult(
androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
@Suppress("DEPRECATION")
val updatedEvent: EventResponse? = result.data?.getParcelableExtra("updatedEvent")
updatedEvent?.let { eventEditReply(it) }
}
}
@ -64,20 +78,52 @@ class DayViewActivity : AppCompatActivity() {
tvDate.text = calendarDay.date.toString()
rvEvents.layoutManager = LinearLayoutManager(this)
rvEvents.adapter = EventCardAdapter(events, this)
rvEvents.adapter = EventCardAdapter(events, this, ::onEventEdit, ::onEventDelete)
btnBack.setOnClickListener { finish() }
btnAddEvent.setOnClickListener { navigateToCreateEventActivity() }
btnAddEvent.setOnClickListener { onEventCreate() }
}
fun navigateToCreateEventActivity() {
/**
* This is triggered on the add button click.
*/
private fun onEventCreate() {
val intent = Intent(this, CreateEventActivity::class.java).apply {
putExtra("mode", EventMode.CREATE.name)
putExtra("date", calendarDay.date.toString())
}
createEventLauncher.launch(intent)
}
private fun addNewEvent(newEvent: EventResponse) {
/**
* This is triggered on the edit button click from the event card adapter.
*/
private fun onEventEdit(event: EventResponse, position: Int) {
val intent = Intent(this, CreateEventActivity::class.java).apply {
putExtra("mode", EventMode.EDIT.name)
putExtra("event", event)
}
editEventLauncher.launch(intent)
}
/**
* This is triggered on the delete button click from the event card adapter.
*/
private fun onEventDelete(event: EventResponse, position: Int) {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
RetrofitClient.eventsService.deleteEvent(event.id)
}
events.removeAt(position)
rvEvents.adapter!!.notifyItemRemoved(position)
}
}
/**
* Triggered with the CreateEventActivity return value.
*/
private fun eventCreateReply(newEvent: EventResponse) {
events.add(newEvent)
events.sortBy { it.start_time }
@ -85,4 +131,21 @@ class DayViewActivity : AppCompatActivity() {
@Suppress("NotifyDataSetChanged")
rvEvents.adapter!!.notifyDataSetChanged()
}
/**
* Triggered with the CreateEventActivity return value.
*/
private fun eventEditReply(updatedEvent: EventResponse) {
val index = events.indexOfFirst { it.id == updatedEvent.id }
if (index == -1) {
throw IllegalStateException("Updated event ID wasn't found in the events list")
}
events[index] = updatedEvent
events.sortBy { it.start_time }
// We can't be more specific, we don't know the id due to sorting
@Suppress("NotifyDataSetChanged")
rvEvents.adapter!!.notifyDataSetChanged()
}
}

View file

@ -1,5 +1,7 @@
package com.p_vacho.neat_calendar.adapters
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -10,6 +12,8 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.p_vacho.neat_calendar.R
import com.p_vacho.neat_calendar.activities.CreateEventActivity
import com.p_vacho.neat_calendar.activities.EventMode
import com.p_vacho.neat_calendar.api.RetrofitClient
import com.p_vacho.neat_calendar.api.models.EventResponse
import kotlinx.coroutines.Dispatchers
@ -22,7 +26,9 @@ import java.util.Locale
class EventCardAdapter(
private val events: MutableList<EventResponse>,
private val lifecycleOwner: LifecycleOwner
private val lifecycleOwner: LifecycleOwner,
private val onEditEvent: (EventResponse, Int) -> Unit,
private val onDeleteEvent: (EventResponse, Int) -> Unit
) :
RecyclerView.Adapter<EventCardAdapter.EventViewHolder>() {
@ -57,13 +63,9 @@ class EventCardAdapter(
holder.tvDescription.text = event.description
}
// Handle Delete & Edit Button Clicks
holder.btnEdit.setOnClickListener {
editEvent(event)
}
holder.btnDelete.setOnClickListener {
deleteEvent(event, position)
}
// Forward the Delete & Edit Button Clicks
holder.btnEdit.setOnClickListener { onEditEvent(event, position) }
holder.btnDelete.setOnClickListener { onDeleteEvent(event, position) }
// Initialize empty state for categories
holder.rvCategories.layoutManager =
@ -113,20 +115,4 @@ class EventCardAdapter(
holder.rvCategories.adapter = CategoryChipAdapter(categories)
}
}
private fun deleteEvent(event: EventResponse, position: Int) {
lifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.IO) {
RetrofitClient.eventsService.deleteEvent(event.id)
}
// Remove the event from the list and notify the adapter
events.removeAt(position)
notifyItemRemoved(position)
}
}
private fun editEvent(event: EventResponse) {
TODO("Implement event editing")
}
}

View file

@ -29,4 +29,13 @@ data class EventRequest(
val start_time: OffsetDateTime,
val end_time: OffsetDateTime,
val color: Color,
)
data class PartialEventRequest(
val title: String?,
val description: String?,
val category_ids: List<String>?,
val start_time: OffsetDateTime?,
val end_time: OffsetDateTime?,
val color: Color?,
)

View file

@ -2,9 +2,11 @@ package com.p_vacho.neat_calendar.api.services
import com.p_vacho.neat_calendar.api.models.EventRequest
import com.p_vacho.neat_calendar.api.models.EventResponse
import com.p_vacho.neat_calendar.api.models.PartialEventRequest
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.PATCH
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
@ -37,4 +39,7 @@ interface EventsService {
@POST("events")
suspend fun createEvent(@Body eventData: EventRequest): EventResponse
@PATCH("events/{event_id}")
suspend fun updateEvent(@Path("event_id") eventId: String, @Body eventData: PartialEventRequest): EventResponse
}

View file

@ -43,4 +43,5 @@
<string name="select_color">Select Color</string>
<string name="remove_category">Remove the category</string>
<string name="add_category">Add category</string>
<string name="update_event">Update Event</string>
</resources>