OrderFragment.kt (37002B)
1 /* 2 * This file is part of GNU Taler 3 * (C) 2020 Taler Systems S.A. 4 * 5 * GNU Taler is free software; you can redistribute it and/or modify it under the 6 * terms of the GNU General Public License as published by the Free Software 7 * Foundation; either version 3, or (at your option) any later version. 8 * 9 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY 10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along with 14 * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 package net.taler.merchantpos.order 18 19 import android.os.Bundle 20 import androidx.compose.foundation.BorderStroke 21 import androidx.compose.foundation.Image 22 import androidx.compose.foundation.background 23 import androidx.compose.foundation.clickable 24 import androidx.compose.foundation.horizontalScroll 25 import androidx.compose.foundation.verticalScroll 26 import androidx.compose.foundation.layout.Arrangement 27 import androidx.compose.foundation.layout.Box 28 import androidx.compose.foundation.layout.BoxWithConstraints 29 import androidx.compose.foundation.layout.Column 30 import androidx.compose.foundation.layout.IntrinsicSize 31 import androidx.compose.foundation.layout.Row 32 import androidx.compose.foundation.layout.Spacer 33 import androidx.compose.foundation.layout.fillMaxHeight 34 import androidx.compose.foundation.layout.fillMaxSize 35 import androidx.compose.foundation.layout.fillMaxWidth 36 import androidx.compose.foundation.layout.height 37 import androidx.compose.foundation.layout.heightIn 38 import androidx.compose.foundation.layout.padding 39 import androidx.compose.foundation.layout.size 40 import androidx.compose.foundation.layout.statusBarsPadding 41 import androidx.compose.foundation.layout.width 42 import androidx.compose.foundation.lazy.LazyColumn 43 import androidx.compose.foundation.lazy.items 44 import androidx.compose.foundation.rememberScrollState 45 import androidx.compose.material3.AlertDialog 46 import androidx.compose.material3.Button 47 import androidx.compose.material3.ButtonDefaults 48 import androidx.compose.material3.Card 49 import androidx.compose.material3.CardDefaults 50 import androidx.compose.material3.HorizontalDivider 51 import androidx.compose.material3.Icon 52 import androidx.compose.material3.MaterialTheme 53 import androidx.compose.material3.OutlinedButton 54 import androidx.compose.material3.OutlinedTextField 55 import androidx.compose.material3.Surface 56 import androidx.compose.material3.Text 57 import androidx.compose.runtime.Composable 58 import androidx.compose.runtime.LaunchedEffect 59 import androidx.compose.runtime.getValue 60 import androidx.compose.runtime.livedata.observeAsState 61 import androidx.compose.runtime.mutableStateOf 62 import androidx.compose.runtime.remember 63 import androidx.compose.runtime.saveable.rememberSaveable 64 import androidx.compose.runtime.setValue 65 import androidx.compose.ui.Alignment 66 import androidx.compose.ui.Modifier 67 import androidx.compose.ui.graphics.Color 68 import androidx.compose.ui.graphics.asImageBitmap 69 import androidx.compose.ui.platform.ComposeView 70 import androidx.compose.ui.platform.LocalConfiguration 71 import androidx.compose.ui.platform.ViewCompositionStrategy 72 import androidx.compose.ui.res.painterResource 73 import androidx.compose.ui.res.stringResource 74 import androidx.compose.ui.text.font.FontWeight 75 import androidx.compose.ui.text.style.TextAlign 76 import androidx.compose.ui.text.style.TextOverflow 77 import androidx.compose.ui.unit.dp 78 import androidx.fragment.app.Fragment 79 import androidx.fragment.app.activityViewModels 80 import net.taler.common.Amount 81 import net.taler.common.AmountParserException 82 import net.taler.lib.android.base64Bitmap 83 import net.taler.merchantpos.MainActivity 84 import net.taler.merchantpos.MainViewModel 85 import net.taler.merchantpos.PosDestination 86 import net.taler.merchantpos.R 87 import net.taler.merchantpos.compose.PosTheme 88 import net.taler.merchantpos.config.Category 89 import net.taler.merchantpos.config.ConfigProduct 90 import net.taler.merchantpos.showPosError 91 import kotlinx.coroutines.delay 92 import net.taler.merchantpos.order.RestartState.DISABLED 93 import androidx.compose.foundation.shape.RoundedCornerShape 94 95 class OrderFragment : Fragment() { 96 97 private val viewModel: MainViewModel by activityViewModels() 98 private val orderManager by lazy { viewModel.orderManager } 99 private val paymentManager by lazy { viewModel.paymentManager } 100 101 override fun onCreateView( 102 inflater: android.view.LayoutInflater, 103 container: android.view.ViewGroup?, 104 savedInstanceState: Bundle?, 105 ) = ComposeView(requireContext()).apply { 106 setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) 107 setContent { 108 OrderRoute( 109 viewModel = viewModel, 110 onNavigate = { destination -> 111 (requireActivity() as MainActivity).navigateTo(destination) 112 }, 113 onShowMessage = { message -> 114 requireActivity().showPosError(message) 115 }, 116 ) 117 } 118 } 119 120 override fun onStart() { 121 super.onStart() 122 if (!viewModel.configManager.config.isValid()) { 123 (requireActivity() as MainActivity).navigateTo(PosDestination.Config) 124 } else if (viewModel.configManager.currency == null) { 125 (requireActivity() as MainActivity).navigateTo(PosDestination.ConfigFetcher) 126 } 127 } 128 } 129 130 @Composable 131 private fun OrderRoute( 132 viewModel: MainViewModel, 133 onNavigate: (PosDestination) -> Unit, 134 onShowMessage: (String) -> Unit, 135 ) { 136 val orderManager = remember { viewModel.orderManager } 137 val paymentManager = remember { viewModel.paymentManager } 138 val currentOrderId by orderManager.currentOrderId.observeAsState() 139 val categories by orderManager.categories.observeAsState(emptyList()) 140 val products by orderManager.products.observeAsState(emptyList()) 141 val currency = viewModel.configManager.currency 142 val currencySpec = viewModel.configManager.currencySpec 143 144 val orderId = currentOrderId ?: return 145 val liveOrder = remember(orderId) { orderManager.getOrder(orderId) } 146 val order by liveOrder.order.observeAsState() 147 val orderTotal by liveOrder.orderTotal.observeAsState( 148 Amount.zero(currency ?: "").withSpec(currencySpec), 149 ) 150 val restartState by liveOrder.restartState.observeAsState(DISABLED) 151 val modifyAllowed by liveOrder.modifyOrderAllowed.observeAsState(false) 152 val increaseAllowed by liveOrder.increaseOrderAllowed.observeAsState(false) 153 val hasNextOrder by orderManager.hasNextOrder(orderId).observeAsState(false) 154 var selectedProductKey by rememberSaveable(orderId) { mutableStateOf(liveOrder.selectedProductKey) } 155 var selectedCategoryId by rememberSaveable { 156 mutableStateOf(categories.firstOrNull { it.selected }?.id) 157 } 158 var showCustomDialog by rememberSaveable { 159 mutableStateOf(false) 160 } 161 val reloadingText = stringResource(R.string.toast_reloading) 162 163 LaunchedEffect(Unit) { 164 while (true) { 165 delay(30_000) 166 viewModel.configManager.refreshInventory() 167 } 168 } 169 170 LaunchedEffect(order?.products, liveOrder.lastAddedProduct?.id) { 171 val productsInOrder = order?.products.orEmpty() 172 val selected = selectedProductKey?.let { key -> productsInOrder.find { it.id == key } } 173 val nextSelection = liveOrder.lastAddedProduct?.takeIf { added -> 174 productsInOrder.any { it.id == added.id } 175 } ?: selected ?: productsInOrder.lastOrNull() 176 selectedProductKey = nextSelection?.id 177 liveOrder.selectOrderLine(nextSelection) 178 } 179 180 LaunchedEffect(categories.map { it.id }) { 181 if (selectedCategoryId == null) { 182 selectedCategoryId = categories.firstOrNull { it.selected }?.id 183 } else if (categories.none { it.id == selectedCategoryId }) { 184 selectedCategoryId = categories.firstOrNull { it.selected }?.id 185 } 186 } 187 188 if (showCustomDialog && currency != null) { 189 CustomProductDialog( 190 currency = currency, 191 currencySpec = currencySpec, 192 onDismiss = { showCustomDialog = false }, 193 onAdd = { description, amount -> 194 val product = ConfigProduct( 195 description = description, 196 price = amount, 197 categories = listOf(Int.MIN_VALUE), 198 ) 199 orderManager.addProduct(orderId, product) 200 showCustomDialog = false 201 }, 202 ) 203 } 204 205 PosTheme { 206 Box( 207 modifier = Modifier 208 .fillMaxSize() 209 .statusBarsPadding(), 210 ) { 211 val isTabletLayout = LocalConfiguration.current.smallestScreenWidthDp >= 720 212 if (isTabletLayout) { 213 TabletOrderScreen( 214 categories = categories, 215 selectedCategoryId = selectedCategoryId, 216 products = products, 217 order = order, 218 increaseAllowed = increaseAllowed, 219 modifyAllowed = modifyAllowed, 220 orderTotal = orderTotal.toString(), 221 selectedProductKey = selectedProductKey, 222 onCategorySelected = { category -> 223 selectedCategoryId = category.id 224 orderManager.setCurrentCategory(category) 225 }, 226 onProductSelected = { product -> 227 orderManager.addProduct(orderId, product) 228 }, 229 onSelectProduct = { 230 selectedProductKey = it?.id 231 liveOrder.selectOrderLine(it) 232 }, 233 onIncrease = { liveOrder.increaseSelectedOrderLine() }, 234 onDecrease = { liveOrder.decreaseSelectedOrderLine() }, 235 onAddCustom = { showCustomDialog = true }, 236 onComplete = { 237 val currentOrder = order ?: return@TabletOrderScreen 238 paymentManager.createPayment(currentOrder) 239 onNavigate(PosDestination.ProcessPayment) 240 }, 241 ) 242 } else { 243 PhoneOrderScreen( 244 categories = categories, 245 selectedCategoryId = selectedCategoryId, 246 products = products, 247 order = order, 248 increaseAllowed = increaseAllowed, 249 modifyAllowed = modifyAllowed, 250 orderTotal = orderTotal.toString(), 251 selectedProductKey = selectedProductKey, 252 onCategorySelected = { category -> 253 selectedCategoryId = category.id 254 orderManager.setCurrentCategory(category) 255 }, 256 onProductSelected = { product -> 257 orderManager.addProduct(orderId, product) 258 }, 259 onSelectProduct = { 260 selectedProductKey = it?.id 261 liveOrder.selectOrderLine(it) 262 }, 263 onIncrease = { liveOrder.increaseSelectedOrderLine() }, 264 onDecrease = { liveOrder.decreaseSelectedOrderLine() }, 265 onAddCustom = { showCustomDialog = true }, 266 onComplete = { 267 val currentOrder = order ?: return@PhoneOrderScreen 268 paymentManager.createPayment(currentOrder) 269 onNavigate(PosDestination.ProcessPayment) 270 }, 271 ) 272 } 273 } 274 } 275 } 276 277 @Composable 278 private fun TabletOrderScreen( 279 categories: List<Category>, 280 selectedCategoryId: Int?, 281 products: List<ConfigProduct>, 282 order: Order?, 283 increaseAllowed: Boolean, 284 modifyAllowed: Boolean, 285 orderTotal: String, 286 selectedProductKey: String?, 287 onCategorySelected: (Category) -> Unit, 288 onProductSelected: (ConfigProduct) -> Unit, 289 onSelectProduct: (ConfigProduct?) -> Unit, 290 onIncrease: () -> Unit, 291 onDecrease: () -> Unit, 292 onAddCustom: () -> Unit, 293 onComplete: () -> Unit, 294 ) { 295 Row( 296 modifier = Modifier.fillMaxSize(), 297 horizontalArrangement = Arrangement.spacedBy(0.dp), 298 ) { 299 CategoriesPane( 300 categories = categories, 301 selectedCategoryId = selectedCategoryId, 302 modifier = Modifier.weight(0.25f), 303 compact = false, 304 onCategorySelected = onCategorySelected, 305 ) 306 Spacer( 307 modifier = Modifier 308 .width(1.dp) 309 .fillMaxHeight() 310 .background(MaterialTheme.colorScheme.outlineVariant), 311 ) 312 ProductsPane( 313 products = products, 314 modifier = Modifier.weight(0.50f), 315 compact = false, 316 onProductSelected = onProductSelected, 317 ) 318 Spacer( 319 modifier = Modifier 320 .width(1.dp) 321 .fillMaxHeight() 322 .background(MaterialTheme.colorScheme.outlineVariant), 323 ) 324 OrderColumnPane( 325 order = order, 326 increaseAllowed = increaseAllowed, 327 modifyAllowed = modifyAllowed, 328 orderTotal = orderTotal, 329 orderIsEmpty = order?.total?.isZero() != false, 330 selectedProductKey = selectedProductKey, 331 modifier = Modifier.weight(0.25f), 332 compact = false, 333 onSelectProduct = onSelectProduct, 334 onIncrease = onIncrease, 335 onDecrease = onDecrease, 336 onAddCustom = onAddCustom, 337 onComplete = onComplete, 338 ) 339 } 340 } 341 342 @Composable 343 private fun PhoneOrderScreen( 344 categories: List<Category>, 345 selectedCategoryId: Int?, 346 products: List<ConfigProduct>, 347 order: Order?, 348 increaseAllowed: Boolean, 349 modifyAllowed: Boolean, 350 orderTotal: String, 351 selectedProductKey: String?, 352 onCategorySelected: (Category) -> Unit, 353 onProductSelected: (ConfigProduct) -> Unit, 354 onSelectProduct: (ConfigProduct?) -> Unit, 355 onIncrease: () -> Unit, 356 onDecrease: () -> Unit, 357 onAddCustom: () -> Unit, 358 onComplete: () -> Unit, 359 ) { 360 Row( 361 modifier = Modifier 362 .fillMaxSize() 363 .padding(horizontal = 8.dp, vertical = 10.dp), 364 horizontalArrangement = Arrangement.spacedBy(0.dp), 365 ) { 366 CategoriesPane( 367 categories = categories, 368 selectedCategoryId = selectedCategoryId, 369 modifier = Modifier.weight(0.22f), 370 compact = true, 371 onCategorySelected = onCategorySelected, 372 ) 373 Spacer( 374 modifier = Modifier 375 .width(1.dp) 376 .fillMaxHeight() 377 .background(MaterialTheme.colorScheme.outlineVariant), 378 ) 379 ProductsPane( 380 products = products, 381 modifier = Modifier.weight(0.43f), 382 compact = true, 383 onProductSelected = onProductSelected, 384 ) 385 Spacer( 386 modifier = Modifier 387 .width(1.dp) 388 .fillMaxHeight() 389 .background(MaterialTheme.colorScheme.outlineVariant), 390 ) 391 OrderColumnPane( 392 order = order, 393 increaseAllowed = increaseAllowed, 394 modifyAllowed = modifyAllowed, 395 orderTotal = orderTotal, 396 orderIsEmpty = order?.total?.isZero() != false, 397 selectedProductKey = selectedProductKey, 398 modifier = Modifier.weight(0.35f), 399 compact = true, 400 onSelectProduct = onSelectProduct, 401 onIncrease = onIncrease, 402 onDecrease = onDecrease, 403 onAddCustom = onAddCustom, 404 onComplete = onComplete, 405 ) 406 } 407 } 408 409 @Composable 410 private fun CategoriesPane( 411 categories: List<Category>, 412 selectedCategoryId: Int?, 413 modifier: Modifier = Modifier, 414 compact: Boolean = false, 415 onCategorySelected: (Category) -> Unit, 416 ) { 417 Surface(modifier = modifier.fillMaxWidth()) { 418 LazyColumn( 419 modifier = Modifier 420 .fillMaxSize() 421 .padding( 422 start = if (compact) 4.dp else 8.dp, 423 top = if (compact) 8.dp else 12.dp, 424 end = if (compact) 4.dp else 8.dp, 425 ), 426 verticalArrangement = Arrangement.spacedBy(if (compact) 6.dp else 8.dp), 427 ) { 428 items(categories, key = { it.id }) { category -> 429 CategoryButton( 430 text = category.localizedName, 431 selected = category.id == selectedCategoryId, 432 compact = compact, 433 onClick = { onCategorySelected(category) }, 434 ) 435 } 436 } 437 } 438 } 439 440 @Composable 441 private fun CategoryButton( 442 text: String, 443 selected: Boolean, 444 compact: Boolean = false, 445 onClick: () -> Unit, 446 ) { 447 Surface( 448 modifier = Modifier 449 .fillMaxWidth() 450 .clickable(onClick = onClick), 451 color = if (selected) { 452 MaterialTheme.colorScheme.secondary 453 } else { 454 MaterialTheme.colorScheme.secondaryContainer 455 }, 456 contentColor = if (selected) { 457 MaterialTheme.colorScheme.onSecondary 458 } else { 459 MaterialTheme.colorScheme.onSecondaryContainer 460 }, 461 shape = RoundedCornerShape(30.dp), 462 border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), 463 ) { 464 Text( 465 text = text, 466 modifier = Modifier 467 .fillMaxWidth() 468 .padding( 469 horizontal = if (compact) 8.dp else 16.dp, 470 vertical = if (compact) 8.dp else 12.dp, 471 ), 472 textAlign = TextAlign.Center, 473 fontWeight = FontWeight.SemiBold, 474 style = if (compact) MaterialTheme.typography.bodySmall else MaterialTheme.typography.bodyMedium, 475 ) 476 } 477 } 478 479 @Composable 480 private fun ProductsPane( 481 products: List<ConfigProduct>, 482 modifier: Modifier = Modifier, 483 compact: Boolean = false, 484 onProductSelected: (ConfigProduct) -> Unit, 485 ) { 486 Surface(modifier = modifier.fillMaxWidth()) { 487 BoxWithConstraints( 488 modifier = Modifier 489 .fillMaxSize() 490 .padding( 491 start = if (compact) 4.dp else 8.dp, 492 top = if (compact) 8.dp else 12.dp, 493 end = if (compact) 4.dp else 8.dp, 494 ), 495 ) { 496 val spacing = if (compact) 6.dp else 8.dp 497 val minTileWidth = if (compact) 96.dp else 150.dp 498 val columns = maxOf(1, ((maxWidth + spacing) / (minTileWidth + spacing)).toInt()) 499 val productRows = remember(products, columns) { products.chunked(columns) } 500 501 Column( 502 modifier = Modifier.verticalScroll(rememberScrollState()), 503 verticalArrangement = Arrangement.spacedBy(spacing), 504 ) { 505 productRows.forEach { rowProducts -> 506 val rowHasImage = rowProducts.any { !it.image.isNullOrBlank() } 507 Row( 508 modifier = Modifier 509 .fillMaxWidth() 510 .height(IntrinsicSize.Min), 511 horizontalArrangement = Arrangement.spacedBy(spacing), 512 ) { 513 rowProducts.forEach { product -> 514 ProductCard( 515 product = product, 516 rowHasImage = rowHasImage, 517 compact = compact, 518 onClick = { onProductSelected(product) }, 519 modifier = Modifier 520 .weight(1f) 521 .fillMaxHeight(), 522 ) 523 } 524 repeat(columns - rowProducts.size) { 525 Spacer(modifier = Modifier.weight(1f)) 526 } 527 } 528 } 529 } 530 } 531 } 532 } 533 534 @Composable 535 private fun ProductCard( 536 product: ConfigProduct, 537 rowHasImage: Boolean, 538 compact: Boolean = false, 539 onClick: () -> Unit, 540 modifier: Modifier = Modifier, 541 ) { 542 val cardContainerColor = if (product.availableToSell) { 543 MaterialTheme.colorScheme.surface 544 } else { 545 MaterialTheme.colorScheme.surfaceVariant 546 } 547 val cardBorderColor = if (product.availableToSell) { 548 MaterialTheme.colorScheme.outlineVariant 549 } else { 550 MaterialTheme.colorScheme.error.copy(alpha = 0.4f) 551 } 552 Card( 553 modifier = modifier 554 .fillMaxWidth() 555 .fillMaxHeight() 556 .clickable(enabled = product.availableToSell, onClick = onClick), 557 colors = CardDefaults.cardColors( 558 containerColor = cardContainerColor, 559 ), 560 border = BorderStroke(1.dp, cardBorderColor), 561 elevation = CardDefaults.cardElevation(defaultElevation = 1.dp), 562 ) { 563 Column( 564 modifier = Modifier 565 .fillMaxSize() 566 .padding(if (compact) 6.dp else 8.dp), 567 verticalArrangement = Arrangement.spacedBy(if (compact) 3.dp else 4.dp), 568 ) { 569 val productBitmap = product.image?.base64Bitmap 570 val imageSize = if (compact) 40.dp else 64.dp 571 if (productBitmap != null) { 572 Image( 573 bitmap = productBitmap.asImageBitmap(), 574 contentDescription = product.displayName, 575 modifier = Modifier 576 .size(imageSize) 577 .align(Alignment.CenterHorizontally), 578 ) 579 } else if (rowHasImage) { 580 Spacer( 581 modifier = Modifier 582 .height(imageSize) 583 .fillMaxWidth(), 584 ) 585 } 586 Text( 587 text = product.displayName, 588 fontWeight = FontWeight.Bold, 589 textAlign = TextAlign.Center, 590 maxLines = 2, 591 overflow = TextOverflow.Ellipsis, 592 modifier = Modifier.fillMaxWidth(), 593 style = if (compact) MaterialTheme.typography.bodySmall else MaterialTheme.typography.bodyMedium, 594 ) 595 product.displayDescription?.let { 596 Text( 597 text = it, 598 style = if (compact) MaterialTheme.typography.labelSmall else MaterialTheme.typography.bodySmall, 599 textAlign = TextAlign.Center, 600 maxLines = 2, 601 overflow = TextOverflow.Ellipsis, 602 modifier = Modifier.fillMaxWidth(), 603 ) 604 } 605 Spacer(modifier = Modifier.weight(1f)) 606 Text( 607 text = product.displayPrice, 608 style = if (compact) MaterialTheme.typography.bodySmall else MaterialTheme.typography.bodyMedium, 609 textAlign = TextAlign.Center, 610 modifier = Modifier.fillMaxWidth(), 611 ) 612 if (!product.availableToSell) { 613 Text( 614 text = when { 615 product.currencyMismatch -> stringResource(R.string.product_wrong_currency) 616 product.remainingStock == 0 -> stringResource(R.string.product_out_of_stock) 617 else -> stringResource(R.string.product_unavailable) 618 }, 619 color = MaterialTheme.colorScheme.error, 620 style = if (compact) MaterialTheme.typography.labelSmall else MaterialTheme.typography.bodySmall, 621 fontWeight = FontWeight.Bold, 622 textAlign = TextAlign.Center, 623 modifier = Modifier.fillMaxWidth(), 624 ) 625 } 626 } 627 } 628 } 629 630 @Composable 631 private fun OrderPane( 632 order: Order?, 633 selectedProductKey: String?, 634 modifier: Modifier = Modifier, 635 compact: Boolean = false, 636 onSelectProduct: (ConfigProduct?) -> Unit, 637 ) { 638 Surface(modifier = modifier.fillMaxWidth()) { 639 Column(modifier = Modifier.fillMaxSize()) { 640 LazyColumn( 641 modifier = Modifier.weight(1f), 642 ) { 643 items(order?.products.orEmpty(), key = { it.id }) { product -> 644 val selected = selectedProductKey == product.id 645 OrderRow( 646 product = product, 647 selected = selected, 648 compact = compact, 649 onClick = { onSelectProduct(product) }, 650 ) 651 HorizontalDivider() 652 } 653 } 654 } 655 } 656 } 657 658 @Composable 659 private fun OrderRow( 660 product: ConfigProduct, 661 selected: Boolean, 662 compact: Boolean = false, 663 onClick: () -> Unit, 664 ) { 665 val rowPadding = if (compact) 6.dp else 8.dp 666 val imageSize = if (compact) 24.dp else 32.dp 667 val countWidth = if (compact) 18.dp else 24.dp 668 Row( 669 modifier = Modifier 670 .fillMaxWidth() 671 .clickable(onClick = onClick) 672 .background( 673 if (selected) MaterialTheme.colorScheme.secondaryContainer else Color.Transparent, 674 ) 675 .padding(rowPadding), 676 horizontalArrangement = Arrangement.spacedBy(if (compact) 6.dp else 8.dp), 677 verticalAlignment = Alignment.Top, 678 ) { 679 Text( 680 text = product.quantity.toString(), 681 modifier = Modifier.width(countWidth), 682 style = if (compact) MaterialTheme.typography.bodySmall else MaterialTheme.typography.bodyMedium, 683 ) 684 product.image?.base64Bitmap?.let { bitmap -> 685 Image( 686 bitmap = bitmap.asImageBitmap(), 687 contentDescription = product.displayName, 688 modifier = Modifier.size(imageSize), 689 ) 690 } ?: Spacer(Modifier.width(imageSize)) 691 Column(modifier = Modifier.weight(1f)) { 692 Text( 693 product.displayName, 694 style = if (compact) MaterialTheme.typography.bodySmall else MaterialTheme.typography.bodyMedium, 695 ) 696 product.displayDescription?.let { 697 Text(it, style = if (compact) MaterialTheme.typography.labelSmall else MaterialTheme.typography.bodySmall) 698 } 699 } 700 Text( 701 product.totalPrice.toString(), 702 style = if (compact) MaterialTheme.typography.bodySmall else MaterialTheme.typography.bodyMedium, 703 ) 704 } 705 } 706 707 @Composable 708 private fun OrderActionBar( 709 modifyAllowed: Boolean, 710 increaseAllowed: Boolean, 711 orderTotal: String, 712 orderIsEmpty: Boolean, 713 modifier: Modifier = Modifier, 714 compact: Boolean = false, 715 onIncrease: () -> Unit, 716 onDecrease: () -> Unit, 717 onAddCustom: () -> Unit, 718 onComplete: () -> Unit, 719 ) { 720 Column( 721 modifier = modifier.fillMaxWidth(), 722 verticalArrangement = Arrangement.spacedBy(12.dp), 723 ) { 724 Row( 725 modifier = Modifier 726 .fillMaxWidth() 727 .horizontalScroll(rememberScrollState()), 728 horizontalArrangement = Arrangement.spacedBy(if (compact) 8.dp else 12.dp), 729 ) { 730 Button(onClick = onIncrease, enabled = increaseAllowed, colors = orderControlButtonColors()) { 731 Text("+1", style = if (compact) MaterialTheme.typography.labelLarge else MaterialTheme.typography.bodyMedium) 732 } 733 Button(onClick = onDecrease, enabled = modifyAllowed, colors = orderControlButtonColors()) { 734 Text("-1", style = if (compact) MaterialTheme.typography.labelLarge else MaterialTheme.typography.bodyMedium) 735 } 736 Button(onClick = onAddCustom, colors = orderControlButtonColors()) { 737 Icon( 738 painter = painterResource(R.drawable.ic_dialpad), 739 contentDescription = stringResource(R.string.order_custom), 740 modifier = Modifier.size(if (compact) 20.dp else 24.dp), 741 ) 742 } 743 } 744 Button( 745 onClick = onComplete, 746 enabled = !orderIsEmpty, 747 modifier = Modifier 748 .fillMaxWidth() 749 .height(if (compact) 72.dp else 96.dp), 750 colors = completeButtonColors(), 751 ) { 752 Text( 753 if (orderIsEmpty) { 754 stringResource(R.string.order_complete) 755 } else { 756 stringResource(R.string.order_complete_with_amount, orderTotal) 757 }, 758 style = if (compact) MaterialTheme.typography.bodyLarge else MaterialTheme.typography.titleMedium, 759 fontWeight = FontWeight.Bold, 760 textAlign = TextAlign.Center, 761 ) 762 } 763 } 764 } 765 766 @Composable 767 private fun OrderColumnPane( 768 order: Order?, 769 increaseAllowed: Boolean, 770 modifyAllowed: Boolean, 771 orderTotal: String, 772 orderIsEmpty: Boolean, 773 selectedProductKey: String?, 774 modifier: Modifier = Modifier, 775 compact: Boolean = false, 776 onSelectProduct: (ConfigProduct?) -> Unit, 777 onIncrease: () -> Unit, 778 onDecrease: () -> Unit, 779 onAddCustom: () -> Unit, 780 onComplete: () -> Unit, 781 ) { 782 Column( 783 modifier = modifier 784 .fillMaxSize() 785 .padding(top = 12.dp, end = 12.dp, bottom = 12.dp), 786 verticalArrangement = Arrangement.spacedBy(12.dp), 787 ) { 788 OrderPane( 789 order = order, 790 selectedProductKey = selectedProductKey, 791 modifier = Modifier.weight(1f), 792 compact = compact, 793 onSelectProduct = onSelectProduct, 794 ) 795 OrderActionBar( 796 modifyAllowed = modifyAllowed, 797 increaseAllowed = increaseAllowed, 798 orderTotal = orderTotal, 799 orderIsEmpty = orderIsEmpty, 800 modifier = Modifier.padding(start = 12.dp), 801 compact = compact, 802 onIncrease = onIncrease, 803 onDecrease = onDecrease, 804 onAddCustom = onAddCustom, 805 onComplete = onComplete, 806 ) 807 } 808 } 809 810 @Composable 811 private fun orderControlButtonColors() = ButtonDefaults.buttonColors( 812 containerColor = MaterialTheme.colorScheme.primaryContainer, 813 contentColor = MaterialTheme.colorScheme.onPrimaryContainer, 814 disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant, 815 disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant, 816 ) 817 818 @Composable 819 private fun completeButtonColors() = ButtonDefaults.buttonColors( 820 containerColor = MaterialTheme.colorScheme.primary, 821 contentColor = MaterialTheme.colorScheme.onPrimary, 822 disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant, 823 disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant, 824 ) 825 826 @Composable 827 private fun CustomProductDialog( 828 currency: String, 829 currencySpec: net.taler.common.CurrencySpecification?, 830 onDismiss: () -> Unit, 831 onAdd: (String, Amount) -> Unit, 832 initialDescription: String? = null, 833 initialAmount: String? = null, 834 ) { 835 val defaultDescription = stringResource(R.string.order_custom_product_default) 836 val invalidAmountText = stringResource(R.string.refund_error_invalid_amount) 837 var description by rememberSaveable { mutableStateOf(initialDescription ?: defaultDescription) } 838 var amountText by rememberSaveable { mutableStateOf(initialAmount ?: "") } 839 var errorText by rememberSaveable { mutableStateOf<String?>(null) } 840 841 AlertDialog( 842 onDismissRequest = onDismiss, 843 title = { Text(stringResource(R.string.order_custom)) }, 844 text = { 845 Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { 846 OutlinedTextField( 847 value = description, 848 onValueChange = { description = it }, 849 label = { Text(stringResource(R.string.order_custom_product)) }, 850 ) 851 OutlinedTextField( 852 value = amountText, 853 onValueChange = { 854 amountText = it 855 errorText = null 856 }, 857 label = { Text(currency) }, 858 supportingText = errorText?.let { { Text(it) } }, 859 ) 860 } 861 }, 862 confirmButton = { 863 Button(onClick = { 864 val amount = try { 865 Amount.fromString(currency, amountText).withSpec(currencySpec) 866 } catch (_: AmountParserException) { 867 errorText = invalidAmountText 868 return@Button 869 } 870 onAdd(description, amount) 871 }) { 872 Text(stringResource(R.string.order_custom_add_button)) 873 } 874 }, 875 dismissButton = { 876 OutlinedButton(onClick = onDismiss) { 877 Text(stringResource(R.string.refund_abort)) 878 } 879 }, 880 ) 881 } 882 883 @Composable 884 internal fun OrderScreenContent( 885 viewModel: MainViewModel, 886 showCustomDialog: Boolean = false, 887 customDescription: String? = null, 888 customAmount: String? = null, 889 ) { 890 val orderManager = remember { viewModel.orderManager } 891 val currentOrderId by orderManager.currentOrderId.observeAsState() 892 val categories by orderManager.categories.observeAsState(emptyList()) 893 val products by orderManager.products.observeAsState(emptyList()) 894 val currency = viewModel.configManager.currency 895 val currencySpec = viewModel.configManager.currencySpec 896 897 val orderId = currentOrderId ?: return 898 val liveOrder = remember(orderId) { orderManager.getOrder(orderId) } 899 val order by liveOrder.order.observeAsState() 900 val orderTotal by liveOrder.orderTotal.observeAsState( 901 Amount.zero(currency ?: "").withSpec(currencySpec), 902 ) 903 val modifyAllowed by liveOrder.modifyOrderAllowed.observeAsState(false) 904 val increaseAllowed by liveOrder.increaseOrderAllowed.observeAsState(false) 905 var selectedProductKey by rememberSaveable(orderId) { mutableStateOf(liveOrder.selectedProductKey) } 906 var selectedCategoryId by rememberSaveable { 907 mutableStateOf(categories.firstOrNull { it.selected }?.id) 908 } 909 910 if (showCustomDialog && currency != null) { 911 CustomProductDialog( 912 currency = currency, 913 currencySpec = currencySpec, 914 onDismiss = {}, 915 onAdd = { _, _ -> }, 916 initialDescription = customDescription, 917 initialAmount = customAmount, 918 ) 919 } 920 921 PosTheme { 922 Box( 923 modifier = Modifier.fillMaxSize(), 924 ) { 925 val isTabletLayout = LocalConfiguration.current.smallestScreenWidthDp >= 720 926 if (isTabletLayout) { 927 TabletOrderScreen( 928 categories = categories, 929 selectedCategoryId = selectedCategoryId, 930 products = products, 931 order = order, 932 increaseAllowed = increaseAllowed, 933 modifyAllowed = modifyAllowed, 934 orderTotal = orderTotal.toString(), 935 selectedProductKey = selectedProductKey, 936 onCategorySelected = { selectedCategoryId = it.id }, 937 onProductSelected = {}, 938 onSelectProduct = { selectedProductKey = it?.id }, 939 onIncrease = {}, 940 onDecrease = {}, 941 onAddCustom = {}, 942 onComplete = {}, 943 ) 944 } else { 945 PhoneOrderScreen( 946 categories = categories, 947 selectedCategoryId = selectedCategoryId, 948 products = products, 949 order = order, 950 increaseAllowed = increaseAllowed, 951 modifyAllowed = modifyAllowed, 952 orderTotal = orderTotal.toString(), 953 selectedProductKey = selectedProductKey, 954 onCategorySelected = { selectedCategoryId = it.id }, 955 onProductSelected = {}, 956 onSelectProduct = { selectedProductKey = it?.id }, 957 onIncrease = {}, 958 onDecrease = {}, 959 onAddCustom = {}, 960 onComplete = {}, 961 ) 962 } 963 } 964 } 965 }