Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package br.com.cauezito.schedrix.presentation

import br.com.cauezito.schedrix.domain.model.Appointment
import br.com.cauezito.schedrix.domain.useCase.GetAvailableAppointmentTimesUseCase
import br.com.cauezito.schedrix.extensions.DateExtensions
import br.com.cauezito.schedrix.extensions.DateExtensions.availableTimesFromSelectedDate
Expand Down Expand Up @@ -34,29 +35,32 @@ class AppointmentScreenModel(
val state: StateFlow<AppointmentState> = _state

private val todayDate = DateExtensions.getCurrentDate().date
private val currentTimeZone = TimeZone.currentSystemDefault().toString().formatTimezone()
private var selectedTimeZone: TimeZone = TimeZone.currentSystemDefault()
private val currentTimeZoneFormatted = selectedTimeZone.toString().formatTimezone()
private var appointments: List<AppointmentDateTime> = emptyList()
private var choseStartAndEndDates: Pair<String, String> = Pair("", "")
private var appointmentDomain: Appointment? = null
private var choseStartAndEndDates: Pair<String, String> = "" to ""

init {
choseStartAndEndDates = todayDate.formatStartAndEnd()
_state.value = _state.value.copy(
currentMonthYear = todayDate,
currentTimezone = currentTimeZone
currentTimezone = currentTimeZoneFormatted
)
}

internal fun fetchAvailableTimes() = screenModelScope.launch {
try {
val monthPlaceHolder = _state.value.currentMonthYear.month.number.defineMonthPlaceholder()

val result = getAvailableTimes(
val domainAppointment = getAvailableTimes(
choseStartAndEndDates.first,
choseStartAndEndDates.second,
monthPlaceHolder
).asPresentation()
)

appointments = result.availableAppointments
appointmentDomain = domainAppointment
appointments = domainAppointment.asPresentation(selectedTimeZone).availableAppointments

_state.value = _state.value.copy(
calendarDays = mapToAppointmentCalendarDay(
Expand Down Expand Up @@ -115,6 +119,29 @@ class AppointmentScreenModel(
)
}

internal fun changeTimezone(timezoneId: String) {
selectedTimeZone = TimeZone.of(timezoneId)
_state.value = _state.value.copy(
currentTimezone = timezoneId.formatTimezone()
)

appointmentDomain?.let { domainAppointment ->
appointments = domainAppointment.asPresentation(selectedTimeZone).availableAppointments
_state.value.selectedDate?.let { date ->
val availableTimes = date.availableTimesFromSelectedDate(appointments)
_state.value = _state.value.copy(selectedDateTimes = availableTimes)
}

_state.value = _state.value.copy(
calendarDays = mapToAppointmentCalendarDay(
currentMonth = _state.value.currentMonthYear,
appointments = appointments,
selectedDate = _state.value.selectedDate
)
)
}
}

internal fun selectAppointmentTime(dateTime: AppointmentDateTime) {
_state.value = _state.value.copy(
isNameValid = null,
Expand All @@ -127,7 +154,7 @@ class AppointmentScreenModel(
internal fun tryAgainAfterError() {
_state.value = _state.value.copy(
currentMonthYear = todayDate,
currentTimezone = currentTimeZone,
currentTimezone = selectedTimeZone.toString().formatTimezone(),
showContentLoading = true,
isNameValid = null,
isEmailValid = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime

fun Appointment.asPresentation(): AppointmentPresentation {
val availableAppointments = this.availableTimes.map { date ->
fun Appointment.asPresentation(timeZone: TimeZone): AppointmentPresentation {
val availableAppointments = availableTimes.map { date ->
val instant = Instant.parse(date)
val localDateTime = instant.toLocalDateTime(TimeZone.currentSystemDefault())
val localDateTime = instant.toLocalDateTime(timeZone)

AppointmentDateTime(availableAppointmentDateTime = localDateTime)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import br.com.cauezito.schedrix.ui.tokens.Strings.CONFIRMATION_SECTION_INPUT_NAM
import br.com.cauezito.schedrix.ui.tokens.Strings.CONFIRMATION_SECTION_NAME_INPUT_SUPPORT_TEXT
import br.com.cauezito.schedrix.ui.tokens.Strings.CONFIRMATION_SECTION_TITLE
import br.com.cauezito.schedrix.ui.tokens.Strings.CONFIRMATION_SECTION_TOP_BAR_CONTENT_DESCRIPTION
import kotlinx.datetime.TimeZone

@Composable
internal fun AppointmentConfirmationSection(
Expand All @@ -51,6 +52,7 @@ internal fun AppointmentConfirmationSection(
redirectLink = generateGoogleCalendarLink(
userName = name,
startDateTime = state.finalSelectedDateTime,
timezone = TimeZone.of(state.currentTimezone.replace(" - ", "/").replace(" ", "_"))
Copy link

Copilot AI Jun 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Using string replace calls to map the formatted timezone back into a valid ID is brittle. Consider extracting or centralizing this mapping logic into a helper function or using a reliable lookup.

Suggested change
timezone = TimeZone.of(state.currentTimezone.replace(" - ", "/").replace(" ", "_"))
timezone = TimeZone.of(mapFormattedTimezoneToId(state.currentTimezone))

Copilot uses AI. Check for mistakes.
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ internal class AppointmentTimeScreen() : Screen {
AppointmentTimeScreenContent(
state = state,
onSelectedTime = onSelectedTime,
onTimezoneChange = screenModel::changeTimezone,
onBackPressed = onBackPressed
)
}
Expand All @@ -36,11 +37,13 @@ internal class AppointmentTimeScreen() : Screen {
private fun AppointmentTimeScreenContent(
state: AppointmentState,
onSelectedTime: (AppointmentDateTime) -> Unit,
onTimezoneChange: (String) -> Unit,
onBackPressed: () -> Unit
) {
AppointmentTimeSelectionSection(
state = state,
onSelectedTime = onSelectedTime,
onTimezoneChange = onTimezoneChange,
onBackPressed
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import br.com.cauezito.schedrix.presentation.AppointmentState
import br.com.cauezito.schedrix.presentation.model.AppointmentDateTime
import br.com.cauezito.schedrix.ui.components.shared.ScaffoldStructure
import br.com.cauezito.schedrix.ui.components.shared.TimeSelectorItem
import br.com.cauezito.schedrix.ui.components.shared.TimezoneSelector
import br.com.cauezito.schedrix.ui.tokens.Dimens.dimens_100
import br.com.cauezito.schedrix.ui.tokens.Dimens.dimens_12
import br.com.cauezito.schedrix.ui.tokens.Dimens.dimens_14
Expand All @@ -41,6 +42,7 @@ import br.com.cauezito.schedrix.ui.tokens.Strings.appointmentQuestion
internal fun AppointmentTimeSelectionSection(
state: AppointmentState,
onSelectedTime: (AppointmentDateTime) -> Unit,
onTimezoneChange: (String) -> Unit,
onBackPressed: () -> Unit
) {
ScaffoldStructure(
Expand Down Expand Up @@ -90,10 +92,9 @@ internal fun AppointmentTimeSelectionSection(
color = Color.Black,
)

Text(
text = state.currentTimezone,
style = MaterialTheme.typography.bodyMedium,
color = Color.Black
TimezoneSelector(
current = state.currentTimezone,
onTimezoneSelected = onTimezoneChange
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package br.com.cauezito.schedrix.ui.components.shared

import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import br.com.cauezito.schedrix.extensions.StringExtensions.formatTimezone
import br.com.cauezito.schedrix.presentation.model.timezones

@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun TimezoneSelector(
current: String,
onTimezoneSelected: (String) -> Unit,
modifier: Modifier = Modifier
) {
var expanded by remember { mutableStateOf(false) }

ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = !expanded },
modifier = modifier
) {
// Prevent manual typing so the user picks from the dropdown instead
OutlinedTextField(
value = current.formatTimezone(),
onValueChange = {},
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
modifier = Modifier.menuAnchor()
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
timezones.forEach { zone ->
DropdownMenuItem(
text = { Text(zone.formatTimezone()) },
onClick = {
onTimezoneSelected(zone)
expanded = false
}
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,18 @@ internal class AppointmentScreenModelTest {
assertFalse(state.showContentLoading)
assertTrue(state.calendarDays.isNotEmpty())
}

@Test
fun `WHEN changeTimezone THEN updates timezone and recalculates times`() = runTest(dispatcher) {
screenModel.fetchAvailableTimes()
advanceUntilIdle()
screenModel.changeSelectedDate(fakeDate)
val firstTime = screenModel.state.value.selectedDateTimes.first()

screenModel.changeTimezone("Asia/Tokyo")

val updatedState = screenModel.state.value
assertTrue(updatedState.currentTimezone.contains("Asia"))
assertNotEquals(firstTime, updatedState.selectedDateTimes.first())
}
}