feat(veranstaltung): Wizard für neue Veranstaltung implementiert und ZNS-Light-Integration hinzugefügt
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
||||
package at.mocode.frontend.core.domain.zns
|
||||
|
||||
data class ZnsImportState(
|
||||
val selectedFilePath: String? = null,
|
||||
val isUploading: Boolean = false,
|
||||
val jobId: String? = null,
|
||||
val jobStatus: String? = null,
|
||||
val progress: Int = 0,
|
||||
val progressDetail: String = "",
|
||||
val errors: List<String> = emptyList(),
|
||||
val errorMessage: String? = null,
|
||||
val isFinished: Boolean = false,
|
||||
)
|
||||
|
||||
interface ZnsImportProvider {
|
||||
val state: ZnsImportState
|
||||
fun onFileSelected(path: String)
|
||||
fun startImport(mode: String = "FULL")
|
||||
fun reset()
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
|
||||
/**
|
||||
* Feature-Modul: Veranstaltungs-Verwaltung (Desktop-only)
|
||||
* Kapselt alle Screens und Logik für Veranstaltungs-Übersicht, -Detail und -Neuanlage.
|
||||
* kapselt alle Screens und Logik für Veranstaltungs-Übersicht, -Detail und -Neuanlage.
|
||||
*/
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
@@ -40,6 +40,7 @@ kotlin {
|
||||
implementation(projects.frontend.core.network)
|
||||
implementation(projects.frontend.core.domain)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.frontend.core.auth)
|
||||
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.runtime)
|
||||
|
||||
+113
-94
@@ -13,6 +13,26 @@ import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.components.MsButton
|
||||
import at.mocode.frontend.core.designsystem.components.MsTextField
|
||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
import javax.swing.JFileChooser
|
||||
import javax.swing.filechooser.FileNameExtensionFilter
|
||||
|
||||
enum class MsWizardStep {
|
||||
ZNS_BASIS,
|
||||
META_DATEN,
|
||||
FACHLICHER_TYP
|
||||
}
|
||||
|
||||
private fun pickZipFile(): String? {
|
||||
val chooser = JFileChooser()
|
||||
val filter = FileNameExtensionFilter("ZNS ZIP Datei", "zip")
|
||||
chooser.fileFilter = filter
|
||||
val returnVal = chooser.showOpenDialog(null)
|
||||
return if (returnVal == JFileChooser.APPROVE_OPTION) {
|
||||
chooser.selectedFile.absolutePath
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formular zum Anlegen einer neuen Veranstaltung (Vision_03: /veranstaltung/neu).
|
||||
@@ -23,24 +43,17 @@ fun VeranstaltungNeuScreen(
|
||||
onBack: () -> Unit,
|
||||
onSave: () -> Unit,
|
||||
) {
|
||||
var selectedTab by remember { mutableIntStateOf(0) } // Stammdaten ist Standard-Tab
|
||||
val tabs = listOf("Stammdaten (A-Satz)", "Organisation", "Preisliste")
|
||||
var currentStep by remember { mutableStateOf(MsWizardStep.ZNS_BASIS) }
|
||||
|
||||
// Formular-State für Stammdaten
|
||||
// Formular-State
|
||||
var name by remember { mutableStateOf("") }
|
||||
var ort by remember { mutableStateOf("") }
|
||||
var startDatum by remember { mutableStateOf("") }
|
||||
var endDatum by remember { mutableStateOf("") }
|
||||
var veranstalter by remember { mutableStateOf("") }
|
||||
|
||||
// Validierung
|
||||
val isNameValid = name.isNotBlank()
|
||||
val isOrtValid = ort.isNotBlank()
|
||||
val isStartDatumValid = startDatum.length >= 8 // Einfache Prüfung für DD.MM.YY
|
||||
val isFormValid = isNameValid && isOrtValid && isStartDatumValid
|
||||
var veranstalterId by remember { mutableStateOf("") }
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Toolbar (Header)
|
||||
// Wizard Header
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shadowElevation = 2.dp,
|
||||
@@ -52,33 +65,58 @@ fun VeranstaltungNeuScreen(
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
) {
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||
IconButton(onClick = onBack) {
|
||||
IconButton(onClick = {
|
||||
if (currentStep == MsWizardStep.ZNS_BASIS) onBack()
|
||||
else currentStep = MsWizardStep.entries[currentStep.ordinal - 1]
|
||||
}) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
||||
}
|
||||
Spacer(Modifier.width(Dimens.SpacingS))
|
||||
Text(
|
||||
text = "Neue Veranstaltung anlegen",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = "Neue Veranstaltung anlegen",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = when (currentStep) {
|
||||
MsWizardStep.ZNS_BASIS -> "Schritt 1: ZNS-Basis & Veranstalter"
|
||||
MsWizardStep.META_DATEN -> "Schritt 2: Meta-Daten & DB-Initialisierung"
|
||||
MsWizardStep.FACHLICHER_TYP -> "Schritt 3: Fachlicher Typ"
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
|
||||
if (currentStep != MsWizardStep.FACHLICHER_TYP) {
|
||||
MsButton(
|
||||
text = "Weiter",
|
||||
onClick = { currentStep = MsWizardStep.entries[currentStep.ordinal + 1] },
|
||||
enabled = when (currentStep) {
|
||||
MsWizardStep.ZNS_BASIS -> true // Vereinfacht für Prototypen
|
||||
MsWizardStep.META_DATEN -> name.isNotBlank() && ort.isNotBlank()
|
||||
else -> true
|
||||
}
|
||||
)
|
||||
} else {
|
||||
MsButton(
|
||||
text = "Veranstaltung finalisieren",
|
||||
onClick = onSave,
|
||||
enabled = true
|
||||
)
|
||||
}
|
||||
}
|
||||
MsButton(
|
||||
text = "Veranstaltung speichern",
|
||||
onClick = onSave,
|
||||
enabled = isFormValid
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
PrimaryTabRow(selectedTabIndex = selectedTab) {
|
||||
tabs.forEachIndexed { index, title ->
|
||||
Tab(
|
||||
selected = selectedTab == index,
|
||||
onClick = { selectedTab = index },
|
||||
text = { Text(title) },
|
||||
)
|
||||
}
|
||||
}
|
||||
// Step Indicator
|
||||
LinearProgressIndicator(
|
||||
progress = { (currentStep.ordinal + 1).toFloat() / MsWizardStep.entries.size.toFloat() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -86,88 +124,69 @@ fun VeranstaltungNeuScreen(
|
||||
.padding(Dimens.SpacingL)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
when (selectedTab) {
|
||||
0 -> { // Stammdaten
|
||||
when (currentStep) {
|
||||
MsWizardStep.ZNS_BASIS -> {
|
||||
Column(
|
||||
modifier = Modifier.widthIn(max = 800.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)
|
||||
) {
|
||||
Text("ZNS-Daten importieren (Light-Modus)", style = MaterialTheme.typography.titleLarge)
|
||||
Text("Invertierter Workflow: Zuerst ZNS-Basisdaten laden, dann Veranstaltung anlegen.")
|
||||
|
||||
MsButton(
|
||||
text = "ZNS.zip auswählen & importieren",
|
||||
onClick = { pickZipFile() }
|
||||
)
|
||||
|
||||
MsTextField(
|
||||
value = veranstalterId,
|
||||
onValueChange = { veranstalterId = it },
|
||||
label = "Veranstalter (aus importierten Vereinen)",
|
||||
placeholder = "Verein suchen..."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MsWizardStep.META_DATEN -> {
|
||||
Column(
|
||||
modifier = Modifier.widthIn(max = 600.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)
|
||||
) {
|
||||
Text(
|
||||
"Allgemeine Informationen (A-Satz)",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
MsTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = "Name der Veranstaltung (z.B. Pfingstturnier)",
|
||||
placeholder = "Name eingeben",
|
||||
isError = name.isBlank(),
|
||||
errorMessage = if (name.isBlank()) "Name ist erforderlich" else null
|
||||
)
|
||||
|
||||
MsTextField(
|
||||
value = veranstalter,
|
||||
onValueChange = { veranstalter = it },
|
||||
label = "Veranstalter (Verein)",
|
||||
placeholder = "Verein suchen oder eingeben"
|
||||
)
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
|
||||
MsTextField(
|
||||
value = ort,
|
||||
onValueChange = { ort = it },
|
||||
label = "Ort",
|
||||
modifier = Modifier.weight(1f),
|
||||
isError = ort.isBlank()
|
||||
)
|
||||
MsTextField(
|
||||
value = "Österreich",
|
||||
onValueChange = {},
|
||||
label = "Land",
|
||||
modifier = Modifier.weight(0.5f),
|
||||
readOnly = true
|
||||
)
|
||||
}
|
||||
|
||||
Text("Veranstaltungs-Details", style = MaterialTheme.typography.titleLarge)
|
||||
MsTextField(value = name, onValueChange = { name = it }, label = "Name")
|
||||
MsTextField(value = ort, onValueChange = { ort = it }, label = "Ort")
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
|
||||
MsTextField(
|
||||
value = startDatum,
|
||||
onValueChange = { startDatum = it },
|
||||
label = "Startdatum (TT.MM.JJJJ)",
|
||||
placeholder = "24.04.2026",
|
||||
label = "Start",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
MsTextField(
|
||||
value = endDatum,
|
||||
onValueChange = { endDatum = it },
|
||||
label = "Enddatum (TT.MM.JJJJ)",
|
||||
placeholder = "26.04.2026",
|
||||
label = "Ende",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
"Hinweis: Diese Daten bilden die Basis für den ZNS-Import und die Abrechnung.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
1 -> { // Organisation
|
||||
Column(verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
|
||||
Text("Funktionäre & Verantwortliche", style = MaterialTheme.typography.titleLarge)
|
||||
Text("Hier werden Richter, Parcourschefs und Tierärzte zugewiesen.")
|
||||
// Später: MsSearchableSelect für Funktionäre
|
||||
}
|
||||
}
|
||||
|
||||
2 -> { // Preisliste
|
||||
Column(verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
|
||||
Text("Gebühren & Preisliste", style = MaterialTheme.typography.titleLarge)
|
||||
Text("Definition der Nenngebühren und Pauschalen gemäß ÖTO.")
|
||||
MsWizardStep.FACHLICHER_TYP -> {
|
||||
Column(
|
||||
modifier = Modifier.widthIn(max = 600.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)
|
||||
) {
|
||||
Text("Typ der Veranstaltung", style = MaterialTheme.typography.titleLarge)
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(Dimens.SpacingM),
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
) {
|
||||
Text("🏆 Turnier", modifier = Modifier.weight(1f), style = MaterialTheme.typography.titleMedium)
|
||||
RadioButton(selected = true, onClick = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package at.mocode.veranstaltung.feature.presentation
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import at.mocode.frontend.core.auth.data.AuthTokenManager
|
||||
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
|
||||
import at.mocode.frontend.core.network.NetworkConfig
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
enum class WizardStep {
|
||||
ZNS_IMPORT,
|
||||
META_DATA,
|
||||
TYPE_SELECTION
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
data class VeranstaltungWizardState(
|
||||
val currentStep: WizardStep = WizardStep.ZNS_IMPORT,
|
||||
val veranstalterId: Uuid? = null,
|
||||
val name: String = "",
|
||||
val ort: String = "",
|
||||
val startDatum: LocalDate? = null,
|
||||
val endDatum: LocalDate? = null,
|
||||
val isSaving: Boolean = false,
|
||||
val error: String? = null,
|
||||
val createdVeranstaltungId: Uuid? = null
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
class VeranstaltungWizardViewModel(
|
||||
private val httpClient: HttpClient,
|
||||
private val authTokenManager: AuthTokenManager,
|
||||
val znsViewModel: ZnsImportProvider
|
||||
) : ViewModel() {
|
||||
|
||||
var state by mutableStateOf(VeranstaltungWizardState())
|
||||
private set
|
||||
|
||||
fun nextStep() {
|
||||
state = state.copy(
|
||||
currentStep = when (state.currentStep) {
|
||||
WizardStep.ZNS_IMPORT -> WizardStep.META_DATA
|
||||
WizardStep.META_DATA -> WizardStep.TYPE_SELECTION
|
||||
WizardStep.TYPE_SELECTION -> WizardStep.TYPE_SELECTION
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun previousStep() {
|
||||
state = state.copy(
|
||||
currentStep = when (state.currentStep) {
|
||||
WizardStep.ZNS_IMPORT -> WizardStep.ZNS_IMPORT
|
||||
WizardStep.META_DATA -> WizardStep.ZNS_IMPORT
|
||||
WizardStep.TYPE_SELECTION -> WizardStep.META_DATA
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun updateMetaData(name: String, ort: String, start: LocalDate?, end: LocalDate?) {
|
||||
state = state.copy(name = name, ort = ort, startDatum = start, endDatum = end)
|
||||
}
|
||||
|
||||
fun setVeranstalter(id: Uuid) {
|
||||
state = state.copy(veranstalterId = id)
|
||||
}
|
||||
|
||||
fun saveVeranstaltung() {
|
||||
val veranstalterId = state.veranstalterId ?: return
|
||||
val start = state.startDatum ?: return
|
||||
val end = state.endDatum ?: return
|
||||
|
||||
viewModelScope.launch {
|
||||
state = state.copy(isSaving = true, error = null)
|
||||
try {
|
||||
val token = authTokenManager.authState.value.token
|
||||
val response = httpClient.post("${NetworkConfig.baseUrl}/api/events") {
|
||||
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(
|
||||
CreateEventRequest(
|
||||
name = state.name,
|
||||
startDatum = start,
|
||||
endDatum = end,
|
||||
ort = state.ort,
|
||||
veranstalterVereinId = veranstalterId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (response.status == HttpStatusCode.Created) {
|
||||
// Hier müsste die ID aus dem Response gelesen werden, falls benötigt
|
||||
state = state.copy(isSaving = false)
|
||||
nextStep()
|
||||
} else {
|
||||
state = state.copy(isSaving = false, error = "Fehler beim Speichern: ${response.status}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
state = state.copy(isSaving = false, error = "Netzwerkfehler: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
data class CreateEventRequest(
|
||||
val name: String,
|
||||
val startDatum: LocalDate,
|
||||
val endDatum: LocalDate,
|
||||
val ort: String,
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val veranstalterVereinId: Uuid
|
||||
)
|
||||
+8
-16
@@ -6,6 +6,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.mocode.frontend.core.auth.data.AuthTokenManager
|
||||
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
|
||||
import at.mocode.frontend.core.domain.zns.ZnsImportState
|
||||
import at.mocode.frontend.core.network.NetworkConfig
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.request.*
|
||||
@@ -20,17 +22,6 @@ import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
data class ZnsImportState(
|
||||
val selectedFilePath: String? = null,
|
||||
val isUploading: Boolean = false,
|
||||
val jobId: String? = null,
|
||||
val jobStatus: String? = null,
|
||||
val progress: Int = 0,
|
||||
val progressDetail: String = "",
|
||||
val errors: List<String> = emptyList(),
|
||||
val errorMessage: String? = null,
|
||||
val isFinished: Boolean = false,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class JobIdResponse(val jobId: String)
|
||||
@@ -51,19 +42,19 @@ private const val MAX_VISIBLE_ERRORS = 50
|
||||
class ZnsImportViewModel(
|
||||
private val httpClient: HttpClient,
|
||||
private val authTokenManager: AuthTokenManager,
|
||||
) : ViewModel() {
|
||||
) : ViewModel(), ZnsImportProvider {
|
||||
|
||||
var state by mutableStateOf(ZnsImportState())
|
||||
override var state by mutableStateOf(ZnsImportState())
|
||||
private set
|
||||
|
||||
private var pollingJob: Job? = null
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
fun onFileSelected(path: String) {
|
||||
override fun onFileSelected(path: String) {
|
||||
state = ZnsImportState(selectedFilePath = path)
|
||||
}
|
||||
|
||||
fun startImport() {
|
||||
override fun startImport(mode: String) {
|
||||
val filePath = state.selectedFilePath ?: return
|
||||
val file = File(filePath)
|
||||
if (!file.exists() || !file.name.endsWith(".zip", ignoreCase = true)) {
|
||||
@@ -79,6 +70,7 @@ class ZnsImportViewModel(
|
||||
try {
|
||||
val token = authTokenManager.authState.value.token
|
||||
val response: HttpResponse = httpClient.post("${NetworkConfig.baseUrl}/api/v1/import/zns") {
|
||||
parameter("mode", mode)
|
||||
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
|
||||
setBody(MultiPartFormDataContent(formData {
|
||||
append("file", file.readBytes(), Headers.build {
|
||||
@@ -129,7 +121,7 @@ class ZnsImportViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
override fun reset() {
|
||||
pollingJob?.cancel()
|
||||
state = ZnsImportState()
|
||||
}
|
||||
|
||||
+2
@@ -1,9 +1,11 @@
|
||||
package at.mocode.zns.feature.di
|
||||
|
||||
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
|
||||
import at.mocode.zns.feature.ZnsImportViewModel
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val znsImportModule = module {
|
||||
factory<ZnsImportProvider> { ZnsImportViewModel(get(named("apiClient")), get()) }
|
||||
factory { ZnsImportViewModel(get(named("apiClient")), get()) }
|
||||
}
|
||||
|
||||
+8
-2
@@ -114,7 +114,10 @@ fun StammdatenImportScreen(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(Icons.Default.Error, contentDescription = null, tint = MaterialTheme.colorScheme.error)
|
||||
Text(state.errorMessage, color = MaterialTheme.colorScheme.onErrorContainer)
|
||||
val errorMsg = state.errorMessage
|
||||
if (errorMsg != null) {
|
||||
Text(errorMsg, color = MaterialTheme.colorScheme.onErrorContainer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,7 +132,10 @@ fun StammdatenImportScreen(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text("Status", style = MaterialTheme.typography.titleMedium)
|
||||
StatusChip(state.jobStatus)
|
||||
val statusMsg = state.jobStatus
|
||||
if (statusMsg != null) {
|
||||
StatusChip(statusMsg)
|
||||
}
|
||||
}
|
||||
|
||||
LinearProgressIndicator(
|
||||
|
||||
Reference in New Issue
Block a user