diff --git a/README.md b/README.md
index ddbea3351e0bb6f99aa20cf5d78bbfc2704dc555..d6214e3349f98bc4ed5be292eb0f1d7946daa97c 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ technologies as little as possible.
-### 🔧 Features Overview
+### Features Overview
| Feature | Description |
|----------------------------|--------------------------------------------------------------|
@@ -54,6 +54,7 @@ technologies as little as possible.
```bash
cp backend/app/src/main/resources/env.example backend/app/src/main/resources/.env
```
+ Edit the `.env` file with your configuration.
4. Set up required credentials:
- Add `google-credentials.json` for Google Calendar API
@@ -75,6 +76,12 @@ technologies as little as possible.
./gradlew :backend:app:bootRun --args='--spring.profiles.active=local'
```
+#### Run Clients
+1. Open the project in Android Studio or IntelliJ IDEA
+2. Sync the Gradle project to download dependencies
+3. Choose the appropriate run configuration in IDE
+4. Run (Shift+F10 or Control+R)
+
For detailed installation instructions, including setting up credentials and running client applications, see our [Getting Started Guide](https://github.com/effective-dev-opensource/Effective-Office/wiki/Getting-Started-with-Effective-Office) in the wiki.
## Project Structure
@@ -123,7 +130,7 @@ An application for tracking foosball match results. It allows users to log games
- [Tatyana Terleeva](https://t.me/tatyana_terleeva)
- [Stanislav Radchenko](https://github.com/Radch-enko)
- [Vitaly Smirnov](https://github.com/KrugarValdes)
-- [Victoria Maksimovna](https://t.me/the_koheskine)
+- [Viktoriya Kokh](https://t.me/the_koheskine)
## License
The code is available as open source under the terms of the [MIT LICENSE](LICENSE).
diff --git a/backend/feature/booking/calendar/google/src/main/kotlin/band/effective/office/backend/feature/booking/calendar/google/GoogleCalendarProvider.kt b/backend/feature/booking/calendar/google/src/main/kotlin/band/effective/office/backend/feature/booking/calendar/google/GoogleCalendarProvider.kt
index 39b38216327ae7412788f12d516841d57392be4c..6c2e4cd9485f90a38a8fe7ba248fb9f44bc7efda 100644
--- a/backend/feature/booking/calendar/google/src/main/kotlin/band/effective/office/backend/feature/booking/calendar/google/GoogleCalendarProvider.kt
+++ b/backend/feature/booking/calendar/google/src/main/kotlin/band/effective/office/backend/feature/booking/calendar/google/GoogleCalendarProvider.kt
@@ -33,6 +33,10 @@ class GoogleCalendarProvider(
private val logger = LoggerFactory.getLogger(GoogleCalendarProvider::class.java)
+ companion object {
+ private const val RESPONSE_STATUS_DECLINED = "declined"
+ }
+
@Value("\${calendar.default-calendar}")
private lateinit var defaultCalendar: String
@@ -203,6 +207,42 @@ class GoogleCalendarProvider(
}
}
+ /**
+ * Deletes an event from Google Calendar.
+ *
+ * @param calendarId The ID of the calendar containing the event
+ * @param eventId The ID of the event to delete
+ * @return true if the event was successfully deleted, false otherwise
+ */
+ private fun deleteEvent(calendarId: String, eventId: String): Boolean {
+ return try {
+ calendar.events().delete(calendarId, eventId).execute()
+ logger.info("Successfully deleted event $eventId from calendar $calendarId because all participants declined")
+ true
+ } catch (e: GoogleJsonResponseException) {
+ logger.error("Failed to delete event $eventId from calendar $calendarId: {}", e.details)
+ false
+ } catch (e: Exception) {
+ logger.error("Unexpected error when deleting event $eventId from calendar $calendarId", e)
+ false
+ }
+ }
+
+ /**
+ * Retrieves a list of events from a Google Calendar within a specified time range.
+ *
+ * This method performs the following operations:
+ * 1. Fetches events from the specified calendar within the given time range
+ * 2. Filters out events where all human participants have declined (and optionally deletes them)
+ * 3. Filters out events where any resource (room/workspace) has declined
+ *
+ * @param calendarId The ID of the Google Calendar to query
+ * @param from The start time for the query (inclusive)
+ * @param to The end time for the query (exclusive), or null for no end time limit
+ * @param q Optional search term to filter events by
+ * @param returnInstances Whether to expand recurring events into individual instances
+ * @return A filtered list of Google Calendar events
+ */
private fun listEvents(
calendarId: String,
from: Instant,
@@ -210,31 +250,111 @@ class GoogleCalendarProvider(
q: String? = null,
returnInstances: Boolean = true
): List {
- val eventsRequest = calendar.events().list(calendarId)
+ // Build the events request with required parameters
+ val eventsRequest = buildEventsRequest(calendarId, from, to, q, returnInstances)
+
+ return try {
+ // Execute the request and get the events
+ val fetchedEvents = eventsRequest.execute().items ?: emptyList()
+
+ // Process and filter the events
+ processEvents(fetchedEvents)
+ } catch (e: GoogleJsonResponseException) {
+ handleGoogleJsonException(e, calendarId)
+ emptyList()
+ } catch (e: Exception) {
+ logger.error("Unexpected error when listing events from Google Calendar for calendar ID: $calendarId", e)
+ emptyList()
+ }
+ }
+
+ /**
+ * Builds a Google Calendar events request with the specified parameters.
+ */
+ private fun buildEventsRequest(
+ calendarId: String,
+ from: Instant,
+ to: Instant?,
+ q: String?,
+ returnInstances: Boolean
+ ): Calendar.Events.List {
+ val request = calendar.events().list(calendarId)
.setTimeMin(DateTime(from.toEpochMilli()))
.setSingleEvents(returnInstances)
- if (to != null) {
- eventsRequest.timeMax = DateTime(to.toEpochMilli())
+ // Add optional parameters if provided
+ to?.let { request.timeMax = DateTime(it.toEpochMilli()) }
+ q?.let { request.q = it }
+
+ return request
+ }
+
+ /**
+ * Processes and filters the fetched events according to business rules.
+ */
+ private fun processEvents(events: List): List {
+ // Remove events where all human participants have declined
+ val eventsAfterParticipantCheck = events.filter { event ->
+ !shouldRemoveEventWithAllDeclinedParticipants(event)
}
- if (q != null) {
- eventsRequest.q = q
+ // Remove events where any resource has declined
+ return eventsAfterParticipantCheck.filter { event ->
+ !hasAnyResourceDeclined(event)
}
+ }
- return try {
- eventsRequest.execute().items ?: emptyList()
- } catch (e: GoogleJsonResponseException) {
- logger.error("Failed to list events from Google Calendar: {}", e.details)
- if (e.statusCode == 404) {
- logger.warn("Calendar with ID {} not found", calendarId)
- } else if (e.statusCode == 403) {
- logger.warn("Permission denied for calendar with ID {}", calendarId)
- }
- emptyList()
- } catch (e: Exception) {
- logger.error("Unexpected error when listing events from Google Calendar", e)
- emptyList()
+ /**
+ * Checks if an event should be removed because all human participants have declined.
+ * If all participants have declined, attempts to delete the event.
+ *
+ * @return true if the event should be removed from results, false otherwise
+ */
+ private fun shouldRemoveEventWithAllDeclinedParticipants(event: Event): Boolean {
+ val isDefaultOrganizer = event.organizer?.email == defaultCalendar
+
+ // Only check events organized by our default calendar
+ if (event.attendees == null || event.attendees.isEmpty() || !isDefaultOrganizer) {
+ return false
+ }
+
+ // Get all human participants (non-resource attendees)
+ val humanParticipants = event.attendees.filter { it.resource == false || it.resource == null }
+
+ // Check if all human participants have declined
+ val allHumansDeclined = humanParticipants.isNotEmpty() &&
+ humanParticipants.all { it.responseStatus == RESPONSE_STATUS_DECLINED }
+
+ // If all humans declined, try to delete the event
+ if (allHumansDeclined) {
+ logger.warn("Deleting event with ID {} because all participants declined", event.id)
+ val deletionSucceeded = deleteEvent(defaultCalendar, event.id)
+ return deletionSucceeded
+ }
+
+ return false
+ }
+
+ /**
+ * Checks if any resource (room/workspace) attendee has declined the event.
+ *
+ * @return true if any resource has declined, false otherwise
+ */
+ private fun hasAnyResourceDeclined(event: Event): Boolean {
+ return event.attendees?.any { attendee ->
+ attendee.resource == true && attendee.responseStatus == RESPONSE_STATUS_DECLINED
+ } ?: false
+ }
+
+ /**
+ * Handles Google JSON API exceptions with appropriate logging.
+ */
+ private fun handleGoogleJsonException(e: GoogleJsonResponseException, calendarId: String) {
+ logger.error("Failed to list events from Google Calendar: {}", e.details)
+ when (e.statusCode) {
+ 404 -> logger.warn("Calendar with ID {} not found", calendarId)
+ 403 -> logger.warn("Permission denied for calendar with ID {}", calendarId)
+ else -> logger.error("Google API error with status code {} for calendar ID {}", e.statusCode, calendarId)
}
}
diff --git a/backend/feature/notifications/src/main/kotlin/band/effective/office/backend/feature/notifications/controller/NotificationDeduplicator.kt b/backend/feature/notifications/src/main/kotlin/band/effective/office/backend/feature/notifications/controller/NotificationDeduplicator.kt
index 1ddba93e3c4b91d2f4c405cb803a7d1d6d31610b..44a0d9bdab64d446f086f14f67d2c4e2cd9b8dbd 100644
--- a/backend/feature/notifications/src/main/kotlin/band/effective/office/backend/feature/notifications/controller/NotificationDeduplicator.kt
+++ b/backend/feature/notifications/src/main/kotlin/band/effective/office/backend/feature/notifications/controller/NotificationDeduplicator.kt
@@ -10,7 +10,7 @@ import org.springframework.stereotype.Component
@Component
class NotificationDeduplicator {
- private val ttlSeconds = 10L
+ private val ttlSeconds = 5L
@OptIn(ExperimentalTime::class)
private val seenEvents = ConcurrentHashMap()
diff --git a/clients/tablet/composeApp/build.gradle.kts b/clients/tablet/composeApp/build.gradle.kts
index 25a3b5a3810acd3d723c768bf73cc47710cd1bb0..b439ce2ec755aba5115f00cbff580abb8de339d0 100644
--- a/clients/tablet/composeApp/build.gradle.kts
+++ b/clients/tablet/composeApp/build.gradle.kts
@@ -84,8 +84,8 @@ android {
targetSdk = libs.versions.android.targetSdk.get().toInt()
applicationId = "band.effective.office.tablet"
- versionCode = 1
- versionName = "0.0.2"
+ versionCode = 2
+ versionName = "1.0.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
diff --git a/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/App.android.kt b/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/App.android.kt
index babbbbe2df3c9fb6c550a004d9f9ee1c193c550c..262007a3a0dabba75d84fcd86fa52385e9617ddc 100644
--- a/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/App.android.kt
+++ b/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/App.android.kt
@@ -10,6 +10,7 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi
import band.effective.office.tablet.root.RootComponent
+import band.effective.office.tablet.time.TimeReceiver
import com.arkivanov.decompose.defaultComponentContext
class AppActivity : ComponentActivity() {
@@ -18,15 +19,25 @@ class AppActivity : ComponentActivity() {
var isRunKioskMode = false
}
+ val timeReceiver by lazy { TimeReceiver(this) }
+
@RequiresApi(Build.VERSION_CODES.P)
override fun onCreate(savedInstanceState: Bundle?) {
runKioskMode()
super.onCreate(savedInstanceState)
+
+ timeReceiver.register()
enableEdgeToEdge()
val root = RootComponent(componentContext = defaultComponentContext())
setContent { App(root) }
}
+ override fun onDestroy() {
+ // Unregister the time receiver
+ timeReceiver.unregister()
+ super.onDestroy()
+ }
+
@RequiresApi(Build.VERSION_CODES.P)
private fun runKioskMode(){
val context = this
diff --git a/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/App.kt b/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/App.kt
index c5c283e46b39e427bbf8d511c51159197d93e29d..446f999b6b5f87b0472ec5edd31ff8d3a47a8633 100644
--- a/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/App.kt
+++ b/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/App.kt
@@ -1,13 +1,15 @@
package band.effective.office.tablet
import android.app.Application
-import android.util.Log
+import band.effective.office.tablet.core.domain.manager.DateResetManager
import band.effective.office.tablet.core.domain.model.SettingsManager
+import band.effective.office.tablet.core.ui.inactivity.InactivityLifecycleCallbacks
import band.effective.office.tablet.di.KoinInitializer
import com.google.firebase.messaging.FirebaseMessaging
import com.russhwolf.settings.SharedPreferencesSettings
import org.koin.android.ext.android.get
import org.koin.core.qualifier.named
+import kotlin.time.Duration.Companion.minutes
class App : Application() {
@@ -24,6 +26,7 @@ class App : Application() {
)
)
subscribeOnFirebaseTopics()
+ initializeInactivitySystem()
}
private fun subscribeOnFirebaseTopics() {
@@ -32,4 +35,14 @@ class App : Application() {
FirebaseMessaging.getInstance().subscribeToTopic(topic)
}
}
+
+ private fun initializeInactivitySystem() {
+ InactivityLifecycleCallbacks.initialize(
+ application = this,
+ timeoutMs = 1.minutes.inWholeMilliseconds,
+ callback = {
+ DateResetManager.resetDate()
+ }
+ )
+ }
}
\ No newline at end of file
diff --git a/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt b/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt
new file mode 100644
index 0000000000000000000000000000000000000000..60c5e84f787653ce72fa36b9c9b22c96b96cc4d4
--- /dev/null
+++ b/clients/tablet/composeApp/src/androidMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt
@@ -0,0 +1,56 @@
+package band.effective.office.tablet.time
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.util.Log
+import band.effective.office.tablet.feature.main.domain.CurrentTimeHolder
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.datetime.Clock
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+
+/**
+ * A broadcast receiver that listens for time-related broadcasts and emits the current time.
+ */
+actual class TimeReceiver(private val context: Context) {
+
+ actual val currentTime: StateFlow = CurrentTimeHolder.currentTime
+
+ private val receiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ when (intent?.action) {
+ Intent.ACTION_TIME_TICK, Intent.ACTION_TIME_CHANGED -> {
+ CurrentTimeHolder.updateTime(getCurrentTime())
+ }
+ }
+ }
+ }
+
+ /**
+ * Registers the broadcast receiver to listen for time-related broadcasts.
+ */
+ fun register() {
+ val filter = IntentFilter().apply {
+ addAction(Intent.ACTION_TIME_TICK)
+ addAction(Intent.ACTION_TIME_CHANGED)
+ }
+ context.registerReceiver(receiver, filter)
+ }
+
+ /**
+ * Unregisters the broadcast receiver.
+ */
+ fun unregister() {
+ context.unregisterReceiver(receiver)
+ }
+
+ /**
+ * Gets the current time as a LocalDateTime.
+ */
+ private fun getCurrentTime(): LocalDateTime {
+ return Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
+ }
+}
diff --git a/clients/tablet/composeApp/src/commonMain/kotlin/band/effective/office/tablet/root/RootComponent.kt b/clients/tablet/composeApp/src/commonMain/kotlin/band/effective/office/tablet/root/RootComponent.kt
index 1ad530cd5a0a8f59f2ce70f00a431bd7b8af7759..d50101048ab69cd05f57073f75600c36ad2bbf88 100644
--- a/clients/tablet/composeApp/src/commonMain/kotlin/band/effective/office/tablet/root/RootComponent.kt
+++ b/clients/tablet/composeApp/src/commonMain/kotlin/band/effective/office/tablet/root/RootComponent.kt
@@ -2,7 +2,6 @@ package band.effective.office.tablet.root
import band.effective.office.tablet.core.domain.model.EventInfo
import band.effective.office.tablet.core.domain.model.RoomInfo
-import band.effective.office.tablet.core.domain.model.Slot
import band.effective.office.tablet.core.domain.useCase.CheckSettingsUseCase
import band.effective.office.tablet.core.domain.useCase.ResourceDisposerUseCase
import band.effective.office.tablet.core.ui.common.ModalWindow
@@ -153,16 +152,10 @@ class RootComponent(
componentContext = componentContext,
initialEvent = config.event,
roomName = config.room,
- onDeleteEvent = ::handleDeleteEvent,
onCloseRequest = modalNavigation::dismiss,
)
}
- private fun handleDeleteEvent(slot: Slot) {
- val mainComponent = (childStack.value.active.instance as? Child.MainChild)?.component
- mainComponent?.handleDeleteEvent(slot)
- }
-
private fun createFastBookingComponent(
config: ModalWindowsConfig.FastEvent,
componentContext: ComponentContext
diff --git a/clients/tablet/composeApp/src/commonMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt b/clients/tablet/composeApp/src/commonMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt
new file mode 100644
index 0000000000000000000000000000000000000000..59afa56c0fa56319ba169c2642ed8af41ee12bbf
--- /dev/null
+++ b/clients/tablet/composeApp/src/commonMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt
@@ -0,0 +1,14 @@
+package band.effective.office.tablet.time
+
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.datetime.LocalDateTime
+
+/**
+ * A receiver that emits the current time.
+ */
+expect class TimeReceiver {
+ /**
+ * A flow that emits the current time.
+ */
+ val currentTime: StateFlow
+}
diff --git a/clients/tablet/composeApp/src/iosMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt b/clients/tablet/composeApp/src/iosMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt
new file mode 100644
index 0000000000000000000000000000000000000000..17e7c1556180125be9e1adabf34fcf670f1978fe
--- /dev/null
+++ b/clients/tablet/composeApp/src/iosMain/kotlin/band/effective/office/tablet/time/TimeReceiver.kt
@@ -0,0 +1,44 @@
+package band.effective.office.tablet.time
+
+import band.effective.office.tablet.feature.main.domain.CurrentTimeHolder
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.datetime.Clock
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+import platform.Foundation.NSNotificationCenter
+import platform.Foundation.NSDefaultRunLoopMode
+import platform.Foundation.NSRunLoop
+import platform.Foundation.NSDate
+import platform.Foundation.NSTimer
+import platform.Foundation.timeIntervalSince1970
+
+/**
+ * iOS implementation of TimeReceiver that uses a timer to update the current time.
+ */
+actual class TimeReceiver {
+ actual val currentTime: StateFlow = CurrentTimeHolder.currentTime
+
+ private var timer: NSTimer? = null
+
+ init {
+ // Update time every minute
+ timer = NSTimer.scheduledTimerWithTimeInterval(
+ 60.0, // 60 seconds
+ true, // repeats
+ {
+ CurrentTimeHolder.updateTime(getCurrentTime())
+ }
+ )
+
+ // Add timer to the main run loop
+ NSRunLoop.mainRunLoop.addTimer(timer!!, NSDefaultRunLoopMode)
+ }
+
+ /**
+ * Gets the current time as a LocalDateTime.
+ */
+ private fun getCurrentTime(): LocalDateTime {
+ return Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
+ }
+}
diff --git a/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/manager/DateResetManager.kt b/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/manager/DateResetManager.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2324e09259948dba2c1aa6301941a1b7cb167fb4
--- /dev/null
+++ b/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/manager/DateResetManager.kt
@@ -0,0 +1,51 @@
+package band.effective.office.tablet.core.domain.manager
+
+import band.effective.office.tablet.core.domain.util.currentLocalDateTime
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
+import kotlinx.datetime.LocalDateTime
+
+/**
+ * Singleton manager that handles date reset operations.
+ * This class facilitates communication between different parts of the application
+ * that need to reset dates, such as when inactivity is detected.
+ */
+object DateResetManager {
+ // Create a CoroutineScope with a SupervisorJob to handle suspending functions
+ private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
+
+ // Use a suspending function type for the callback
+ private var dateResetCallback: (suspend (LocalDateTime) -> Unit)? = null
+
+ /**
+ * Registers a callback that will be called when the date needs to be reset.
+ *
+ * @param callback The callback to execute when the date needs to be reset.
+ * The callback receives the current local date and time.
+ */
+ fun registerDateResetCallback(callback: suspend (LocalDateTime) -> Unit) {
+ dateResetCallback = callback
+ }
+
+ /**
+ * Unregisters the date reset callback.
+ */
+ fun unregisterDateResetCallback() {
+ dateResetCallback = null
+ }
+
+ /**
+ * Triggers a date reset with the current local date and time.
+ * This will call the registered callback if one exists.
+ */
+ fun resetDate() {
+ val callback = dateResetCallback ?: return
+
+ // Launch a coroutine to execute the suspending callback
+ coroutineScope.launch {
+ callback(currentLocalDateTime)
+ }
+ }
+}
diff --git a/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/useCase/SlotUseCase.kt b/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/useCase/SlotUseCase.kt
index 470d539fe71aeecfcaa47c8322d4d4614e707819..ca171fe975aa6296d8ad50cb2d37a9df68df8845 100644
--- a/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/useCase/SlotUseCase.kt
+++ b/clients/tablet/core/domain/src/commonMain/kotlin/band/effective/office/tablet/core/domain/useCase/SlotUseCase.kt
@@ -22,7 +22,7 @@ class SlotUseCase(
currentEvent: EventInfo?,
): List {
return events
- .filter { it.startTime >= start && it.startTime <= finish }
+ .filter { it.startTime >= start && it.startTime < finish && it.finishTime <= finish }
.fold(
getEmptyMinSlots(start, finish, minSlotDur)
) { acc, eventInfo -> acc.addEvent(eventInfo) }
@@ -61,9 +61,12 @@ class SlotUseCase(
if (isEmpty()) return listOf(eventInfo.toSlot())
val list = this.toMutableList()
list.removeEmptySlot(eventInfo)
- val predSlot = list.firstOrNull { it.finish > eventInfo.startTime } ?: list.first()
- val predSlotIndex = list.indexOf(predSlot)
- list.add(predSlotIndex, eventInfo.toSlot())
+ val predSlotIndex = list.indexOfFirst { it.start >= eventInfo.startTime }
+ if (predSlotIndex != -1) {
+ list.add(predSlotIndex, eventInfo.toSlot())
+ } else {
+ list.add(eventInfo.toSlot())
+ }
return list
}
diff --git a/clients/tablet/core/ui/build.gradle.kts b/clients/tablet/core/ui/build.gradle.kts
index 0b70ba248cdba4297b68035aeb57c4e7fdf86d2e..3e7671809ccd832159075017d4ae85d1913dfb97 100644
--- a/clients/tablet/core/ui/build.gradle.kts
+++ b/clients/tablet/core/ui/build.gradle.kts
@@ -10,6 +10,7 @@ kotlin {
implementation(libs.decompose.compose.jetbrains)
implementation(libs.essenty.lifecycle)
implementation(libs.essenty.state.keeper)
+ api(libs.kotlinx.datetime)
}
}
}
diff --git a/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/inactivity/InactivityLifecycleCallbacks.kt b/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/inactivity/InactivityLifecycleCallbacks.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bfeb2d4ca6dba3a3d9af09f87f3d7ea9d6087acd
--- /dev/null
+++ b/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/inactivity/InactivityLifecycleCallbacks.kt
@@ -0,0 +1,70 @@
+package band.effective.office.tablet.core.ui.inactivity
+
+import android.app.Activity
+import android.app.Application
+import android.os.Bundle
+
+/**
+ * ActivityLifecycleCallbacks implementation that hooks into all activities
+ * and sets up the Window.Callback wrapper to track user inactivity.
+ */
+class InactivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks {
+
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
+ // Get the original Window.Callback
+ val originalCallback = activity.window.callback
+
+ // Set our custom Window.Callback wrapper
+ activity.window.callback = InactivityWindowCallback(originalCallback)
+ }
+
+ override fun onActivityStarted(activity: Activity) {
+ // No implementation needed
+ }
+
+ override fun onActivityResumed(activity: Activity) {
+ // Reset the inactivity timer when an activity is resumed
+ InactivityManager.resetTimer()
+ }
+
+ override fun onActivityPaused(activity: Activity) {
+ // No implementation needed
+ }
+
+ override fun onActivityStopped(activity: Activity) {
+ // No implementation needed
+ }
+
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
+ // No implementation needed
+ }
+
+ override fun onActivityDestroyed(activity: Activity) {
+ // No implementation needed
+ }
+
+ companion object {
+ /**
+ * Registers the InactivityLifecycleCallbacks with the Application
+ * and starts tracking user inactivity.
+ *
+ * @param application The Application instance.
+ * @param timeoutMs The inactivity timeout in milliseconds. Default is 1 minute.
+ * @param callback The callback to execute when inactivity is detected.
+ */
+ fun initialize(
+ application: Application,
+ timeoutMs: Long = InactivityManager.DEFAULT_INACTIVITY_TIMEOUT_MS,
+ callback: (() -> Unit)? = null,
+ ) {
+ // Register the ActivityLifecycleCallbacks
+ application.registerActivityLifecycleCallbacks(InactivityLifecycleCallbacks())
+
+ // Start tracking inactivity
+ InactivityManager.startTracking(timeoutMs) {
+ // Execute the original callback
+ callback?.invoke()
+ }
+ }
+ }
+}
diff --git a/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/inactivity/InactivityManager.kt b/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/inactivity/InactivityManager.kt
new file mode 100644
index 0000000000000000000000000000000000000000..12eb6b0b2eef749dfae7d3119c60a4e6a60a5e06
--- /dev/null
+++ b/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/inactivity/InactivityManager.kt
@@ -0,0 +1,76 @@
+package band.effective.office.tablet.core.ui.inactivity
+
+import android.os.Handler
+import android.os.Looper
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/**
+ * Singleton manager that tracks user inactivity across the application.
+ * It resets a timer whenever there is any UI interaction (touch event, key event)
+ * and executes a callback after a specified period of inactivity.
+ */
+object InactivityManager {
+ /**
+ * Default inactivity timeout in milliseconds (1 minute)
+ */
+ const val DEFAULT_INACTIVITY_TIMEOUT_MS = 60000L
+
+ private val handler = Handler(Looper.getMainLooper())
+ private var inactivityTimeoutMs = DEFAULT_INACTIVITY_TIMEOUT_MS
+ private var inactivityCallback: (() -> Unit)? = null
+ private var isTracking = false
+
+ private val _inactivityState = MutableStateFlow(false)
+ val inactivityState: StateFlow = _inactivityState.asStateFlow()
+
+ private val inactivityRunnable = Runnable {
+ _inactivityState.value = true
+ inactivityCallback?.invoke()
+ }
+
+ /**
+ * Starts tracking user inactivity.
+ *
+ * @param timeoutMs The inactivity timeout in milliseconds. Default is 1 minute.
+ * @param callback The callback to execute when inactivity is detected.
+ */
+ fun startTracking(
+ timeoutMs: Long = DEFAULT_INACTIVITY_TIMEOUT_MS,
+ callback: (() -> Unit)? = null
+ ) {
+ inactivityTimeoutMs = timeoutMs
+ inactivityCallback = callback
+ isTracking = true
+ resetTimer()
+ }
+
+ /**
+ * Stops tracking user inactivity.
+ */
+ fun stopTracking() {
+ isTracking = false
+ handler.removeCallbacks(inactivityRunnable)
+ _inactivityState.value = false
+ }
+
+ /**
+ * Resets the inactivity timer. This should be called whenever a user interaction is detected.
+ */
+ fun resetTimer() {
+ if (!isTracking) return
+
+ handler.removeCallbacks(inactivityRunnable)
+ _inactivityState.value = false
+ handler.postDelayed(inactivityRunnable, inactivityTimeoutMs)
+ }
+
+ /**
+ * Notifies the manager that a user interaction has occurred.
+ * This will reset the inactivity timer.
+ */
+ fun onUserInteraction() {
+ resetTimer()
+ }
+}
diff --git a/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/inactivity/InactivityWindowCallback.kt b/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/inactivity/InactivityWindowCallback.kt
new file mode 100644
index 0000000000000000000000000000000000000000..645bd35f60e7f22acd0d8c3c7c89b12095f39bc7
--- /dev/null
+++ b/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/inactivity/InactivityWindowCallback.kt
@@ -0,0 +1,42 @@
+package band.effective.office.tablet.core.ui.inactivity
+
+import android.view.KeyEvent
+import android.view.MotionEvent
+import android.view.Window
+
+/**
+ * A Window.Callback wrapper that intercepts touch and key events to track user activity.
+ * It preserves the original Window.Callback behavior by delegating to it after intercepting events.
+ *
+ * @param original The original Window.Callback to delegate to.
+ */
+class InactivityWindowCallback(private val original: Window.Callback) : Window.Callback by original {
+
+ /**
+ * Intercepts touch events and notifies the InactivityManager before delegating to the original callback.
+ *
+ * @param event The touch event.
+ * @return The result of the original callback's dispatchTouchEvent method.
+ */
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean {
+ // Notify the InactivityManager that a user interaction has occurred
+ InactivityManager.onUserInteraction()
+
+ // Delegate to the original callback
+ return original.dispatchTouchEvent(event)
+ }
+
+ /**
+ * Intercepts key events and notifies the InactivityManager before delegating to the original callback.
+ *
+ * @param event The key event.
+ * @return The result of the original callback's dispatchKeyEvent method.
+ */
+ override fun dispatchKeyEvent(event: KeyEvent): Boolean {
+ // Notify the InactivityManager that a user interaction has occurred
+ InactivityManager.onUserInteraction()
+
+ // Delegate to the original callback
+ return original.dispatchKeyEvent(event)
+ }
+}
\ No newline at end of file
diff --git a/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/util/DateFormatter.android.kt b/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/util/DateFormatter.android.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e6c60aa7c5431c70377488334b23bef0b08ac0b0
--- /dev/null
+++ b/clients/tablet/core/ui/src/androidMain/kotlin/band/effective/office/tablet/core/ui/util/DateFormatter.android.kt
@@ -0,0 +1,13 @@
+package band.effective.office.tablet.core.ui.utils
+
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.toJavaLocalDateTime
+import java.time.format.DateTimeFormatter
+import java.util.Locale
+
+actual fun LocalDateTime.toLocalisedString(pattern: String): String {
+ val formatter = DateTimeFormatter.ofPattern(pattern, Locale.getDefault())
+ return this.toJavaLocalDateTime().format(formatter)
+}
+
+actual fun getCurrentLanguageCode(): String = Locale.getDefault().language
\ No newline at end of file
diff --git a/clients/tablet/core/ui/src/commonMain/composeResources/values-ru/strings_ru.xml b/clients/tablet/core/ui/src/commonMain/composeResources/values-ru/strings_ru.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3c13527d1cc2f297ec6b61419bb8da0d79a4e58c
--- /dev/null
+++ b/clients/tablet/core/ui/src/commonMain/composeResources/values-ru/strings_ru.xml
@@ -0,0 +1,36 @@
+
+
+ Нет соединения с сервером
+ Перезагрузите приложение
+ Перезагрузить приложение
+ Когда
+ На сколько
+ Организатор
+ ${date} с ${time}
+ - 15 мин
+ + 30 мин
+ мин
+ ч
+ Выберите организатора
+ Неверный организатор, попробуйте еще раз
+ Занять с
+ Подтвердить
+ Нет подключения к интернету
+ Забронирована до %1$s
+ Отменить бронь
+ Свободных нет
+ Через %1$s мин освободится %2$s. Пока что попробуйте найти тихое место в офисе
+ Вас кто-то опередил
+ Попробуйте занять другую переговорку, либо выберите другое время брони
+ Это время уже занято
+ %1$s, %2$s — %3$s
+ На главную
+ Вы заняли %1$s
+ Отмена
+ Загрузка слота на время: %1$s — %2$s
+ Свободно %1$s мин
+ Занято %1$s
+ %1$s брони
+ Слот загружается
+ Ошибка. Повторите попытку позже
+
\ No newline at end of file
diff --git a/clients/tablet/core/ui/src/commonMain/composeResources/values/strings.xml b/clients/tablet/core/ui/src/commonMain/composeResources/values/strings.xml
index 3ccdf63f76759e4c30816f2c672b21b69629f637..9f52148492849de2daca01a877debe5714fd074e 100644
--- a/clients/tablet/core/ui/src/commonMain/composeResources/values/strings.xml
+++ b/clients/tablet/core/ui/src/commonMain/composeResources/values/strings.xml
@@ -1,36 +1,36 @@
- Нет соединения с сервером
- Перезагрузите приложение
- Перезагрузить приложение
- Когда
- На сколько
- Организатор
- ${date} с ${time}
- - 15 мин
- + 30 мин
- мин
- ч
- Выберите организатора
- Неверный организатор, попробуйте еще раз
- Занять с
- Подтвердить
- Нет подключения к интернету
- Забронирована до %1$s
- Отменить бронь
- Свободных нет
- Через %1$s мин освободится %2$s. Пока что попробуйте найти тихое место в офисе
- Вас кто-то опередил
- Попробуйте занять другую переговорку, либо выберите другое время брони
- Это время уже занято
+ No server connection
+ Restart the app
+ Restart app
+ When
+ Duration
+ Organizer
+ ${date} at ${time}
+ - 15 min
+ + 30 min
+ min
+ h
+ Choose organizer
+ Invalid organizer, try again
+ Book from
+ Confirm
+ No internet connection
+ Booked until %1$s
+ Cancel booking
+ No free rooms
+ In %1$s min, %2$s will be free. For now, try finding a quiet place in the office
+ Someone booked it before you
+ Try booking another meeting room or choose a different time
+ This time is already booked
%1$s, %2$s — %3$s
- На главную
- Вы заняли %1$s
- Отмена
- Загрузка слота на время: %1$s - %2$s
- Свободно %1$s мин
- Занято %1$s
- %1$s брони
- Слот загружается
- Ошибка. Повторите попытку позже
+ Back to main
+ You booked %1$s
+ Cancel
+ Loading slot for time: %1$s — %2$s
+ Available for %1$s min
+ Booked by %1$s
+ %1$s bookings
+ Slot is loading
+ Error. Try again later
\ No newline at end of file
diff --git a/clients/tablet/core/ui/src/commonMain/kotlin/band/effective/office/tablet/core/ui/common/EventDurationView.kt b/clients/tablet/core/ui/src/commonMain/kotlin/band/effective/office/tablet/core/ui/common/EventDurationView.kt
index 994efdabf08dbecf77fb7fc387c5d003fd07f60c..4fc370f9faccfd6a8a725d30185dda0a9000bc88 100644
--- a/clients/tablet/core/ui/src/commonMain/kotlin/band/effective/office/tablet/core/ui/common/EventDurationView.kt
+++ b/clients/tablet/core/ui/src/commonMain/kotlin/band/effective/office/tablet/core/ui/common/EventDurationView.kt
@@ -30,12 +30,15 @@ import band.effective.office.tablet.core.ui.theme.h6
import band.effective.office.tablet.core.ui.theme.h8
import org.jetbrains.compose.resources.stringResource
+private const val MIN_EVENT_DURATION_MINUTES = 15
+
@Composable
fun EventDurationView(
modifier: Modifier = Modifier,
currentDuration: Int,
increment: () -> Unit,
- decrement: () -> Unit
+ decrement: () -> Unit,
+ canIncrementDuration: Boolean = true
) {
Column(modifier = modifier) {
Text(
@@ -52,6 +55,7 @@ fun EventDurationView(
Button(
modifier = Modifier.fillMaxHeight().weight(1f).clip(RoundedCornerShape(15.dp)),
onClick = { decrement() },
+ enabled = currentDuration > MIN_EVENT_DURATION_MINUTES,
colors = ButtonDefaults.buttonColors(
containerColor = LocalCustomColorsPalette.current.elevationBackground
)
@@ -73,6 +77,7 @@ fun EventDurationView(
onClick = {
increment()
},
+ enabled = canIncrementDuration,
colors = ButtonDefaults.buttonColors(
containerColor = LocalCustomColorsPalette.current.elevationBackground
)
@@ -92,9 +97,9 @@ private fun Int.getDurationString(): String {
val hours = this / 60
val minutes = this % 60
return when {
- hours == 0 -> "$minutes${stringResource(Res.string.short_minuets)}"
- minutes == 0 -> "$hours${stringResource(Res.string.short_hours)}"
- else -> "$hours${stringResource( Res.string.short_hours)} " +
- "$minutes${stringResource(Res.string.short_minuets)}"
+ hours == 0 -> "$minutes ${stringResource(Res.string.short_minuets)}"
+ minutes == 0 -> "$hours ${stringResource(Res.string.short_hours)}"
+ else -> "$hours ${stringResource( Res.string.short_hours)} " +
+ "$minutes ${stringResource(Res.string.short_minuets)}"
}
}
diff --git a/clients/tablet/core/ui/src/commonMain/kotlin/band/effective/office/tablet/core/ui/date/DateTimeView.kt b/clients/tablet/core/ui/src/commonMain/kotlin/band/effective/office/tablet/core/ui/date/DateTimeView.kt
index 14e96ef835d7d798b3911dafe4ed563258c489c0..810cd692781d139ebc3850eb880363c0bd690e80 100644
--- a/clients/tablet/core/ui/src/commonMain/kotlin/band/effective/office/tablet/core/ui/date/DateTimeView.kt
+++ b/clients/tablet/core/ui/src/commonMain/kotlin/band/effective/office/tablet/core/ui/date/DateTimeView.kt
@@ -40,13 +40,14 @@ import kotlin.time.ExperimentalTime
import kotlinx.datetime.LocalDateTime
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
+import band.effective.office.tablet.core.ui.utils.DateDisplayMapper
@OptIn(ExperimentalTime::class)
@Composable
fun DateTimeView(
modifier: Modifier,
selectDate: LocalDateTime,
- currentDate: LocalDateTime? = null,
+ currentDate: LocalDateTime,
increment: () -> Unit,
decrement: () -> Unit,
onOpenDateTimePickerModal: () -> Unit,
@@ -89,7 +90,7 @@ private fun RowScope.NextDateButton(increment: () -> Unit) {
private fun RowScope.SelectedDate(
onOpenDateTimePickerModal: () -> Unit,
selectDate: LocalDateTime,
- currentDate: LocalDateTime?
+ currentDate: LocalDateTime
) {
val timeDayMonthDateFormat = remember { dateTimeFormat }
val dayMonthDateFormat = remember { dayMonthFormat }
@@ -100,39 +101,67 @@ private fun RowScope.SelectedDate(
else AnimatedContentTransitionScope.SlideDirection.Right
}
- val displayedFormat by remember(selectDate) {
- mutableStateOf(
- if (currentDate != null && selectDate.date > currentDate.date) dayMonthDateFormat else timeDayMonthDateFormat
- )
- }
-
LaunchedEffect(selectDate) {
previousDate = selectDate
}
- Button(
- modifier = Modifier
- .fillMaxHeight()
- .weight(4f)
- .clip(RoundedCornerShape(15.dp)),
- onClick = onOpenDateTimePickerModal,
- colors = ButtonDefaults.buttonColors(
- containerColor = LocalCustomColorsPalette.current.elevationBackground
- ),
- contentPadding = PaddingValues(0.dp)
- ) {
- AnimatedContent(
- targetState = displayedFormat.format(selectDate),
- transitionSpec = {
- slideIntoContainer(slideDirection) + fadeIn() with
- slideOutOfContainer(slideDirection.opposite()) + fadeOut()
- },
- label = "AnimatedDateChange"
- ) { formattedDate ->
- Text(
- text = formattedDate,
- style = MaterialTheme.typography.h6
- )
+ if (currentDate.date < selectDate.date) {
+ Button(
+ modifier = Modifier
+ .fillMaxHeight()
+ .weight(4f)
+ .clip(RoundedCornerShape(15.dp)),
+ onClick = onOpenDateTimePickerModal,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = LocalCustomColorsPalette.current.elevationBackground
+ ),
+ contentPadding = PaddingValues(0.dp)
+ ) {
+ AnimatedContent(
+ targetState = DateDisplayMapper.map(
+ selectDate = selectDate,
+ currentDate = currentDate
+ ),
+ transitionSpec = {
+ slideIntoContainer(slideDirection) + fadeIn() with
+ slideOutOfContainer(slideDirection.opposite()) + fadeOut()
+ },
+ label = "AnimatedDateChange"
+ ) { formattedDate ->
+ Text(
+ text = formattedDate,
+ style = MaterialTheme.typography.h6
+ )
+ }
+ }
+ } else {
+ Button(
+ modifier = Modifier
+ .fillMaxHeight()
+ .weight(4f)
+ .clip(RoundedCornerShape(15.dp)),
+ onClick = onOpenDateTimePickerModal,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = LocalCustomColorsPalette.current.elevationBackground
+ ),
+ contentPadding = PaddingValues(0.dp)
+ ) {
+ AnimatedContent(
+ targetState = DateDisplayMapper.map(
+ selectDate = currentDate,
+ currentDate = currentDate
+ ),
+ transitionSpec = {
+ slideIntoContainer(slideDirection) + fadeIn() with
+ slideOutOfContainer(slideDirection.opposite()) + fadeOut()
+ },
+ label = "AnimatedDateChange"
+ ) { formattedDate ->
+ Text(
+ text = formattedDate,
+ style = MaterialTheme.typography.h6
+ )
+ }
}
}
}
diff --git a/clients/tablet/core/ui/src/commonMain/kotlin/band/effective/office/tablet/core/ui/utils/DateDisplayMapper.kt b/clients/tablet/core/ui/src/commonMain/kotlin/band/effective/office/tablet/core/ui/utils/DateDisplayMapper.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0486415abe6734f801009848395781af2897267f
--- /dev/null
+++ b/clients/tablet/core/ui/src/commonMain/kotlin/band/effective/office/tablet/core/ui/utils/DateDisplayMapper.kt
@@ -0,0 +1,66 @@
+package band.effective.office.tablet.core.ui.utils
+
+import kotlinx.datetime.LocalDateTime
+
+/**
+ * Data class to store date and time format patterns for a specific locale.
+ *
+ * @param default Format for full date and time.
+ * @param future Format for future dates.
+ * @param time Format for time only.
+ */
+private data class LocaleFormatPatterns(
+ val default: String,
+ val future: String,
+ val time: String
+)
+
+/**
+ * Utility object for formatting dates and times based on the current locale.
+ * Falls back to English formats for unsupported locales.
+ */
+object DateDisplayMapper {
+
+ private val formatPatterns: Map = mapOf(
+ "ru" to LocaleFormatPatterns(
+ default = "d MMMM, HH:mm",
+ future = "d MMMM",
+ time = "HH:mm"
+ ),
+ "en" to LocaleFormatPatterns(
+ default = "MMM d h:mm a",
+ future = "MMM d",
+ time = "h:mm a"
+ )
+ )
+ private val defaultFormats: LocaleFormatPatterns = formatPatterns["en"]!!
+
+ private fun getPatternsForLocale(): LocaleFormatPatterns {
+ val currentLanguage = getCurrentLanguageCode()
+ return formatPatterns[currentLanguage] ?: defaultFormats
+ }
+
+ fun map(selectDate: LocalDateTime, currentDate: LocalDateTime?): String {
+ val patterns = getPatternsForLocale()
+ val pattern = if (currentDate != null && selectDate.date > currentDate.date) {
+ patterns.future
+ } else {
+ patterns.default
+ }
+ return selectDate.toLocalisedString(pattern)
+ }
+
+ fun formatForPicker(date: LocalDateTime): String {
+ val patterns = getPatternsForLocale()
+ return date.toLocalisedString(patterns.future)
+ }
+
+ fun formatTime(time: LocalDateTime): String {
+ val patterns = getPatternsForLocale()
+ return time.toLocalisedString(patterns.time)
+ }
+
+ fun is24HourFormat(): Boolean {
+ return getCurrentLanguageCode() != "en"
+ }
+}
\ No newline at end of file
diff --git a/clients/tablet/core/ui/src/commonMain/kotlin/band/effective/office/tablet/core/ui/utils/DateFormatter.kt b/clients/tablet/core/ui/src/commonMain/kotlin/band/effective/office/tablet/core/ui/utils/DateFormatter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..72f9dce67273278eede313af4ac1f36aaec7b460
--- /dev/null
+++ b/clients/tablet/core/ui/src/commonMain/kotlin/band/effective/office/tablet/core/ui/utils/DateFormatter.kt
@@ -0,0 +1,7 @@
+package band.effective.office.tablet.core.ui.utils
+
+import kotlinx.datetime.LocalDateTime
+
+@OptIn(kotlinx.datetime.format.FormatStringsInDatetimeFormats::class)
+expect fun LocalDateTime.toLocalisedString(pattern: String): String
+expect fun getCurrentLanguageCode(): String
\ No newline at end of file
diff --git a/clients/tablet/core/ui/src/iosMain/kotlin/band/effective/office/tablet/core/ui/utils/DateFormatter.ios.kt b/clients/tablet/core/ui/src/iosMain/kotlin/band/effective/office/tablet/core/ui/utils/DateFormatter.ios.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b4ca439d9a6fbf0d37748b7e042cccf0454f5583
--- /dev/null
+++ b/clients/tablet/core/ui/src/iosMain/kotlin/band/effective/office/tablet/core/ui/utils/DateFormatter.ios.kt
@@ -0,0 +1,25 @@
+package band.effective.office.tablet.core.ui.utils
+
+import kotlinx.datetime.LocalDateTime
+import platform.Foundation.NSDate
+import platform.Foundation.NSDateFormatter
+import platform.Foundation.NSLocale
+import platform.Foundation.currentLocale
+
+
+import kotlinx.datetime.toNSDateComponents
+import platform.Foundation.*
+
+actual fun LocalDateTime.toLocalisedString(pattern: String): String {
+ val dateFormatter = NSDateFormatter()
+ dateFormatter.dateFormat = pattern
+ dateFormatter.locale = NSLocale.currentLocale
+
+ val calendar = NSCalendar.currentCalendar
+ val dateComponents = toNSDateComponents()
+
+ val date = calendar.dateFromComponents(dateComponents) ?: NSDate()
+ return dateFormatter.stringFromDate(date)
+}
+
+actual fun getCurrentLanguageCode(): String = NSLocale.currentLocale.languageCode
\ No newline at end of file
diff --git a/clients/tablet/feature/bookingEditor/src/commonMain/composeResources/values-ru/strings_ru.xml b/clients/tablet/feature/bookingEditor/src/commonMain/composeResources/values-ru/strings_ru.xml
new file mode 100644
index 0000000000000000000000000000000000000000..95b290008edc6321c5bc7a3d71373a2a6cbe2d1f
--- /dev/null
+++ b/clients/tablet/feature/bookingEditor/src/commonMain/composeResources/values-ru/strings_ru.xml
@@ -0,0 +1,12 @@
+
+
+ Занять %1$s
+ Изменить бронь
+ Занять c %1$s до %2$s
+ Сохранить изменения
+ Удалить бронь
+ Произошла ошибка
+ Ошибка, выбрана неправильная дата
+ Ошибка при создании события
+ Ошибка при удалении события
+
diff --git a/clients/tablet/feature/bookingEditor/src/commonMain/composeResources/values/strings.xml b/clients/tablet/feature/bookingEditor/src/commonMain/composeResources/values/strings.xml
index c73b265e276f65216228bcd4ab75fefc66cdd2b4..0e4ac31daa8dc6f464b0f27ef336c154c9c8772e 100644
--- a/clients/tablet/feature/bookingEditor/src/commonMain/composeResources/values/strings.xml
+++ b/clients/tablet/feature/bookingEditor/src/commonMain/composeResources/values/strings.xml
@@ -1,12 +1,12 @@
- Занять %1$s
- Измененить бронь
- Занять c %1$s до %2$s
- Изменить
- Удалить бронь
- Произошла ошибка
- Ошибка, выбрана неправильная дата
+ Book %1$s
+ Edit booking
+ Book from %1$s to %2$s
+ Confirm changes
+ Delete booking
+ An error occurred
+ Error, incorrect date selected
Error creating event
- Error deleting event"
+ Error deleting event
\ No newline at end of file
diff --git a/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditor.kt b/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditor.kt
index 92e6b5ee2f8a562fdb53985f8c9514283d8fc122..038c919792aaf65685f78c2ae8a0a751f5648138 100644
--- a/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditor.kt
+++ b/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditor.kt
@@ -120,7 +120,8 @@ fun BookingEditor(
finish = state.event.finishTime.format(timeFormatter),
room = component.roomName,
isTimeInPastError = state.isTimeInPastError,
- isEditable = state.event.isEditable
+ isEditable = state.event.isEditable,
+ canIncrementDuration = state.canIncrementDuration
)
}
}
@@ -163,7 +164,8 @@ private fun BookingEditor(
finish: String,
room: String,
isTimeInPastError: Boolean,
- isEditable: Boolean = true
+ isEditable: Boolean = true,
+ canIncrementDuration: Boolean
) {
val snackbarHostState = remember { SnackbarHostState() }
val timeInPastErrorMessage = stringResource(Res.string.is_time_in_past_error)
@@ -197,14 +199,16 @@ private fun BookingEditor(
increment = incrementData,
decrement = decrementData,
onOpenDateTimePickerModal = onOpenDateTimePickerModal,
- showTitle = true
+ showTitle = true,
+ currentDate = selectData,
)
Spacer(modifier = Modifier.height(15.dp))
EventDurationView(
modifier = Modifier.fillMaxWidth().height(100.dp),
currentDuration = selectDuration,
increment = incrementDuration,
- decrement = decrementDuration
+ decrement = decrementDuration,
+ canIncrementDuration = canIncrementDuration
)
Spacer(modifier = Modifier.height(15.dp))
EventOrganizerView(
diff --git a/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditorComponent.kt b/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditorComponent.kt
index 08f4e5a96841c05894a511bc596d6fbee767ca69..c1bd35dfe1f17096dc51ff82175b5a406357b31a 100644
--- a/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditorComponent.kt
+++ b/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/BookingEditorComponent.kt
@@ -1,12 +1,13 @@
package band.effective.office.tablet.feature.bookingEditor.presentation
+import band.effective.office.tablet.core.domain.Either
import band.effective.office.tablet.core.domain.OfficeTime
import band.effective.office.tablet.core.domain.model.EventInfo
import band.effective.office.tablet.core.domain.model.Organizer
-import band.effective.office.tablet.core.domain.model.Slot
import band.effective.office.tablet.core.domain.unbox
import band.effective.office.tablet.core.domain.useCase.CheckBookingUseCase
import band.effective.office.tablet.core.domain.useCase.CreateBookingUseCase
+import band.effective.office.tablet.core.domain.useCase.DeleteBookingUseCase
import band.effective.office.tablet.core.domain.useCase.OrganizersInfoUseCase
import band.effective.office.tablet.core.domain.useCase.UpdateBookingUseCase
import band.effective.office.tablet.core.domain.util.asInstant
@@ -24,6 +25,7 @@ import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.minutes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
@@ -41,11 +43,12 @@ import org.koin.core.component.inject
* Component responsible for editing booking events.
* Handles creating new bookings and updating existing ones.
*/
+const val DURATION_INCREMENT_MINUTES = 30
+const val DELETE_SUCCESS_DELAY = 2000L
class BookingEditorComponent(
componentContext: ComponentContext,
initialEvent: EventInfo,
val roomName: String,
- private val onDeleteEvent: (Slot) -> Unit,
private val onCloseRequest: () -> Unit,
) : ComponentContext by componentContext, KoinComponent, ModalWindow {
@@ -68,6 +71,7 @@ class BookingEditorComponent(
private val checkBookingUseCase: CheckBookingUseCase by inject()
private val updateBookingUseCase: UpdateBookingUseCase by inject()
private val createBookingUseCase: CreateBookingUseCase by inject()
+ private val deleteBookingUseCase: DeleteBookingUseCase by inject()
// Mappers
private val eventInfoMapper: EventInfoMapper by inject()
@@ -158,9 +162,25 @@ class BookingEditorComponent(
*/
private fun deleteEvent() = coroutineScope.launch {
mutableState.update { it.copy(isLoadDelete = true) }
- onDeleteEvent(eventInfoMapper.mapToSlot(state.value.event))
- mutableState.update { it.copy(isLoadDelete = false) }
- onCloseRequest()
+ val deleteResult = withContext(Dispatchers.IO) {
+ deleteBookingUseCase(roomName, state.value.event)
+ }
+
+ when (deleteResult) {
+ is Either.Error -> {
+ mutableState.update {
+ it.copy(
+ isLoadDelete = false,
+ isErrorDelete = true,
+ )
+ }
+ }
+
+ is Either.Success -> {
+ delay(DELETE_SUCCESS_DELAY)
+ onCloseRequest()
+ }
+ }
}
/**
@@ -231,18 +251,26 @@ class BookingEditorComponent(
)
val isTimeInPast = newDate <= getCurrentTime()
+ val newFinishTime = newDate.asInstant.plus(duration.minutes).asLocalDateTime
+ val finishWorkTime = OfficeTime.finishWorkTime(newDate.date)
+ val isFinishTimeExceeded = newFinishTime > finishWorkTime
+
+ val nextIncrementFinishTime = newDate.asInstant.plus((duration + DURATION_INCREMENT_MINUTES).minutes).asLocalDateTime
+ val canIncrementDuration = nextIncrementFinishTime <= finishWorkTime
+
updateStateWithNewEventDetails(
newDate = newDate,
newDuration = duration,
newOrganizer = selectOrganizer,
busyEvents = busyEvents,
- isTimeInPast = isTimeInPast
+ isTimeInPast = isTimeInPast,
+ canIncrementDuration = canIncrementDuration
)
if (selectOrganizer != Organizer.default) {
updateButtonState(
inputError = isInputError,
- busyEvent = busyEvents.isNotEmpty()
+ busyEvent = busyEvents.isNotEmpty() || isFinishTimeExceeded
)
}
}
@@ -263,6 +291,14 @@ class BookingEditorComponent(
it.fullName == newOrganizer.fullName
} ?: event.organizer
val isTimeInPast = newDate <= getCurrentTime()
+
+ val finishWorkTime = OfficeTime.finishWorkTime(newDate.date)
+ val newFinishTime = newDate.asInstant.plus(newDuration.minutes).asLocalDateTime
+ val isFinishTimeExceeded = newFinishTime > finishWorkTime
+
+ val nextIncrementFinishTime = newDate.asInstant.plus((newDuration + DURATION_INCREMENT_MINUTES).minutes).asLocalDateTime
+ val canIncrementDuration = nextIncrementFinishTime <= finishWorkTime
+
val busyEvents = checkForBusyEvents(
date = newDate,
duration = newDuration,
@@ -274,12 +310,13 @@ class BookingEditorComponent(
newDuration = newDuration,
newOrganizer = resolvedOrganizer,
busyEvents = busyEvents,
- isTimeInPast = isTimeInPast
+ isTimeInPast = isTimeInPast,
+ canIncrementDuration = canIncrementDuration
)
updateButtonState(
inputError = !organizers.contains(resolvedOrganizer),
- busyEvent = busyEvents.isNotEmpty()
+ busyEvent = busyEvents.isNotEmpty() || isFinishTimeExceeded
)
}
}
@@ -334,7 +371,8 @@ class BookingEditorComponent(
newDuration: Int,
newOrganizer: Organizer,
busyEvents: List,
- isTimeInPast: Boolean
+ isTimeInPast: Boolean,
+ canIncrementDuration: Boolean
) {
val updatedEvent = createEventInfo(
id = state.value.event.id,
@@ -350,7 +388,8 @@ class BookingEditorComponent(
selectOrganizer = newOrganizer,
event = updatedEvent,
isBusyEvent = busyEvents.isNotEmpty(),
- isTimeInPastError = isTimeInPast
+ isTimeInPastError = isTimeInPast,
+ canIncrementDuration = canIncrementDuration
)
}
}
diff --git a/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/State.kt b/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/State.kt
index aeed462f0b8d5b94f0490c4b769e50935373572f..18acb27d74e7e0a76fb945d6b958702457cddecd 100644
--- a/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/State.kt
+++ b/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/State.kt
@@ -24,7 +24,8 @@ data class State(
val showSelectDate: Boolean,
val enableUpdateButton: Boolean,
val isBusyEvent: Boolean,
- val isTimeInPastError: Boolean
+ val isTimeInPastError: Boolean,
+ val canIncrementDuration: Boolean
) {
companion object {
val defaultValue = State(
@@ -46,7 +47,8 @@ data class State(
showSelectDate = false,
enableUpdateButton = false,
isBusyEvent = false,
- isTimeInPastError = false
+ isTimeInPastError = false,
+ canIncrementDuration = true
)
}
diff --git a/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/datetimepicker/DateTimePickerModalView.kt b/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/datetimepicker/DateTimePickerModalView.kt
index 62c63d4195e6ca1c104669662ea7800fb3c53d15..98a2a784113a088853cef78c8e3934806833cc21 100644
--- a/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/datetimepicker/DateTimePickerModalView.kt
+++ b/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/datetimepicker/DateTimePickerModalView.kt
@@ -24,11 +24,11 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
-import band.effective.office.tablet.core.domain.util.toFormattedString
import band.effective.office.tablet.core.ui.common.CrossButtonView
import band.effective.office.tablet.core.ui.theme.LocalCustomColorsPalette
import band.effective.office.tablet.core.ui.theme.header8
import band.effective.office.tablet.core.ui.time_booked
+import band.effective.office.tablet.core.ui.utils.DateDisplayMapper
import band.effective.office.tablet.feature.bookingEditor.presentation.datetimepicker.components.DatePickerView
import band.effective.office.tablet.feature.bookingEditor.presentation.datetimepicker.components.TimePickerView
import kotlinx.datetime.LocalDate
@@ -114,7 +114,7 @@ fun DateTimePickerModalView(
) {
Text(
text = when (enableDateButton) {
- true -> "${currentDate.dayOfMonth} ${currentDate.month.name} ${currentDate.toFormattedString("HH:mm")}"
+ true -> DateDisplayMapper.formatForPicker(currentDate)
false -> stringResource(band.effective.office.tablet.core.ui.Res.string.time_booked)
},
style = header8,
diff --git a/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/datetimepicker/components/TimePickerView.kt b/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/datetimepicker/components/TimePickerView.kt
index 3f5bdca478cea5f88f73c817d505aa2b0fab2cec..b13bc3637bc7b4d8dfb7bf2e5214cf951a8b2e89 100644
--- a/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/datetimepicker/components/TimePickerView.kt
+++ b/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/datetimepicker/components/TimePickerView.kt
@@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import band.effective.office.tablet.core.ui.theme.LocalCustomColorsPalette
+import band.effective.office.tablet.core.ui.utils.DateDisplayMapper
import com.mohamedrejeb.calf.ui.timepicker.AdaptiveTimePicker
import com.mohamedrejeb.calf.ui.timepicker.rememberAdaptiveTimePickerState
import kotlinx.datetime.LocalDateTime
@@ -19,10 +20,12 @@ fun TimePickerView(
currentDate: LocalDateTime,
onSnap: (LocalTime) -> Unit
) {
+ val is24Hour = DateDisplayMapper.is24HourFormat()
+
val state = rememberAdaptiveTimePickerState(
initialHour = currentDate.hour,
initialMinute = currentDate.minute,
- is24Hour = true
+ is24Hour = is24Hour
)
LaunchedEffect(state.hour, state.minute) {
val hour = state.hour
diff --git a/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/mapper/EventInfoMapper.kt b/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/mapper/EventInfoMapper.kt
index 3b19223f1be563599034c20e048add6f986748fc..6c76139a454e29903b91ac3fcde03caaf7026d4e 100644
--- a/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/mapper/EventInfoMapper.kt
+++ b/clients/tablet/feature/bookingEditor/src/commonMain/kotlin/band/effective/office/tablet/feature/bookingEditor/presentation/mapper/EventInfoMapper.kt
@@ -1,5 +1,6 @@
package band.effective.office.tablet.feature.bookingEditor.presentation.mapper
+import band.effective.office.tablet.core.domain.OfficeTime
import band.effective.office.tablet.core.domain.model.EventInfo
import band.effective.office.tablet.core.domain.model.Slot
import band.effective.office.tablet.core.domain.util.asInstant
@@ -12,12 +13,15 @@ class EventInfoMapper {
fun mapToUpdateBookingState(eventInfo: EventInfo): State {
val duration = eventInfo.finishTime.asInstant.minus(eventInfo.startTime.asInstant).inWholeMinutes.toInt()
+ val finishWorkTime = OfficeTime.finishWorkTime(eventInfo.startTime.date)
+ val canIncrementDuration = eventInfo.finishTime < finishWorkTime
return State.defaultValue.copy(
date = eventInfo.startTime,
duration = duration,
selectOrganizer = eventInfo.organizer,
inputText = eventInfo.organizer.fullName,
- event = eventInfo
+ event = eventInfo,
+ canIncrementDuration = canIncrementDuration
)
}
}
\ No newline at end of file
diff --git a/clients/tablet/feature/main/src/commonMain/composeResources/values-ru/strings_ru.xml b/clients/tablet/feature/main/src/commonMain/composeResources/values-ru/strings_ru.xml
new file mode 100644
index 0000000000000000000000000000000000000000..78dbe7c3950f3e5b3374351521ea69d92d4eafd6
--- /dev/null
+++ b/clients/tablet/feature/main/src/commonMain/composeResources/values-ru/strings_ru.xml
@@ -0,0 +1,52 @@
+
+
+ Занято до %1$s
+ · До конца %1$s
+ Свободна до %1$s
+ · До начала %1$s
+ Освободить
+ TV
+ USB
+ Розетка
+ Розетки
+ Розеток
+ час
+ часа
+ часов
+ минута
+ минуты
+ минут
+ Занять
+ Занять %1$s
+ До конца
+ Смотреть другие переговорки
+ на сегодня
+ %1$s с %2$s
+ %1$s мин
+ Занятое время
+ Занятое время
+ Выберите организатора
+ В это время будет другая бронь
+ Нет подключения к интернету
+ Нет соединения с сервером
+ Перезагрузите приложение
+ Перезагрузить приложение
+ Выход
+ Выбрать %1$s
+ Сейчас свободна
+ Данные брони могут быть неактуальны
+ %1$s мин
+ Занять любую переговорку
+ Освободить переговорку?
+ Попробовать ещё раз
+ Освободить
+ с %1$s до %2$s
+ Занять %2$s?
+ Когда
+ На сколько
+ Организатор
+ %1$s мин
+ %1$sч %2$sмин
+ Занять %1$s
+ Это время уже занято
+
\ No newline at end of file
diff --git a/clients/tablet/feature/main/src/commonMain/composeResources/values/strings.xml b/clients/tablet/feature/main/src/commonMain/composeResources/values/strings.xml
index bed24afc7c0287bcb05d113b470f9d7c937405be..dbf9a9bb43b8a97d0da3ac6d9904a9aec00c939d 100644
--- a/clients/tablet/feature/main/src/commonMain/composeResources/values/strings.xml
+++ b/clients/tablet/feature/main/src/commonMain/composeResources/values/strings.xml
@@ -1,52 +1,52 @@
- Занято до %1$s
- · До конца %1$s
- Свободна до %1$s
- · До начала %1$s
- Освободить
+ Booked until %1$s
+ · %1$s left
+ Free until %1$s
+ · Until start %1$s
+ Release
TV
USB
- Розетка
- Розетки
- Розеток
- час
- часа
- часов
- минута
- минуты
- минут
- Занять
- Занять %1$s
- До конца
- Смотреть другие переговорки
- на сегодня
- %1$s с %2$s
- %1$s мин
- Занятое время
- Занятое время
- Выберите организатора
- В это время будет другая бронь
- Нет подключения к интернету
- Нет соединения с сервером
- Перезагрузите приложение
- Перезагрузить приложение
- Выход
- Выбрать %1$s
- Сейчас свободна
- Данные брони могут быть неактуальны
- %1$s мин
- Занять любую переговорку на:
- Освободить переговорку?
- Попробовать ещё раз
- Освободить
- с %1$s до %2$s
- Занять %2$s?
- Когда
- На сколько
- Организатор
- %1$s мин
- %1$sч %2$sмин
- Занять %1$s
- Это время уже занято
+ Socket
+ Sockets
+ Sockets
+ hour
+ hours
+ hours
+ minute
+ minutes
+ minutes
+ Book
+ Book %1$s
+ Until end
+ View other meeting rooms
+ for today
+ %1$s at %2$s
+ %1$s min
+ Booked time
+ Booked time
+ Select an organizer
+ Another booking is scheduled at this time
+ No internet connection
+ No server connection
+ Restart the app
+ Restart app
+ Exit
+ Choose %1$s
+ Available now
+ Booking data may be outdated
+ %1$s min
+ Book any available room
+ Release the meeting room?
+ Try again
+ Release
+ from %1$s to %2$s
+ Book %2$s?
+ When
+ Duration
+ Organizer
+ %1$s min
+ %1$s h %2$s min
+ Book %1$s
+ This time is already booked
\ No newline at end of file
diff --git a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/components/uiComponent/CommonRoomInfoComponent.kt b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/components/uiComponent/CommonRoomInfoComponent.kt
index 9f1554bd2c8751104dc1c863c0a09793dd7a35db..77e1ed98385698ae3ce533c62b3cd23195a2c303 100644
--- a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/components/uiComponent/CommonRoomInfoComponent.kt
+++ b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/components/uiComponent/CommonRoomInfoComponent.kt
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -85,19 +86,20 @@ fun RoomProperty(
electricSocketCount: Int
) {
Row {
- RoomPropertyComponent(
- image = Res.drawable.quantity,
- text = "$capacity",
- color = roomInfoColor
- )
if (isHaveTv) {
- Spacer(modifier = Modifier.width(spaceBetweenProperty))
RoomPropertyComponent(
image = Res.drawable.tv,
- text = stringResource(band.effective.office.tablet.feature.main.Res.string.tv_property),
+ text = "",
color = roomInfoColor
)
+ Spacer(modifier = Modifier.width(spaceBetweenProperty))
}
+ RoomPropertyComponent(
+ image = Res.drawable.quantity,
+ text = "$capacity",
+ color = roomInfoColor,
+ textModifier = Modifier.widthIn(min = 20.dp)
+ )
if (electricSocketCount > 0) {
Spacer(modifier = Modifier.width(spaceBetweenProperty))
RoomPropertyComponent(
@@ -110,7 +112,12 @@ fun RoomProperty(
}
@Composable
-fun RoomPropertyComponent(image: DrawableResource, text: String, color: Color) {
+fun RoomPropertyComponent(
+ image: DrawableResource,
+ text: String,
+ color: Color,
+ textModifier: Modifier = Modifier
+) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
@@ -121,7 +128,9 @@ fun RoomPropertyComponent(image: DrawableResource, text: String, color: Color) {
contentDescription = null,
colorFilter = ColorFilter.tint(color)
)
- Spacer(modifier = Modifier.width(8.dp))
- Text(text = text, style = MaterialTheme.typography.h8)
+ if (text.isNotEmpty()) {
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(text = text, style = MaterialTheme.typography.h8, modifier = textModifier)
+ }
}
}
\ No newline at end of file
diff --git a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/components/uiComponent/RoomInfoLeftPanel.kt b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/components/uiComponent/RoomInfoLeftPanel.kt
index 7171e485a82430803d3953f34b01c647298267d9..1e19aeae9d0d3fc9abeb55bbdb5438973cfc7bca 100644
--- a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/components/uiComponent/RoomInfoLeftPanel.kt
+++ b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/components/uiComponent/RoomInfoLeftPanel.kt
@@ -90,8 +90,15 @@ fun RoomInfoLeftPanel(
SlotView(
slotUi = it,
onClick = { slotComponent.sendIntent(SlotIntent.ClickToEdit(this)) },
- onToggle = { (it as? SlotUi.MultiSlot)?.let { slotComponent.sendIntent(SlotIntent.ClickToToggle(it)) } },
- onCancel = { deleteSlot -> slotComponent.sendIntent(SlotIntent.OnCancelDelete(deleteSlot)) }
+ onToggle = {
+ (it as? SlotUi.MultiSlot)?.let {
+ slotComponent.sendIntent(
+ SlotIntent.ClickToToggle(
+ it
+ )
+ )
+ }
+ },
)
}
Spacer(Modifier.height(20.dp))
diff --git a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/domain/CurrentTimeHolder.kt b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/domain/CurrentTimeHolder.kt
new file mode 100644
index 0000000000000000000000000000000000000000..50204e6104d50596b4b39c5fc21e8d49c0ca2ca2
--- /dev/null
+++ b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/domain/CurrentTimeHolder.kt
@@ -0,0 +1,33 @@
+package band.effective.office.tablet.feature.main.domain
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.datetime.Clock
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+
+/**
+ * A singleton that holds the current time.
+ */
+object CurrentTimeHolder {
+ private val defaultTimeZone = TimeZone.Companion.currentSystemDefault()
+
+ private val _currentTime = MutableStateFlow(Clock.System.now().toLocalDateTime(defaultTimeZone))
+ val currentTime: StateFlow = _currentTime.asStateFlow()
+
+ /**
+ * Updates the current time.
+ */
+ fun updateTime(time: LocalDateTime) {
+ _currentTime.value = time
+ }
+
+ /**
+ * Gets the current time.
+ */
+ fun getCurrentTime(): LocalDateTime {
+ return _currentTime.value
+ }
+}
\ No newline at end of file
diff --git a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/presentation/main/MainComponent.kt b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/presentation/main/MainComponent.kt
index 9439e74324f6c1635d30f8ddebef5a4bbde5df1d..04dd80f5c079c8a178e4559ddbc832817941e37e 100644
--- a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/presentation/main/MainComponent.kt
+++ b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/presentation/main/MainComponent.kt
@@ -2,9 +2,9 @@ package band.effective.office.tablet.feature.main.presentation.main
import band.effective.office.tablet.core.domain.Either
import band.effective.office.tablet.core.domain.ErrorWithData
+import band.effective.office.tablet.core.domain.manager.DateResetManager
import band.effective.office.tablet.core.domain.model.EventInfo
import band.effective.office.tablet.core.domain.model.RoomInfo
-import band.effective.office.tablet.core.domain.model.Slot
import band.effective.office.tablet.core.domain.useCase.CheckSettingsUseCase
import band.effective.office.tablet.core.domain.useCase.DeleteBookingUseCase
import band.effective.office.tablet.core.domain.useCase.RoomInfoUseCase
@@ -15,6 +15,7 @@ import band.effective.office.tablet.core.domain.util.currentLocalDateTime
import band.effective.office.tablet.core.domain.util.minus
import band.effective.office.tablet.core.domain.util.plus
import band.effective.office.tablet.core.ui.utils.componentCoroutineScope
+import band.effective.office.tablet.feature.main.domain.CurrentTimeHolder
import band.effective.office.tablet.feature.main.domain.GetRoomIndexUseCase
import band.effective.office.tablet.feature.main.domain.GetTimeToNextEventUseCase
import band.effective.office.tablet.feature.slot.presentation.SlotComponent
@@ -22,7 +23,6 @@ import band.effective.office.tablet.feature.slot.presentation.SlotIntent
import com.arkivanov.decompose.ComponentContext
import kotlin.math.abs
import kotlin.time.Duration.Companion.days
-import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlin.time.ExperimentalTime
import kotlinx.coroutines.Dispatchers
@@ -94,6 +94,23 @@ class MainComponent(
// Set up event listeners
setupEventListeners()
+
+ // Initialize date reset manager
+ initializeDateResetManager()
+ }
+
+ /**
+ * Initializes the DateResetManager to handle date reset on inactivity.
+ * This registers a callback that will reset the selected date and current room
+ * when inactivity is detected.
+ */
+ private fun initializeDateResetManager() {
+ DateResetManager.registerDateResetCallback { date ->
+ mutableState.update { it.copy(selectedDate = date) }
+ reboot(refresh = true, resetSelectRoom = true)
+ updateTimeToNextEvent()
+ slotComponent.sendIntent(SlotIntent.InactivityTimeout)
+ }
}
/**
@@ -110,13 +127,6 @@ class MainComponent(
}
}
- // Update time to next event periodically
- timerUseCase.timer(coroutineScope, 1.seconds) { _ ->
- withContext(Dispatchers.Main) {
- updateTimeToNextEvent()
- }
- }
-
// Listen for room info changes
coroutineScope.launch(Dispatchers.Main) {
roomInfoUseCase.subscribe().collect { roomsInfo ->
@@ -126,12 +136,8 @@ class MainComponent(
}
}
- // reset select date
- currentTimeTimer.start(1.minutes) {
- withContext(Dispatchers.Main) {
- mutableState.update { it.copy(selectedDate = currentLocalDateTime) }
- slotComponent.sendIntent(SlotIntent.UpdateDate(currentLocalDateTime))
- }
+ coroutineScope.launch {
+ CurrentTimeHolder.currentTime.collect { updateTimeToNextEvent() }
}
}
@@ -199,34 +205,6 @@ class MainComponent(
}
}
- /**
- * Handles deleting an event.
- */
- fun handleDeleteEvent(slot: Slot) {
- slotComponent.sendIntent(
- SlotIntent.Delete(
- slot = slot,
- onDelete = {
- deleteEventFromSlot(slot)
- }
- )
- )
- }
-
- /**
- * Deletes an event from a slot.
- */
- private fun deleteEventFromSlot(slot: Slot) {
- coroutineScope.launch {
- (slot as? Slot.EventSlot)?.eventInfo?.let { eventInfo ->
- deleteBookingUseCase(
- eventInfo = eventInfo,
- roomName = getCurrentRoomName()
- )
- }
- }
- }
-
/**
* Updates the selected date.
*/
@@ -239,7 +217,8 @@ class MainComponent(
// Only update if the new date is not in the past
if (newDate.date >= currentLocalDateTime.date) {
mutableState.update { it.copy(selectedDate = newDate) }
- slotComponent.sendIntent(SlotIntent.UpdateDate(newDate))
+ val selectedRoom = state.value.roomList[state.value.indexSelectRoom]
+ slotComponent.sendIntent(SlotIntent.UpdateRequest(selectedRoom.name, state.value.selectedDate))
}
}
@@ -270,18 +249,10 @@ class MainComponent(
val selectedRoom = state.value.roomList.getOrNull(index)
if (selectedRoom != null) {
- updateComponents(selectedRoom, state.value.selectedDate)
+ slotComponent.sendIntent(SlotIntent.UpdateRequest(room = selectedRoom.name, state.value.selectedDate))
}
}
- /**
- * Updates child components with new room and date information.
- */
- private fun updateComponents(roomInfo: RoomInfo, date: LocalDateTime) {
- slotComponent.sendIntent(SlotIntent.UpdateRequest(room = roomInfo.name))
- slotComponent.sendIntent(SlotIntent.UpdateDate(date))
- }
-
/**
* Data class to hold the result of loading rooms.
*/
@@ -337,20 +308,31 @@ class MainComponent(
*/
private fun updateStateWithRoomsResult(roomsResult: RoomsResult) {
mutableState.update {
- it.copy(
- isLoad = false,
- isData = roomsResult.isSuccess,
- isError = !roomsResult.isSuccess,
- roomList = roomsResult.roomList,
- indexSelectRoom = roomsResult.indexSelectRoom,
- timeToNextEvent = getTimeToNextEventUseCase(
- state.value.roomList,
- state.value.indexSelectRoom
- ),
- )
+ if (roomsResult.roomList.isEmpty()) {
+ it.copy(
+ isLoad = false,
+ isData = false,
+ isError = true,
+ roomList = listOf(RoomInfo.defaultValue),
+ indexSelectRoom = 0,
+ timeToNextEvent = 0
+ )
+ } else {
+ val selectedRoom = roomsResult.roomList[roomsResult.indexSelectRoom.coerceIn(0, roomsResult.roomList.size - 1)]
+ slotComponent.sendIntent(SlotIntent.UpdateRequest(selectedRoom.name, state.value.selectedDate))
+ it.copy(
+ isLoad = false,
+ isData = roomsResult.isSuccess,
+ isError = !roomsResult.isSuccess,
+ roomList = roomsResult.roomList,
+ indexSelectRoom = roomsResult.indexSelectRoom,
+ timeToNextEvent = getTimeToNextEventUseCase(
+ rooms = roomsResult.roomList,
+ selectedRoomIndex = roomsResult.indexSelectRoom
+ )
+ )
+ }
}
- val selectedRoom = roomsResult.roomList[roomsResult.indexSelectRoom]
- updateComponents(selectedRoom, state.value.selectedDate)
}
/**
@@ -375,7 +357,7 @@ class MainComponent(
loadRooms(roomIndex)
currentState.roomList.getOrNull(roomIndex)?.let { roomInfo ->
- updateComponents(roomInfo, currentState.selectedDate)
+ slotComponent.sendIntent(SlotIntent.UpdateRequest(roomInfo.name, currentState.selectedDate))
}
}
diff --git a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/presentation/main/MainScreen.kt b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/presentation/main/MainScreen.kt
index 629febe2eb66dfb61c2b06e1a7652d508f9aefd8..1b5be6381d7f97b7eb64f806f214b21c4be2f6fd 100644
--- a/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/presentation/main/MainScreen.kt
+++ b/clients/tablet/feature/main/src/commonMain/kotlin/band/effective/office/tablet/feature/main/presentation/main/MainScreen.kt
@@ -11,12 +11,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import band.effective.office.tablet.core.ui.LoadMainScreen
import band.effective.office.tablet.core.ui.common.ErrorMainScreen
+import band.effective.office.tablet.feature.main.domain.CurrentTimeHolder
import kotlin.time.ExperimentalTime
@OptIn(ExperimentalTime::class)
@Composable
fun MainScreen(component: MainComponent) {
val state by component.state.collectAsState()
+ val currentDate by CurrentTimeHolder.currentTime.collectAsState()
Column(
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
@@ -39,7 +41,7 @@ fun MainScreen(component: MainComponent) {
onIncrementData = { component.sendIntent(Intent.OnUpdateSelectDate(updateInDays = 1)) },
onDecrementData = { component.sendIntent(Intent.OnUpdateSelectDate(updateInDays = -1)) },
selectedDate = state.selectedDate,
- currentDate = state.currentDate,
+ currentDate = currentDate,
onOpenDateTimePickerModalRequest = {}, // TODO
)
}
diff --git a/clients/tablet/feature/settings/src/commonMain/composeResources/values-ru/strings_ru.xml b/clients/tablet/feature/settings/src/commonMain/composeResources/values-ru/strings_ru.xml
new file mode 100644
index 0000000000000000000000000000000000000000..45c0da64618a195eaad61e644867da1707a63007
--- /dev/null
+++ b/clients/tablet/feature/settings/src/commonMain/composeResources/values-ru/strings_ru.xml
@@ -0,0 +1,6 @@
+
+
+ Выход
+ Переговорки
+ Выбрать ${nameRoom}
+
\ No newline at end of file
diff --git a/clients/tablet/feature/settings/src/commonMain/composeResources/values/strings.xml b/clients/tablet/feature/settings/src/commonMain/composeResources/values/strings.xml
index 45c0da64618a195eaad61e644867da1707a63007..3f37dbb1417e5b787582b954da8ce9ec3decaca8 100644
--- a/clients/tablet/feature/settings/src/commonMain/composeResources/values/strings.xml
+++ b/clients/tablet/feature/settings/src/commonMain/composeResources/values/strings.xml
@@ -1,6 +1,6 @@
- Выход
- Переговорки
- Выбрать ${nameRoom}
+ Exit
+ Meeting Rooms
+ Choose ${nameRoom}
\ No newline at end of file
diff --git a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotComponent.kt b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotComponent.kt
index 74bf5858deb376d9000c387ed8969e75945a49a0..e140a019d38cf18c7964f50c3580678bccc4ed1d 100644
--- a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotComponent.kt
+++ b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotComponent.kt
@@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import kotlinx.datetime.LocalDateTime
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -60,6 +59,24 @@ class SlotComponent(
}
}
+ private fun resetAllMultiSlotStates() {
+ val slots = state.value.slots.toMutableList()
+ var hasChanges = false
+
+ // Find all MultiSlot instances that are open and close them
+ slots.forEachIndexed { index, slot ->
+ if (slot is SlotUi.MultiSlot && slot.isOpen) {
+ slots[index] = slot.copy(isOpen = false)
+ hasChanges = true
+ }
+ }
+
+ // Only update state if there were changes
+ if (hasChanges) {
+ mutableState.update { it.copy(slots = slots) }
+ }
+ }
+
private suspend fun updateSlots(uiSlots: List) = withContext(Dispatchers.Main.immediate) {
if (uiSlots.isNotEmpty()) {
val firstSlotStartInstant = uiSlots.first().slot.start.asInstant
@@ -74,10 +91,8 @@ class SlotComponent(
when (intent) {
is SlotIntent.ClickToEdit -> handleClickToEdit(intent.slot)
is SlotIntent.ClickToToggle -> openMultislot(intent.slot)
- is SlotIntent.Delete -> deleteSlot(intent)
- is SlotIntent.OnCancelDelete -> cancelDeletingSlot(intent)
- is SlotIntent.UpdateDate -> updateDate(intent.newDate)
is SlotIntent.UpdateRequest -> updateRequest(intent)
+ SlotIntent.InactivityTimeout -> resetAllMultiSlotStates()
}
}
@@ -90,83 +105,20 @@ class SlotComponent(
}
private fun updateRequest(intent: SlotIntent.UpdateRequest) = coroutineScope.launch {
- roomInfoUseCase.getRoom(intent.room)?.let { roomInfo ->
- val slots = getSlotsByRoomUseCase(roomInfo = roomInfo)
+ roomInfoUseCase.getRoom(room = intent.room)?.let { roomInfo ->
+ val slots = getSlotsByRoomUseCase(
+ roomInfo = roomInfo,
+ start = maxOf(
+ OfficeTime.startWorkTime(intent.newDate.date).asInstant,
+ currentInstant,
+ ).asLocalDateTime
+ )
val uiSlots = slots.map(slotUiMapper::map)
- mutableState.update { it.copy(slots = uiSlots) }
- }
- }
-
- private fun cancelDeletingSlot(intent: SlotIntent.OnCancelDelete) {
- val slots = state.value.slots
- val original = intent.slot.original
- val newSlots = if (intent.slot.mainSlotIndex == null) {
- slots.toMutableList().apply { this[intent.slot.index] = original }
- } else {
- val mainSlot =
- (slots[intent.slot.mainSlotIndex as Int] as SlotUi.MultiSlot).run {
- copy(
- subSlots = subSlots.toMutableList()
- .apply { this[intent.slot.index] = original }
- )
- }
- slots.toMutableList().apply { this[intent.slot.mainSlotIndex as Int] = mainSlot }
+ // Use updateSlots instead of directly updating state to preserve DeleteSlot and isOpen states
+ updateSlots(uiSlots)
}
- mutableState.update { it.copy(slots = newSlots) }
}
- private fun deleteSlot(intent: SlotIntent.Delete) {
- val slots = state.value.slots
- var mainSlot: SlotUi.MultiSlot? = null
- val uiSlot = slots.firstOrNull { it.slot == intent.slot }
- ?: slots.mapNotNull { (it as? SlotUi.MultiSlot)?.subSlots }.flatten()
- .firstOrNull { it.slot == intent.slot }
- ?.apply {
- mainSlot = slots.mapNotNull { it as? SlotUi.MultiSlot }
- .first { it.subSlots.contains(this) }
- }
- when {
- uiSlot == null -> {}
- mainSlot != null -> {
- val indexInMultiSlot = mainSlot!!.subSlots.indexOf(uiSlot)
- val indexMultiSlot = slots.indexOf(mainSlot)
- val newMainSlot = mainSlot!!.copy(
- subSlots = mainSlot!!.subSlots.toMutableList().apply {
- this[indexInMultiSlot] =
- SlotUi.DeleteSlot(
- slot = intent.slot,
- onDelete = intent.onDelete,
- original = uiSlot,
- index = indexInMultiSlot,
- mainSlotIndex = indexMultiSlot
- )
- })
- mutableState.update {
- it.copy(slots = slots.toMutableList().apply {
- this[indexMultiSlot] = newMainSlot
- })
- }
- }
-
- else -> {
- val index = slots.indexOf(uiSlot)
- mutableState.update {
- it.copy(
- slots = slots.toMutableList().apply {
- this[index] =
- SlotUi.DeleteSlot(
- slot = intent.slot,
- onDelete = intent.onDelete,
- original = uiSlot,
- index = index,
- mainSlotIndex = null
- )
- }
- )
- }
- }
- }
- }
private fun Slot.execute() = when (this) {
is Slot.EmptySlot -> executeFreeSlot(this)
is Slot.EventSlot -> executeEventSlot(this)
@@ -205,20 +157,6 @@ class SlotComponent(
isLoading = false,
)
- private fun updateDate(newDate: LocalDateTime) = coroutineScope.launch {
- roomInfoUseCase.getRoom(room = roomName())?.let { roomInfo ->
- val slots = getSlotsByRoomUseCase(
- roomInfo = roomInfo,
- start = maxOf(
- OfficeTime.startWorkTime(newDate.date).asInstant,
- currentInstant,
- ).asLocalDateTime
- )
- val uiSlots = slots.map(slotUiMapper::map)
- mutableState.update { it.copy(slots = uiSlots) }
- }
- }
-
private fun setupRoomAvailabilityWatcher() {
updateTimer.init(SLOT_UPDATE_INTERVAL_MINUTES) {
withContext(Dispatchers.Main) {
diff --git a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotIntent.kt b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotIntent.kt
index bcb31aeefdfefcf3b2653d350b3fe784c1124523..ec6faa411220f4e825cf467d69f717a5b48ebbfd 100644
--- a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotIntent.kt
+++ b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotIntent.kt
@@ -1,14 +1,11 @@
package band.effective.office.tablet.feature.slot.presentation
-import band.effective.office.tablet.core.domain.model.Slot
import kotlinx.datetime.LocalDateTime
sealed interface SlotIntent {
data class ClickToEdit(val slot: SlotUi) : SlotIntent
data class ClickToToggle(val slot: SlotUi.MultiSlot) : SlotIntent
- data class UpdateDate(val newDate: LocalDateTime) : SlotIntent
- data class UpdateRequest(val room: String) : SlotIntent
- data class Delete(val slot: Slot, val onDelete: () -> Unit) : SlotIntent
- data class OnCancelDelete(val slot: SlotUi.DeleteSlot) : SlotIntent
+ data class UpdateRequest(val room: String, val newDate: LocalDateTime) : SlotIntent
+ data object InactivityTimeout: SlotIntent
}
diff --git a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotUi.kt b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotUi.kt
index e61244349dbe2edcd9618890193fa25285499d26..2270986f32bcd6fb67878b7074126bd1b9905048 100644
--- a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotUi.kt
+++ b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/SlotUi.kt
@@ -9,14 +9,6 @@ sealed interface SlotUi {
data class MultiSlot(override val slot: Slot, val subSlots: List, val isOpen: Boolean) :
SlotUi
- data class DeleteSlot(
- override val slot: Slot,
- val onDelete: () -> Unit,
- val original: SlotUi,
- val index: Int,
- val mainSlotIndex: Int?
- ) : SlotUi
-
data class NestedSlot(override val slot: Slot) : SlotUi
data class LoadingSlot(override val slot: Slot) : SlotUi
}
diff --git a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/BorderIndicator.kt b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/BorderIndicator.kt
deleted file mode 100644
index 80610fc8e296adb2da74dbb39c06049d358e88ac..0000000000000000000000000000000000000000
--- a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/BorderIndicator.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-package band.effective.office.tablet.feature.slot.presentation.components
-
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.CornerRadius
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.RoundRect
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Path
-import androidx.compose.ui.graphics.PathMeasure
-import androidx.compose.ui.graphics.drawscope.Stroke
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import band.effective.office.tablet.core.ui.theme.deleteProgressColor
-import kotlinx.coroutines.delay
-
-//https://stackoverflow.com/questions/75745905/rectangle-border-progress-bar
-@Composable
-fun BorderIndicator(
- modifier: Modifier = Modifier.fillMaxSize(),
- startDurationInSeconds: Int = 10,
- stokeWidth: Dp,
- onDispose: () -> Unit
-) {
- var targetValue by remember {
- mutableStateOf(100f)
- }
- // This is the progress path which wis changed using path measure
- val pathWithProgress by remember { mutableStateOf(Path()) }
-
- // using path
- val pathMeasure by remember { mutableStateOf(PathMeasure()) }
-
- val path = remember { Path() }
-
- val progress by animateFloatAsState(
- targetValue = targetValue,
- animationSpec = tween(startDurationInSeconds * 1000, easing = LinearEasing), label = ""
- )
-
- Box(contentAlignment = Alignment.Center) {
- Canvas(modifier = modifier) {
- if (path.isEmpty) {
- path.addRoundRect(
- RoundRect(
- Rect(offset = Offset.Zero, size),
- cornerRadius = CornerRadius(100.dp.toPx(), 100.dp.toPx())
- )
- )
- }
- pathWithProgress.reset()
- pathMeasure.setPath(path, forceClosed = false)
- pathMeasure.getSegment(
- startDistance = 0f,
- stopDistance = pathMeasure.length * progress / 100f,
- destination = pathWithProgress,
- startWithMoveTo = true
- )
-
- drawPath(
- path = path,
- style = Stroke(stokeWidth.toPx()),
- color = Color.Gray
- )
-
- drawPath(
- path = pathWithProgress,
- style = Stroke(stokeWidth.toPx()),
- color = deleteProgressColor
- )
- }
- }
- LaunchedEffect(Unit) {
- targetValue = 0f
- delay(startDurationInSeconds * 1000L)
- onDispose()
- }
-}
\ No newline at end of file
diff --git a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/CommonSlotView.kt b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/CommonSlotView.kt
index e4d18c9dbe98224351af3fea421202413c9ffbef..15e075052c9ca0409736b4fbc33b516268b8aa6e 100644
--- a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/CommonSlotView.kt
+++ b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/CommonSlotView.kt
@@ -9,9 +9,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
-import band.effective.office.tablet.core.domain.util.toFormattedString
import band.effective.office.tablet.core.ui.theme.h5
import band.effective.office.tablet.core.ui.theme.h7
+import band.effective.office.tablet.core.ui.utils.DateDisplayMapper
import band.effective.office.tablet.feature.slot.presentation.SlotUi
@Composable
@@ -28,7 +28,7 @@ fun CommonSlotView(
) {
Column {
Text(
- text = "${slot.start.toFormattedString("HH:mm")} - ${slot.finish.toFormattedString("HH:mm")}",
+ text = "${DateDisplayMapper.formatTime(slot.start)} — ${DateDisplayMapper.formatTime(slot.finish)}",
style = MaterialTheme.typography.h5,
color = MaterialTheme.colorScheme.onPrimary
)
diff --git a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/DeletedSlotView.kt b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/DeletedSlotView.kt
deleted file mode 100644
index a0c0eb9dd9b4cb0fe325998cff5506a7f5f4da36..0000000000000000000000000000000000000000
--- a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/DeletedSlotView.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-package band.effective.office.tablet.feature.slot.presentation.components
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.IntrinsicSize
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-import band.effective.office.tablet.core.ui.Res
-import band.effective.office.tablet.core.ui.cancel
-import band.effective.office.tablet.core.ui.theme.h7
-import band.effective.office.tablet.feature.slot.presentation.SlotUi
-import org.jetbrains.compose.resources.stringResource
-
-@Composable
-fun DeletedSlotView(
- modifier: Modifier = Modifier,
- slotUi: SlotUi.DeleteSlot,
- onCancel: (SlotUi.DeleteSlot) -> Unit,
- paddingValues: PaddingValues
-) {
- Box(modifier = modifier.height(IntrinsicSize.Min).width(IntrinsicSize.Min)) {
- var isCancelability by remember { mutableStateOf(true) }
- CommonSlotView(
- modifier = Modifier
- .padding(paddingValues)
- .fillMaxWidth(),
- slotUi = slotUi
- ) {
- if (isCancelability) {
- Text(
- modifier = Modifier.clickable { onCancel(slotUi) },
- text = stringResource(Res.string.cancel),
- style = MaterialTheme.typography.h7,
- color = Color.White
- )
- }
- }
- BorderIndicator(onDispose = {
- slotUi.onDelete()
- isCancelability = false
- }, stokeWidth = 10.dp)
- }
-}
diff --git a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/LoadingSlotView.kt b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/LoadingSlotView.kt
index 0e890a7f422b397243798d1d5c3265c182dfbf05..7e7fb4036974f1a00a9ccb430b74a899af974fdf 100644
--- a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/LoadingSlotView.kt
+++ b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/LoadingSlotView.kt
@@ -9,11 +9,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
-import band.effective.office.tablet.core.domain.util.toFormattedString
import band.effective.office.tablet.core.ui.Res
import band.effective.office.tablet.core.ui.loading_slot_for_time
import band.effective.office.tablet.core.ui.theme.h5
import band.effective.office.tablet.core.ui.theme.h7
+import band.effective.office.tablet.core.ui.utils.DateDisplayMapper
import band.effective.office.tablet.feature.slot.presentation.SlotUi
import org.jetbrains.compose.resources.stringResource
@@ -33,8 +33,8 @@ fun LoadingSlotView(
Text(
text = stringResource(
Res.string.loading_slot_for_time,
- slot.start.toFormattedString("HH:mm"),
- slot.finish.toFormattedString("HH:mm")
+ DateDisplayMapper.formatTime(slot.start),
+ DateDisplayMapper.formatTime(slot.finish)
),
style = MaterialTheme.typography.h5,
color = MaterialTheme.colorScheme.onPrimary
diff --git a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/MultiSlotView.kt b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/MultiSlotView.kt
index d522e9f663c6e115d2a7354ea3903d9b9daf5efa..0fe0e9153f67ec1d46598cc0e093744ba2ab2412 100644
--- a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/MultiSlotView.kt
+++ b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/MultiSlotView.kt
@@ -2,7 +2,6 @@ package band.effective.office.tablet.feature.slot.presentation.components
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
@@ -21,19 +20,16 @@ fun MultiSlotView(
modifier: Modifier = Modifier,
slotUi: SlotUi.MultiSlot,
onItemClick: SlotUi.() -> Unit,
- onToggle: SlotUi.() -> Unit,
- onCancel: (SlotUi.DeleteSlot) -> Unit
) {
Column(Modifier.animateContentSize()) {
CommonSlotView(
- modifier = modifier.clickable { onToggle(slotUi) },
+ modifier = modifier,
slotUi = slotUi
) {
Image(
modifier = Modifier
.fillMaxHeight()
- .rotate(if (slotUi.isOpen) 180f else 0f)
- .clickable { onToggle(slotUi) },
+ .rotate(if (slotUi.isOpen) 180f else 0f),
painter = painterResource(Res.drawable.arrow_to_down),
contentDescription = null
)
@@ -45,8 +41,7 @@ fun MultiSlotView(
SlotView(
slotUi = it,
onClick = onItemClick,
- onToggle = onToggle,
- onCancel = onCancel
+ onToggle = { /*Nothing*/ },
)
}
}
diff --git a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/SlotView.kt b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/SlotView.kt
index e96e33cc1b0e5515c8d4d85fae9a85707294444c..4c1b0dec435acb1eb6ba380ce6f1cefa50143236 100644
--- a/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/SlotView.kt
+++ b/clients/tablet/feature/slot/src/commonMain/kotlin/band/effective/office/tablet/feature/slot/presentation/components/SlotView.kt
@@ -3,7 +3,6 @@ package band.effective.office.tablet.feature.slot.presentation.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
@@ -11,7 +10,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import band.effective.office.tablet.core.domain.model.Slot
import band.effective.office.tablet.core.domain.util.freeTime
@@ -30,44 +28,31 @@ fun SlotView(
slotUi: SlotUi,
onClick: SlotUi.() -> Unit,
onToggle: SlotUi.() -> Unit,
- onCancel: (SlotUi.DeleteSlot) -> Unit
) {
val borderShape = CircleShape
- val baseModifier = Modifier
+
+ val itemModifier = Modifier
.fillMaxWidth()
.clip(borderShape)
- .background(MaterialTheme.colorScheme.surface)
-
- val itemModifier = baseModifier
- .run {
- when (slotUi) {
- is SlotUi.MultiSlot -> this
- else -> clickable { slotUi.onClick() }
- }
- }
.then(
- if (slotUi !is SlotUi.DeleteSlot) Modifier.border(
- width = 5.dp,
- color = slotUi.borderColor(),
- shape = borderShape
- ).padding(vertical = 15.dp, horizontal = 30.dp)
- else Modifier
+ if (slotUi is SlotUi.MultiSlot)
+ Modifier.clickable { slotUi.onToggle() }
+ else
+ Modifier.clickable { slotUi.onClick() }
)
+ .background(MaterialTheme.colorScheme.surface)
+ .border(
+ width = 5.dp,
+ color = slotUi.borderColor(),
+ shape = borderShape
+ ).padding(vertical = 15.dp, horizontal = 30.dp)
when (slotUi) {
- is SlotUi.DeleteSlot -> DeletedSlotView(
- modifier = itemModifier,
- slotUi = slotUi,
- onCancel = onCancel,
- paddingValues = PaddingValues(vertical = 15.dp, horizontal = 30.dp)
- )
is SlotUi.MultiSlot -> MultiSlotView(
modifier = itemModifier,
slotUi = slotUi,
onItemClick = onClick,
- onToggle = onToggle,
- onCancel = onCancel
)
is SlotUi.SimpleSlot -> CommonSlotView(
@@ -89,7 +74,6 @@ fun SlotView(
@Composable
private fun SlotUi.borderColor() = when (this) {
- is SlotUi.DeleteSlot -> Color.Gray
is SlotUi.MultiSlot -> LocalCustomColorsPalette.current.busyStatus
is SlotUi.NestedSlot -> subslotColor
is SlotUi.SimpleSlot -> when (slot) {
diff --git a/gradle.properties b/gradle.properties
index cfac532387f9fee05f205ef648a58e582b1c81f5..7f2c12bb8c089134868c061f1060fc1019c3f089 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -9,6 +9,6 @@ kotlin.incremental=true
# Project properties
group=band.effective.office
-version=0.0.2
+version=1.0.0
android.useAndroidX=true
diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist
index 11845e1da7e0b58bbd6e684ba65da1e31ec829ae..bb19ebf6a4a3338d7cf1566b32de08fa60bf4a9d 100644
--- a/iosApp/iosApp/Info.plist
+++ b/iosApp/iosApp/Info.plist
@@ -4,5 +4,12 @@
CADisableMinimumFrameDurationOnPhone
+ CFBundleDevelopmentRegion
+ en
+ CFBundleLocalizations
+
+ en
+ ru
+
diff --git a/media/tablet/demo-tablet.gif b/media/tablet/demo-tablet.gif
index e6afa21c6e39706073a51847c0126be983877a52..4255f2a26ee1eff70b49f222d297a49e8c4b352a 100644
Binary files a/media/tablet/demo-tablet.gif and b/media/tablet/demo-tablet.gif differ