Skip to content

Commit

Permalink
#437 Basic mobile UI (#2564)
Browse files Browse the repository at this point in the history
* Initial import for basic mobile UI served from web application

* Added product list and details pages; improved dashboard and choose location pages; added library to detect barcode scans

* Added improvements to mobile UI

* Improvements for mobile UI

* #437 Fixed a few issues with the basic mobile UI before merging

* #437 Reverted changes to inventory snapshot page
  • Loading branch information
jmiranda committed Jul 30, 2021
1 parent d6459c3 commit c00dc83
Show file tree
Hide file tree
Showing 18 changed files with 583 additions and 15 deletions.
1 change: 1 addition & 0 deletions grails-app/conf/BuildConfig.groovy
Expand Up @@ -169,6 +169,7 @@ grails.project.dependency.resolution = {
compile(":webflow:1.3.8")
compile(":yui:2.8.2.1")
compile(":spring-events:1.2")
compile(":browser-detection:0.4.3")
//compile(":bubbling:2.1.4")

// Not critical to application (might require code changes)
Expand Down
8 changes: 6 additions & 2 deletions grails-app/conf/SecurityFilters.groovy
Expand Up @@ -18,10 +18,10 @@ import org.pih.warehouse.util.RequestUtil
class SecurityFilters {

static ArrayList controllersWithAuthUserNotRequired = ['test', 'errors']
static ArrayList actionsWithAuthUserNotRequired = ['status', 'test', 'login', 'logout', 'handleLogin', 'signup', 'handleSignup', 'json', 'updateAuthUserLocale', 'viewLogo', 'changeLocation']
static ArrayList actionsWithAuthUserNotRequired = ['status', 'test', 'login', 'logout', 'handleLogin', 'signup', 'handleSignup', 'json', 'updateAuthUserLocale', 'viewLogo', 'changeLocation', 'menu']

static ArrayList controllersWithLocationNotRequired = ['categoryApi', 'productApi', 'genericApi', 'api']
static ArrayList actionsWithLocationNotRequired = ['status', 'test', 'login', 'logout', 'handleLogin', 'signup', 'handleSignup', 'json', 'updateAuthUserLocale', 'viewLogo', 'chooseLocation']
static ArrayList actionsWithLocationNotRequired = ['status', 'test', 'login', 'logout', 'handleLogin', 'signup', 'handleSignup', 'json', 'updateAuthUserLocale', 'viewLogo', 'chooseLocation', 'menu']

def authService
def filters = {
Expand Down Expand Up @@ -59,6 +59,10 @@ class SecurityFilters {
return true
}

// This allows the menu to be g:include'd on mobile page (allowing for dynamic content to be added)
if (controllerName.equals("mobile") && actionName.equals("menu")) {
return true
}

// Not sure when this happens
if (params.controller == null) {
Expand Down
95 changes: 95 additions & 0 deletions grails-app/controllers/org/pih/warehouse/MobileController.groovy
@@ -0,0 +1,95 @@
/**
* Copyright (c) 2012 Partners In Health. All rights reserved.
* The use and distribution terms for this software are covered by the
* Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
* which can be found in the file epl-v10.html at the root of this distribution.
* By using this software in any fashion, you are agreeing to be bound by
* the terms of this license.
* You must not remove this notice, or any other, from this software.
**/
package org.pih.warehouse

import org.pih.warehouse.core.Location
import org.pih.warehouse.core.User
import org.pih.warehouse.order.Order
import org.pih.warehouse.order.OrderTypeCode
import org.pih.warehouse.product.Product
import org.pih.warehouse.product.ProductSummary
import org.pih.warehouse.requisition.Requisition

class MobileController {

def userService
def productService
def inventoryService
def locationService
def megamenuService

def index = {

Location location = Location.get(session.warehouse.id)
def productCount = ProductSummary.countByLocation(location)
def productListUrl = g.createLink(controller: "mobile", action: "productList")

def orderCount = Order.createCriteria().count {
eq("destination", location)
orderType {
eq("orderTypeCode", OrderTypeCode.PURCHASE_ORDER)
}
}

def requisitionCount = Requisition.createCriteria().count {
eq("origin", location)
}

[
data: [
[name: "Inventory Items", class: "fa fa-box", count: productCount, url: g.createLink(controller: "mobile", action: "productList")],
[name: "Purchase Orders", class: "fa fa-shopping-cart", count: orderCount, url: g.createLink(controller: "order", action: "list", params: ['origin.id', location.id])],
[name: "Replenishment Orders", class: "fa fa-truck", count: requisitionCount, url: g.createLink(controller: "stockMovement", action: "list", params: ['origin.id', location.id])],
]
]
}

def login = {

}

def menu = {
Map menuConfig = grailsApplication.config.openboxes.megamenu
//User user = User.get(session?.user?.id)
//Location location = Location.get(session.warehouse?.id)
//List translatedMenu = megamenuService.buildAndTranslateMenu(menuConfig, user, location)
[menuConfig:menuConfig]
}

def chooseLocation = {
User user = User.get(session.user.id)
Location warehouse = Location.get(session.warehouse.id)
render (view: "/mobile/chooseLocation",
model: [savedLocations: user.warehouse ? [user.warehouse] : null, loginLocationsMap: locationService.getLoginLocationsMap(user, warehouse)])
}

def productList = {
Location location = Location.get(session.warehouse.id)
def terms = params?.q ? params?.q?.split(" ") : "".split(" ")
def productSummaries = ProductSummary.createCriteria().list(max: params.max ?: 10, offset: params.offset ?: 0) {
eq("location", location)
order("product", "asc")
}
[productSummaries:productSummaries]
}

def productDetails = {
Product product = Product.findByIdOrProductCode(params.id, params.id)
Location location = Location.get(session.warehouse.id)
def productSummary = ProductSummary.findByProductAndLocation(product, location)
if (productSummary) {
[productSummary: productSummary]
}
else {
flash.message = "Product ${product.productCode} is not available in ${location.locationNumber}"
redirect(action: "productList")
}
}
}
Expand Up @@ -20,6 +20,7 @@ class ErrorsController {
MailService mailService
def userService
def grailsApplication
def userAgentIdentService

def handleException = {
if (RequestUtil.isAjax(request)) {
Expand All @@ -28,6 +29,11 @@ class ErrorsController {

render([errorCode: 500, cause: cause?.class, errorMessage: message] as JSON)
} else {
if (userAgentIdentService.isMobile()) {
render(view: "/mobile/error")
return
}

render(view: "/error")
}
}
Expand Down
Expand Up @@ -22,14 +22,14 @@ class AuthController {
def grailsApplication
def recaptchaService
def ravenClient
def userAgentIdentService

static allowedMethods = [login: "GET", doLogin: "POST", logout: "GET"]

/**
* Show index page - just a redirect to the list page.
*/
def index = {
log.info "auth controller index"
redirect(action: "login", params: params)
}

Expand All @@ -52,6 +52,11 @@ class AuthController {
redirect(controller: "dashboard", action: "index")
}

if (userAgentIdentService.isMobile()) {
redirect(controller: "mobile", action: "login")
return
}

}


Expand Down
Expand Up @@ -16,6 +16,9 @@ import org.apache.commons.lang.StringEscapeUtils
import org.pih.warehouse.core.Comment
import org.pih.warehouse.core.Location
import org.pih.warehouse.core.Tag
import org.pih.warehouse.core.UnitOfMeasure
import org.pih.warehouse.core.UnitOfMeasureClass
import org.pih.warehouse.core.UnitOfMeasureType
import org.pih.warehouse.core.User
import org.pih.warehouse.inventory.InventoryItem
import org.pih.warehouse.inventory.Transaction
Expand All @@ -40,6 +43,7 @@ class DashboardController {
def sessionFactory
def grailsApplication
def locationService
def userAgentIdentService

def showCacheStatistics = {
def statistics = sessionFactory.statistics
Expand Down Expand Up @@ -96,6 +100,11 @@ class DashboardController {
}

def index = {
if (userAgentIdentService.isMobile()) {
redirect(controller: "mobile")
return
}

render(template: "/common/react")
}

Expand Down Expand Up @@ -193,6 +202,14 @@ class DashboardController {
return
}

if (userAgentIdentService.isMobile()) {
render (view: "/mobile/chooseLocation",
model: [savedLocations: user.warehouse ? [user.warehouse] : null, loginLocationsMap: locationService.getLoginLocationsMap(user, warehouse)])
return
}



[savedLocations: user.warehouse ? [user.warehouse] : null, loginLocationsMap: locationService.getLoginLocationsMap(user, warehouse)]
}

Expand Down
22 changes: 11 additions & 11 deletions grails-app/views/auth/login.gsp
Expand Up @@ -12,23 +12,23 @@
</head>
<body>
<div class="body">
<g:form controller="auth" action="handleLogin" method="post">
<g:form controller="auth" action="handleLogin" method="post">

<g:hiddenField name="targetUri" value="${params?.targetUri}" />
<g:hiddenField id="browserTimezone" name="browserTimezone" />

<div id="loginContainer" class="dialog">
<div id="loginForm">
<g:if test="${flash.message}">
<div class="message">${flash.message}</div>
</g:if>
</g:if>

<g:hasErrors bean="${userInstance}">
<div class="errors">
<g:renderErrors bean="${userInstance}" as="list" />
</div>
</g:hasErrors>

<div id="loginBox" class="box">
<h2>
<img src="${createLinkTo(dir:'images/icons/silk',file:'lock.png')}" class="middle"/>
Expand All @@ -51,7 +51,7 @@
<td class="middle center">
<button type="submit" class="button big" id="loginButton">
<g:message code="auth.login.label"/>
</button>
</button>
</td>
</tr>

Expand All @@ -65,13 +65,13 @@
</tbody>
</table>
</div>

</div>
</div>
</g:form>
</div>
<script type="text/javascript">

<script type="text/javascript">
$(document).ready(function() {
var timezone = jzTimezoneDetector.determine_timezone().timezone; // Now you have an instance of the TimeZone object.
Expand All @@ -82,6 +82,6 @@
openboxes.expireFromLocal();
});
</script>
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion grails-app/views/inventorySnapshot/show.gsp
Expand Up @@ -14,4 +14,4 @@
<body>

</body>
</html>
</html>
60 changes: 60 additions & 0 deletions grails-app/views/layouts/bootstrap.gsp
@@ -0,0 +1,60 @@
<%@ page contentType="text/html;charset=UTF-8" %>
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><g:layoutTitle default="OpenBoxes"/></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl"
crossorigin="anonymous">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css"
rel="stylesheet"/>


</head>

<body>
<g:include controller="mobile" action="menu"/>
<h1><g:layoutTitle/></h1>
<g:layoutBody/>
<script
src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
integrity="sha256-4+XzXVhsDmqanXGHaHvgh1gMQKX40OUvDEBTu8JcmNs="
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-b5kHyXgcpbZJO/tY9Ul7kGkf1S0CWuKcCD38l8YkeH8z8QjE0GmW1gYU5S9FOnJ0"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/js/all.min.js"></script>
<script src="/openboxes/js/onScan/onScan.min.js" type="text/javascript"></script>
<script>
$(document).ready(function() {
// Enable scan events for the entire document
console.log("initialize onScan");
onScan.attachTo(document, {
minLength: 3,
suffixKeyCodes: [13], // enter-key expected at the end of a scan
//reactToPaste: true, // Compatibility to built-in scanners in paste-mode (as opposed to keyboard-mode)
onScan: function(scanned, count) {
console.log('Scanned: ', count, 'x ', scanned);
alert("Scanned " + scanned)
},
onKeyDetect: function(keyCode, event){
console.log('Pressed: ', keyCode, event);
},
onScanError: function(obj) {
console.log('onScanError: ', obj);
},
onScanButtonLongPress: function(obj) {
console.log('onScanButtonLongPress: ', obj);
},
onKeyProcess: function(char, event) {
console.log('onKeyProcess: ', char, event);
}
});
});
</script>
</body>
</html>

0 comments on commit c00dc83

Please sign in to comment.