forked from Olympic/NTO-2025-Android-TeamTask
No ava
This commit is contained in:
@@ -3,52 +3,97 @@ package ru.myitschool.work.data.repo
|
|||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import ru.myitschool.work.core.DateUtils
|
import ru.myitschool.work.core.DateUtils
|
||||||
import ru.myitschool.work.data.source.NetworkDataSource
|
import ru.myitschool.work.data.source.NetworkDataSource
|
||||||
|
import ru.myitschool.work.data.source.dto.AvailablePlaceDto
|
||||||
|
import ru.myitschool.work.data.source.dto.BookedPlaceDto
|
||||||
import ru.myitschool.work.data.source.dto.UserDto
|
import ru.myitschool.work.data.source.dto.UserDto
|
||||||
import ru.myitschool.work.domain.entities.BookingEntity
|
import ru.myitschool.work.domain.entities.BookingEntity
|
||||||
import ru.myitschool.work.domain.entities.UserEntity
|
import ru.myitschool.work.domain.entities.UserEntity
|
||||||
|
|
||||||
|
|
||||||
object UserRepository {
|
object UserRepository {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение информации о пользователе через GET /api/<CODE>/info
|
||||||
|
*/
|
||||||
suspend fun getUserInfo(): Result<UserEntity> {
|
suspend fun getUserInfo(): Result<UserEntity> {
|
||||||
val code = AuthRepository.getSavedCode()
|
val code = AuthRepository.getSavedCode()
|
||||||
?: return Result.failure(IllegalStateException("Auth code is not saved"))
|
?: return Result.failure(IllegalStateException("Auth code is not saved"))
|
||||||
|
|
||||||
return NetworkDataSource.getUserInfo(code).map { dto ->
|
return NetworkDataSource
|
||||||
val bookings = dto.booking
|
.getUserInfo(code)
|
||||||
.map { it.toDomain() }
|
.map { dto -> dto.toDomainUser() }
|
||||||
.sortedWith(
|
|
||||||
compareBy<BookingEntity> { booking ->
|
|
||||||
DateUtils.parseDate(booking.time) ?: LocalDate.MAX
|
|
||||||
}.thenBy { it.time }
|
|
||||||
)
|
|
||||||
|
|
||||||
UserEntity(
|
|
||||||
name = dto.name,
|
|
||||||
bookings = bookings,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Доступные слоты бронирования через GET /api/<CODE>/booking
|
||||||
|
*/
|
||||||
suspend fun getAvailableBookings(): Result<List<BookingEntity>> {
|
suspend fun getAvailableBookings(): Result<List<BookingEntity>> {
|
||||||
val code = AuthRepository.getSavedCode()
|
val code = AuthRepository.getSavedCode()
|
||||||
?: return Result.failure(IllegalStateException("Auth code is not saved"))
|
?: return Result.failure(IllegalStateException("Auth code is not saved"))
|
||||||
|
|
||||||
return NetworkDataSource.getAvailableBookings(code).map { list ->
|
return NetworkDataSource
|
||||||
list.map { it.toDomain() }
|
.getAvailableBookings(code)
|
||||||
}
|
.map { map -> map.toDomainBookings() }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun book(room: String, time: String): Result<Unit> {
|
/**
|
||||||
|
* Создание нового бронирования через POST /api/<CODE>/book
|
||||||
|
*/
|
||||||
|
suspend fun book(date: String, placeId: Int): Result<Unit> {
|
||||||
val code = AuthRepository.getSavedCode()
|
val code = AuthRepository.getSavedCode()
|
||||||
?: return Result.failure(IllegalStateException("Auth code is not saved"))
|
?: return Result.failure(IllegalStateException("Auth code is not saved"))
|
||||||
|
|
||||||
return NetworkDataSource.book(code, room, time)
|
return NetworkDataSource.book(
|
||||||
|
code = code,
|
||||||
|
date = date,
|
||||||
|
placeId = placeId,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun UserDto.BookingDto.toDomain(): BookingEntity =
|
// -------------------- Маппинг DTO -> domain --------------------
|
||||||
BookingEntity(
|
|
||||||
roomName = room,
|
private fun UserDto.toDomainUser(): UserEntity {
|
||||||
time = time,
|
val bookings = booking
|
||||||
|
.flatMap { (dateString, bookedPlace) ->
|
||||||
|
listOfNotNull(bookedPlace.toDomainBooking(dateString))
|
||||||
|
}
|
||||||
|
.sortedBy { booking ->
|
||||||
|
DateUtils.parseDate(booking.time) ?: LocalDate.MAX
|
||||||
|
}
|
||||||
|
|
||||||
|
return UserEntity(
|
||||||
|
name = name,
|
||||||
|
photoUrl = photoUrl,
|
||||||
|
bookings = bookings,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Map<String, List<AvailablePlaceDto>>.toDomainBookings(): List<BookingEntity> {
|
||||||
|
return entries
|
||||||
|
.flatMap { (dateString, places) ->
|
||||||
|
places.mapNotNull { dto ->
|
||||||
|
dto.toDomainBooking(dateString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sortedBy { booking ->
|
||||||
|
DateUtils.parseDate(booking.time) ?: LocalDate.MAX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BookedPlaceDto.toDomainBooking(dateString: String): BookingEntity? {
|
||||||
|
val parsedDate = DateUtils.parseDate(dateString) ?: return null
|
||||||
|
return BookingEntity(
|
||||||
|
id = id,
|
||||||
|
roomName = place,
|
||||||
|
time = parsedDate.toString(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun AvailablePlaceDto.toDomainBooking(dateString: String): BookingEntity? {
|
||||||
|
val parsedDate = DateUtils.parseDate(dateString) ?: return null
|
||||||
|
return BookingEntity(
|
||||||
|
id = id,
|
||||||
|
roomName = place,
|
||||||
|
time = parsedDate.toString(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import io.ktor.client.HttpClient
|
|||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.body
|
||||||
import io.ktor.client.engine.cio.CIO
|
import io.ktor.client.engine.cio.CIO
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
import io.ktor.client.request.forms.MultiPartFormDataContent
|
|
||||||
import io.ktor.client.request.forms.formData
|
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.request.post
|
import io.ktor.client.request.post
|
||||||
import io.ktor.client.request.setBody
|
import io.ktor.client.request.setBody
|
||||||
@@ -16,14 +14,19 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import ru.myitschool.work.core.Constants
|
import ru.myitschool.work.core.Constants
|
||||||
|
import ru.myitschool.work.data.source.dto.AvailablePlaceDto
|
||||||
|
import ru.myitschool.work.data.source.dto.BookRequestDto
|
||||||
|
import ru.myitschool.work.data.source.dto.BookedPlaceDto
|
||||||
import ru.myitschool.work.data.source.dto.UserDto
|
import ru.myitschool.work.data.source.dto.UserDto
|
||||||
|
|
||||||
object NetworkDataSource {
|
object NetworkDataSource {
|
||||||
|
|
||||||
/** Поставь false, когда поднимешь настоящий бэкенд */
|
/**
|
||||||
private const val USE_STUB = false
|
* Поставь false, когда поднимешь настоящий бэкенд.
|
||||||
|
* При true используются локальные заглушки.
|
||||||
|
*/
|
||||||
|
private const val USE_STUB: Boolean = false
|
||||||
|
|
||||||
// Реальный клиент Ktor (для будущего)
|
|
||||||
private val client by lazy {
|
private val client by lazy {
|
||||||
HttpClient(CIO) {
|
HttpClient(CIO) {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
@@ -31,7 +34,7 @@ object NetworkDataSource {
|
|||||||
Json {
|
Json {
|
||||||
isLenient = true
|
isLenient = true
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
explicitNulls = true
|
explicitNulls = false
|
||||||
encodeDefaults = true
|
encodeDefaults = true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -39,35 +42,44 @@ object NetworkDataSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------- Заглушечные данные -----------------
|
// --------- Заглушечные данные ---------
|
||||||
|
|
||||||
private val stubUser = UserDto(
|
private val stubUser = UserDto(
|
||||||
name = "Тестовый пользователь",
|
name = "Тестовый пользователь",
|
||||||
booking = listOf(
|
photoUrl = null,
|
||||||
UserDto.BookingDto(
|
booking = mapOf(
|
||||||
room = "Опенспейс 1",
|
"2025-01-05" to BookedPlaceDto(id = 1, place = "102"),
|
||||||
time = "2025-01-05"
|
"2025-01-06" to BookedPlaceDto(id = 2, place = "209.13"),
|
||||||
),
|
"2025-01-09" to BookedPlaceDto(id = 3, place = "Зона 51. 50"),
|
||||||
UserDto.BookingDto(
|
),
|
||||||
room = "Опенспейс 2",
|
|
||||||
time = "2025-01-06"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private val stubAvailableBookings: List<UserDto.BookingDto> = listOf(
|
private val stubAvailableBookings: Map<String, List<AvailablePlaceDto>> = mapOf(
|
||||||
UserDto.BookingDto(room = "Опенспейс 1", time = "2025-01-10"),
|
"2025-01-05" to listOf(
|
||||||
UserDto.BookingDto(room = "Опенспейс 1", time = "2025-01-11"),
|
AvailablePlaceDto(id = 1, place = "102"),
|
||||||
UserDto.BookingDto(room = "Опенспейс 2", time = "2025-01-10"),
|
AvailablePlaceDto(id = 2, place = "209.13"),
|
||||||
UserDto.BookingDto(room = "Опенспейс 3", time = "2025-01-12"),
|
),
|
||||||
|
"2025-01-06" to listOf(
|
||||||
|
AvailablePlaceDto(id = 3, place = "Зона 51. 50"),
|
||||||
|
),
|
||||||
|
"2025-01-07" to listOf(
|
||||||
|
AvailablePlaceDto(id = 4, place = "102"),
|
||||||
|
AvailablePlaceDto(id = 5, place = "209.13"),
|
||||||
|
),
|
||||||
|
"2025-01-08" to listOf(
|
||||||
|
AvailablePlaceDto(id = 6, place = "209.13"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// ----------------- Публичные методы -----------------
|
// ----------------- Публичные методы -----------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка кода авторизации через GET /api/<CODE>/auth
|
||||||
|
*/
|
||||||
suspend fun checkAuth(code: String): Result<Boolean> {
|
suspend fun checkAuth(code: String): Result<Boolean> {
|
||||||
if (USE_STUB) {
|
if (USE_STUB) {
|
||||||
// Примитивная проверка заглушки: 4 символа → ок
|
// Примитивная проверка заглушки: 4+ символа → ок
|
||||||
return Result.success(code.length == 4)
|
return Result.success(code.length >= 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
@@ -75,12 +87,17 @@ object NetworkDataSource {
|
|||||||
val response = client.get(getUrl(code, Constants.AUTH_URL))
|
val response = client.get(getUrl(code, Constants.AUTH_URL))
|
||||||
when (response.status) {
|
when (response.status) {
|
||||||
HttpStatusCode.OK -> true
|
HttpStatusCode.OK -> true
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
HttpStatusCode.Unauthorized -> error(response.bodyAsText())
|
||||||
else -> error(response.bodyAsText())
|
else -> error(response.bodyAsText())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение информации о пользователе через GET /api/<CODE>/info
|
||||||
|
*/
|
||||||
suspend fun getUserInfo(code: String): Result<UserDto> {
|
suspend fun getUserInfo(code: String): Result<UserDto> {
|
||||||
if (USE_STUB) {
|
if (USE_STUB) {
|
||||||
return Result.success(stubUser)
|
return Result.success(stubUser)
|
||||||
@@ -91,15 +108,20 @@ object NetworkDataSource {
|
|||||||
val response = client.get(getUrl(code, Constants.INFO_URL))
|
val response = client.get(getUrl(code, Constants.INFO_URL))
|
||||||
when (response.status) {
|
when (response.status) {
|
||||||
HttpStatusCode.OK -> response.body<UserDto>()
|
HttpStatusCode.OK -> response.body<UserDto>()
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
HttpStatusCode.Unauthorized -> error(response.bodyAsText())
|
||||||
else -> error(response.bodyAsText())
|
else -> error(response.bodyAsText())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение доступных слотов бронирования через GET /api/<CODE>/booking
|
||||||
|
*/
|
||||||
suspend fun getAvailableBookings(
|
suspend fun getAvailableBookings(
|
||||||
code: String
|
code: String,
|
||||||
): Result<List<UserDto.BookingDto>> {
|
): Result<Map<String, List<AvailablePlaceDto>>> {
|
||||||
if (USE_STUB) {
|
if (USE_STUB) {
|
||||||
return Result.success(stubAvailableBookings)
|
return Result.success(stubAvailableBookings)
|
||||||
}
|
}
|
||||||
@@ -108,20 +130,28 @@ object NetworkDataSource {
|
|||||||
runCatching {
|
runCatching {
|
||||||
val response = client.get(getUrl(code, Constants.BOOKING_URL))
|
val response = client.get(getUrl(code, Constants.BOOKING_URL))
|
||||||
when (response.status) {
|
when (response.status) {
|
||||||
HttpStatusCode.OK -> response.body<List<UserDto.BookingDto>>()
|
HttpStatusCode.OK ->
|
||||||
|
response.body<Map<String, List<AvailablePlaceDto>>>()
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
HttpStatusCode.Unauthorized -> error(response.bodyAsText())
|
||||||
else -> error(response.bodyAsText())
|
else -> error(response.bodyAsText())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создание нового бронирования через POST /api/<CODE>/book
|
||||||
|
*
|
||||||
|
* Тело: { "date": "2025-01-05", "placeID": 1 }
|
||||||
|
*/
|
||||||
suspend fun book(
|
suspend fun book(
|
||||||
code: String,
|
code: String,
|
||||||
room: String,
|
date: String,
|
||||||
time: String
|
placeId: Int,
|
||||||
): Result<Unit> {
|
): Result<Unit> {
|
||||||
if (USE_STUB) {
|
if (USE_STUB) {
|
||||||
// Типа всё хорошо
|
// В режиме заглушки считаем, что всё прошло успешно
|
||||||
return Result.success(Unit)
|
return Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,22 +159,24 @@ object NetworkDataSource {
|
|||||||
runCatching {
|
runCatching {
|
||||||
val response = client.post(getUrl(code, Constants.BOOK_URL)) {
|
val response = client.post(getUrl(code, Constants.BOOK_URL)) {
|
||||||
setBody(
|
setBody(
|
||||||
MultiPartFormDataContent(
|
BookRequestDto(
|
||||||
formData {
|
date = date,
|
||||||
append("room", room)
|
placeId = placeId,
|
||||||
append("time", time)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
when (response.status) {
|
when (response.status) {
|
||||||
|
HttpStatusCode.Created,
|
||||||
HttpStatusCode.OK -> Unit
|
HttpStatusCode.OK -> Unit
|
||||||
|
HttpStatusCode.Conflict -> error("Уже забронировано")
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
HttpStatusCode.Unauthorized -> error(response.bodyAsText())
|
||||||
else -> error(response.bodyAsText())
|
else -> error(response.bodyAsText())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUrl(code: String, targetUrl: String) =
|
private fun getUrl(code: String, targetUrl: String): String =
|
||||||
"${Constants.HOST}/api/$code$targetUrl"
|
"${Constants.HOST}/api/$code$targetUrl"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,67 @@ package ru.myitschool.work.data.source.dto
|
|||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO для ответа GET /api/<CODE>/info
|
||||||
|
*
|
||||||
|
* Пример:
|
||||||
|
* {
|
||||||
|
* "name":"Иванов Петр Федорович",
|
||||||
|
* "photoUrl":"https://...",
|
||||||
|
* "booking":{
|
||||||
|
* "2025-01-05": {"id":1,"place":"102"},
|
||||||
|
* "2025-01-06": {"id":2,"place":"209.13"}
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UserDto(
|
data class UserDto(
|
||||||
@SerialName("name")
|
@SerialName("name")
|
||||||
val name: String = "Administrator",
|
val name: String,
|
||||||
|
@SerialName("photoUrl")
|
||||||
|
val photoUrl: String? = null,
|
||||||
@SerialName("booking")
|
@SerialName("booking")
|
||||||
val booking: List<BookingDto>
|
val booking: Map<String, BookedPlaceDto> = emptyMap(),
|
||||||
) {
|
)
|
||||||
@Serializable
|
|
||||||
data class BookingDto(
|
/**
|
||||||
@SerialName("room")
|
* Элемент бронирования в ответе /info и /booking.
|
||||||
val room: String,
|
*/
|
||||||
@SerialName("time")
|
@Serializable
|
||||||
val time: String,
|
data class BookedPlaceDto(
|
||||||
)
|
@SerialName("id")
|
||||||
}
|
val id: Int,
|
||||||
|
@SerialName("place")
|
||||||
|
val place: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO для доступных мест в ответе GET /api/<CODE>/booking:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* "2025-01-05": [{"id": 1, "place": "102"}, ...]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class AvailablePlaceDto(
|
||||||
|
@SerialName("id")
|
||||||
|
val id: Int,
|
||||||
|
@SerialName("place")
|
||||||
|
val place: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Тело запроса POST /api/<CODE>/book:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* "date": "2025-01-05",
|
||||||
|
* "placeID": 1
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class BookRequestDto(
|
||||||
|
@SerialName("date")
|
||||||
|
val date: String,
|
||||||
|
@SerialName("placeID")
|
||||||
|
val placeId: Int,
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,10 +2,16 @@ package ru.myitschool.work.domain.booking
|
|||||||
|
|
||||||
import ru.myitschool.work.data.repo.UserRepository
|
import ru.myitschool.work.data.repo.UserRepository
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Юзкейс для создания бронирования.
|
||||||
|
*
|
||||||
|
* @param date дата бронирования в формате yyyy-MM-dd
|
||||||
|
* @param placeId идентификатор места (placeID из бэкенда)
|
||||||
|
*/
|
||||||
class BookPlaceUseCase(
|
class BookPlaceUseCase(
|
||||||
private val repository: UserRepository
|
private val repository: UserRepository,
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(room: String, time: String): Result<Unit> {
|
suspend operator fun invoke(date: String, placeId: Int): Result<Unit> {
|
||||||
return repository.book(room, time)
|
return repository.book(date, placeId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package ru.myitschool.work.domain.entities
|
package ru.myitschool.work.domain.entities
|
||||||
|
|
||||||
data class BookingEntity(
|
data class BookingEntity(
|
||||||
|
val id: Int,
|
||||||
val roomName: String,
|
val roomName: String,
|
||||||
val time: String,
|
val time: String,
|
||||||
)
|
)
|
||||||
@@ -2,5 +2,6 @@ package ru.myitschool.work.domain.entities
|
|||||||
|
|
||||||
data class UserEntity(
|
data class UserEntity(
|
||||||
val name: String,
|
val name: String,
|
||||||
|
val photoUrl: String?,
|
||||||
val bookings: List<BookingEntity>,
|
val bookings: List<BookingEntity>,
|
||||||
)
|
)
|
||||||
@@ -129,7 +129,7 @@ class BookViewModel : ViewModel() {
|
|||||||
val group = groups[index]
|
val group = groups[index]
|
||||||
val places = group.slots.mapIndexed { idx, slot ->
|
val places = group.slots.mapIndexed { idx, slot ->
|
||||||
BookPlaceItem(
|
BookPlaceItem(
|
||||||
id = idx,
|
id = slot.id,
|
||||||
roomName = slot.roomName,
|
roomName = slot.roomName,
|
||||||
time = slot.time,
|
time = slot.time,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
@@ -162,7 +162,7 @@ class BookViewModel : ViewModel() {
|
|||||||
val place = current.places.firstOrNull { it.id == placeId } ?: return
|
val place = current.places.firstOrNull { it.id == placeId } ?: return
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
bookPlaceUseCase(place.roomName, place.time)
|
bookPlaceUseCase(place.roomName, place.id)
|
||||||
.fold(
|
.fold(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
_actionFlow.emit(Action.CloseWithSuccess)
|
_actionFlow.emit(Action.CloseWithSuccess)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package ru.myitschool.work.ui.screen.main
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -11,21 +12,28 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.SegmentedButtonDefaults.Icon
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import coil3.compose.SubcomposeAsyncImage
|
||||||
import ru.myitschool.work.core.TestIds
|
import ru.myitschool.work.core.TestIds
|
||||||
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
||||||
import ru.myitschool.work.ui.nav.BookScreenDestination
|
import ru.myitschool.work.ui.nav.BookScreenDestination
|
||||||
@@ -131,7 +139,11 @@ private fun MainDataContent(
|
|||||||
.testTag(TestIds.Main.PROFILE_IMAGE),
|
.testTag(TestIds.Main.PROFILE_IMAGE),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text("🙂")
|
UserAvatar(
|
||||||
|
photoUrl = state.photoUrl,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(64.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
Text(
|
Text(
|
||||||
@@ -196,3 +208,42 @@ private fun MainDataContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@Composable
|
||||||
|
private fun UserAvatar(
|
||||||
|
photoUrl: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (photoUrl.isNullOrBlank()) {
|
||||||
|
// Фолбек на старый смайл / иконку
|
||||||
|
Image(
|
||||||
|
imageVector = Icons.Default.Person,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = modifier
|
||||||
|
.size(64.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SubcomposeAsyncImage(
|
||||||
|
model = photoUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = modifier
|
||||||
|
.size(64.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
loading = {
|
||||||
|
Image(
|
||||||
|
imageVector = Icons.Default.Person,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(64.dp),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
error = {
|
||||||
|
Image(
|
||||||
|
imageVector = Icons.Default.Person,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(64.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ sealed interface MainState {
|
|||||||
|
|
||||||
data class Data(
|
data class Data(
|
||||||
val name: String,
|
val name: String,
|
||||||
|
val photoUrl: String?,
|
||||||
val bookings: List<MainBookingItem>,
|
val bookings: List<MainBookingItem>,
|
||||||
) : MainState
|
) : MainState
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ class MainViewModel : ViewModel() {
|
|||||||
|
|
||||||
_uiState.value = MainState.Data(
|
_uiState.value = MainState.Data(
|
||||||
name = user.name,
|
name = user.name,
|
||||||
|
photoUrl = user.photoUrl,
|
||||||
bookings = bookings,
|
bookings = bookings,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user