Проект пересобран без модуля desktopMain
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M19,2h-4.18C14.4,0.84 13.3,0 12,0c-1.3,0 -2.4,0.84 -2.82,2L5,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,4c0,-1.1 -0.9,-2 -2,-2zM12,2c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM19,20L5,20L5,4h2v3h10L17,4h2v16z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M2,15.5v2h20v-2L2,15.5zM2,10.5v2h20v-2L2,10.5zM2,5.5v2h20v-2L2,5.5z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M11,7h2v2h-2V7zM11,11h2v6h-2V11zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8s8,3.59 8,8S16.41,20 12,20z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M10,8m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"/>
|
||||
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M10.67,13.02C10.45,13.01 10.23,13 10,13c-2.42,0 -4.68,0.67 -6.61,1.82C2.51,15.34 2,16.32 2,17.35V20h9.26C10.47,18.87 10,17.49 10,16C10,14.93 10.25,13.93 10.67,13.02z"/>
|
||||
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M20.75,16c0,-0.22 -0.03,-0.42 -0.06,-0.63l1.14,-1.01l-1,-1.73l-1.45,0.49c-0.32,-0.27 -0.68,-0.48 -1.08,-0.63L18,11h-2l-0.3,1.49c-0.4,0.15 -0.76,0.36 -1.08,0.63l-1.45,-0.49l-1,1.73l1.14,1.01c-0.03,0.21 -0.06,0.41 -0.06,0.63s0.03,0.42 0.06,0.63l-1.14,1.01l1,1.73l1.45,-0.49c0.32,0.27 0.68,0.48 1.08,0.63L16,21h2l0.3,-1.49c0.4,-0.15 0.76,-0.36 1.08,-0.63l1.45,0.49l1,-1.73l-1.14,-1.01C20.72,16.42 20.75,16.22 20.75,16zM17,18c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2s2,0.9 2,2S18.1,18 17,18z"/>
|
||||
|
||||
</vector>
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M7.58,4.08L6.15,2.65C3.75,4.48 2.17,7.3 2.03,10.5h2c0.15,-2.65 1.51,-4.97 3.55,-6.42zM19.97,10.5h2c-0.15,-3.2 -1.73,-6.02 -4.12,-7.85l-1.42,1.43c2.02,1.45 3.39,3.77 3.54,6.42zM18,11c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2v-5zM12,22c0.14,0 0.27,-0.01 0.4,-0.04 0.65,-0.14 1.18,-0.58 1.44,-1.18 0.1,-0.24 0.15,-0.5 0.15,-0.78h-4c0.01,1.1 0.9,2 2.01,2z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M13,8c0,-2.21 -1.79,-4 -4,-4S5,5.79 5,8s1.79,4 4,4S13,10.21 13,8zM15,10v2h3v3h2v-3h3v-2h-3V7h-2v3H15zM1,18v2h16v-2c0,-2.66 -5.33,-4 -8,-4S1,15.34 1,18z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.95,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#ffffff" android:pathData="M440,520L200,520L200,440L440,440L440,200L520,200L520,440L760,440L760,520L520,520L520,760L440,760L440,520Z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#ffffff" android:pathData="M480,640L280,440L336,382L440,486L440,160L520,160L520,486L624,382L680,440L480,640ZM240,800Q207,800 183.5,776.5Q160,753 160,720L160,600L240,600L240,720Q240,720 240,720Q240,720 240,720L720,720Q720,720 720,720Q720,720 720,720L720,600L800,600L800,720Q800,753 776.5,776.5Q753,800 720,800L240,800Z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#ffffff" android:pathData="M11.71,15.29l2.59,-2.59c0.39,-0.39 0.39,-1.02 0,-1.41L11.71,8.7c-0.63,-0.62 -1.71,-0.18 -1.71,0.71v5.17c0,0.9 1.08,1.34 1.71,0.71z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#ffffff" android:pathData="M9,16.17L5.53,12.7c-0.39,-0.39 -1.02,-0.39 -1.41,0 -0.39,0.39 -0.39,1.02 0,1.41l4.18,4.18c0.39,0.39 1.02,0.39 1.41,0L20.29,7.71c0.39,-0.39 0.39,-1.02 0,-1.41 -0.39,-0.39 -1.02,-0.39 -1.41,0L9,16.17z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#ffffff" android:pathData="M18.3,5.71c-0.39,-0.39 -1.02,-0.39 -1.41,0L12,10.59 7.11,5.7c-0.39,-0.39 -1.02,-0.39 -1.41,0 -0.39,0.39 -0.39,1.02 0,1.41L10.59,12 5.7,16.89c-0.39,0.39 -0.39,1.02 0,1.41 0.39,0.39 1.02,0.39 1.41,0L12,13.41l4.89,4.89c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L13.41,12l4.89,-4.89c0.38,-0.38 0.38,-1.02 0,-1.4z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#ffffff" android:pathData="M15,20H5V7c0,-0.55 -0.45,-1 -1,-1h0C3.45,6 3,6.45 3,7v13c0,1.1 0.9,2 2,2h10c0.55,0 1,-0.45 1,-1v0C16,20.45 15.55,20 15,20zM20,16V4c0,-1.1 -0.9,-2 -2,-2H9C7.9,2 7,2.9 7,4v12c0,1.1 0.9,2 2,2h9C19.1,18 20,17.1 20,16zM18,16H9V4h9V16z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#ffffff" android:pathData="M10.67,13.02C10.45,13.01 10.23,13 10,13c-2.42,0 -4.68,0.67 -6.61,1.82C2.51,15.34 2,16.32 2,17.35V19c0,0.55 0.45,1 1,1h8.26C10.47,18.87 10,17.49 10,16C10,14.93 10.25,13.93 10.67,13.02z"/>
|
||||
|
||||
<path android:fillColor="#ffffff" android:pathData="M10,8m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"/>
|
||||
|
||||
<path android:fillColor="#ffffff" android:pathData="M20.75,16c0,-0.22 -0.03,-0.42 -0.06,-0.63l0.84,-0.73c0.18,-0.16 0.22,-0.42 0.1,-0.63l-0.59,-1.02c-0.12,-0.21 -0.37,-0.3 -0.59,-0.22l-1.06,0.36c-0.32,-0.27 -0.68,-0.48 -1.08,-0.63l-0.22,-1.09c-0.05,-0.23 -0.25,-0.4 -0.49,-0.4h-1.18c-0.24,0 -0.44,0.17 -0.49,0.4l-0.22,1.09c-0.4,0.15 -0.76,0.36 -1.08,0.63l-1.06,-0.36c-0.23,-0.08 -0.47,0.02 -0.59,0.22l-0.59,1.02c-0.12,0.21 -0.08,0.47 0.1,0.63l0.84,0.73c-0.03,0.21 -0.06,0.41 -0.06,0.63s0.03,0.42 0.06,0.63l-0.84,0.73c-0.18,0.16 -0.22,0.42 -0.1,0.63l0.59,1.02c0.12,0.21 0.37,0.3 0.59,0.22l1.06,-0.36c0.32,0.27 0.68,0.48 1.08,0.63l0.22,1.09c0.05,0.23 0.25,0.4 0.49,0.4h1.18c0.24,0 0.44,-0.17 0.49,-0.4l0.22,-1.09c0.4,-0.15 0.76,-0.36 1.08,-0.63l1.06,0.36c0.23,0.08 0.47,-0.02 0.59,-0.22l0.59,-1.02c0.12,-0.21 0.08,-0.47 -0.1,-0.63l-0.84,-0.73C20.72,16.42 20.75,16.22 20.75,16zM17,18c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2s2,0.9 2,2S18.1,18 17,18z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#ffffff" android:pathData="M19.5,12c0,-0.23 -0.01,-0.45 -0.03,-0.68l1.86,-1.41c0.4,-0.3 0.51,-0.86 0.26,-1.3l-1.87,-3.23c-0.25,-0.44 -0.79,-0.62 -1.25,-0.42l-2.15,0.91c-0.37,-0.26 -0.76,-0.49 -1.17,-0.68l-0.29,-2.31C14.8,2.38 14.37,2 13.87,2h-3.73C9.63,2 9.2,2.38 9.14,2.88L8.85,5.19c-0.41,0.19 -0.8,0.42 -1.17,0.68L5.53,4.96c-0.46,-0.2 -1,-0.02 -1.25,0.42L2.41,8.62c-0.25,0.44 -0.14,0.99 0.26,1.3l1.86,1.41C4.51,11.55 4.5,11.77 4.5,12s0.01,0.45 0.03,0.68l-1.86,1.41c-0.4,0.3 -0.51,0.86 -0.26,1.3l1.87,3.23c0.25,0.44 0.79,0.62 1.25,0.42l2.15,-0.91c0.37,0.26 0.76,0.49 1.17,0.68l0.29,2.31C9.2,21.62 9.63,22 10.13,22h3.73c0.5,0 0.93,-0.38 0.99,-0.88l0.29,-2.31c0.41,-0.19 0.8,-0.42 1.17,-0.68l2.15,0.91c0.46,0.2 1,0.02 1.25,-0.42l1.87,-3.23c0.25,-0.44 0.14,-0.99 -0.26,-1.3l-1.86,-1.41C19.49,12.45 19.5,12.23 19.5,12zM12.04,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5s3.5,1.57 3.5,3.5S13.97,15.5 12.04,15.5z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M440,520L440,520Q440,520 440,520Q440,520 440,520L440,520L440,520Q440,520 440,520Q440,520 440,520L440,520L440,520Q440,520 440,520Q440,520 440,520L440,520Q440,520 440,520Q440,520 440,520L440,520Q440,520 440,520Q440,520 440,520L440,520L440,520ZM120,840Q87,840 63.5,816.5Q40,793 40,760L40,280Q40,247 63.5,223.5Q87,200 120,200L246,200L296,146Q307,134 322.5,127Q338,120 355,120L520,120Q537,120 548.5,131.5Q560,143 560,160L560,160Q560,177 548.5,188.5Q537,200 520,200L355,200L282,280L120,280Q120,280 120,280Q120,280 120,280L120,760Q120,760 120,760Q120,760 120,760L760,760Q760,760 760,760Q760,760 760,760L760,440Q760,423 771.5,411.5Q783,400 800,400L800,400Q817,400 828.5,411.5Q840,423 840,440L840,760Q840,793 816.5,816.5Q793,840 760,840L120,840ZM760,200L720,200Q703,200 691.5,188.5Q680,177 680,160Q680,143 691.5,131.5Q703,120 720,120L760,120L760,80Q760,63 771.5,51.5Q783,40 800,40Q817,40 828.5,51.5Q840,63 840,80L840,120L880,120Q897,120 908.5,131.5Q920,143 920,160Q920,177 908.5,188.5Q897,200 880,200L840,200L840,240Q840,257 828.5,268.5Q817,280 800,280Q783,280 771.5,268.5Q760,257 760,240L760,200ZM440,700Q515,700 567.5,647.5Q620,595 620,520Q620,445 567.5,392.5Q515,340 440,340Q365,340 312.5,392.5Q260,445 260,520Q260,595 312.5,647.5Q365,700 440,700ZM440,620Q398,620 369,591Q340,562 340,520Q340,478 369,449Q398,420 440,420Q482,420 511,449Q540,478 540,520Q540,562 511,591Q482,620 440,620Z"/>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,330 @@
|
||||
package ru.cit71.pchelovod71
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
import frontend.composeapp.generated.resources.Res
|
||||
import frontend.composeapp.generated.resources.baseline_content_paste_24
|
||||
import frontend.composeapp.generated.resources.baseline_person_24
|
||||
import frontend.composeapp.generated.resources.baseline_person_add_alt_1_24
|
||||
import frontend.composeapp.generated.resources.round_settings_24
|
||||
import org.jetbrains.compose.resources.DrawableResource
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import ru.cit71.pchelovod71.core.Api
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import ru.cit71.pchelovod71.core.Platform
|
||||
import ru.cit71.pchelovod71.ui.components.MainScaffold
|
||||
import ru.cit71.pchelovod71.ui.screens.RegistrationRequestsScreen
|
||||
import ru.cit71.pchelovod71.ui.screens.RegistrationUserScreen
|
||||
import ru.cit71.pchelovod71.ui.screens.RejectRegistrationUserScreen
|
||||
import ru.cit71.pchelovod71.ui.screens.SettingsScreen
|
||||
import ru.cit71.pchelovod71.ui.screens.UserRequestsScreen
|
||||
import ru.cit71.pchelovod71.ui.screens.UsersScreen
|
||||
import ru.cit71.pchelovod71.ui.screens.WaitRegistrationUserScreen
|
||||
|
||||
interface Screen {
|
||||
val route: String
|
||||
val title: String
|
||||
val icon: DrawableResource
|
||||
}
|
||||
|
||||
enum class AdminScreen(
|
||||
override val route: String,
|
||||
override val title: String,
|
||||
override val icon: DrawableResource,
|
||||
) : Screen {
|
||||
RegistrationRequests(
|
||||
route = "registration-requests",
|
||||
title = "Запросы на регистрацию",
|
||||
icon = Res.drawable.baseline_person_add_alt_1_24,
|
||||
),
|
||||
Users(
|
||||
route = "users",
|
||||
title = "Пользователи",
|
||||
icon = Res.drawable.baseline_person_24,
|
||||
),
|
||||
UserRequests(
|
||||
route = "user-requests",
|
||||
title = "Обращения пользователей",
|
||||
icon = Res.drawable.baseline_content_paste_24,
|
||||
),
|
||||
}
|
||||
|
||||
enum class UserScreen(
|
||||
override val route: String,
|
||||
override val title: String,
|
||||
override val icon: DrawableResource,
|
||||
) : Screen {
|
||||
MyRequests(
|
||||
route = "my-requests",
|
||||
title = "Мои обращения",
|
||||
icon = Res.drawable.baseline_content_paste_24,
|
||||
),
|
||||
Settings(
|
||||
route = "settings",
|
||||
title = "Настройки",
|
||||
icon = Res.drawable.round_settings_24,
|
||||
),
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AdminApp(
|
||||
paddingValues: PaddingValues,
|
||||
userDomain: Models.User.Domain,
|
||||
) {
|
||||
val navHostController = rememberNavController()
|
||||
|
||||
MainScaffold(
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
val navBackStackEntry = navHostController.currentBackStackEntryAsState()
|
||||
|
||||
AdminScreen.entries.forEach { screen ->
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(screen.icon),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = screen.title,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
},
|
||||
selected = navBackStackEntry.value?.destination?.hierarchy?.any {
|
||||
it.route == screen.route
|
||||
} == true,
|
||||
onClick = {
|
||||
navHostController.navigate(screen.route)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(paddingValues),
|
||||
) { mainScaffoldParams ->
|
||||
NavHost(
|
||||
navController = navHostController,
|
||||
startDestination = AdminScreen.UserRequests.route,
|
||||
) {
|
||||
composable(AdminScreen.RegistrationRequests.route) {
|
||||
RegistrationRequestsScreen(
|
||||
mainScaffoldParams = mainScaffoldParams,
|
||||
)
|
||||
}
|
||||
|
||||
composable(AdminScreen.Users.route) {
|
||||
UsersScreen(
|
||||
mainScaffoldParams = mainScaffoldParams,
|
||||
)
|
||||
}
|
||||
|
||||
composable(AdminScreen.UserRequests.route) {
|
||||
UserRequestsScreen(
|
||||
mainScaffoldParams = mainScaffoldParams,
|
||||
userDomain = userDomain,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserApp(
|
||||
paddingValues: PaddingValues,
|
||||
userDomain: Models.User.Domain,
|
||||
) {
|
||||
val navHostController = rememberNavController()
|
||||
|
||||
MainScaffold(
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
val navBackStackEntry = navHostController.currentBackStackEntryAsState()
|
||||
|
||||
UserScreen.entries.forEach { screen ->
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(screen.icon),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = screen.title,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
},
|
||||
selected = navBackStackEntry.value?.destination?.hierarchy?.any {
|
||||
it.route == screen.route
|
||||
} == true,
|
||||
onClick = {
|
||||
navHostController.navigate(screen.route)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(paddingValues),
|
||||
) { mainScaffoldParams ->
|
||||
NavHost(
|
||||
navController = navHostController,
|
||||
startDestination = UserScreen.MyRequests.route,
|
||||
) {
|
||||
composable(UserScreen.MyRequests.route) {
|
||||
UserRequestsScreen(
|
||||
mainScaffoldParams = mainScaffoldParams,
|
||||
userDomain = userDomain,
|
||||
)
|
||||
}
|
||||
|
||||
composable(UserScreen.Settings.route) {
|
||||
SettingsScreen(
|
||||
mainScaffoldParams = mainScaffoldParams,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun App() {
|
||||
var userDomain by remember { mutableStateOf<Models.User.Domain?>(null) }
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = darkColorScheme(),
|
||||
) {
|
||||
MainScaffold { mainScaffoldParams ->
|
||||
suspend fun reloadUserDomain() {
|
||||
Api.Users.auth(
|
||||
authParams = Models.AuthParams.DTO.Send(
|
||||
initData = Api.telegramInitData,
|
||||
),
|
||||
onSuccess = { user ->
|
||||
userDomain = user.toDomain()
|
||||
},
|
||||
onError = {},
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.telegramInitData = Platform.getTelegramInitData()
|
||||
reloadUserDomain()
|
||||
}
|
||||
}
|
||||
|
||||
when (userDomain) {
|
||||
null -> {
|
||||
var telegramId by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
suspend fun reloadTelegramId() {
|
||||
Api.Users.getTelegramId(
|
||||
authParams = Models.AuthParams.DTO.Send(
|
||||
initData = Platform.getTelegramInitData(),
|
||||
),
|
||||
onSuccess = {
|
||||
telegramId = it.telegramId
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось авторизоваться с помощью учетной записи Telegram"
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
when (val bindUuid = Platform.getBindUuid()) {
|
||||
null -> {
|
||||
reloadTelegramId()
|
||||
}
|
||||
else -> {
|
||||
Api.Users.bind(
|
||||
uuid = bindUuid,
|
||||
onSuccess = {
|
||||
reloadUserDomain()
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Учетная запись Telegram успешно привязана"
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
reloadTelegramId()
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось привязать учетную запись Telegram"
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
telegramId?.let {
|
||||
RegistrationUserScreen(
|
||||
mainScaffoldParams = mainScaffoldParams,
|
||||
telegramId = it,
|
||||
reloadUserDomain = ::reloadUserDomain,
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
userDomain?.let { userDomain ->
|
||||
when (userDomain.deleted) {
|
||||
true -> {
|
||||
RejectRegistrationUserScreen(
|
||||
mainScaffoldParams = mainScaffoldParams,
|
||||
)
|
||||
}
|
||||
false -> {
|
||||
when (userDomain.accepted) {
|
||||
true -> {
|
||||
when (userDomain.role) {
|
||||
Models.Role.Admin -> {
|
||||
AdminApp(
|
||||
paddingValues = mainScaffoldParams.paddingValues,
|
||||
userDomain = userDomain,
|
||||
)
|
||||
}
|
||||
Models.Role.User -> {
|
||||
UserApp(
|
||||
paddingValues = mainScaffoldParams.paddingValues,
|
||||
userDomain = userDomain,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
false -> {
|
||||
WaitRegistrationUserScreen(
|
||||
mainScaffoldParams = mainScaffoldParams,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
package ru.cit71.pchelovod71.core
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.DefaultRequest
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.logging.LogLevel
|
||||
import io.ktor.client.plugins.logging.Logger
|
||||
import io.ktor.client.plugins.logging.Logging
|
||||
import io.ktor.client.request.delete
|
||||
import io.ktor.client.request.forms.MultiPartFormDataContent
|
||||
import io.ktor.client.request.forms.formData
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.patch
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.Headers
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.clone
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import ru.cit71.pchelovod71.core.Platform.base64Encode
|
||||
|
||||
object Api {
|
||||
lateinit var telegramInitData: String
|
||||
|
||||
private val httpClient = HttpClient {
|
||||
install(ContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
|
||||
install(DefaultRequest) {
|
||||
header("Authorization", "Bearer ${base64Encode(telegramInitData)}")
|
||||
}
|
||||
|
||||
if (Config.DEBUG)
|
||||
install(Logging) {
|
||||
logger = object : Logger {
|
||||
override fun log(message: String) {
|
||||
println(message)
|
||||
}
|
||||
}
|
||||
level = LogLevel.ALL
|
||||
}
|
||||
}
|
||||
|
||||
private val apiUrlBuilder = Config.apiUrlBuilder
|
||||
|
||||
object Users {
|
||||
suspend fun getTelegramId(
|
||||
authParams: Models.AuthParams.DTO.Send,
|
||||
onSuccess: suspend (Models.AuthParams.DTO.Get) -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("auth", "telegram")
|
||||
}.build()
|
||||
val response = httpClient.post(url) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(authParams)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess(response.body())
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun auth(
|
||||
authParams: Models.AuthParams.DTO.Send,
|
||||
onSuccess: suspend (Models.User.DTO.Get) -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("auth", "user")
|
||||
}.build()
|
||||
val response = httpClient.post(url) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(authParams)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess(response.body())
|
||||
}
|
||||
HttpStatusCode.Unauthorized -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun get(
|
||||
verified: Boolean? = null,
|
||||
deleted: Boolean? = null,
|
||||
onSuccess: suspend (List<Models.User.DTO.Get>) -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("users", "")
|
||||
verified?.let {
|
||||
parameters.append("verified", "$it")
|
||||
}
|
||||
deleted?.let {
|
||||
parameters.append("deleted_status", "$it")
|
||||
}
|
||||
}.build()
|
||||
val response = httpClient.get(url)
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess(response.body())
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
user: Models.User.DTO.Create,
|
||||
onSuccess: suspend (Models.User.DTO.Get) -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("users", "")
|
||||
}.build()
|
||||
val response = httpClient.post(url) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(user)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess(response.body())
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun update(
|
||||
user: Models.User.DTO.Update,
|
||||
onSuccess: suspend () -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("users", "${user.id}")
|
||||
}.build()
|
||||
val response = httpClient.patch(url) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(user)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess()
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun bind(
|
||||
uuid: String,
|
||||
onSuccess: suspend () -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("users", "binding", "")
|
||||
parameters["user_uuid"] = uuid
|
||||
}.build()
|
||||
val response = httpClient.patch(url)
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess()
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun subscribe(
|
||||
id: Int,
|
||||
user: Models.User.DTO.Subscribe,
|
||||
onSuccess: suspend () -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("users", "$id")
|
||||
}.build()
|
||||
val response = httpClient.patch(url) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(user)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess()
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun accept(
|
||||
id: Int,
|
||||
user: Models.User.DTO.Accept,
|
||||
onSuccess: suspend () -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("users", "$id")
|
||||
}.build()
|
||||
val response = httpClient.patch(url) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(user)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess()
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun reject(
|
||||
id: Int,
|
||||
user: Models.User.DTO.Reject,
|
||||
onSuccess: suspend () -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("users", "delete", "$id")
|
||||
}.build()
|
||||
val response = httpClient.delete(url) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(user)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess()
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun delete(
|
||||
id: Int,
|
||||
onSuccess: suspend () -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("users", "delete", "$id")
|
||||
}.build()
|
||||
val response = httpClient.delete(url)
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess()
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object UserRequests {
|
||||
suspend fun get(
|
||||
userId: Int? = null,
|
||||
status: Models.RequestStatus,
|
||||
onSuccess: suspend (List<Models.UserRequest.DTO.Get>) -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("requests")
|
||||
userId?.let {
|
||||
parameters.append("user_id", "$it")
|
||||
}
|
||||
parameters.append("status", status.value)
|
||||
}.build()
|
||||
val response = httpClient.get(url)
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess(response.body())
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
userRequest: Models.UserRequest.DTO.Create,
|
||||
onSuccess: suspend () -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += "requests"
|
||||
}.build()
|
||||
val response = httpClient.post(url) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(userRequest)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess()
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun accept(
|
||||
id: Int,
|
||||
request: Models.UserRequest.DTO.Accept,
|
||||
onSuccess: suspend () -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("requests", "$id")
|
||||
}.build()
|
||||
val response = httpClient.patch(url) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess()
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun uploadImage(
|
||||
type: String,
|
||||
data: ByteArray,
|
||||
onSuccess: suspend (Models.Image.DTO.Get) -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("requests", "upload_photo")
|
||||
}.build()
|
||||
val response = httpClient.post(url) {
|
||||
setBody(
|
||||
MultiPartFormDataContent(
|
||||
formData {
|
||||
append(
|
||||
key = "file",
|
||||
value = data,
|
||||
headers = Headers.build {
|
||||
append(HttpHeaders.ContentType, type)
|
||||
append(HttpHeaders.ContentDisposition, "form-data; name=\"file\"; filename=\"image\"")
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess(response.body())
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Municipalities {
|
||||
suspend fun get(
|
||||
onSuccess: suspend (List<Models.Municipality.DTO.Get>) -> Unit,
|
||||
onError: suspend () -> Unit,
|
||||
) {
|
||||
val url = apiUrlBuilder.clone().apply {
|
||||
pathSegments += listOf("districts", "")
|
||||
}.build()
|
||||
val response = httpClient.get(url)
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
onSuccess(response.body())
|
||||
}
|
||||
else -> {
|
||||
onError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package ru.cit71.pchelovod71.core
|
||||
|
||||
import io.ktor.http.URLBuilder
|
||||
import io.ktor.http.URLProtocol
|
||||
|
||||
data object Config {
|
||||
const val DEBUG = false
|
||||
private const val TELEGRAM_INIT_DATA = "YOUR-TELEGRAM-INIT-DATA"
|
||||
|
||||
val botUrlBuilder = URLBuilder(
|
||||
protocol = URLProtocol.HTTPS,
|
||||
host = "t.me",
|
||||
pathSegments = listOf("pchelovod71_bot"),
|
||||
)
|
||||
|
||||
val apiUrlBuilder = when (DEBUG) {
|
||||
true -> {
|
||||
URLBuilder(
|
||||
protocol = URLProtocol.HTTP,
|
||||
host = "localhost",
|
||||
port = 5500,
|
||||
pathSegments = listOf("api"),
|
||||
)
|
||||
}
|
||||
false -> {
|
||||
URLBuilder(
|
||||
protocol = URLProtocol.HTTPS,
|
||||
host = "pchelovod71.tularegion.ru",
|
||||
pathSegments = listOf("api"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val telegramInitData = when (DEBUG) {
|
||||
true -> TELEGRAM_INIT_DATA
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
package ru.cit71.pchelovod71.core
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
object Models {
|
||||
enum class Module {
|
||||
DesktopMain,
|
||||
WasmJsMain,
|
||||
}
|
||||
|
||||
enum class Role {
|
||||
Admin,
|
||||
User,
|
||||
}
|
||||
|
||||
object AuthParams {
|
||||
object DTO {
|
||||
@Serializable
|
||||
data class Get(
|
||||
@SerialName("telegram_id") val telegramId: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Send(
|
||||
@SerialName("init_data") val initData: String,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object User {
|
||||
data class Domain(
|
||||
val id: Int = 0,
|
||||
val role: Role = Role.User,
|
||||
val fullName: String = "",
|
||||
val organization: String = "",
|
||||
val phoneNumber: String = "",
|
||||
val telegramId: String? = null,
|
||||
val municipality: Municipality.Domain,
|
||||
val accepted: Boolean = false,
|
||||
val subscribed: Boolean = false,
|
||||
val deleted: Boolean = false,
|
||||
val deleteReason: String? = null,
|
||||
val uuid: String = "",
|
||||
) {
|
||||
fun toCreateDTO(): DTO.Create {
|
||||
return DTO.Create(
|
||||
fullName = fullName,
|
||||
organization = organization,
|
||||
phoneNumber = phoneNumber,
|
||||
telegramId = telegramId,
|
||||
municipality = municipality.toGetDTO(),
|
||||
accepted = accepted,
|
||||
)
|
||||
}
|
||||
|
||||
fun toUpdateDTO(): DTO.Update {
|
||||
return DTO.Update(
|
||||
id = id,
|
||||
fullName = fullName,
|
||||
organization = organization,
|
||||
phoneNumber = phoneNumber,
|
||||
telegramId = telegramId,
|
||||
municipality = municipality.toGetDTO(),
|
||||
accepted = accepted,
|
||||
subscribed = subscribed,
|
||||
)
|
||||
}
|
||||
|
||||
fun toAcceptDTO(): DTO.Accept {
|
||||
return DTO.Accept(
|
||||
fullName = fullName,
|
||||
organization = organization,
|
||||
phoneNumber = phoneNumber,
|
||||
telegramId = telegramId,
|
||||
municipality = municipality.toGetDTO(),
|
||||
accepted = true,
|
||||
)
|
||||
}
|
||||
|
||||
fun toSubscribeDTO(): DTO.Subscribe {
|
||||
return DTO.Subscribe(
|
||||
subscribed = subscribed,
|
||||
)
|
||||
}
|
||||
|
||||
fun toRejectDTO(): DTO.Reject {
|
||||
return DTO.Reject(
|
||||
deleteReason = deleteReason!!,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data object DTO {
|
||||
@Serializable
|
||||
data class Get(
|
||||
@SerialName("id") val id: Int,
|
||||
@SerialName("role_id") val roleId: Int = 2, // TODO: вынужденный костыль, т. к. при создании пользователя в returning возвращается модель отличная от стандартной модели User.DTO.Get
|
||||
@SerialName("fio") val fullName: String,
|
||||
@SerialName("organization") val organization: String,
|
||||
@SerialName("phone_number") val phoneNumber: String,
|
||||
@SerialName("telegram_id") val telegramId: String?,
|
||||
@SerialName("district") val municipality: Municipality.DTO.Get,
|
||||
@SerialName("verified") val accepted: Boolean,
|
||||
@SerialName("processing_alert_status") val subscribed: Boolean,
|
||||
@SerialName("deleted_status") val deleted: Boolean,
|
||||
@SerialName("deleted_comment") val deleteReason: String?,
|
||||
@SerialName("user_uuid") val uuid: String,
|
||||
) {
|
||||
fun toDomain(): Domain {
|
||||
return Domain(
|
||||
id = id,
|
||||
role = when (roleId) {
|
||||
1 -> Role.Admin
|
||||
else -> Role.User
|
||||
},
|
||||
fullName = fullName,
|
||||
organization = organization,
|
||||
phoneNumber = phoneNumber,
|
||||
telegramId = telegramId,
|
||||
municipality = municipality.toDomain(),
|
||||
accepted = accepted,
|
||||
subscribed = subscribed,
|
||||
deleted = deleted,
|
||||
deleteReason = deleteReason,
|
||||
uuid = uuid,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Create(
|
||||
@SerialName("fio") val fullName: String,
|
||||
@SerialName("organization") val organization: String,
|
||||
@SerialName("phone_number") val phoneNumber: String,
|
||||
@SerialName("telegram_id") val telegramId: String?,
|
||||
@SerialName("district") val municipality: Municipality.DTO.Get,
|
||||
@SerialName("verified") val accepted: Boolean = false, // FIXME: зачем принимает verified? Любой идиот может отправить на API null значение и фронт упадет, т. к. полагается на то, что это не nullable значение (пост. Иваницкий Г.О., отв. Бондарев А.И.)
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Update(
|
||||
@SerialName("id") val id: Int,
|
||||
@SerialName("fio") val fullName: String,
|
||||
@SerialName("organization") val organization: String,
|
||||
@SerialName("phone_number") val phoneNumber: String,
|
||||
@SerialName("telegram_id") val telegramId: String?,
|
||||
@SerialName("district") val municipality: Municipality.DTO.Get,
|
||||
@SerialName("verified") val accepted: Boolean,
|
||||
@SerialName("processing_alert_status") val subscribed: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Accept(
|
||||
@SerialName("fio") val fullName: String,
|
||||
@SerialName("organization") val organization: String,
|
||||
@SerialName("phone_number") val phoneNumber: String,
|
||||
@SerialName("telegram_id") val telegramId: String?,
|
||||
@SerialName("district") val municipality: Municipality.DTO.Get,
|
||||
@SerialName("verified") val accepted: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Subscribe(
|
||||
@SerialName("processing_alert_status") val subscribed: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Reject(
|
||||
@SerialName("deleted_comment") val deleteReason: String,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum class RequestStatus(
|
||||
val value: String,
|
||||
) {
|
||||
New("new"),
|
||||
Accepted("accept"),
|
||||
Rejected("reject"),
|
||||
}
|
||||
|
||||
object RequestType {
|
||||
data class Domain(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
object DTO {
|
||||
@Serializable
|
||||
data class Get(
|
||||
@SerialName("id") val id: Int,
|
||||
@SerialName("request_type_name") val name: String,
|
||||
) {
|
||||
fun toDomain(): Domain {
|
||||
return Domain(
|
||||
id = id,
|
||||
name = name,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object UserRequest {
|
||||
data class Domain(
|
||||
val id: Int = 0,
|
||||
val user: User.Domain,
|
||||
val type: RequestType.Domain = RequestType.Domain(
|
||||
id = 1,
|
||||
name = "Фиксация гибели пчел",
|
||||
),
|
||||
val address: String = "",
|
||||
val text: String = "",
|
||||
val imageList: List<Image.Domain> = emptyList(),
|
||||
val adminComment: String? = null,
|
||||
val accepted: Boolean? = null,
|
||||
val rejectComment: String? = null,
|
||||
) {
|
||||
fun toCreateDTO(): DTO.Create {
|
||||
return DTO.Create(
|
||||
userId = user.id,
|
||||
typeId = type.id,
|
||||
address = address,
|
||||
text = text,
|
||||
imageFileIdList = imageList.map { it.id }
|
||||
)
|
||||
}
|
||||
|
||||
fun toAcceptDTO(): DTO.Accept {
|
||||
return DTO.Accept(
|
||||
adminComment = adminComment,
|
||||
accepted = accepted!!,
|
||||
rejectComment = rejectComment,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data object DTO {
|
||||
@Serializable
|
||||
data class Get(
|
||||
@SerialName("id") val id: Int,
|
||||
@SerialName("user") val user: User.DTO.Get,
|
||||
@SerialName("request_type") val type: RequestType.DTO.Get,
|
||||
@SerialName("address") val address: String,
|
||||
@SerialName("request_description") val text: String,
|
||||
@SerialName("upload_file") val imageList: List<Image.DTO.Get> = emptyList(),
|
||||
@SerialName("admin_correction") val adminComment: String?,
|
||||
@SerialName("status") val accepted: Boolean?,
|
||||
@SerialName("reject_comment") val rejectComment: String?,
|
||||
) {
|
||||
fun toDomain(): Domain {
|
||||
return Domain(
|
||||
id = id,
|
||||
user = user.toDomain(),
|
||||
type = type.toDomain(),
|
||||
address = address,
|
||||
text = text,
|
||||
imageList = imageList.map { it.toDomain() },
|
||||
adminComment = adminComment,
|
||||
accepted = accepted,
|
||||
rejectComment = rejectComment,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Create(
|
||||
@SerialName("user_id") val userId: Int,
|
||||
@SerialName("request_type_id") val typeId: Int,
|
||||
@SerialName("address") val address: String,
|
||||
@SerialName("request_description") val text: String,
|
||||
@SerialName("file_id_list") val imageFileIdList: List<Int>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Accept(
|
||||
@SerialName("admin_correction") val adminComment: String?,
|
||||
@SerialName("status") val accepted: Boolean,
|
||||
@SerialName("reject_comment") val rejectComment: String?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object Image {
|
||||
data class Domain(
|
||||
val id: Int,
|
||||
val userRequestId: Int?,
|
||||
val fileName: String,
|
||||
val fileSize: Int,
|
||||
val fileType: String,
|
||||
val uploadedAt: String,
|
||||
val url: String,
|
||||
)
|
||||
|
||||
object DTO {
|
||||
@Serializable
|
||||
data class Get(
|
||||
@SerialName("id") val id: Int,
|
||||
@SerialName("request_id") val userRequestId: Int?,
|
||||
@SerialName("file_name") val fileName: String,
|
||||
@SerialName("file_size") val fileSize: Int,
|
||||
@SerialName("file_type") val fileType: String,
|
||||
@SerialName("uploaded_at") val uploadedAt: String,
|
||||
@SerialName("url") val url: String,
|
||||
) {
|
||||
fun toDomain(): Domain {
|
||||
return Domain(
|
||||
id = id,
|
||||
userRequestId = userRequestId,
|
||||
fileName = fileName,
|
||||
fileSize = fileSize,
|
||||
fileType = fileType,
|
||||
uploadedAt = uploadedAt,
|
||||
url = url,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Municipality {
|
||||
data class Domain(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
) {
|
||||
fun toGetDTO(): DTO.Get {
|
||||
return DTO.Get(
|
||||
id = id,
|
||||
name = name,
|
||||
)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
object DTO {
|
||||
@Serializable
|
||||
data class Get(
|
||||
@SerialName("id") val id: Int,
|
||||
@SerialName("district_name") val name: String,
|
||||
) {
|
||||
fun toDomain(): Domain {
|
||||
return Domain(
|
||||
id = id,
|
||||
name = name,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package ru.cit71.pchelovod71.core
|
||||
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.browser.window
|
||||
import kotlinx.coroutines.await
|
||||
import org.khronos.webgl.Uint8Array
|
||||
import org.khronos.webgl.get
|
||||
import org.khronos.webgl.set
|
||||
import org.w3c.dom.CanvasRenderingContext2D
|
||||
import org.w3c.dom.HTMLCanvasElement
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import org.w3c.dom.HTMLTextAreaElement
|
||||
import org.w3c.dom.Image
|
||||
import org.w3c.files.FileReader
|
||||
import org.w3c.files.get
|
||||
import kotlin.math.round
|
||||
|
||||
const val MAX_WIDTH = 1920.0
|
||||
const val MAX_HEIGHT = 1080.0
|
||||
const val TYPE = "image/jpeg"
|
||||
val QUALITY: JsAny = js(".8")
|
||||
val STARTAPP_PATTERN = Regex("^.*tgWebAppStartParam=(.*?)(?:&.*)?$")
|
||||
|
||||
@JsFun("() => getTelegramInitData()")
|
||||
private external fun topLevelGetTelegramInitData(): String
|
||||
|
||||
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
|
||||
object Platform {
|
||||
val module = Models.Module.WasmJsMain
|
||||
|
||||
suspend fun getTelegramInitData() = Config.telegramInitData ?: topLevelGetTelegramInitData()
|
||||
|
||||
suspend fun getBindUuid() = STARTAPP_PATTERN.find(window.location.search)?.groupValues[1]
|
||||
|
||||
fun base64Encode(
|
||||
data: String,
|
||||
) = window.btoa(data)
|
||||
|
||||
suspend fun copyToClipboard(
|
||||
data: String,
|
||||
) {
|
||||
window.navigator.clipboard.writeText(data).await<Nothing?>()
|
||||
}
|
||||
|
||||
fun initIme(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
singleLine: Boolean,
|
||||
label: String,
|
||||
keyboardType: KeyboardType,
|
||||
) {
|
||||
val blank = document.createElement("div") as HTMLDivElement
|
||||
blank.classList.add("blank")
|
||||
blank.addEventListener("click", {
|
||||
blank.remove()
|
||||
})
|
||||
|
||||
val container = document.createElement("div") as HTMLDivElement
|
||||
container.classList.add("container")
|
||||
container.addEventListener("click", { event ->
|
||||
event.stopPropagation()
|
||||
})
|
||||
|
||||
val textField = when (singleLine) {
|
||||
true -> document.createElement("input") as HTMLInputElement
|
||||
false -> document.createElement("textarea") as HTMLTextAreaElement
|
||||
}
|
||||
textField.classList.add("text-field")
|
||||
when (textField) {
|
||||
is HTMLInputElement -> {
|
||||
textField.value = value
|
||||
textField.placeholder = label
|
||||
textField.type = when (keyboardType) {
|
||||
KeyboardType.Text -> "text"
|
||||
KeyboardType.Number -> "number"
|
||||
KeyboardType.Decimal -> "number"
|
||||
KeyboardType.Phone -> "tel"
|
||||
KeyboardType.Email -> "email"
|
||||
KeyboardType.Password -> "password"
|
||||
KeyboardType.NumberPassword -> "password"
|
||||
else -> "text"
|
||||
}
|
||||
}
|
||||
is HTMLTextAreaElement -> {
|
||||
textField.value = value
|
||||
textField.placeholder = label
|
||||
textField.rows = 4
|
||||
}
|
||||
}
|
||||
|
||||
val button = document.createElement("input") as HTMLInputElement
|
||||
button.classList.add("continue")
|
||||
button.type = "button"
|
||||
button.value = "Продолжить"
|
||||
button.addEventListener("click", {
|
||||
blank.remove()
|
||||
})
|
||||
|
||||
container.appendChild(textField)
|
||||
container.appendChild(button)
|
||||
blank.appendChild(container)
|
||||
document.body!!.appendChild(blank)
|
||||
|
||||
textField.focus()
|
||||
|
||||
textField.addEventListener("input", {
|
||||
when (textField) {
|
||||
is HTMLInputElement -> onValueChange(textField.value)
|
||||
is HTMLTextAreaElement -> onValueChange(textField.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun pickFile(
|
||||
callback: (String, ByteArray) -> Unit,
|
||||
) {
|
||||
val input = document.createElement("input") as HTMLInputElement
|
||||
input.type = "file"
|
||||
input.style.display = "none"
|
||||
input.accept = "image/*"
|
||||
input.setAttribute("capture", "environment")
|
||||
|
||||
input.onchange = { _ ->
|
||||
input.files?.let { it[0] }?.let { file ->
|
||||
val fileReader = FileReader()
|
||||
|
||||
fileReader.onload = { _ ->
|
||||
val img = Image()
|
||||
|
||||
img.onload = {
|
||||
val canvas = document.createElement("canvas") as HTMLCanvasElement
|
||||
var width = img.width.toDouble()
|
||||
var height = img.height.toDouble()
|
||||
|
||||
if (width > MAX_WIDTH || height > MAX_HEIGHT) {
|
||||
if (width / MAX_WIDTH > height / MAX_HEIGHT) {
|
||||
width = MAX_WIDTH
|
||||
height = round((img.height * (MAX_WIDTH / img.width)))
|
||||
} else {
|
||||
height = MAX_HEIGHT
|
||||
width = round(img.width * (MAX_HEIGHT / img.height))
|
||||
}
|
||||
}
|
||||
|
||||
canvas.width = width.toInt()
|
||||
canvas.height = height.toInt()
|
||||
val ctx = canvas.getContext("2d") as CanvasRenderingContext2D
|
||||
ctx.drawImage(img, .0, .0, width, height)
|
||||
val dataUrl = canvas.toDataURL(TYPE, QUALITY)
|
||||
|
||||
val base64Data = dataUrl.substringAfter(",")
|
||||
val binaryString = window.atob(base64Data)
|
||||
val uint8Array = Uint8Array(binaryString.length)
|
||||
for (i in binaryString.indices)
|
||||
uint8Array[i] = binaryString[i].code.toByte()
|
||||
val byteArray = ByteArray(uint8Array.length) { i -> uint8Array[i] }
|
||||
callback(
|
||||
file.type,
|
||||
byteArray,
|
||||
)
|
||||
}
|
||||
|
||||
fileReader.result?.let {
|
||||
img.src = "$it"
|
||||
}
|
||||
}
|
||||
|
||||
fileReader.readAsDataURL(file)
|
||||
}
|
||||
}
|
||||
|
||||
document.body?.appendChild(input)
|
||||
input.click()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.cit71.pchelovod71
|
||||
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.window.ComposeViewport
|
||||
import kotlinx.browser.document
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
fun main() {
|
||||
ComposeViewport(document.body!!) {
|
||||
App()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun BooleanFilter(
|
||||
modifier: Modifier = Modifier,
|
||||
valueMap: Map<String, Boolean?>,
|
||||
value: Boolean?,
|
||||
onValueChange: (Boolean?) -> Unit,
|
||||
) {
|
||||
SingleChoiceSegmentedButtonRow(
|
||||
modifier = modifier
|
||||
.padding(
|
||||
vertical = 12.dp,
|
||||
),
|
||||
) {
|
||||
valueMap.entries.forEachIndexed { index, entry ->
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
index = index,
|
||||
count = valueMap.size,
|
||||
),
|
||||
onClick = {
|
||||
onValueChange(entry.value)
|
||||
},
|
||||
selected = value == entry.value,
|
||||
label = {
|
||||
Text(
|
||||
text = entry.key,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun ConfirmButton(
|
||||
modifier: Modifier,
|
||||
onClick: () -> Unit,
|
||||
label: String,
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.then(modifier),
|
||||
onClick = onClick,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DismissButton(
|
||||
modifier: Modifier,
|
||||
onClick: () -> Unit,
|
||||
label: String,
|
||||
) {
|
||||
OutlinedButton(
|
||||
modifier = Modifier
|
||||
.then(modifier),
|
||||
onClick = onClick,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun MainColumn(
|
||||
modifier: Modifier = Modifier,
|
||||
paddingValues: PaddingValues = PaddingValues(),
|
||||
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(
|
||||
state = rememberScrollState(),
|
||||
)
|
||||
.then(modifier),
|
||||
horizontalAlignment = horizontalAlignment,
|
||||
verticalArrangement = verticalArrangement,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T>MainColumnList(
|
||||
modifier: Modifier = Modifier,
|
||||
paddingValues: PaddingValues = PaddingValues(),
|
||||
onScrollChange: ((Boolean) -> Unit)? = null,
|
||||
valueList: List<T>,
|
||||
placeholder: String,
|
||||
content: @Composable (T) -> Unit,
|
||||
) {
|
||||
val verticalScrollState = rememberScrollState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
onScrollChange?.let { onScrollChange ->
|
||||
var previousValue = 0
|
||||
snapshotFlow {
|
||||
verticalScrollState.value
|
||||
}.collect {
|
||||
onScrollChange(it <= previousValue)
|
||||
previousValue = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (valueList.isEmpty()) {
|
||||
true -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = placeholder,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
false -> {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(
|
||||
state = verticalScrollState,
|
||||
)
|
||||
.then(modifier),
|
||||
) {
|
||||
valueList.forEach {
|
||||
content(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.PointerIcon
|
||||
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
||||
import frontend.composeapp.generated.resources.Res
|
||||
import frontend.composeapp.generated.resources.round_content_copy_24
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import ru.cit71.pchelovod71.core.Platform
|
||||
|
||||
@Composable
|
||||
fun CopyField(
|
||||
mainScaffoldParams: MainScaffoldParams,
|
||||
value: String,
|
||||
label: String,
|
||||
supportingText: String? = null,
|
||||
) {
|
||||
val clipboardCoroutineScope = rememberCoroutineScope()
|
||||
val outlinedTextFieldDefaults = OutlinedTextFieldDefaults.colors()
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.pointerHoverIcon(
|
||||
icon = PointerIcon.Default,
|
||||
overrideDescendants = true,
|
||||
),
|
||||
value = value,
|
||||
onValueChange = {},
|
||||
enabled = false,
|
||||
label = {
|
||||
Text(
|
||||
text = label,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
clipboardCoroutineScope.launch {
|
||||
Platform.copyToClipboard(value)
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Ссылка скопирована в буфер обмена"
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(Res.drawable.round_content_copy_24),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
supportingText = supportingText?.let {
|
||||
{
|
||||
Text(
|
||||
text = it,
|
||||
)
|
||||
}
|
||||
},
|
||||
singleLine = true,
|
||||
colors = outlinedTextFieldDefaults.copy(
|
||||
disabledTextColor = outlinedTextFieldDefaults.unfocusedTextColor,
|
||||
disabledContainerColor = outlinedTextFieldDefaults.unfocusedContainerColor,
|
||||
disabledIndicatorColor = outlinedTextFieldDefaults.unfocusedIndicatorColor,
|
||||
disabledLeadingIconColor = outlinedTextFieldDefaults.unfocusedLeadingIconColor,
|
||||
disabledTrailingIconColor = outlinedTextFieldDefaults.unfocusedTrailingIconColor,
|
||||
disabledLabelColor = outlinedTextFieldDefaults.unfocusedLabelColor,
|
||||
disabledPlaceholderColor = outlinedTextFieldDefaults.unfocusedPlaceholderColor,
|
||||
disabledSupportingTextColor = outlinedTextFieldDefaults.unfocusedSupportingTextColor,
|
||||
disabledPrefixColor = outlinedTextFieldDefaults.unfocusedPrefixColor,
|
||||
disabledSuffixColor = outlinedTextFieldDefaults.unfocusedSuffixColor,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import frontend.composeapp.generated.resources.Res
|
||||
import frontend.composeapp.generated.resources.baseline_close_24
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
||||
interface MainDialogParams {
|
||||
val onDismiss: () -> Unit
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainDialog(
|
||||
title: String,
|
||||
onDismiss: () -> Unit,
|
||||
buttons: @Composable RowScope.() -> Unit,
|
||||
content: @Composable (MainScaffoldParams) -> Unit,
|
||||
) {
|
||||
Dialog(
|
||||
onDismissRequest = {},
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
),
|
||||
) {
|
||||
MainScaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
softWrap = false,
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onDismiss,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(Res.drawable.baseline_close_24),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
content = buttons,
|
||||
)
|
||||
}
|
||||
) { mainScaffoldParams ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
),
|
||||
) {
|
||||
content(mainScaffoldParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
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.input.pointer.PointerIcon
|
||||
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun <T>DropdownField(
|
||||
modifier: Modifier = Modifier,
|
||||
valueList: List<T>,
|
||||
value: T,
|
||||
onValueChange: (T) -> Unit,
|
||||
enabled: Boolean = true,
|
||||
label: String,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = enabled && expanded,
|
||||
onExpandedChange = {
|
||||
expanded = it
|
||||
}
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(
|
||||
type = ExposedDropdownMenuAnchorType.PrimaryEditable,
|
||||
)
|
||||
.pointerHoverIcon(
|
||||
icon = PointerIcon.Default,
|
||||
overrideDescendants = true,
|
||||
),
|
||||
enabled = enabled,
|
||||
readOnly = true,
|
||||
value = value.toString(),
|
||||
onValueChange = {},
|
||||
label = {
|
||||
Text(
|
||||
text = label,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(
|
||||
expanded = enabled && expanded
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = enabled && expanded,
|
||||
onDismissRequest = {
|
||||
expanded = false
|
||||
}
|
||||
) {
|
||||
valueList.forEach {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = it.toString(),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onValueChange(it)
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.carousel.CarouselState
|
||||
import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import frontend.composeapp.generated.resources.Res
|
||||
import frontend.composeapp.generated.resources.baseline_close_24
|
||||
import coil3.compose.rememberAsyncImagePainter
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
|
||||
enum class InteractionType {
|
||||
Upload,
|
||||
Download,
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ImagePicker(
|
||||
imageList: List<Models.Image.Domain>,
|
||||
onImageAdd: (() -> Unit)? = null,
|
||||
onImageDrop: (Int) -> Unit,
|
||||
onImageSelect: (Models.Image.Domain) -> Unit,
|
||||
interactionType: InteractionType,
|
||||
) {
|
||||
Column {
|
||||
if (interactionType == InteractionType.Upload)
|
||||
onImageAdd?.let {
|
||||
OutlinedButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
onClick = it,
|
||||
) {
|
||||
Text(
|
||||
text = "Добавить изображение",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
when (imageList.size) {
|
||||
0 -> {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = 4.dp,
|
||||
),
|
||||
text = "Изображения не выбраны",
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
HorizontalMultiBrowseCarousel(
|
||||
state = remember(imageList.size) {
|
||||
CarouselState(
|
||||
currentItem = imageList.size,
|
||||
itemCount = {
|
||||
imageList.size
|
||||
},
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(
|
||||
vertical = 8.dp,
|
||||
),
|
||||
preferredItemWidth = 186.dp,
|
||||
itemSpacing = 8.dp,
|
||||
) { i ->
|
||||
val image = imageList[i]
|
||||
Box(
|
||||
contentAlignment = Alignment.TopEnd,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.height(205.dp)
|
||||
.maskClip(MaterialTheme.shapes.extraLarge)
|
||||
.clickable {
|
||||
onImageSelect(image)
|
||||
},
|
||||
painter = rememberAsyncImagePainter(
|
||||
model = image.url,
|
||||
contentScale = ContentScale.None,
|
||||
),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
|
||||
if (interactionType == InteractionType.Upload)
|
||||
IconButton(
|
||||
onClick = {
|
||||
onImageDrop(i)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(Res.drawable.baseline_close_24),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (interactionType == InteractionType.Upload && imageList.size > 1)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
text = "Для прокрутки изображений на компьютерах и ноутбуках удерживайте клавишу Shift",
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.LoadingIndicator
|
||||
import androidx.compose.material3.LoadingIndicatorDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun MainLoadingIndicator() {
|
||||
LoadingIndicator(
|
||||
modifier = Modifier
|
||||
.size(
|
||||
width = LoadingIndicatorDefaults.ContainerWidth * 3,
|
||||
height = LoadingIndicatorDefaults.ContainerHeight * 3,
|
||||
),
|
||||
)
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardColors
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import frontend.composeapp.generated.resources.Res
|
||||
import frontend.composeapp.generated.resources.round_check_24
|
||||
import frontend.composeapp.generated.resources.round_close_24
|
||||
import frontend.composeapp.generated.resources.round_manage_accounts_24
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
|
||||
@Composable
|
||||
fun RegistrationRequestCard(
|
||||
userDomain: Models.User.Domain,
|
||||
selected: Boolean,
|
||||
onSelectedChange: ((Models.User.Domain)?) -> Unit,
|
||||
onAccept: (Models.User.Domain) -> Unit,
|
||||
onEdit: (Models.User.Domain) -> Unit,
|
||||
onDecline: (Models.User.Domain) -> Unit,
|
||||
) {
|
||||
val backgroundColor = animateColorAsState(
|
||||
targetValue = when (selected) {
|
||||
true -> MaterialTheme.colorScheme.primary.copy(
|
||||
alpha = 0.12f,
|
||||
)
|
||||
false -> Color.Transparent
|
||||
},
|
||||
)
|
||||
val horizontalPadding = animateDpAsState(
|
||||
targetValue = when (selected) {
|
||||
true -> 16.dp
|
||||
false -> 0.dp
|
||||
},
|
||||
)
|
||||
val shapeState = animateDpAsState(
|
||||
targetValue = when (selected) {
|
||||
true -> 28.0.dp
|
||||
false -> 0.0.dp
|
||||
},
|
||||
)
|
||||
val cardColors = CardDefaults.cardColors()
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontalPadding.value),
|
||||
shape = RoundedCornerShape(shapeState.value),
|
||||
colors = CardColors(
|
||||
containerColor = backgroundColor.value,
|
||||
contentColor = cardColors.contentColor,
|
||||
disabledContainerColor = cardColors.disabledContainerColor,
|
||||
disabledContentColor = cardColors.disabledContentColor,
|
||||
),
|
||||
) {
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onSelectedChange(
|
||||
when (selected) {
|
||||
true -> null
|
||||
false -> userDomain
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = userDomain.fullName,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = userDomain.municipality.name,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 3,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = selected,
|
||||
) {
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
RegistrationRequestButton(
|
||||
modifier = Modifier
|
||||
.weight(
|
||||
weight = 1f,
|
||||
fill = true,
|
||||
),
|
||||
title = "Принять",
|
||||
painter = painterResource(Res.drawable.round_check_24),
|
||||
onClick = {
|
||||
onAccept(userDomain)
|
||||
},
|
||||
)
|
||||
RegistrationRequestButton(
|
||||
modifier = Modifier
|
||||
.weight(
|
||||
weight = 1f,
|
||||
fill = true,
|
||||
),
|
||||
title = "Изменить",
|
||||
painter = painterResource(Res.drawable.round_manage_accounts_24),
|
||||
onClick = {
|
||||
onEdit(userDomain)
|
||||
},
|
||||
)
|
||||
RegistrationRequestButton(
|
||||
modifier = Modifier
|
||||
.weight(
|
||||
weight = 1f,
|
||||
fill = true,
|
||||
),
|
||||
title = "Отклонить",
|
||||
painter = painterResource(Res.drawable.round_close_24),
|
||||
onClick = {
|
||||
onDecline(userDomain)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RegistrationRequestButton(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
painter: Painter,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
.fillMaxHeight(),
|
||||
shape = RectangleShape,
|
||||
colors = ButtonDefaults.textButtonColors(),
|
||||
contentPadding = PaddingValues(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.fillMaxHeight(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
painter = painter,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = title,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.expandHorizontally
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkHorizontally
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class MainScaffoldParams(
|
||||
val paddingValues: PaddingValues,
|
||||
val showSuccessSnackbar: (String) -> Unit,
|
||||
val showErrorSnackbar: (String) -> Unit,
|
||||
val setFloatingActionButton: ((@Composable AnimatedVisibilityScope.() -> Unit)?) -> Unit,
|
||||
val withLoadingIndicator: suspend (suspend () -> Unit) -> Unit,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun MainScaffold(
|
||||
modifier: Modifier = Modifier,
|
||||
topBar: @Composable () -> Unit = {},
|
||||
bottomBar: @Composable () -> Unit = {},
|
||||
content: (@Composable (MainScaffoldParams) -> Unit)? = null,
|
||||
) {
|
||||
var loadingIndicator by remember { mutableStateOf(false) }
|
||||
|
||||
suspend fun withLoadingIndicator(
|
||||
function: suspend () -> Unit,
|
||||
) {
|
||||
loadingIndicator = true
|
||||
try {
|
||||
function()
|
||||
} finally {
|
||||
loadingIndicator = false
|
||||
}
|
||||
}
|
||||
|
||||
val snackbarCoroutineScope = rememberCoroutineScope()
|
||||
val successSnackbarHostState = SnackbarHostState()
|
||||
val errorSnackbarHostState = SnackbarHostState()
|
||||
|
||||
fun showSuccessSnackbar(
|
||||
message: String,
|
||||
) {
|
||||
snackbarCoroutineScope.launch {
|
||||
successSnackbarHostState.showSnackbar(
|
||||
message = message,
|
||||
withDismissAction = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showErrorSnackbar(
|
||||
message: String,
|
||||
) {
|
||||
snackbarCoroutineScope.launch {
|
||||
errorSnackbarHostState.showSnackbar(
|
||||
message = message,
|
||||
withDismissAction = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var floatingActionButton by remember { mutableStateOf<(@Composable AnimatedVisibilityScope.() -> Unit)?>(null) }
|
||||
var floatingActionButtonVisible by remember { mutableStateOf(false) }
|
||||
|
||||
fun setFloatingActionButton(
|
||||
content: (@Composable AnimatedVisibilityScope.() -> Unit)?,
|
||||
) {
|
||||
when (content) {
|
||||
null -> {
|
||||
floatingActionButtonVisible = false
|
||||
}
|
||||
else -> {
|
||||
floatingActionButton = content
|
||||
floatingActionButtonVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(modifier),
|
||||
topBar = topBar,
|
||||
bottomBar = bottomBar,
|
||||
floatingActionButton = {
|
||||
floatingActionButton?.let {
|
||||
AnimatedVisibility(
|
||||
visible = floatingActionButtonVisible,
|
||||
enter = fadeIn() + expandHorizontally(),
|
||||
exit = shrinkHorizontally() + fadeOut(),
|
||||
content = it,
|
||||
)
|
||||
}
|
||||
},
|
||||
snackbarHost = {
|
||||
MainSnackbarHost(
|
||||
successSnackbarHostState = successSnackbarHostState,
|
||||
errorSnackbarHostState = errorSnackbarHostState,
|
||||
)
|
||||
},
|
||||
content = { paddingValues ->
|
||||
content?.let {
|
||||
it(
|
||||
MainScaffoldParams(
|
||||
paddingValues = paddingValues,
|
||||
showSuccessSnackbar = ::showSuccessSnackbar,
|
||||
showErrorSnackbar = ::showErrorSnackbar,
|
||||
setFloatingActionButton = ::setFloatingActionButton,
|
||||
withLoadingIndicator = ::withLoadingIndicator,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
visible = loadingIndicator,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
color = when (content) {
|
||||
null -> Color.Unspecified
|
||||
else -> Color.Black.copy(
|
||||
alpha = .5f,
|
||||
)
|
||||
},
|
||||
)
|
||||
.pointerInput(Unit) {
|
||||
awaitPointerEventScope {
|
||||
while (true) {
|
||||
awaitPointerEvent()
|
||||
}
|
||||
}
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
MainLoadingIndicator()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun SettingCard(
|
||||
name: String,
|
||||
caption: String,
|
||||
checked: Boolean,
|
||||
onCheckedChange: ((Boolean) -> Unit),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
onClick = {
|
||||
onCheckedChange(!checked)
|
||||
},
|
||||
)
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
){
|
||||
Text(
|
||||
text = name,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
softWrap = false,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = caption,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Snackbar
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Composable
|
||||
fun MainSnackbarHost(
|
||||
successSnackbarHostState: SnackbarHostState,
|
||||
errorSnackbarHostState: SnackbarHostState,
|
||||
) {
|
||||
val successContainerDark = Color(0xFF009424)
|
||||
val onSuccessContainerDark = Color(0xFFD6FFEA)
|
||||
|
||||
SnackbarHost(successSnackbarHostState) {
|
||||
Snackbar(
|
||||
snackbarData = it,
|
||||
contentColor = onSuccessContainerDark,
|
||||
containerColor = successContainerDark,
|
||||
dismissActionContentColor = onSuccessContainerDark,
|
||||
)
|
||||
}
|
||||
|
||||
SnackbarHost(errorSnackbarHostState) {
|
||||
Snackbar(
|
||||
snackbarData = it,
|
||||
contentColor = MaterialTheme.colorScheme.onErrorContainer,
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
dismissActionContentColor = MaterialTheme.colorScheme.onErrorContainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package ru.cit71.pchelovod71.ui.components// TODO: спойлер для расширенных параметров пользователя; не пригодился, но оставлен до востребования
|
||||
|
||||
//package ru.cit71.bee_frontend.ui.components
|
||||
//
|
||||
//import androidx.compose.animation.AnimatedVisibility
|
||||
//import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
//import androidx.compose.animation.core.animateFloatAsState
|
||||
//import androidx.compose.animation.core.tween
|
||||
//import androidx.compose.foundation.layout.Arrangement
|
||||
//import androidx.compose.foundation.layout.Column
|
||||
//import androidx.compose.foundation.layout.ColumnScope
|
||||
//import androidx.compose.foundation.layout.Row
|
||||
//import androidx.compose.foundation.layout.fillMaxWidth
|
||||
//import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
//import androidx.compose.material3.Icon
|
||||
//import androidx.compose.material3.IconToggleButton
|
||||
//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.Alignment
|
||||
//import androidx.compose.ui.Modifier
|
||||
//import androidx.compose.ui.draw.rotate
|
||||
//import androidx.compose.ui.text.style.TextOverflow
|
||||
//import androidx.compose.ui.unit.dp
|
||||
//import frontend.composeapp.generated.resources.Res
|
||||
//import frontend.composeapp.generated.resources.round_arrow_right_24
|
||||
//import org.jetbrains.compose.resources.painterResource
|
||||
//
|
||||
//@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
//@Composable
|
||||
//fun Spoiler(
|
||||
// name: String,
|
||||
// content: @Composable ColumnScope.() -> Unit,
|
||||
//) {
|
||||
// var expanded by remember { mutableStateOf(false) }
|
||||
// val rotation by animateFloatAsState(
|
||||
// targetValue = if (expanded) 90f else 0f,
|
||||
// animationSpec = tween(
|
||||
// durationMillis = 300,
|
||||
// easing = FastOutSlowInEasing
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
// Column(
|
||||
// verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
// ) {
|
||||
// Row(
|
||||
// horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
// verticalAlignment = Alignment.CenterVertically,
|
||||
// ) {
|
||||
// IconToggleButton(
|
||||
// checked = expanded,
|
||||
// onCheckedChange = {
|
||||
// expanded = it
|
||||
// },
|
||||
// ) {
|
||||
// Icon(
|
||||
// modifier = Modifier
|
||||
// .rotate(rotation),
|
||||
// painter = painterResource(Res.drawable.round_arrow_right_24),
|
||||
// contentDescription = null,
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// Text(
|
||||
// text = name,
|
||||
// overflow = TextOverflow.Ellipsis,
|
||||
// softWrap = false,
|
||||
// style = MaterialTheme.typography.titleMedium,
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// AnimatedVisibility(
|
||||
// visible = expanded,
|
||||
// ) {
|
||||
// Column(
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth(),
|
||||
// verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
// content = content,
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,89 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import ru.cit71.pchelovod71.core.Platform
|
||||
|
||||
@Composable
|
||||
fun MainTextField(
|
||||
modifier: Modifier = Modifier,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
enabled: Boolean = true,
|
||||
label: String,
|
||||
singleLine: Boolean = true,
|
||||
minLines: Int = 1,
|
||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||
keyboardType: KeyboardType = KeyboardType.Unspecified,
|
||||
) {
|
||||
val outlinedTextFieldDefaults = OutlinedTextFieldDefaults.colors()
|
||||
|
||||
val moduleModifier = when (Platform.module) {
|
||||
Models.Module.DesktopMain -> Modifier
|
||||
Models.Module.WasmJsMain -> Modifier
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
) {
|
||||
if (enabled)
|
||||
Platform.initIme(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
singleLine = singleLine,
|
||||
label = label,
|
||||
keyboardType = keyboardType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.then(moduleModifier),
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
enabled = when (Platform.module) {
|
||||
Models.Module.DesktopMain -> enabled
|
||||
Models.Module.WasmJsMain -> false
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = label,
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = keyboardType,
|
||||
),
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
minLines = minLines,
|
||||
colors = when (Platform.module) {
|
||||
Models.Module.DesktopMain -> outlinedTextFieldDefaults
|
||||
Models.Module.WasmJsMain -> when (enabled) {
|
||||
true -> outlinedTextFieldDefaults.copy(
|
||||
disabledTextColor = outlinedTextFieldDefaults.unfocusedTextColor,
|
||||
disabledContainerColor = outlinedTextFieldDefaults.unfocusedContainerColor,
|
||||
disabledIndicatorColor = outlinedTextFieldDefaults.unfocusedIndicatorColor,
|
||||
disabledLeadingIconColor = outlinedTextFieldDefaults.unfocusedLeadingIconColor,
|
||||
disabledTrailingIconColor = outlinedTextFieldDefaults.unfocusedTrailingIconColor,
|
||||
disabledLabelColor = outlinedTextFieldDefaults.unfocusedLabelColor,
|
||||
disabledPlaceholderColor = outlinedTextFieldDefaults.unfocusedPlaceholderColor,
|
||||
disabledSupportingTextColor = outlinedTextFieldDefaults.unfocusedSupportingTextColor,
|
||||
disabledPrefixColor = outlinedTextFieldDefaults.unfocusedPrefixColor,
|
||||
disabledSuffixColor = outlinedTextFieldDefaults.unfocusedSuffixColor,
|
||||
)
|
||||
false -> outlinedTextFieldDefaults
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
|
||||
@Composable
|
||||
fun UserCard(
|
||||
userDomain: Models.User.Domain,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
){
|
||||
Text(
|
||||
text = userDomain.fullName,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = userDomain.organization,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = userDomain.phoneNumber,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = userDomain.municipality.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package ru.cit71.pchelovod71.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
|
||||
@Composable
|
||||
fun UserRequestCard(
|
||||
userRequestDomain: Models.UserRequest.Domain,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
){
|
||||
Text(
|
||||
text = userRequestDomain.type.name,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = userRequestDomain.user.municipality.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = userRequestDomain.address,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = when (userRequestDomain.accepted) {
|
||||
true -> "Принято"
|
||||
false -> "Отклонено"
|
||||
null -> "Новое"
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package ru.cit71.pchelovod71.ui.dialogs
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
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.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import ru.cit71.pchelovod71.ui.components.ConfirmButton
|
||||
import ru.cit71.pchelovod71.ui.components.DismissButton
|
||||
import ru.cit71.pchelovod71.ui.components.MainDialogParams
|
||||
import ru.cit71.pchelovod71.ui.components.MainTextField
|
||||
|
||||
sealed interface ConfirmDialogParams : MainDialogParams
|
||||
|
||||
data class ConfirmCommentDialogParams(
|
||||
override val onDismiss: () -> Unit,
|
||||
val onConfirm: (String) -> Unit,
|
||||
) : ConfirmDialogParams
|
||||
|
||||
data class ConfirmOnlyDialogParams(
|
||||
override val onDismiss: () -> Unit,
|
||||
val onConfirm: () -> Unit,
|
||||
val title: String,
|
||||
) : ConfirmDialogParams
|
||||
|
||||
@Composable
|
||||
fun ConfirmDialog(
|
||||
confirmDialogParams: ConfirmDialogParams,
|
||||
dismissButtonText: String,
|
||||
confirmButtonText: String,
|
||||
) {
|
||||
var comment by remember { mutableStateOf("") }
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = confirmDialogParams.onDismiss,
|
||||
properties = DialogProperties(
|
||||
dismissOnClickOutside = false,
|
||||
),
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.wrapContentSize(),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
when (confirmDialogParams) {
|
||||
is ConfirmCommentDialogParams -> {
|
||||
MainTextField(
|
||||
value = comment,
|
||||
onValueChange = {
|
||||
comment = it
|
||||
},
|
||||
label = "Комментарий",
|
||||
keyboardType = KeyboardType.Text,
|
||||
singleLine = false,
|
||||
minLines = 3,
|
||||
maxLines = 6,
|
||||
)
|
||||
}
|
||||
is ConfirmOnlyDialogParams -> {
|
||||
Text(
|
||||
text = confirmDialogParams.title,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
softWrap = false,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
ConfirmButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
when (confirmDialogParams) {
|
||||
is ConfirmCommentDialogParams -> {
|
||||
confirmDialogParams.onConfirm(comment)
|
||||
}
|
||||
is ConfirmOnlyDialogParams -> {
|
||||
confirmDialogParams.onConfirm()
|
||||
}
|
||||
}
|
||||
},
|
||||
label = confirmButtonText,
|
||||
)
|
||||
|
||||
DismissButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
confirmDialogParams.onDismiss()
|
||||
},
|
||||
label = dismissButtonText,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package ru.cit71.pchelovod71.ui.dialogs
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.gestures.TransformableState
|
||||
import androidx.compose.foundation.gestures.transformable
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
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.geometry.Offset
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import coil3.compose.rememberAsyncImagePainter
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import kotlin.math.max
|
||||
|
||||
@Composable
|
||||
fun ImageViewDialog(
|
||||
image: Models.Image.Domain,
|
||||
onClose: () -> Unit,
|
||||
) {
|
||||
var zoom by remember { mutableStateOf(1f) }
|
||||
var pan by remember { mutableStateOf(Offset.Zero) }
|
||||
var rotation by remember { mutableStateOf(0f) }
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onClose,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.transformable(
|
||||
state = TransformableState { zoomChange, panChange, rotationChange ->
|
||||
zoom = max(zoom * zoomChange, 1f)
|
||||
pan += panChange
|
||||
rotation += rotationChange
|
||||
},
|
||||
),
|
||||
painter = rememberAsyncImagePainter(
|
||||
model = image.url,
|
||||
contentScale = ContentScale.None,
|
||||
),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package ru.cit71.pchelovod71.ui.dialogs
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.ktor.http.clone
|
||||
import ru.cit71.pchelovod71.core.Config
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import ru.cit71.pchelovod71.ui.components.ConfirmButton
|
||||
import ru.cit71.pchelovod71.ui.components.CopyField
|
||||
import ru.cit71.pchelovod71.ui.components.DismissButton
|
||||
import ru.cit71.pchelovod71.ui.components.MainColumn
|
||||
import ru.cit71.pchelovod71.ui.components.MainDialog
|
||||
import ru.cit71.pchelovod71.ui.components.MainDialogParams
|
||||
import ru.cit71.pchelovod71.ui.components.DropdownField
|
||||
import ru.cit71.pchelovod71.ui.components.MainTextField
|
||||
|
||||
sealed interface UserDialogParams : MainDialogParams
|
||||
|
||||
data class CreateUserDialogParams(
|
||||
override val onDismiss: () -> Unit,
|
||||
val onCreate: ((Models.User.Domain) -> Unit),
|
||||
) : UserDialogParams
|
||||
|
||||
data class UpdateUserDialogParams(
|
||||
override val onDismiss: () -> Unit,
|
||||
val onSave: ((Models.User.Domain) -> Unit),
|
||||
val onDelete: ((Models.User.Domain) -> Unit),
|
||||
) : UserDialogParams
|
||||
|
||||
data class AcceptUserDialogParams(
|
||||
override val onDismiss: () -> Unit,
|
||||
val onAccept: ((Models.User.Domain) -> Unit),
|
||||
val onDecline: ((Models.User.Domain) -> Unit),
|
||||
) : UserDialogParams
|
||||
|
||||
@Composable
|
||||
fun UserDialog(
|
||||
userDomain: Models.User.Domain,
|
||||
municipalityDomainList: List<Models.Municipality.Domain>,
|
||||
userDialogParams: UserDialogParams,
|
||||
) {
|
||||
var outUserDomain by remember { mutableStateOf(userDomain) }
|
||||
|
||||
MainDialog(
|
||||
title = when (userDialogParams) {
|
||||
is CreateUserDialogParams -> "Добавить пользователя"
|
||||
is UpdateUserDialogParams -> "Изменить пользователя"
|
||||
is AcceptUserDialogParams -> "Принять заявку на регистрацию"
|
||||
},
|
||||
onDismiss = userDialogParams.onDismiss,
|
||||
buttons = {
|
||||
when (userDialogParams) {
|
||||
is CreateUserDialogParams -> {
|
||||
ConfirmButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
userDialogParams.onCreate(outUserDomain)
|
||||
},
|
||||
label = "Добавить",
|
||||
)
|
||||
}
|
||||
is UpdateUserDialogParams -> {
|
||||
ConfirmButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
userDialogParams.onSave(outUserDomain)
|
||||
},
|
||||
label = "Сохранить",
|
||||
)
|
||||
DismissButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
userDialogParams.onDelete(outUserDomain)
|
||||
},
|
||||
label = "Удалить",
|
||||
)
|
||||
}
|
||||
is AcceptUserDialogParams -> {
|
||||
ConfirmButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
userDialogParams.onAccept(outUserDomain)
|
||||
},
|
||||
label = "Принять",
|
||||
)
|
||||
DismissButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
userDialogParams.onDecline(outUserDomain)
|
||||
},
|
||||
label = "Отклонить",
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
) { mainScaffoldParams ->
|
||||
MainColumn(
|
||||
paddingValues = mainScaffoldParams.paddingValues,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
MainTextField(
|
||||
value = outUserDomain.fullName,
|
||||
onValueChange = {
|
||||
outUserDomain = outUserDomain.copy(
|
||||
fullName = it,
|
||||
)
|
||||
},
|
||||
label = "ФИО",
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
|
||||
MainTextField(
|
||||
value = outUserDomain.organization,
|
||||
onValueChange = {
|
||||
outUserDomain = outUserDomain.copy(
|
||||
organization = it,
|
||||
)
|
||||
},
|
||||
label = "Организация",
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
|
||||
MainTextField(
|
||||
value = outUserDomain.phoneNumber,
|
||||
onValueChange = {
|
||||
outUserDomain = outUserDomain.copy(
|
||||
phoneNumber = it,
|
||||
)
|
||||
},
|
||||
label = "Номер телефона",
|
||||
keyboardType = KeyboardType.Phone,
|
||||
)
|
||||
|
||||
DropdownField(
|
||||
valueList = municipalityDomainList,
|
||||
value = outUserDomain.municipality,
|
||||
onValueChange = {
|
||||
outUserDomain = outUserDomain.copy(
|
||||
municipality = it,
|
||||
)
|
||||
},
|
||||
label = "Муниципальное образование",
|
||||
)
|
||||
|
||||
if (userDialogParams is UpdateUserDialogParams && userDomain.telegramId == null) {
|
||||
val inviteUrl = Config.botUrlBuilder.clone().apply {
|
||||
parameters["startapp"] = userDomain.uuid
|
||||
}.buildString()
|
||||
|
||||
CopyField(
|
||||
mainScaffoldParams = mainScaffoldParams,
|
||||
value = inviteUrl,
|
||||
label = "Пригласительная ссылка",
|
||||
supportingText = "Пользователь не привязан к учетной записи Telegram. Скопируйте ссылку и передайте ее владельцу",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
package ru.cit71.pchelovod71.ui.dialogs
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.cit71.pchelovod71.core.Api
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import ru.cit71.pchelovod71.core.Platform
|
||||
import ru.cit71.pchelovod71.ui.components.ConfirmButton
|
||||
import ru.cit71.pchelovod71.ui.components.DismissButton
|
||||
import ru.cit71.pchelovod71.ui.components.DropdownField
|
||||
import ru.cit71.pchelovod71.ui.components.ImagePicker
|
||||
import ru.cit71.pchelovod71.ui.components.InteractionType
|
||||
import ru.cit71.pchelovod71.ui.components.MainColumn
|
||||
import ru.cit71.pchelovod71.ui.components.MainDialog
|
||||
import ru.cit71.pchelovod71.ui.components.MainDialogParams
|
||||
import ru.cit71.pchelovod71.ui.components.MainTextField
|
||||
|
||||
sealed interface UserRequestDialogParams : MainDialogParams
|
||||
|
||||
data class ShowUserRequestDialogParams(
|
||||
override val onDismiss: () -> Unit,
|
||||
) : UserRequestDialogParams
|
||||
|
||||
data class CreateUserRequestDialogParams(
|
||||
override val onDismiss: () -> Unit,
|
||||
val onCreate: (Models.UserRequest.Domain) -> Unit,
|
||||
) : UserRequestDialogParams
|
||||
|
||||
data class AcceptUserRequestDialogParams(
|
||||
override val onDismiss: () -> Unit,
|
||||
val onAccept: ((Models.UserRequest.Domain) -> Unit),
|
||||
val onDecline: ((Models.UserRequest.Domain) -> Unit),
|
||||
) : UserRequestDialogParams
|
||||
|
||||
@Composable
|
||||
fun UserRequestDialog(
|
||||
userRequestDomain: Models.UserRequest.Domain,
|
||||
userRequestDialogParams: UserRequestDialogParams,
|
||||
) {
|
||||
val apiCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
var outUserRequestDomain by remember { mutableStateOf(userRequestDomain) }
|
||||
|
||||
var imageView by remember { mutableStateOf<Models.Image.Domain?>(null) }
|
||||
|
||||
MainDialog(
|
||||
title = when (userRequestDialogParams) {
|
||||
is ShowUserRequestDialogParams -> "Просмотреть обращение"
|
||||
is CreateUserRequestDialogParams -> "Добавить обращение"
|
||||
is AcceptUserRequestDialogParams -> "Принять обращение"
|
||||
},
|
||||
onDismiss = userRequestDialogParams.onDismiss,
|
||||
buttons = {
|
||||
when (userRequestDialogParams) {
|
||||
is ShowUserRequestDialogParams -> {}
|
||||
is CreateUserRequestDialogParams -> {
|
||||
ConfirmButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
userRequestDialogParams.onCreate(outUserRequestDomain)
|
||||
},
|
||||
label = "Добавить",
|
||||
)
|
||||
}
|
||||
is AcceptUserRequestDialogParams -> {
|
||||
if (userRequestDomain.accepted == null) {
|
||||
ConfirmButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
userRequestDialogParams.onAccept(outUserRequestDomain)
|
||||
},
|
||||
label = "Принять",
|
||||
)
|
||||
DismissButton(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
userRequestDialogParams.onDecline(outUserRequestDomain)
|
||||
},
|
||||
label = "Отклонить",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
) { mainScaffoldParams ->
|
||||
MainColumn(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
bottom = 16.dp,
|
||||
),
|
||||
paddingValues = mainScaffoldParams.paddingValues,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
MainTextField(
|
||||
value = userRequestDomain.user.fullName,
|
||||
onValueChange = {},
|
||||
enabled = false,
|
||||
label = "ФИО",
|
||||
)
|
||||
|
||||
MainTextField(
|
||||
value = userRequestDomain.user.organization,
|
||||
onValueChange = {},
|
||||
enabled = false,
|
||||
label = "Организация",
|
||||
)
|
||||
|
||||
MainTextField(
|
||||
value = userRequestDomain.user.phoneNumber,
|
||||
onValueChange = {},
|
||||
enabled = false,
|
||||
label = "Номер телефона",
|
||||
)
|
||||
|
||||
MainTextField(
|
||||
value = outUserRequestDomain.user.municipality.name,
|
||||
onValueChange = {},
|
||||
enabled = false,
|
||||
label = "Муниципальное образование",
|
||||
)
|
||||
|
||||
val enabledField = when (userRequestDialogParams) {
|
||||
is ShowUserRequestDialogParams -> false
|
||||
is AcceptUserRequestDialogParams -> false
|
||||
is CreateUserRequestDialogParams -> true
|
||||
}
|
||||
|
||||
val requestTypeList = listOf( // TODO: по хорошему, нужно стягивать список по API
|
||||
Models.RequestType.Domain(
|
||||
id = 1,
|
||||
name = "Фиксация гибели пчел",
|
||||
),
|
||||
Models.RequestType.Domain(
|
||||
id = 2,
|
||||
name = "Фиксация факта нарушения сроков обработки полей",
|
||||
),
|
||||
)
|
||||
|
||||
DropdownField(
|
||||
valueList = requestTypeList,
|
||||
value = outUserRequestDomain.type,
|
||||
onValueChange = {
|
||||
outUserRequestDomain = outUserRequestDomain.copy(
|
||||
type = it,
|
||||
)
|
||||
},
|
||||
enabled = enabledField,
|
||||
label = "Тип обращения",
|
||||
)
|
||||
|
||||
MainTextField(
|
||||
value = outUserRequestDomain.address,
|
||||
onValueChange = {
|
||||
outUserRequestDomain = outUserRequestDomain.copy(
|
||||
address = it,
|
||||
)
|
||||
},
|
||||
enabled = enabledField,
|
||||
label = "Адрес",
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
|
||||
MainTextField(
|
||||
value = outUserRequestDomain.text,
|
||||
onValueChange = {
|
||||
outUserRequestDomain = outUserRequestDomain.copy(
|
||||
text = it,
|
||||
)
|
||||
},
|
||||
enabled = enabledField,
|
||||
label = "Сутьевая часть",
|
||||
keyboardType = KeyboardType.Text,
|
||||
singleLine = false,
|
||||
minLines = 3,
|
||||
maxLines = 6,
|
||||
)
|
||||
|
||||
if (userRequestDialogParams is AcceptUserRequestDialogParams)
|
||||
MainTextField(
|
||||
value = outUserRequestDomain.adminComment ?: "",
|
||||
onValueChange = {
|
||||
outUserRequestDomain = outUserRequestDomain.copy(
|
||||
adminComment = it,
|
||||
)
|
||||
},
|
||||
enabled = userRequestDomain.accepted == null,
|
||||
label = "Уточнение администратора",
|
||||
keyboardType = KeyboardType.Text,
|
||||
singleLine = false,
|
||||
minLines = 3,
|
||||
maxLines = 6,
|
||||
)
|
||||
|
||||
ImagePicker(
|
||||
imageList = outUserRequestDomain.imageList,
|
||||
onImageAdd = {
|
||||
Platform.pickFile { type, data ->
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.UserRequests.uploadImage(
|
||||
type = type,
|
||||
data = data,
|
||||
onSuccess = { imageGetDTO ->
|
||||
val imageList = outUserRequestDomain.imageList.toMutableList()
|
||||
imageList.add(imageGetDTO.toDomain())
|
||||
outUserRequestDomain = outUserRequestDomain.copy(
|
||||
imageList = imageList,
|
||||
)
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Файл успешно добавлен",
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось добавить файл",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onImageDrop = { i ->
|
||||
val imageList = outUserRequestDomain.imageList.toMutableList()
|
||||
imageList.removeAt(i)
|
||||
outUserRequestDomain = outUserRequestDomain.copy(
|
||||
imageList = imageList,
|
||||
)
|
||||
},
|
||||
onImageSelect = {
|
||||
imageView = it
|
||||
},
|
||||
interactionType = when (userRequestDialogParams) {
|
||||
is ShowUserRequestDialogParams -> InteractionType.Download
|
||||
is CreateUserRequestDialogParams -> InteractionType.Upload
|
||||
is AcceptUserRequestDialogParams -> InteractionType.Download
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
imageView?.let {
|
||||
ImageViewDialog(
|
||||
image = it,
|
||||
onClose = {
|
||||
imageView = null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
package ru.cit71.pchelovod71.ui.screens
|
||||
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.cit71.pchelovod71.core.Api
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import ru.cit71.pchelovod71.ui.components.MainColumnList
|
||||
import ru.cit71.pchelovod71.ui.components.MainScaffoldParams
|
||||
import ru.cit71.pchelovod71.ui.components.RegistrationRequestCard
|
||||
import ru.cit71.pchelovod71.ui.dialogs.AcceptUserDialogParams
|
||||
import ru.cit71.pchelovod71.ui.dialogs.ConfirmCommentDialogParams
|
||||
import ru.cit71.pchelovod71.ui.dialogs.ConfirmDialog
|
||||
import ru.cit71.pchelovod71.ui.dialogs.UserDialog
|
||||
|
||||
@Composable
|
||||
fun RegistrationRequestsScreen(
|
||||
mainScaffoldParams: MainScaffoldParams,
|
||||
) {
|
||||
mainScaffoldParams.setFloatingActionButton(null)
|
||||
|
||||
val apiCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
var userDomainList by remember { mutableStateOf(listOf<Models.User.Domain>()) }
|
||||
var municipalityDomainList by remember { mutableStateOf(listOf<Models.Municipality.Domain>()) }
|
||||
|
||||
var userDomainCard by remember { mutableStateOf<Models.User.Domain?>(null) }
|
||||
var userDomainDialog by remember { mutableStateOf<Models.User.Domain?>(null) }
|
||||
|
||||
var confirmCommentDialog by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Municipalities.get(
|
||||
onSuccess = { municipalityDTOList ->
|
||||
municipalityDomainList = municipalityDTOList.map { it.toDomain() }
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось загрузить список муниципальных образований"
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateUserDomainList() {
|
||||
Api.Users.get(
|
||||
verified = false,
|
||||
onSuccess = { userDTOList ->
|
||||
userDomainList = userDTOList.map { it.toDomain() }
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось загрузить список запросов на регистрацию",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
updateUserDomainList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun onAccept(
|
||||
userDomain: Models.User.Domain,
|
||||
) {
|
||||
Api.Users.accept(
|
||||
id = userDomain.id,
|
||||
user = userDomain.toAcceptDTO(),
|
||||
onSuccess = {
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Пользователь успешно принят",
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось принять пользователя",
|
||||
)
|
||||
},
|
||||
)
|
||||
updateUserDomainList()
|
||||
}
|
||||
|
||||
MainColumnList(
|
||||
paddingValues = mainScaffoldParams.paddingValues,
|
||||
valueList = userDomainList,
|
||||
placeholder = "Список заявок на регистрацию пуст",
|
||||
) {
|
||||
RegistrationRequestCard(
|
||||
userDomain = it,
|
||||
selected = it == userDomainCard,
|
||||
onSelectedChange = {
|
||||
userDomainCard = it
|
||||
},
|
||||
onAccept = { outUserDomain ->
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
onAccept(outUserDomain)
|
||||
}
|
||||
}
|
||||
},
|
||||
onEdit = {
|
||||
userDomainDialog = it
|
||||
},
|
||||
onDecline = { outUserDomain ->
|
||||
userDomainCard = outUserDomain
|
||||
confirmCommentDialog = true
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
userDomainDialog?.let { inUserDomain ->
|
||||
UserDialog(
|
||||
userDomain = inUserDomain,
|
||||
municipalityDomainList = municipalityDomainList,
|
||||
userDialogParams = AcceptUserDialogParams(
|
||||
onDismiss = {
|
||||
userDomainDialog = null
|
||||
},
|
||||
onAccept = { outUserDomain ->
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
onAccept(outUserDomain)
|
||||
}
|
||||
}
|
||||
userDomainDialog = null
|
||||
},
|
||||
onDecline = { outUserDomain ->
|
||||
userDomainCard = outUserDomain
|
||||
confirmCommentDialog = true
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
userDomainCard?.let { inUserDomain ->
|
||||
if (confirmCommentDialog)
|
||||
ConfirmDialog(
|
||||
confirmDialogParams = ConfirmCommentDialogParams(
|
||||
onDismiss = {
|
||||
confirmCommentDialog = false
|
||||
},
|
||||
onConfirm = { comment ->
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Users.reject(
|
||||
id = inUserDomain.id,
|
||||
user = inUserDomain.copy(
|
||||
deleteReason = comment,
|
||||
).toRejectDTO(),
|
||||
onSuccess = {
|
||||
confirmCommentDialog = false
|
||||
userDomainDialog = null
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Пользователь успешно отклонен",
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось отклонить пользователя",
|
||||
)
|
||||
},
|
||||
)
|
||||
updateUserDomainList()
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
dismissButtonText = "Отмена",
|
||||
confirmButtonText = "Отправить",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package ru.cit71.pchelovod71.ui.screens
|
||||
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.cit71.pchelovod71.core.Api
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import ru.cit71.pchelovod71.core.Platform
|
||||
import ru.cit71.pchelovod71.ui.components.MainColumn
|
||||
import ru.cit71.pchelovod71.ui.components.MainScaffoldParams
|
||||
import ru.cit71.pchelovod71.ui.components.SettingCard
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
mainScaffoldParams: MainScaffoldParams,
|
||||
) {
|
||||
val apiCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
var userDomain by remember { mutableStateOf<Models.User.Domain?>(null) }
|
||||
|
||||
mainScaffoldParams.setFloatingActionButton(null)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Users.auth(
|
||||
authParams = Models.AuthParams.DTO.Send(
|
||||
initData = Platform.getTelegramInitData(),
|
||||
),
|
||||
onSuccess = {
|
||||
userDomain = it.toDomain()
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось загрузить текущие настройки пользователя"
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MainColumn {
|
||||
userDomain?.let {
|
||||
SettingCard(
|
||||
name = "Уведомления",
|
||||
caption = "Подписаться на уведомления по инцидентам вашего муниципального образования",
|
||||
checked = userDomain!!.subscribed,
|
||||
onCheckedChange = {
|
||||
userDomain = userDomain!!.copy(
|
||||
subscribed = it
|
||||
)
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Users.subscribe(
|
||||
id = userDomain!!.id,
|
||||
user = userDomain!!.toSubscribeDTO(),
|
||||
onSuccess = {
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Настройки пользователя успешно применены"
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось применить настройки пользователя"
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
package ru.cit71.pchelovod71.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.cit71.pchelovod71.core.Api
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import ru.cit71.pchelovod71.ui.components.MainColumn
|
||||
import ru.cit71.pchelovod71.ui.components.DropdownField
|
||||
import ru.cit71.pchelovod71.ui.components.MainScaffoldParams
|
||||
import ru.cit71.pchelovod71.ui.components.MainTextField
|
||||
|
||||
@Composable
|
||||
fun RegistrationUserScreen(
|
||||
mainScaffoldParams: MainScaffoldParams,
|
||||
telegramId: String,
|
||||
reloadUserDomain: suspend () -> Unit,
|
||||
) {
|
||||
val apiCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
var municipalityDomainList by remember { mutableStateOf(listOf<Models.Municipality.Domain>()) }
|
||||
var userDomain by remember { mutableStateOf<Models.User.Domain?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Municipalities.get(
|
||||
onSuccess = { municipalityDTOList ->
|
||||
municipalityDomainList = municipalityDTOList.map { it.toDomain() }
|
||||
|
||||
userDomain = Models.User.Domain(
|
||||
telegramId = telegramId,
|
||||
municipality = municipalityDomainList[5],
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось загрузить список муниципальных образований"
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
userDomain?.let {
|
||||
MainColumn(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 32.dp,
|
||||
),
|
||||
paddingValues = mainScaffoldParams.paddingValues,
|
||||
verticalArrangement = Arrangement.spacedBy(32.dp),
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
text = "Помощник пчеловода",
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.displayMedium,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
text = "Заявка на регистрацию",
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
MainTextField(
|
||||
value = userDomain!!.fullName,
|
||||
onValueChange = {
|
||||
userDomain = userDomain!!.copy(
|
||||
fullName = it,
|
||||
)
|
||||
},
|
||||
label = "ФИО",
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
|
||||
DropdownField(
|
||||
valueList = municipalityDomainList,
|
||||
value = userDomain!!.municipality,
|
||||
onValueChange = {
|
||||
userDomain = userDomain!!.copy(
|
||||
municipality = it,
|
||||
)
|
||||
},
|
||||
label = "Муниципальное образование",
|
||||
)
|
||||
|
||||
MainTextField(
|
||||
value = userDomain!!.organization,
|
||||
onValueChange = {
|
||||
userDomain = userDomain!!.copy(
|
||||
organization = it,
|
||||
)
|
||||
},
|
||||
label = "Организация",
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
|
||||
MainTextField(
|
||||
value = userDomain!!.phoneNumber,
|
||||
onValueChange = {
|
||||
userDomain = userDomain!!.copy(
|
||||
phoneNumber = it,
|
||||
)
|
||||
},
|
||||
label = "Номер телефона",
|
||||
keyboardType = KeyboardType.Phone,
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
onClick = {
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Users.create(
|
||||
user = userDomain!!.toCreateDTO(),
|
||||
onSuccess = {
|
||||
reloadUserDomain()
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Заявка на регистрацию успешно отправлена!"
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось отправить заявку на регистрацию"
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = "Отправить",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WaitRegistrationUserScreen(
|
||||
mainScaffoldParams: MainScaffoldParams,
|
||||
) {
|
||||
MainColumn(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 32.dp,
|
||||
),
|
||||
paddingValues = mainScaffoldParams.paddingValues,
|
||||
verticalArrangement = Arrangement.spacedBy(32.dp),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
text = "Помощник пчеловода",
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.displayMedium,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
text = "Заявка на регистрацию находится на рассмотрении сотрудников министерства сельского хозяйства, природных ресурсов и экологии Тульской области. После одобрения вам поступит сообщение от чат-бота.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RejectRegistrationUserScreen(
|
||||
mainScaffoldParams: MainScaffoldParams,
|
||||
) {
|
||||
MainColumn(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 32.dp,
|
||||
),
|
||||
paddingValues = mainScaffoldParams.paddingValues,
|
||||
verticalArrangement = Arrangement.spacedBy(32.dp),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
text = "Помощник пчеловода",
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.displayMedium,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
text = "Заявка на регистрацию была отклонена сотрудником министерства сельского хозяйства, природных ресурсов и экологии Тульской области.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
package ru.cit71.pchelovod71.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import frontend.composeapp.generated.resources.Res
|
||||
import frontend.composeapp.generated.resources.outline_add_24
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import ru.cit71.pchelovod71.core.Api
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import ru.cit71.pchelovod71.ui.components.BooleanFilter
|
||||
import ru.cit71.pchelovod71.ui.components.MainColumnList
|
||||
import ru.cit71.pchelovod71.ui.components.MainScaffoldParams
|
||||
import ru.cit71.pchelovod71.ui.components.UserRequestCard
|
||||
import ru.cit71.pchelovod71.ui.dialogs.AcceptUserRequestDialogParams
|
||||
import ru.cit71.pchelovod71.ui.dialogs.ConfirmCommentDialogParams
|
||||
import ru.cit71.pchelovod71.ui.dialogs.ConfirmDialog
|
||||
import ru.cit71.pchelovod71.ui.dialogs.CreateUserRequestDialogParams
|
||||
import ru.cit71.pchelovod71.ui.dialogs.ShowUserRequestDialogParams
|
||||
import ru.cit71.pchelovod71.ui.dialogs.UserRequestDialog
|
||||
import ru.cit71.pchelovod71.ui.dialogs.UserRequestDialogParams
|
||||
|
||||
@Composable
|
||||
fun UserRequestsScreen(
|
||||
mainScaffoldParams: MainScaffoldParams,
|
||||
userDomain: Models.User.Domain,
|
||||
) {
|
||||
mainScaffoldParams.setFloatingActionButton(null)
|
||||
|
||||
val apiCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
var userRequestDomainList by remember { mutableStateOf(listOf<Models.UserRequest.Domain>()) }
|
||||
|
||||
var userRequestDomain by remember { mutableStateOf<Models.UserRequest.Domain?>(null) }
|
||||
var userRequestDialogParams by remember { mutableStateOf<UserRequestDialogParams?>(null) }
|
||||
|
||||
var confirmCommentDialog by remember { mutableStateOf(false) }
|
||||
|
||||
var floatingActionButton by remember { mutableStateOf(
|
||||
when (userDomain.role) {
|
||||
Models.Role.Admin -> false
|
||||
Models.Role.User -> true
|
||||
}
|
||||
) }
|
||||
|
||||
var acceptedFilter by remember { mutableStateOf<Boolean?>(null) }
|
||||
|
||||
suspend fun updateUserRequestDomainList() {
|
||||
Api.UserRequests.get(
|
||||
userId = when (userDomain.role) {
|
||||
Models.Role.Admin -> null
|
||||
Models.Role.User -> userDomain.id
|
||||
},
|
||||
status = when (acceptedFilter) {
|
||||
null -> Models.RequestStatus.New
|
||||
true -> Models.RequestStatus.Accepted
|
||||
false -> Models.RequestStatus.Rejected
|
||||
},
|
||||
onSuccess = { userRequestList ->
|
||||
userRequestDomainList = userRequestList.map { it.toDomain() }
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось загрузить список обращений",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
updateUserRequestDomainList()
|
||||
}
|
||||
}
|
||||
|
||||
fun onDismiss() {
|
||||
userRequestDomain = null
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
updateUserRequestDomainList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (userDomain.role) {
|
||||
Models.Role.Admin -> {
|
||||
mainScaffoldParams.setFloatingActionButton(null)
|
||||
}
|
||||
Models.Role.User -> {
|
||||
mainScaffoldParams.setFloatingActionButton {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
Text(
|
||||
text = "Добавить обращение",
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(Res.drawable.outline_add_24),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
userRequestDialogParams = CreateUserRequestDialogParams(
|
||||
onDismiss = ::onDismiss,
|
||||
onCreate = { outUserRequestDomain ->
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.UserRequests.create(
|
||||
userRequest = outUserRequestDomain.toCreateDTO(),
|
||||
onSuccess = {
|
||||
userRequestDomain = null
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Обращение успешно добавлено",
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось добавить обращение",
|
||||
)
|
||||
},
|
||||
)
|
||||
updateUserRequestDomainList()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
userRequestDomain = Models.UserRequest.Domain(
|
||||
user = userDomain,
|
||||
)
|
||||
},
|
||||
expanded = floatingActionButton,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(mainScaffoldParams.paddingValues),
|
||||
) {
|
||||
BooleanFilter(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
),
|
||||
valueMap = mapOf(
|
||||
"Принято" to true,
|
||||
"Новые" to null,
|
||||
"Отклонено" to false,
|
||||
),
|
||||
value = acceptedFilter,
|
||||
onValueChange = {
|
||||
acceptedFilter = it
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
updateUserRequestDomainList()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
MainColumnList(
|
||||
onScrollChange = {
|
||||
floatingActionButton = it
|
||||
},
|
||||
valueList = userRequestDomainList,
|
||||
placeholder = "Список обращений пуст",
|
||||
) {
|
||||
UserRequestCard(
|
||||
userRequestDomain = it,
|
||||
onClick = {
|
||||
when (userDomain.role) {
|
||||
Models.Role.Admin -> {
|
||||
userRequestDialogParams = AcceptUserRequestDialogParams(
|
||||
onDismiss = ::onDismiss,
|
||||
onAccept = { outUserRequestDomain ->
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.UserRequests.accept(
|
||||
id = it.id,
|
||||
request = outUserRequestDomain.copy(
|
||||
accepted = true,
|
||||
).toAcceptDTO(),
|
||||
onSuccess = {
|
||||
userRequestDomain = null
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Обращение успешно принято",
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось принять обращение",
|
||||
)
|
||||
},
|
||||
)
|
||||
updateUserRequestDomainList()
|
||||
}
|
||||
}
|
||||
},
|
||||
onDecline = { outUserRequestDomain ->
|
||||
userRequestDomain = outUserRequestDomain
|
||||
confirmCommentDialog = true
|
||||
},
|
||||
)
|
||||
}
|
||||
Models.Role.User -> {
|
||||
userRequestDialogParams = ShowUserRequestDialogParams(
|
||||
onDismiss = ::onDismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
userRequestDomain = it
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
userRequestDomain?.let { inUserRequestDomain ->
|
||||
userRequestDialogParams?.let { userRequestDialogParams ->
|
||||
UserRequestDialog(
|
||||
userRequestDomain = inUserRequestDomain,
|
||||
userRequestDialogParams = userRequestDialogParams,
|
||||
)
|
||||
}
|
||||
|
||||
if (confirmCommentDialog)
|
||||
ConfirmDialog(
|
||||
confirmDialogParams = ConfirmCommentDialogParams(
|
||||
onDismiss = {
|
||||
confirmCommentDialog = false
|
||||
},
|
||||
onConfirm = { comment ->
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.UserRequests.accept(
|
||||
id = inUserRequestDomain.id,
|
||||
request = inUserRequestDomain.copy(
|
||||
accepted = false,
|
||||
rejectComment = comment,
|
||||
).toAcceptDTO(),
|
||||
onSuccess = {
|
||||
confirmCommentDialog = false
|
||||
userRequestDomain = null
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Обращение успешно отклонено",
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось отклонить обращение",
|
||||
)
|
||||
},
|
||||
)
|
||||
updateUserRequestDomainList()
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
dismissButtonText = "Отмена",
|
||||
confirmButtonText = "Отправить",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
package ru.cit71.pchelovod71.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import frontend.composeapp.generated.resources.Res
|
||||
import frontend.composeapp.generated.resources.outline_add_24
|
||||
import io.ktor.http.clone
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import ru.cit71.pchelovod71.core.Api
|
||||
import ru.cit71.pchelovod71.core.Config
|
||||
import ru.cit71.pchelovod71.core.Models
|
||||
import ru.cit71.pchelovod71.core.Platform
|
||||
import ru.cit71.pchelovod71.ui.components.BooleanFilter
|
||||
import ru.cit71.pchelovod71.ui.components.MainColumnList
|
||||
import ru.cit71.pchelovod71.ui.components.MainScaffoldParams
|
||||
import ru.cit71.pchelovod71.ui.components.UserCard
|
||||
import ru.cit71.pchelovod71.ui.dialogs.ConfirmDialog
|
||||
import ru.cit71.pchelovod71.ui.dialogs.ConfirmOnlyDialogParams
|
||||
import ru.cit71.pchelovod71.ui.dialogs.CreateUserDialogParams
|
||||
import ru.cit71.pchelovod71.ui.dialogs.UpdateUserDialogParams
|
||||
import ru.cit71.pchelovod71.ui.dialogs.UserDialog
|
||||
import ru.cit71.pchelovod71.ui.dialogs.UserDialogParams
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun UsersScreen(
|
||||
mainScaffoldParams: MainScaffoldParams,
|
||||
) {
|
||||
val apiCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
var userDomainList by remember { mutableStateOf(listOf<Models.User.Domain>()) }
|
||||
var municipalityDomainList by remember { mutableStateOf(listOf<Models.Municipality.Domain>()) }
|
||||
|
||||
var userDomain by remember { mutableStateOf<Models.User.Domain?>(null) }
|
||||
var userDialogParams by remember { mutableStateOf<UserDialogParams?>(null) }
|
||||
|
||||
var confirmOnlyDialog by remember { mutableStateOf(false) }
|
||||
|
||||
var floatingActionButton by remember { mutableStateOf(true) }
|
||||
|
||||
var deletedFilter by remember { mutableStateOf<Boolean?>(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Municipalities.get(
|
||||
onSuccess = { municipalityDTOList ->
|
||||
municipalityDomainList = municipalityDTOList.map { it.toDomain() }
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось загрузить список муниципальных образований"
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateUserDomainList() {
|
||||
Api.Users.get(
|
||||
verified = if (deletedFilter != false) null else true,
|
||||
deleted = deletedFilter,
|
||||
onSuccess = { userDTOList ->
|
||||
userDomainList = userDTOList.mapNotNull { userDTO ->
|
||||
when (userDTO.roleId) {
|
||||
1 -> null
|
||||
else -> userDTO.toDomain()
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось загрузить список пользователей",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
updateUserDomainList()
|
||||
}
|
||||
}
|
||||
|
||||
fun onDismiss() {
|
||||
userDomain = null
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
updateUserDomainList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mainScaffoldParams.setFloatingActionButton {
|
||||
ExtendedFloatingActionButton(
|
||||
text = {
|
||||
Text(
|
||||
text = "Добавить пользователя",
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(Res.drawable.outline_add_24),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
if (municipalityDomainList.isNotEmpty()) {
|
||||
userDialogParams = CreateUserDialogParams(
|
||||
onDismiss = ::onDismiss,
|
||||
onCreate = { outUserDomain ->
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Users.create(
|
||||
user = outUserDomain.toCreateDTO(),
|
||||
onSuccess = {
|
||||
userDomain = null
|
||||
|
||||
val inviteUrl = Config.botUrlBuilder.clone().apply {
|
||||
parameters["startapp"] = it.uuid
|
||||
}.buildString()
|
||||
Platform.copyToClipboard(inviteUrl)
|
||||
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Пригласительная ссылка скопирована в буфер обмена",
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось добавить пользователя",
|
||||
)
|
||||
},
|
||||
)
|
||||
updateUserDomainList()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
userDomain = Models.User.Domain(
|
||||
municipality = municipalityDomainList[0],
|
||||
accepted = true,
|
||||
)
|
||||
}
|
||||
},
|
||||
expanded = floatingActionButton,
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(mainScaffoldParams.paddingValues),
|
||||
) {
|
||||
BooleanFilter(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
),
|
||||
valueMap = mapOf(
|
||||
"Принято" to false,
|
||||
"Все" to null,
|
||||
"Отклонено" to true,
|
||||
),
|
||||
value = deletedFilter,
|
||||
onValueChange = {
|
||||
deletedFilter = it
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
updateUserDomainList()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
MainColumnList(
|
||||
onScrollChange = {
|
||||
floatingActionButton = it
|
||||
},
|
||||
valueList = userDomainList,
|
||||
placeholder = "Список пользователей пуст",
|
||||
) {
|
||||
UserCard(
|
||||
userDomain = it,
|
||||
onClick = {
|
||||
userDialogParams = UpdateUserDialogParams(
|
||||
onDismiss = ::onDismiss,
|
||||
onSave = { outUserDomain ->
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Users.update(
|
||||
user = outUserDomain.toUpdateDTO(),
|
||||
onSuccess = {
|
||||
userDomain = null
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Пользователь успешно изменен",
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось изменить пользователя",
|
||||
)
|
||||
},
|
||||
)
|
||||
updateUserDomainList()
|
||||
}
|
||||
}
|
||||
},
|
||||
onDelete = {
|
||||
confirmOnlyDialog = true
|
||||
}
|
||||
)
|
||||
userDomain = it
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
userDomain?.let { inUserDomain ->
|
||||
userDialogParams?.let { userDialogParams ->
|
||||
UserDialog(
|
||||
userDomain = inUserDomain,
|
||||
municipalityDomainList = municipalityDomainList,
|
||||
userDialogParams = userDialogParams,
|
||||
)
|
||||
}
|
||||
|
||||
if (confirmOnlyDialog)
|
||||
ConfirmDialog(
|
||||
confirmDialogParams = ConfirmOnlyDialogParams(
|
||||
onDismiss = {
|
||||
confirmOnlyDialog = false
|
||||
},
|
||||
onConfirm = {
|
||||
apiCoroutineScope.launch {
|
||||
mainScaffoldParams.withLoadingIndicator {
|
||||
Api.Users.delete(
|
||||
id = inUserDomain.id,
|
||||
onSuccess = {
|
||||
confirmOnlyDialog = false
|
||||
userDomain = null
|
||||
mainScaffoldParams.showSuccessSnackbar(
|
||||
"Пользователь успешно удален",
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
mainScaffoldParams.showErrorSnackbar(
|
||||
"Не удалось удалить пользователя",
|
||||
)
|
||||
},
|
||||
)
|
||||
updateUserDomainList()
|
||||
}
|
||||
}
|
||||
},
|
||||
title = "Удаление пользователя",
|
||||
),
|
||||
dismissButtonText = "Отмена",
|
||||
confirmButtonText = "Удалить",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru-RU">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<title>Помощник пчеловода</title>
|
||||
<link type="text/css" rel="stylesheet" href="/styles.css" />
|
||||
<script src="https://telegram.org/js/telegram-web-app.js?57"></script>
|
||||
<script>const getTelegramInitData = () => window.Telegram.WebApp.initData</script>
|
||||
<script type="application/javascript" src="/composeApp.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,74 @@
|
||||
html {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
overflow: hidden !important;
|
||||
color-scheme: dark !important;
|
||||
}
|
||||
|
||||
body {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
left: 0 !important;
|
||||
margin: 0 !important;
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
canvas {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
div.blank {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
left: 0 !important;
|
||||
display: flex !important;
|
||||
justify-content: center !important;
|
||||
align-items: center !important;
|
||||
backdrop-filter: brightness(.5) !important;
|
||||
z-index: 99999 !important;
|
||||
user-select: none !important;
|
||||
}
|
||||
|
||||
div.container {
|
||||
display: flex !important;
|
||||
gap: 1.5rem !important;
|
||||
flex-direction: column !important;
|
||||
margin: 1rem !important;
|
||||
padding: 1.25rem 1rem !important;
|
||||
width: 100% !important;
|
||||
max-width: 34.25rem !important;
|
||||
border-radius: 1rem !important;
|
||||
background-color: #141218 !important;
|
||||
}
|
||||
|
||||
input.text-field,
|
||||
textarea.text-field {
|
||||
box-sizing: border-box !important;
|
||||
padding: 1.125rem !important;
|
||||
width: 100% !important;
|
||||
border: solid .0625rem #938F99 !important;
|
||||
border-radius: .25rem !important;
|
||||
outline: none !important;
|
||||
background-color: transparent !important;
|
||||
color: #E6E0E9 !important;
|
||||
font-family: 'Roboto', 'Helvetica', 'Arial', 'Verdana', sans-serif !important;
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
input[type="button"] {
|
||||
padding: .875rem !important;
|
||||
font-family: 'Roboto', 'Helvetica', 'Arial', 'Verdana', sans-serif !important;
|
||||
font-size: .875rem !important;
|
||||
font-weight: normal !important;
|
||||
border-radius: 99rem !important;
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
background: #D0BCFF !important;
|
||||
color: #381E72 !important;
|
||||
}
|
||||
Reference in New Issue
Block a user