feat: Add support for editing events
This commit is contained in:
parent
a6579e6434
commit
c1e3e09bf4
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?,
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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>
|
Loading…
Reference in a new issue