diff options
author | Torsten Grote <t@grobox.de> | 2020-01-27 17:23:30 -0300 |
---|---|---|
committer | Torsten Grote <t@grobox.de> | 2020-01-30 15:03:38 -0300 |
commit | 7d299bf8358c854987aab61d139ca74c83079d17 (patch) | |
tree | b9dcae61779a808a3b0871b76dd9be6c42ba5323 /app | |
parent | 8080e4b4e96655c79fda8cf1bc9b4d8f084dec00 (diff) | |
download | merchant-terminal-android-7d299bf8358c854987aab61d139ca74c83079d17.tar.gz merchant-terminal-android-7d299bf8358c854987aab61d139ca74c83079d17.tar.bz2 merchant-terminal-android-7d299bf8358c854987aab61d139ca74c83079d17.zip |
Add screen to process an order
Diffstat (limited to 'app')
24 files changed, 821 insertions, 67 deletions
diff --git a/app/build.gradle b/app/build.gradle index 1408c8a..3a6c23b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -60,6 +60,9 @@ dependencies { implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" + // JSON parsing and serialization + implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.7" + testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0920771..49c9892 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,27 +1,36 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" package="net.taler.merchantpos"> + xmlns:tools="http://schemas.android.com/tools" + package="net.taler.merchantpos"> <uses-permission android:name="android.permission.NFC" /> - <uses-permission android:name="android.permission.INTERNET"/> - <uses-feature android:name="android.hardware.nfc" + <uses-permission android:name="android.permission.INTERNET" /> + + <uses-feature + android:name="android.hardware.nfc" android:required="true" /> + <uses-feature + android:name="android.hardware.telephony" + android:required="false" /> + <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/AppTheme" tools:ignore="GoogleAppIndexingWarning"> + android:theme="@style/AppTheme" + tools:ignore="GoogleAppIndexingWarning"> <activity android:name=".MainActivity" android:label="@string/app_name" + android:screenOrientation="landscape" android:theme="@style/AppTheme.NoActionBar"> <intent-filter> - <action android:name="android.intent.action.MAIN"/> + <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.LAUNCHER"/> + <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> diff --git a/app/src/main/java/net/taler/merchantpos/CreatePayment.kt b/app/src/main/java/net/taler/merchantpos/CreatePayment.kt index 83dbed8..f92bac7 100644 --- a/app/src/main/java/net/taler/merchantpos/CreatePayment.kt +++ b/app/src/main/java/net/taler/merchantpos/CreatePayment.kt @@ -9,7 +9,7 @@ import android.widget.Button import android.widget.EditText import android.widget.TextView import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProviders +import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.android.volley.Request import com.android.volley.RequestQueue @@ -25,7 +25,7 @@ import org.json.JSONObject */ class CreatePayment : Fragment() { private lateinit var queue: RequestQueue - private lateinit var model: PosTerminalViewModel + private val model: PosTerminalViewModel by activityViewModels() private var paused: Boolean = false @@ -47,10 +47,6 @@ class CreatePayment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - model = activity?.run { - ViewModelProviders.of(this)[PosTerminalViewModel::class.java] - } ?: throw Exception("Invalid Activity") - queue = Volley.newRequestQueue(context) } @@ -92,8 +88,13 @@ class CreatePayment : Fragment() { val params = mapOf("order_id" to orderId, "instance" to merchantConfig.instance) model.activeOrderId = orderId - val req = MerchantInternalRequest(Request.Method.GET, model.merchantConfig!!, "check-payment", params, null, - Response.Listener { onCheckPayment(it) }, Response.ErrorListener { onNetworkError(it) }) + val req = MerchantInternalRequest(Request.Method.GET, + model.merchantConfig!!, + "check-payment", + params, + null, + Response.Listener { onCheckPayment(it) }, + Response.ErrorListener { onNetworkError(it) }) queue.add(req) } @@ -119,7 +120,7 @@ class CreatePayment : Fragment() { ): View? { // Inflate the layout for this fragment val view = inflater.inflate(R.layout.fragment_create_payment, container, false) - val requestPaymentButton = view.findViewById<Button>(R.id.button_request_payment); + val requestPaymentButton = view.findViewById<Button>(R.id.button_request_payment) requestPaymentButton.setOnClickListener { onRequestPayment() } diff --git a/app/src/main/java/net/taler/merchantpos/MainActivity.kt b/app/src/main/java/net/taler/merchantpos/MainActivity.kt index 7c8330a..8cc2788 100644 --- a/app/src/main/java/net/taler/merchantpos/MainActivity.kt +++ b/app/src/main/java/net/taler/merchantpos/MainActivity.kt @@ -6,18 +6,18 @@ import android.nfc.Tag import android.nfc.tech.IsoDep import android.os.Bundle import android.util.Log -import androidx.core.view.GravityCompat +import android.view.Menu import android.view.MenuItem -import androidx.drawerlayout.widget.DrawerLayout -import com.google.android.material.navigation.NavigationView +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar -import android.view.Menu -import androidx.lifecycle.ViewModelProviders +import androidx.core.view.GravityCompat +import androidx.drawerlayout.widget.DrawerLayout import androidx.navigation.NavController import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupWithNavController +import com.google.android.material.navigation.NavigationView import net.taler.merchantpos.Utils.Companion.hexStringToByteArray import org.json.JSONObject import java.io.ByteArrayOutputStream @@ -143,7 +143,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte const val TAG = "taler-merchant" } - private lateinit var model: PosTerminalViewModel + private val model: PosTerminalViewModel by viewModels() private var nfcAdapter: NfcAdapter? = null private var currentTag: IsoDep? = null @@ -265,9 +265,9 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte navView.setNavigationItemSelectedListener(this) val navController = findNavController(R.id.nav_host_fragment) - val appBarConfiguration = - AppBarConfiguration( + val appBarConfiguration = AppBarConfiguration( setOf( + R.id.order, R.id.createPayment, R.id.merchantSettings, R.id.merchantHistory @@ -284,10 +284,8 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte val apiKey = prefs.getString("merchantBackendApiKey", "sandbox") val currency = prefs.getString("merchantBackendCurrency", "TESTKUDOS") - model = ViewModelProviders.of(this)[PosTerminalViewModel::class.java] model.merchantConfig = MerchantConfig(baseUrl!!, instance!!, apiKey!!, currency!!) - } override fun onBackPressed() { @@ -319,9 +317,8 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte // Handle navigation view item clicks here. val nav: NavController = findNavController(R.id.nav_host_fragment) when (item.itemId) { - R.id.nav_home -> { - nav.navigate(R.id.action_global_createPayment) - } + R.id.nav_home -> nav.navigate(R.id.action_global_createPayment) + R.id.nav_order -> nav.navigate(R.id.action_global_order) R.id.nav_history -> { nav.navigate(R.id.action_global_merchantHistory) } diff --git a/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt b/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt index b79f836..577615b 100644 --- a/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt +++ b/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt @@ -4,13 +4,13 @@ package net.taler.merchantpos import android.annotation.SuppressLint import android.os.Bundle import android.util.Log -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -87,7 +87,7 @@ fun parseTalerTimestamp(s: String): Instant { */ class MerchantHistory : Fragment() { private lateinit var queue: RequestQueue - private lateinit var model: PosTerminalViewModel + private val model: PosTerminalViewModel by activityViewModels() private val historyListAdapter = MyAdapter(listOf()) private val isLoading = MutableLiveData<Boolean>().apply { value = false } @@ -95,10 +95,6 @@ class MerchantHistory : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - model = activity?.run { - ViewModelProviders.of(this)[PosTerminalViewModel::class.java] - } ?: throw Exception("Invalid Activity") - queue = Volley.newRequestQueue(context) } @@ -166,7 +162,7 @@ class MerchantHistory : Fragment() { } this.isLoading.observe(this, androidx.lifecycle.Observer { loading -> - Log.v(TAG, "setting refreshing to ${loading}") + Log.v(TAG, "setting refreshing to $loading") refreshLayout.isRefreshing = loading }) @@ -174,6 +170,6 @@ class MerchantHistory : Fragment() { } companion object { - val TAG = "taler-merchant" + const val TAG = "taler-merchant" } } diff --git a/app/src/main/java/net/taler/merchantpos/ProcessPayment.kt b/app/src/main/java/net/taler/merchantpos/ProcessPayment.kt index fbd60c6..d78d873 100644 --- a/app/src/main/java/net/taler/merchantpos/ProcessPayment.kt +++ b/app/src/main/java/net/taler/merchantpos/ProcessPayment.kt @@ -1,28 +1,17 @@ package net.taler.merchantpos -import android.content.Context import android.graphics.Bitmap import android.graphics.Color -import android.net.Uri import android.os.Bundle -import androidx.fragment.app.Fragment +import android.os.Handler import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import com.google.zxing.BarcodeFormat -import com.google.zxing.common.BitMatrix -import com.google.zxing.qrcode.QRCodeWriter -import android.opengl.ETC1.getWidth -import android.opengl.ETC1.getHeight -import android.os.Handler -import android.util.Log import android.widget.Button import android.widget.ImageView import android.widget.TextView -import androidx.activity.OnBackPressedCallback -import androidx.activity.addCallback -import androidx.lifecycle.ViewModelProviders -import androidx.navigation.findNavController +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.android.volley.Request import com.android.volley.RequestQueue @@ -30,8 +19,10 @@ import com.android.volley.Response import com.android.volley.VolleyError import com.android.volley.toolbox.Volley import com.google.android.material.snackbar.Snackbar +import com.google.zxing.BarcodeFormat +import com.google.zxing.common.BitMatrix +import com.google.zxing.qrcode.QRCodeWriter import org.json.JSONObject -import java.net.URLEncoder // TODO: Rename parameter arguments, choose names that match @@ -47,17 +38,12 @@ class ProcessPayment : Fragment() { private var paused: Boolean = true private lateinit var queue: RequestQueue - private lateinit var model: PosTerminalViewModel + private val model: PosTerminalViewModel by activityViewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - model = activity?.run { - ViewModelProviders.of(this)[PosTerminalViewModel::class.java] - } ?: throw Exception("Invalid Activity") - queue = Volley.newRequestQueue(context) - } private fun onCheckPayment(checkPaymentResponse: JSONObject) { diff --git a/app/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt b/app/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt new file mode 100644 index 0000000..9d1ac5e --- /dev/null +++ b/app/src/main/java/net/taler/merchantpos/order/CategoriesFragment.kt @@ -0,0 +1,88 @@ +package net.taler.merchantpos.order + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.INVISIBLE +import android.view.ViewGroup +import android.widget.Button +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.Adapter +import kotlinx.android.synthetic.main.fragment_categories.* +import net.taler.merchantpos.R +import net.taler.merchantpos.order.CategoryAdapter.CategoryViewHolder + +interface CategorySelectionListener { + fun onCategorySelected(category: Category) +} + +class CategoriesFragment : Fragment(), CategorySelectionListener { + + private val viewModel: OrderViewModel by activityViewModels() + private val adapter = CategoryAdapter(this) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_categories, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + categoriesList.apply { + adapter = this@CategoriesFragment.adapter + layoutManager = LinearLayoutManager(requireContext()) + } + + viewModel.categories.observe(viewLifecycleOwner, Observer { categories -> + adapter.setItems(categories) + progressBar.visibility = INVISIBLE + }) + } + + override fun onCategorySelected(category: Category) { + viewModel.setCurrentCategory(category) + } + +} + +private class CategoryAdapter( + private val listener: CategorySelectionListener +) : Adapter<CategoryViewHolder>() { + + private val categories = ArrayList<Category>() + + override fun getItemCount() = categories.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoryViewHolder { + val view = + LayoutInflater.from(parent.context).inflate(R.layout.list_item_category, parent, false) + return CategoryViewHolder(view) + } + + override fun onBindViewHolder(holder: CategoryViewHolder, position: Int) { + holder.bind(categories[position]) + } + + fun setItems(items: List<Category>) { + categories.clear() + categories.addAll(items) + notifyDataSetChanged() + } + + private inner class CategoryViewHolder(v: View) : RecyclerView.ViewHolder(v) { + private val button: Button = v.findViewById(R.id.button) + + fun bind(category: Category) { + button.text = category.name + button.isPressed = category.selected + button.setOnClickListener { listener.onCategorySelected(category) } + } + } + +} diff --git a/app/src/main/java/net/taler/merchantpos/order/Definitions.kt b/app/src/main/java/net/taler/merchantpos/order/Definitions.kt new file mode 100644 index 0000000..ec7f77e --- /dev/null +++ b/app/src/main/java/net/taler/merchantpos/order/Definitions.kt @@ -0,0 +1,25 @@ +package net.taler.merchantpos.order + +import com.fasterxml.jackson.annotation.JsonProperty +import net.taler.merchantpos.Amount + +data class Category( + val id: Int, + val name: String +) { + var selected: Boolean = false +} + +data class Product( + @JsonProperty("product_id") + val id: String, + val description: String, + val price: String, + val categories: List<Int>, + @JsonProperty("delivery_location") + val location: String +) { + val priceAsDouble by lazy { Amount.fromString(price).amount.toDouble() } +} + +typealias OrderLine = Pair<Product, Int> diff --git a/app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt b/app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt new file mode 100644 index 0000000..3743281 --- /dev/null +++ b/app/src/main/java/net/taler/merchantpos/order/OrderFragment.kt @@ -0,0 +1,37 @@ +package net.taler.merchantpos.order + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.NavController +import androidx.navigation.Navigation.findNavController +import kotlinx.android.synthetic.main.fragment_order.* +import net.taler.merchantpos.R + +class OrderFragment : Fragment() { + + private val viewModel: OrderViewModel by activityViewModels() + + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_order, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + restartButton.setOnClickListener { viewModel.restart() } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + val nav: NavController = findNavController(requireActivity(), R.id.nav_host_fragment) + historyButton.setOnClickListener { nav.navigate(R.id.action_global_merchantHistory) } + } + +} diff --git a/app/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt b/app/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt new file mode 100644 index 0000000..928b688 --- /dev/null +++ b/app/src/main/java/net/taler/merchantpos/order/OrderStateFragment.kt @@ -0,0 +1,84 @@ +package net.taler.merchantpos.order + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.fragment_order_state.* +import net.taler.merchantpos.R +import net.taler.merchantpos.order.OrderAdapter.OrderViewHolder + +class OrderStateFragment : Fragment() { + + private val viewModel: OrderViewModel by activityViewModels() + private val adapter = OrderAdapter() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_order_state, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + orderList.apply { + adapter = this@OrderStateFragment.adapter + layoutManager = LinearLayoutManager(requireContext()) + } + + viewModel.order.observe(viewLifecycleOwner, Observer { order -> + adapter.setItems(order) + }) + viewModel.orderTotal.observe(viewLifecycleOwner, Observer { orderTotal -> + if (orderTotal == 0.0) { + totalView.text = null + } else { + totalView.text = getString(R.string.order_total, orderTotal) + } + }) + } + +} + +private class OrderAdapter : RecyclerView.Adapter<OrderViewHolder>() { + + private val orderLines = ArrayList<OrderLine>() + + override fun getItemCount() = orderLines.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OrderViewHolder { + val view = + LayoutInflater.from(parent.context).inflate(R.layout.list_item_order, parent, false) + return OrderViewHolder(view) + } + + override fun onBindViewHolder(holder: OrderViewHolder, position: Int) { + holder.bind(orderLines[position]) + } + + fun setItems(items: HashMap<Product, Int>) { + orderLines.clear() + items.forEach { t -> orderLines.add(t.toPair()) } + notifyDataSetChanged() + } + + private inner class OrderViewHolder(v: View) : RecyclerView.ViewHolder(v) { + private val quantity: TextView = v.findViewById(R.id.quantity) + private val name: TextView = v.findViewById(R.id.name) + private val price: TextView = v.findViewById(R.id.price) + + fun bind(orderLine: OrderLine) { + quantity.text = orderLine.second.toString() + name.text = orderLine.first.description + price.text = String.format("%.2f", orderLine.first.priceAsDouble * orderLine.second) + } + } + +} diff --git a/app/src/main/java/net/taler/merchantpos/order/OrderViewModel.kt b/app/src/main/java/net/taler/merchantpos/order/OrderViewModel.kt new file mode 100644 index 0000000..02ee33f --- /dev/null +++ b/app/src/main/java/net/taler/merchantpos/order/OrderViewModel.kt @@ -0,0 +1,130 @@ +package net.taler.merchantpos.order + +import android.app.Application +import android.util.Log +import androidx.annotation.UiThread +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations.map +import androidx.lifecycle.viewModelScope +import com.android.volley.Request.Method.GET +import com.android.volley.Response.ErrorListener +import com.android.volley.Response.Listener +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.Volley +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.json.JSONObject + +class OrderViewModel(app: Application) : AndroidViewModel(app) { + + companion object { + val TAG = OrderViewModel::class.java.simpleName + } + + private val url = "https://grobox.de/taler/products.json" + private val queue = Volley.newRequestQueue(app) + private val mapper = ObjectMapper() + .registerModule(KotlinModule()) + .configure(FAIL_ON_UNKNOWN_PROPERTIES, false) + + private val productsByCategory = HashMap<Category, ArrayList<Product>>() + + private val mOrder = MutableLiveData<HashMap<Product, Int>>() + internal val order: LiveData<HashMap<Product, Int>> = mOrder + internal val orderTotal: LiveData<Double> = map(mOrder) { map -> getTotal(map) } + + private val mProducts = MutableLiveData<List<Product>>() + internal val products: LiveData<List<Product>> = mProducts + + private val mCategories = MutableLiveData<List<Category>>() + internal val categories: LiveData<List<Category>> = mCategories + + init { + val stringRequest = JsonObjectRequest(GET, url, null, + Listener { response -> onConfigurationReceived(response) }, + ErrorListener { onConfigurationError() } + ) + queue.add(stringRequest) + } + + override fun onCleared() { + queue.cancelAll { !it.isCanceled } + } + + private fun onConfigurationReceived(json: JSONObject) = viewModelScope.launch(Dispatchers.IO) { + // parse categories + val categoriesStr = json.getJSONArray("categories").toString() + val categoriesType = object : TypeReference<List<Category>>() {} + val categories: List<Category> = mapper.readValue(categoriesStr, categoriesType) + mCategories.postValue(categories) + + // parse products (live data gets updated in setCurrentCategory()) + val productsStr = json.getJSONArray("products").toString() + val productsType = object : TypeReference<List<Product>>() {} + val products: List<Product> = mapper.readValue(productsStr, productsType) + + // group products by categories + productsByCategory.clear() + products.forEach { product -> + product.categories.forEach { categoryId -> + val category = categories.find { it.id == categoryId } + if (category == null) { + Log.e(TAG, "Product $product has unknown category $categoryId") + onConfigurationError() + return@launch + } + if (productsByCategory.containsKey(category)) { + productsByCategory[category]?.add(product) + } else { + productsByCategory[category] = ArrayList<Product>().apply { add(product) } + } + } + } + // pre-select the first category + if (productsByCategory.size > 0) setCurrentCategory(categories[0]) + else onConfigurationError() + } + + private fun onConfigurationError() { + Log.e("TEST", "ERROR") + // TODO + } + + internal fun setCurrentCategory(category: Category) { + val newCategories = categories.value?.apply { + forEach { if (it.selected) it.selected = false } + category.selected = true + } + mCategories.postValue(newCategories) + mProducts.postValue(productsByCategory[category]) + } + + @UiThread + internal fun addProduct(product: Product) { + val map = mOrder.value ?: HashMap() + val quantity = map[product] ?: 0 + map[product] = quantity + 1 + mOrder.value = map + } + + @UiThread + internal fun restart() { + mOrder.value = HashMap() + } + + private fun getTotal(map: HashMap<Product, Int>): Double { + var total = 0.0 + map.forEach { + val price = it.key.priceAsDouble + total += price * it.value + } + return total + } + +} diff --git a/app/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt b/app/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt new file mode 100644 index 0000000..2a028c0 --- /dev/null +++ b/app/src/main/java/net/taler/merchantpos/order/ProductsFragment.kt @@ -0,0 +1,93 @@ +package net.taler.merchantpos.order + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.INVISIBLE +import android.view.ViewGroup +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView.Adapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import kotlinx.android.synthetic.main.fragment_products.* +import net.taler.merchantpos.R +import net.taler.merchantpos.order.ProductAdapter.ProductViewHolder + +interface ProductSelectionListener { + fun onProductSelected(product: Product) +} + +class ProductsFragment : Fragment(), ProductSelectionListener { + + private val viewModel: OrderViewModel by activityViewModels() + private val adapter = ProductAdapter(this) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_products, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + productsList.apply { + adapter = this@ProductsFragment.adapter + layoutManager = GridLayoutManager(requireContext(), 3) + } + + viewModel.products.observe(viewLifecycleOwner, Observer { products -> + if (products == null) { + adapter.setItems(emptyList()) + } else { + adapter.setItems(products) + } + progressBar.visibility = INVISIBLE + }) + } + + override fun onProductSelected(product: Product) { + viewModel.addProduct(product) + } + +} + +private class ProductAdapter( + private val listener: ProductSelectionListener +) : Adapter<ProductViewHolder>() { + + private val products = ArrayList<Product>() + + override fun getItemCount() = products.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder { + val view = + LayoutInflater.from(parent.context).inflate(R.layout.list_item_product, parent, false) + return ProductViewHolder(view) + } + + override fun onBindViewHolder(holder: ProductViewHolder, position: Int) { + holder.bind(products[position]) + } + + fun setItems(items: List<Product>) { + products.clear() + products.addAll(items) + notifyDataSetChanged() + } + + private inner class ProductViewHolder(private val v: View) : ViewHolder(v) { + private val name: TextView = v.findViewById(R.id.name) + private val price: TextView = v.findViewById(R.id.price) + + fun bind(product: Product) { + name.text = product.description + price.text = product.priceAsDouble.toString() + v.setOnClickListener { listener.onProductSelected(product) } + } + } + +} diff --git a/app/src/main/res/layout/fragment_categories.xml b/app/src/main/res/layout/fragment_categories.xml new file mode 100644 index 0000000..dcd6bd5 --- /dev/null +++ b/app/src/main/res/layout/fragment_categories.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + xmlns:tools="http://schemas.android.com/tools" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/categoriesList" + android:layout_width="0dp" + tools:listitem="@layout/list_item_category" + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <ProgressBar + android:id="@+id/progressBar" + style="?android:attr/progressBarStyleLarge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/fragment_order.xml b/app/src/main/res/layout/fragment_order.xml new file mode 100644 index 0000000..462264d --- /dev/null +++ b/app/src/main/res/layout/fragment_order.xml @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.fragment.app.FragmentContainerView + android:id="@+id/fragment1" + android:name="net.taler.merchantpos.order.OrderStateFragment" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintBottom_toTopOf="@+id/restartButton" + app:layout_constraintEnd_toStartOf="@+id/guideline1" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:layout="@layout/fragment_order_state" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guideline1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_percent="0.25" /> + + <androidx.fragment.app.FragmentContainerView + android:id="@+id/fragment2" + android:name="net.taler.merchantpos.order.ProductsFragment" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintBottom_toTopOf="@+id/restartButton" + app:layout_constraintEnd_toStartOf="@+id/guideline2" + app:layout_constraintStart_toStartOf="@+id/guideline1" + app:layout_constraintTop_toTopOf="parent" + tools:layout="@layout/fragment_products" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guideline2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_percent="0.75" /> + + <androidx.fragment.app.FragmentContainerView + android:id="@+id/fragment3" + android:name="net.taler.merchantpos.order.CategoriesFragment" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintBottom_toTopOf="@+id/restartButton" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@+id/guideline2" + app:layout_constraintTop_toTopOf="parent" + tools:layout="@layout/fragment_categories" /> + + <Button + android:id="@+id/restartButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:backgroundTint="@color/bottomButtons" + android:text="@string/order_restart" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + + <Button + android:id="@+id/reconfigureButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:backgroundTint="@color/bottomButtons" + android:text="@string/button_reconfigure" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toEndOf="@+id/restartButton" /> + + <Button + android:id="@+id/historyButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:backgroundTint="@color/bottomButtons" + android:text="@string/button_history" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toEndOf="@+id/reconfigureButton" /> + + <Button + android:id="@+id/completeButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:backgroundTint="@color/bottomButtons" + android:text="@string/button_complete" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="1.0" + app:layout_constraintStart_toEndOf="@+id/historyButton" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/fragment_order_state.xml b/app/src/main/res/layout/fragment_order_state.xml new file mode 100644 index 0000000..0cd9c75 --- /dev/null +++ b/app/src/main/res/layout/fragment_order_state.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/orderList" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintBottom_toTopOf="@+id/totalView" + tools:listitem="@layout/list_item_order" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/totalView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="8dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="1.0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/orderList" + app:layout_constraintVertical_bias="1.0" + tools:text="Total: 23.75" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/fragment_products.xml b/app/src/main/res/layout/fragment_products.xml new file mode 100644 index 0000000..909fece --- /dev/null +++ b/app/src/main/res/layout/fragment_products.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + xmlns:tools="http://schemas.android.com/tools" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/productsList" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + tools:listitem="@layout/list_item_product" /> + + <ProgressBar + android:id="@+id/progressBar" + style="?android:attr/progressBarStyleLarge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/list_item_category.xml b/app/src/main/res/layout/list_item_category.xml new file mode 100644 index 0000000..496b96b --- /dev/null +++ b/app/src/main/res/layout/list_item_category.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <Button + android:id="@+id/button" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="Snacks" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/list_item_order.xml b/app/src/main/res/layout/list_item_order.xml new file mode 100644 index 0000000..5dc2f86 --- /dev/null +++ b/app/src/main/res/layout/list_item_order.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingStart="8dp" + android:paddingTop="8dp" + android:paddingEnd="8dp"> + + <TextView + android:id="@+id/quantity" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:gravity="end" + android:minWidth="24dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="31" /> + + <TextView + android:id="@+id/name" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + app:layout_constraintEnd_toStartOf="@+id/price" + app:layout_constraintStart_toEndOf="@+id/quantity" + app:layout_constraintTop_toTopOf="parent" + tools:text="An order product item that in some cases could have a very long name" /> + + <TextView + android:id="@+id/price" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="23.42" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/list_item_product.xml b/app/src/main/res/layout/list_item_product.xml new file mode 100644 index 0000000..51178a5 --- /dev/null +++ b/app/src/main/res/layout/list_item_product.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:clickable="true" + android:focusable="true" + app:cardUseCompatPadding="true"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="8dp"> + + <TextView + android:id="@+id/name" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:textColor="?android:textColorPrimary" + android:textStyle="bold" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="Steak and two Eggs" /> + + <TextView + android:id="@+id/price" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:textColor="?android:textColorSecondary" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@+id/name" + tools:text="7.95" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + +</com.google.android.material.card.MaterialCardView>
\ No newline at end of file diff --git a/app/src/main/res/menu/activity_main_drawer.xml b/app/src/main/res/menu/activity_main_drawer.xml index 9ce4a06..7a70824 100644 --- a/app/src/main/res/menu/activity_main_drawer.xml +++ b/app/src/main/res/menu/activity_main_drawer.xml @@ -1,20 +1,24 @@ <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - tools:showIn="navigation_view"> + xmlns:tools="http://schemas.android.com/tools" + tools:showIn="navigation_view"> <group android:checkableBehavior="single"> <item android:id="@+id/nav_home" android:icon="@drawable/ic_move_money_24dp" - android:title="@string/menu_home"/> + android:title="@string/menu_home" /> + <item + android:id="@+id/nav_order" + android:icon="@drawable/ic_move_money_24dp" + android:title="Order" /> <item android:id="@+id/nav_history" android:icon="@drawable/ic_history_black_24dp" - android:title="History"/> + android:title="History" /> <item android:id="@+id/nav_settings" android:icon="@drawable/ic_menu_manage" - android:title="Settings"/> + android:title="Settings" /> </group> </menu> diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 3a5b470..5951e77 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -1,8 +1,19 @@ <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_graph" - app:startDestination="@id/createPayment"> + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/nav_graph" + app:startDestination="@id/order"> + + <fragment + android:id="@+id/order" + android:name="net.taler.merchantpos.order.OrderFragment" + android:label="Order" + tools:layout="@layout/fragment_order"> + <action + android:id="@+id/action_createPayment_to_processPayment" + app:destination="@id/processPayment" /> + </fragment> <fragment android:id="@+id/createPayment" android:name="net.taler.merchantpos.CreatePayment" android:label="Request Payment" tools:layout="@layout/fragment_create_payment"> @@ -19,6 +30,7 @@ android:label="Payment History" tools:layout="@layout/fragment_merchant_history"/> <action android:id="@+id/action_global_merchantHistory" app:destination="@id/merchantHistory"/> <action android:id="@+id/action_global_createPayment" app:destination="@id/createPayment"/> + <action android:id="@+id/action_global_order" app:destination="@id/order"/> <fragment android:id="@+id/merchantSettings" android:name="net.taler.merchantpos.MerchantSettings" android:label="Merchant Settings" tools:layout="@layout/fragment_merchant_settings"/> <action android:id="@+id/action_global_merchantSettings" app:destination="@id/merchantSettings"/> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index cbe99a2..950c107 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -3,4 +3,6 @@ <color name="colorPrimary">#795548</color> <color name="colorPrimaryDark">#5D4037</color> <color name="colorAccent">#FFEB3B</color> + + <color name="bottomButtons">#9E9D24</color> </resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f3103af..09c7342 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,4 +16,10 @@ <!-- TODO: Remove or change this placeholder text --> <string name="hello_blank_fragment">Hello blank fragment</string> + + <string name="order_total">Total: %1$.2f</string> + <string name="order_restart">Restart</string> + <string name="button_reconfigure">Reconfigure</string> + <string name="button_history">History</string> + <string name="button_complete">Complete</string> </resources> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 1eb4629..9eac8b0 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,6 +1,6 @@ <resources> <!-- Base application theme. --> - <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> + <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> |