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
@@ -0,0 +1,36 @@
package com.cornellappdev.resell.android.model.api

import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST

interface AvailabilityApiService {

@GET("availability/")
suspend fun getMyAvailability(): AvailabilityResponse

@POST("availability/update/")
suspend fun updateAvailability(
@Body request: UpdateAvailabilityRequest
): AvailabilityResponse
}

data class AvailabilityResponse(
val availability: UserAvailability
)

data class UserAvailability(
val id: String,
val userId: String,
val schedule: Map<String, List<AvailabilitySlot>>,
val updatedAt: String
)

data class AvailabilitySlot(
val startDate: String,
val endDate: String
)

data class UpdateAvailabilityRequest(
val schedule: Map<String, List<AvailabilitySlot>>
)
Comment on lines +18 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check which JSON converter is used and whether a naming policy is configured.
rg -nP '\b(GsonConverterFactory|MoshiConverterFactory|setFieldNamingPolicy|FieldNamingPolicy|GsonBuilder|Moshi\.Builder)\b' --type=kotlin -C2

# Check whether other API models in this repo use `@SerializedName` / `@Json` annotations.
rg -nP '@(SerializedName|Json)\b' --type=kotlin -C1 -g 'app/src/main/java/com/cornellappdev/resell/android/model/api/**'

# Spot-check the actual backend contract (if documented) for these endpoints.
rg -nP 'availability/(update)?' --type=kotlin -C2

Repository: cuappdev/resell-android

Length of output: 12237


Add @SerializedName annotations to match backend JSON keys.

The codebase uses GsonConverterFactory.create() without a custom naming policy, and other API models extensively use @SerializedName annotations (e.g., SettingsApiService, LoginApiService, PostsApiService). Fields like userId, updatedAt, startDate, and endDate in UserAvailability and AvailabilitySlot will fail to deserialize if the backend uses snake_case naming, which follows the established pattern in this project.

Add missing annotations
+import com.google.gson.annotations.SerializedName
+
 data class UserAvailability(
     val id: String,
-    val userId: String,
+    `@SerializedName`("user_id") val userId: String,
     val schedule: Map<String, List<AvailabilitySlot>>,
-    val updatedAt: String
+    `@SerializedName`("updated_at") val updatedAt: String
 )

 data class AvailabilitySlot(
-    val startDate: String,
-    val endDate: String
+    `@SerializedName`("start_date") val startDate: String,
+    `@SerializedName`("end_date") val endDate: String
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/resell/android/model/api/AvailabilityApiService.kt`
around lines 18 - 36, The model classes (AvailabilityResponse, UserAvailability,
AvailabilitySlot, UpdateAvailabilityRequest) need Gson `@SerializedName`
annotations for fields that the backend uses in snake_case: add
`@SerializedName`("user_id") to userId, `@SerializedName`("updated_at") to
updatedAt, `@SerializedName`("start_date") and `@SerializedName`("end_date") to
startDate/endDate in AvailabilitySlot, and add `@SerializedName` annotations as
needed for id and schedule (e.g., "id" and "schedule") so
GsonConverterFactory.create() can correctly map JSON keys to the Kotlin
properties; update the data classes to include these annotations on the
corresponding properties.

Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,15 @@ class RetrofitInstance @Inject constructor(
.create(SettingsApiService::class.java)
}

val availabilityApi: AvailabilityApiService by lazy {
Retrofit.Builder()
.baseUrl(BuildConfig.BASE_API_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(AvailabilityApiService::class.java)
}

val transactionApi: TransactionApiService by lazy {
Retrofit.Builder()
.baseUrl(BuildConfig.BASE_API_URL)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.cornellappdev.resell.android.model.profile

import com.cornellappdev.resell.android.model.api.AvailabilitySlot
import com.cornellappdev.resell.android.model.api.RetrofitInstance
import com.cornellappdev.resell.android.model.api.UpdateAvailabilityRequest
import com.cornellappdev.resell.android.model.api.UserAvailability
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class AvailabilityRepository @Inject constructor(
private val retrofitInstance: RetrofitInstance
) {
suspend fun getMyAvailability(): UserAvailability {
return retrofitInstance.availabilityApi.getMyAvailability().availability
}

suspend fun updateAvailability(slots: List<LocalDateTime>): UserAvailability {
// Convert List<LocalDateTime> to Map<"yyyy-MM-dd", List<AvailabilitySlot>>
val schedule = slots
.groupBy { it.toLocalDate().toString() }
.mapValues { (_, daySlots) ->
daySlots.sortedBy { it }.map { start ->
AvailabilitySlot(
startDate = start.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
endDate = start.plusMinutes(30L)
.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
)
}
}
Comment on lines +22 to +32
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for any backend contract docs / sample payloads / tests that pin the expected datetime format.
rg -nP --type=kotlin -C2 'AvailabilitySlot|UpdateAvailabilityRequest|getMyAvailability|updateAvailability'
# And see how the grid defines slot duration so we can share the constant.
rg -nP --type=kotlin -C3 'plusMinutes|slotMinutes|SLOT_|stepMinutes'

Repository: cuappdev/resell-android

Length of output: 12724


🏁 Script executed:

#!/bin/bash
# Search for any SLOT-related constants or Duration definitions
rg -n 'SLOT|MINUTE|DURATION|SLOT_LENGTH|SLOT_HEIGHT' --type=kotlin | head -50

Repository: cuappdev/resell-android

Length of output: 438


🏁 Script executed:

#!/bin/bash
# Search for test files related to AvailabilityRepository or API
fd -e 'Test.kt' -o -e 'Test.java' | xargs rg -l 'Availability' 2>/dev/null

Repository: cuappdev/resell-android

Length of output: 2611


🏁 Script executed:

#!/bin/bash
# Check if there's any configuration or constants file defining slot length
fd -type f \( -name '*Constants*' -o -name '*Config*' \) --type kotlin | head -20

Repository: cuappdev/resell-android

Length of output: 238


🏁 Script executed:

#!/bin/bash
# Look for ISO_LOCAL_DATE_TIME usage elsewhere to see if this is a pattern
rg 'ISO_LOCAL_DATE_TIME|ISO_OFFSET_DATE_TIME|ZonedDateTime|OffsetDateTime' --type=kotlin -n

Repository: cuappdev/resell-android

Length of output: 875


🏁 Script executed:

#!/bin/bash
# Check Functions.kt to see the ZonedDateTime pattern
sed -n '100,110p' app/src/main/java/com/cornellappdev/resell/android/util/Functions.kt

Repository: cuappdev/resell-android

Length of output: 448


🏁 Script executed:

#!/bin/bash
# Look for Retrofit/Gson/Moshi configuration that might handle datetime serialization
rg -n 'GsonBuilder|JsonDeserializer|JsonSerializer|Moshi|kotlinx.serialization' --type=kotlin -C 3

Repository: cuappdev/resell-android

Length of output: 23001


🏁 Script executed:

#!/bin/bash
# Check if AvailabilitySlot has any serialization annotations
rg -B5 -A5 'data class AvailabilitySlot' app/src/main/java/com/cornellappdev/resell/android/model/api/AvailabilityApiService.kt

Repository: cuappdev/resell-android

Length of output: 296


🏁 Script executed:

#!/antml:bash
# Search for where UserAvailability and AvailabilitySlot are deserialized from backend
rg -n 'UserAvailability|AvailabilityResponse' --type=kotlin -B2 -A2 | head -80

Repository: cuappdev/resell-android

Length of output: 5497


🏁 Script executed:

#!/bin/bash
# Check the full context of AvailabilityViewModel parsing
sed -n '100,115p' app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/AvailabilityViewModel.kt

Repository: cuappdev/resell-android

Length of output: 496


🏁 Script executed:

#!/bin/bash
# Look for Retrofit/Interceptor configuration that might set up custom datetime handling
rg -n 'HttpClient|OkHttpClient|Interceptor|GsonBuilder|Converter' --type=kotlin -B2 -A5

Repository: cuappdev/resell-android

Length of output: 17562


🏁 Script executed:

#!/antml:bash
# Check if there are any test files or examples showing the actual API format
fd -name '*.kt' | xargs rg -l 'AvailabilitySlot.*startDate' | head -5

Repository: cuappdev/resell-android

Length of output: 298


🏁 Script executed:

#!/bin/bash
# Look for any backend API documentation or mock responses
fd -name '*.json' -o -name '*.md' | xargs rg -l 'availability\|Availability' 2>/dev/null | head -10

Repository: cuappdev/resell-android

Length of output: 298


Fix datetime format mismatch and extract hardcoded slot duration.

Two issues must be resolved before production use:

  1. Incompatible datetime formats between send/receive. AvailabilityRepository uses ISO_LOCAL_DATE_TIME (produces "2026-04-27T14:30:00" with no offset), but AvailabilityViewModel.toLocalDateTimes() parses with ISO_DATE_TIME (expects "2026-04-27T14:30:00+05:00" with offset). This will cause a DateTimeParseException once the backend returns real data. Align both to the same format — likely ISO_OFFSET_DATE_TIME / ISO_DATE_TIME with timezone info if comparing across clients/zones, or verify the backend contract and use a consistent formatter throughout.

  2. Hardcoded 30-minute slot length. The magic 30L is duplicated across AvailabilityRepository.kt (lines 28), AvailabilityUtil.kt, SelectableAvailabilityGrid.kt, ViewOnlyAvailabilityGrid.kt, and test helpers. Extract to a shared constant (e.g., SLOT_DURATION_MINUTES) so grid and repository use the same value.

Also replace daySlots.sortedBy { it } (line 25) with daySlots.sorted() since LocalDateTime is Comparable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/resell/android/model/profile/AvailabilityRepository.kt`
around lines 22 - 32, Update AvailabilityRepository so the datetime formatter
matches the parser used by AvailabilityViewModel.toLocalDateTimes(): replace
DateTimeFormatter.ISO_LOCAL_DATE_TIME with a formatter that includes
offset/timezone (e.g., DateTimeFormatter.ISO_OFFSET_DATE_TIME or the same
formatter used by toLocalDateTimes()) when formatting startDate and endDate for
AvailabilitySlot; also replace daySlots.sortedBy { it } with daySlots.sorted()
since LocalDateTime is Comparable. Extract the hardcoded 30L into a shared
constant (e.g., SLOT_DURATION_MINUTES) and use that constant in
AvailabilityRepository (endDate calculation), AvailabilityUtil,
SelectableAvailabilityGrid, ViewOnlyAvailabilityGrid, and test helpers so all
code references the same slot duration.


return retrofitInstance.availabilityApi.updateAvailability(
UpdateAvailabilityRequest(schedule = schedule)
).availability
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.cornellappdev.resell.android.ui.components.availability.helper
import androidx.compose.foundation.background

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.cornellappdev.resell.android.ui.components.global.ResellCheckboxRow
import com.cornellappdev.resell.android.ui.components.global.ResellSwitchRow
import com.cornellappdev.resell.android.ui.theme.Style

// TODO: very hard coded right now, should integrate networking here + implement viewmodel
@Composable
fun AvailabilityFilters(
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxWidth()
.background(color = Color(0xFFF7F3F9))
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.Start
) {

HorizontalDivider()

ResellSwitchRow(
title = "Google Calendar Access",
checked = true,
enabled = true,
onCheckedChange = {
// TODO
}
)

HorizontalDivider()

ResellSwitchRow(
title = "Availability Sharing",
checked = true,
enabled = true,
onCheckedChange = {
// TODO
}
)

HorizontalDivider()

Column(
modifier = Modifier.padding(22.dp),
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
Text(
text = "Sub-Calendars",
style = Style.body1,
fontWeight = FontWeight.SemiBold
)

// TODO: replace dummy here with actual sub-calendars from the user
val subCalendars = listOf("Personal", "Leetcode", "Youtube", "Capra")
// TODO: this is just hard coded, make it not hard coded
val checkedStates = remember { mutableStateMapOf<String, Boolean>().apply {
subCalendars.forEach { put(it, it != "Personal" && it != "Capra") }
}}
Comment on lines +78 to +80
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hoist checkedStates to the ViewModel so it survives panel collapse and configuration changes.

AvailabilityFilters is rendered inside an AnimatedVisibility in AvailabilityScreen.kt whose content is removed from composition when the panel closes. Because checkedStates is a plain remember { mutableStateMapOf(...) }, every collapse/expand of the filter panel will reset the user's selections back to the hardcoded defaults. Same applies to rotation (no rememberSaveable).

When you wire this up to the backend, hoist the map (and the two switch values) into AvailabilityViewModel and pass them in as parameters with callbacks, matching the pattern used by NotificationSettings. This will also make the existing // TODO switch handlers natural to fill in.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/resell/android/ui/components/availability/helper/AvailabilityFilters.kt`
around lines 78 - 80, The checkedStates mutable map is currently local to
AvailabilityFilters and reset when the composable is removed; move its state
into AvailabilityViewModel (add a MutableState<Map<String,Boolean>> or similar)
and expose it via state + update callback parameters to AvailabilityFilters
(mirror the pattern used by NotificationSettings). In practice: remove remember
{ mutableStateMapOf... } from AvailabilityFilters, add properties and updater
functions in AvailabilityViewModel (e.g., checkedStates, setCheckedState(key,
value)), also hoist the two switch values into the ViewModel with corresponding
setters, and update AvailabilityFilters to accept the current states and
callbacks so the TODO switch handlers call the supplied setters rather than
mutating local state.


Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
subCalendars.chunked(2).forEach { pair ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
pair.forEach { name ->
ResellCheckboxRow(
title = name,
checked = checkedStates[name] ?: false,
enabled = true,
onCheckedChange = { checkedStates[name] = it },
modifier = Modifier.weight(1f)
)
}
if (pair.size == 1) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
}
}
}

@Preview
@Composable
fun AvailabilityFiltersPreview() {
AvailabilityFilters()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.cornellappdev.resell.android.ui.components.availability.helper

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.cornellappdev.resell.android.ui.theme.Style
import java.time.YearMonth
import java.time.LocalDate


@Composable
fun MonthCalendar(
currentMonth: YearMonth,
selectedDates: List<LocalDate>,
onMonthChange: (YearMonth) -> Unit,
modifier: Modifier = Modifier,
) {
Comment on lines +24 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

onMonthChange is wired in but never invoked — month navigation is unreachable.

AvailabilityScreen.kt passes onMonthChange = { availabilityViewModel.setCurrentMonth(it) }, but MonthCalendar itself never calls the callback. There is no header arrow/chevron, no swipe gesture, and no clickable surface that produces a YearMonth. As written, the user cannot navigate to previous/next months from this calendar at all.

Either add the previous/next month controls (matching the Figma) and invoke onMonthChange(currentMonth.minusMonths(1) / .plusMonths(1)), or drop the parameter from the public API until navigation is implemented to avoid a dead callback.

Also applies to: 60-101

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/cornellappdev/resell/android/ui/components/availability/helper/MonthCalendar.kt`
around lines 24 - 30, MonthCalendar declares onMonthChange but never calls it,
so wire month navigation controls to invoke it: add previous/next buttons or
chevrons in the MonthCalendar header (matching Figma) and call
onMonthChange(currentMonth.minusMonths(1)) for the previous control and
onMonthChange(currentMonth.plusMonths(1)) for the next control; ensure the
clickable composables update accessibility/tap targets and preserve the existing
parameters (currentMonth, selectedDates) when rendering, or if you prefer not to
implement navigation now, remove the onMonthChange parameter from MonthCalendar
and its callers (e.g., AvailabilityScreen ->
availabilityViewModel.setCurrentMonth) to avoid a dead callback.

val firstDayOfMonth = currentMonth.atDay(1)
val firstDayOfWeek = firstDayOfMonth.dayOfWeek.value % 7
val daysInMonth = currentMonth.lengthOfMonth()
val dayLabels = listOf("Su", "Mo", "Tu", "We", "Th", "Fr", "Sa")

val sortedDates = selectedDates.sorted()
val rangeStart = sortedDates.firstOrNull()
val rangeEnd = sortedDates.lastOrNull()

Column(
modifier
.background(color = Color(0xfff7f3f9))
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth()
) {
dayLabels.forEach { label ->
Text(
text = label,
modifier = Modifier.weight(1f),
textAlign = TextAlign.Center,
style = Style.body2,
fontWeight = FontWeight.SemiBold,
color = Color(0xFF707070)
)
}
}

val totalCells = firstDayOfWeek + daysInMonth
val rows = (totalCells + 6) / 7

for (row in 0 until rows) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
for (col in 0..6) {
val cellIndex = row * 7 + col
val day = cellIndex - firstDayOfWeek + 1
val date = if (day in 1..daysInMonth) currentMonth.atDay(day) else null
val isSelected = date != null && date in selectedDates
val isRangeStart = date == rangeStart
val isRangeEnd = date == rangeEnd
val isMiddle = isSelected && !isRangeStart && !isRangeEnd

Box(
modifier = Modifier
.weight(1f)
.background(
color = if (isSelected) Color(0xffe4e0e8) else Color.Transparent,
shape = when {
isRangeStart && isRangeEnd -> RoundedCornerShape(4.dp)
isRangeStart -> RoundedCornerShape(topStart = 4.dp, bottomStart = 4.dp)
isRangeEnd -> RoundedCornerShape(topEnd = 4.dp, bottomEnd = 4.dp)
isMiddle -> RoundedCornerShape(0.dp)
else -> RoundedCornerShape(0.dp)
}
)
.padding(6.dp),
contentAlignment = Alignment.Center
) {
if (date != null) {
Text(text = "$day", style = Style.body2)
}
}
}
}
}
}
}

@Preview
@Composable
fun MonthCalendarPreview() {
MonthCalendar(
currentMonth = YearMonth.of(2026, 4),
selectedDates = listOf(
LocalDate.of(2026, 4, 16),
LocalDate.of(2026, 4, 17),
LocalDate.of(2026, 4, 18),
),
onMonthChange = {}
)
}
Loading