-
Notifications
You must be signed in to change notification settings - Fork 27
Design
Note: much of the information on this page is out of date and needs updating.
We use react native for all of our UI, following the patterns they prescribe in terms of object composition leading to a hierarchy (or dom) of components rendered on screen.
The aim is to keep the business logic out of the files containing rendered components. We instead keep it in other modules. Most of the rules around what happens given certain modifications to data are controlled by code within the classes wrapping database objects. For example, finalising an invoice involves the UI layer calling transaction.finalise(), and the specific instance of a Transaction database object is responsible for carrying out the cascading effects like locking the invoice for editing and making changes to inventory. Other business logic sits in the relevant module, most notably authentication and synchronisation.
We have a module for each distinct, encapsulated part of the app. Each has an index.js, containing exports for any of the classes or functions within the module that should be shared publicly outside of the module. This is a convention that Javascript sets out, and allows those classes/functions exported to be accessed simply by referring to the module name (directory name), e.g. import { formatStatus } from './utilities';
The index.js of each module is also the place to put contain named constants that are used throughout the app.
We don't have any checks to assert that things deep within a module are not accessed, so we as developers need to enforce that only classes/functions/constants that are publicly exposed through the index.js are used outside of the module.
Within each component (this includes pages), we have set up a convention that there is access to two different sets of styles, localStyles and globalStyles. We have been fairly loose (not a good thing) in how we have used the two, but a basic description of what we put in each follows.
Local styles is a stylesheet at the bottom of a particular javascript file which specifies styles for the component in the same file. We use this stylesheet to hold on to styles that are needed by that component whether it is in the mSupply Mobile app or another app, though it may refer to globalStyles to be more specific.
Global styles is a module (globalStyles) that contains exports for all the styling that is specific to the mSupply app as opposed to any other app. This includes colours, fonts, sizes, and specific layouts that may not be used by other apps.
The module is broken down into subfiles representing distinct parts of the app, e.g. buttonStyles, navigationStyles, etc. Anything that needs to be available to components should be exported in the index.js of the module.
mSupply Mobile has two authentication phases - one to initialise the specific tablet with sync site credentials that accompany every sync request, and a second to log in a specific user.
On the first use of a fresh app installation, a login screen is presented allowing the user (often a consultant) to enter the URL of the mSupply server, the sync site name, and the sync site password. These details should have already been configured on the mSupply server, so that the sync site is ready to go (see synchronisation for how to set this up.
- When the user has entered something in all three fields, the 'connect' button becomes enabled.
- When the user presses 'connect', the authenticateAsync utility (in the sussol-utilities npm package) is used to check that the provided credentials authenticate against the provided URL.
- If there is an error (wrong password, server doesn't exist, no internet), the error message is displayed in the connect button
- If the authentication is successful, several details are saved for later use by the app
- The provided URL is saved to sync against in future, along with the provided credentials to pass in the auth header with every sync request
- The server should respond to successful authentication with details about the sync site/store to be saved in settings and used by the app, including ServerID, SiteID, StoreID, NameID, SupplyingStoreID, and SupplyingStoreNameID
- If the authentication is successful, the app also begins its initial sync, to get all of the data it needs for the store on that sync site. It will count down the number of records left to sync in the connect button area. For more on this initial sync, see the initial sync section.
Every time the app is closed and reopened, or the user presses logout, A login screen is presented over the top of the app every time
- The app is closed and reopened
- The user presses logout (on the bottom left of the menu/home screen)
- A regular attempt to reauthenticate fails
- We poll the server every 10 minutes to check that the user's saved credentials are still valid, authenticating in exactly the same way as we do when they press 'Login' (described below)
- This is to allow a mechanism for locking a user out, as you can change their password on the server and it will essentially end their session (unless they are not connected to the internet)
- Because the login screen is simply overlayed on top of the app, when they log back in successfully, the app will be open to the same screen as when they were logged out
When the login screen is presented
- If a user has logged in before, the username field will be prefilled with the last successfully logged in user
- The password field will always start blank; there is no 'auto-login' or 'remember me'
- When something has been entered in both the 'username' and 'password' fields, the 'Login' button becomes enabled. Before this, it is disabled.
- When the user taps the 'Login' button, it uses the same authenticateAsync utility as the sync authentication to authenticate against the mSupply server. The mSupply server URL it uses is the one saved in settings from the initial sync.
- If there is an error due to a bad connection to the server (no internet on either end, server down, etc.), we first check to see if we have this user cached
- If a user with the matching username has logged in successfully before, we will have their last used password stored in the database, so we check the password entered in the password field against the cached value.
- If the password matches, we allow them to log in, and remove the login window to reveal the app
- If the password doesn't match, we show an invalid password error in the login button
- If a user with the matching username has logged in successfully before, we will have their last used password stored in the database, so we check the password entered in the password field against the cached value.
- If there is any error other than a standard connection failure, or there was a connection failure but we couldn't find their username in the cache
- The error is displayed in the login button until the user edits either the username or password, at which point it returns to 'Login'.
- If the user has not edited either field within 10 seconds, it also returns to 'Login' in case the error is something that could be retried without editing the credentials, e.g. the internet was down temporarily. In these cases it would be confusing if you had to edit your username or password to get the 'Login' button back.
- We wipe the password in the database entry for that User, if there is one, as an error may mean that the previously cached password is no longer accepted
- If authentication is successful
- We cache the user's login details, by creating/updating a database entry for that User with their id, username, and password
- We remove the login page, revealing the app underneath
On the login page, there is also a Language button at the bottom left.
- Pressing this brings up a language selector modal, allowing the user to choose the current language the app is presented in.
The app keeps all data in local storage on the device, so that it is accessible offline. We use realm as our database, because it
- Is built and optimised specifically for mobile hardware
- Has tight integration with react-native so we don't need to play with native code
- Works on both android and iOS
- Provides a high level interface, taking away coding complexity, e.g. work with data as instances of classes, adding behaviour to the data by adding functions to the class - realm takes care of binding the class each time you retrieve a piece of data of that type
- Gives some of the trickier database behaviour for free, e.g. will automatically lazy-load data in a ListView without us writing special code
- Is being well maintained and regularly updated with new features
We have built an additional layer on top of the realm database, which is kept in a separate repository/npm package, react-native-database. Amongst other things, this provides fine-grained change notifications, which at the time we released v1.0 of mobile, weren't available in realm. See the readme of react-native-database for how to subscribe and use these notifications, and the section below on change notifications for specifics on how we use them in mSupply mobile. Our extension also adds a call to a destructor when a database object is deleted, if that object's class defines one.
Within mobile we have yet another lay on top of that, which provides access to a couple of derived data types. That is, it allows you to query for, e.g., Customer data, which is not actually a data type specified in the schema, but instead a subset of all Name data filtered by isVisible and isCustomer.
The data types in mSupply mobile's database schema mostly map directly onto a table in mSupply desktop/server. The exceptions are item_line, which is replaced by ItemBatch, transaction_line, which is replaced by TransactionBatch, and the list_master tables, which have the start flipped around to be MasterList.
The full list of data types:
- Address
- Item
- ItemBatch
- ItemCategory
- ItemDepartment
- ItemStoreJoin
- MasterList
- MasterListItem
- MasterListNameJoin
- Name
- NameStoreJoin
- NumberSequence
- NumberToReuse
- Requisition
- RequisitionItem
- Setting
- Stocktake
- StocktakeBatch
- StocktakeItem
- SyncOut
- Transaction
- TransactionBatch
- TransactionCategory
- TransactionItem
- User
For the fields in each, see the Schema in /database/schema.js or at the bottom of each file for those with classes defined, e.g. Item.Schema at the bottom of database/DataTypes/Item.js
(no point in keeping the details here to go out of date as they are pretty clear in the code)
Realm is a bit different to other databases, but I think it's easier than most once you've learnt it. Below are the functions you'll tend to use, but react-native-database just replicates the API of realm, so see their documentation for more details
For querying
- database.objects(dataType) - This is the start of a query, returning all data of that type. Don't worry, it returns a ResultSet rather than the raw data, and the ResultSet loads the data lazily as it is required for display or manipulation, so you won't hog all of the device's memory by running this query.
- results.filtered(filterString, [parameters]) - Once you have a ResultSet out of a database.objects query, you can filter it down to get just the data you want, e.g.
const searchResults = transactions.filtered('serialNumber BEGINSWITH[c] $0', searchTerm);
- results.sorted(sortKey, [reverse]) - You can also sort any ResultSet by a certain field name, e.g.
const sortedResults = searchResults.sorted('serialNumber', true);
Each call to any of these querying functions returns a new ResultSet, which you can then call either filtered or sorted on again, recursively, forever. Calling filtered on the ResultSet produced by a call to filtered is the same as putting both filter strings together with an 'AND'.
For modifying
- database.write(() => { your code }) - Any code that will modify the database needs to be placed inside a call to database.write. The function takes in a callback function as an argument. All code within this function (we just use an arrow function) will be persisted to the database when the write() returns. In this way it is like starting a transaction, executing the callback function, and then ending the transaction.
- implicit assignment within a write, i.e. databaseObject.fieldName = 'Hello' - Within a write, any time you assign a value to a field of a database object, it will persist to local storage, so you don't need to do anything more explicit to edit the data. For example,
database.write(() => {transaction.status = 'finalised';})
would update the transaction's status to finalised and save that to disk. In contrast,transaction.status = 'finalised';
without a write() wrapping it would only update that transaction's status temporarily in memory, until that variable went out of scope. - database.save(dataType, object) - This is a Sussol extension that must be called after any series of implicit assignments within a write that updates a database object, to ensure that change listeners are notified of the update
- database.update(dataType, databaseObject) - Use this function to create or modify a whole database object in one swoop. Even to create a new object, we avoid using database.create in places where it is appropriate for that object to already exist. For this reason .update is used heavily in sync so that changes pushed by the server replace the existing data with the same ID.
- database.create(dataType, object) - Adds a new object of dataType to the database, with the data described in object. As described above .update is preferable when an object with that ID might already exist - .create will throw an error in this case (there are some cases where we are sure an object with that ID shouldn't exist, in which case it is good to have an error if we try to create a duplicate).
- database.delete(dataType, objectOrObjects) - Obviously this one deletes the object (or array of objects) from the database. Just to reiterate, none of these function calls will successfully persist the data if they are not wrapped by a database.write.
Realm provides the ability to define a class that extends Realm.Object, which adds internal functionality to database objects of that type. Each time a database object of that type is brought off disk into memory for access, a new instance of the class you defined is created and returned with the data attached and accessible as though they are fields of the class.
We define a class for the majority of data types. These classes provide access to derived data, e.g. the Item class defines totalQuantity by adding all of the quantities of its ItemBatch children (mobile equivalent of item_lines).
These classes are also where we keep the business logic of the application; as described at the top of this page, instances of each database type are responsible for carrying out any effects of changes to the data. The following is a non-exhaustive list of the most important logic:
Item.addBatchIfUnique, ItemBatch.addTransactionBatchIfUnique, etc.
Adds the given child to the parent, so long as a child with the given id is not already in there
Item.dailyUsage Returns the average daily consumption for this Item over the last three months, by summing the total of each of it's child batches' dailyUsage. See below for how the batches' calculate this.
ItemBatch.dailyUsage
Derived data which returns the average daily consumption for this ItemBatch over the last three months, by summing the consumption in all transactions confirmed within that period. Accounts for recently added ItemBatches by only dividing by the number of days since the earliest transaction, if that was within the last three months
ItemStoreJoin.destructor NameStoreJoin.destructor**
Called when an ItemStoreJoin/NameStoreJoin object is deleted (e.g. by sync), the destructor cleans up by making the related Item/Name no longer visible in the app (in fact this clean up on delete is the only reason we keep ItemStoreJoin and NameStoreJoin records, the actual joining functionality is controlled by the isVisible fields of Item and Name respectively)
MasterListNameJoin.destructor
Called when a MasterListNameJoin object is deleted (e.g. by sync), the destructor cleans up by removing the related MasterList from the related Name (this is the only reason we keep MasterListNameJoin records, the actual joining is maintained in the Name.masterLists field)
NumberSequence.getNextNumber
Gets the next number in the sequence, first looking in its numbersToReuse field, and if none are there, incrementing the current highest number used and returning that
NumberSequence.reuseNumber
Creates a new NumberReuse record with the given number, and adds it to this number sequence's numbersToReuse field (after performing some sanity checks)
Requisition.addItemsFromMasterList
Adds a new RequisitionItem to this requisition for each item in this store's master lists
Requisition.setRequestedToSuggested
Copies all the suggested order values across to be the required valued (i.e. those ordered)
Requisition.pruneRedundantItems
Removes all RequisitionItems with 0 as their required quantity
Requisition.createAutomaticOrder
Calls the three preceding methods in that order, with the result being a requisition of all items that need some amount >0 ordered, with the required quantities all set, ready to finalise. This provides a very fast way to generate and place an order
Requisition.finalise
Prunes off any items that have 0 required quantity, and sets the status to finalised
RequisitionItem.suggestedQuantity
Derives and returns the suggested quantity by multiplying the average daily usage of the related Item by the days to supply of the parent Requisition and subtracting the stock on hand of the related Item. Note that the daily usage and stock on hand are snapshot at the time the Requisition is created.
Stocktake.setItemsById
Sets the stocktake items attached to this stocktake, based on the array of item ids
Stocktake.finalise
Adjusts the inventory by creating an incoming transaction filled with any items that are increased, and an outgoing transaction filled with any items that are decreased, and finalising the transactions so they make the appropriate adjustments to individual items' inventory and deal with all the batch allocation (see the section on dealing with batches). Finally sets the status of the Stocktake to finalised
StocktakeItem.destructor
Ensures the parent Stocktake is not finalised, and throws an error if it is
StocktakeItem.isReducedBelowMinimum
Returns true if this stocktake item's counted quantity would reduce the amount of stock in inventory to negative levels, if it were finalised.
StocktakeItem.applyBatchAdjustments
Because the transaction deals with allocating the adjustments made in a stocktake to the appropriate batches at the time the stocktake is finalised, we needed a way to then take that allocation and apply it to the batches that were snapshot into the stocktake, without unnecessarily duplicating the same allocation logic (which is slightly complex and would easily get out of sync - best to have it in one place.)
Transaction.addItemsFromMasterList
Adds a new TransactionItem to this Transaction for each Item in the associated Name's master lists
Transaction.removeItemsById
Removes the TransactionItems with the given ids from this Transaction, along with all the associated batches
Transaction.confirm
Increases or decreases the associated item batches (depending on whether it is a customer or supplier invoice), and sets the status to confirmed. Given that the app doesn't expose batch information, the transaction uses what I call 'pessimistic FEFO' to increase/decrease the appropriate batches, as a best guess of how the people in the warehouse or clinic would actually have done it. See the section on dealing with batches for details.
Transaction.finalise
First confirms (making the inventory adjustments including appropriate batch allocation) then prunes off any items with 0 total quantity, and sets the status to finalised
TransactionItem.setTotalQuantity
Sets the quantity for the current item by applying the difference to the shortest expiry batches possible, which in turn will adjust inventory if the transaction is confirmed
TransactionBatch.usage
Get the amount of consumption this TransactionBatch represents
TransactionBatch.setTotalquantity
Sets the quantity of this TransactionBatch, and if it belongs to a confirmed Transaction, adjusts inventory accordingly
The sussol maintained extension to realm, react-native-database was written to add fine-grained change notifications.
This allows class instances to be alerted whenever a database object of interest is created, updated, or deleted. We use this in two key places:
- Synchronisation - Any time a database object that syncs is modified, we add it to the sync queue
- Displaying live data - Pages within mSupply Mobile subscribe to be alerted of changes to data types that they display, so that if, e.g., a new customer invoice syncs in, the list of customer invoices updates to display it
To subscribe to notifications of changes:
- database.addListener(callback) - The callback will be called any time a database object is modified. You probably want to put a conditional statement at the top of the callback to check which data type was changed in order to avoid excessive work every time there is a change to the database. The callback will be passed the following parameters:
- changeType - Whether the change was a create, update, delete, or wipe
- dataType - The data type of the modified object
- databaseObject - The database object that was changed in its new form (unless it was deleted)
- extraParams - Any extra parameters that were passed through to the database.create, .update, .save, .delete, or .deleteAll call. In mSupply mobile we use this to record whether the source of the modification was a sync coming in from the server, in which case we won't respond to the change by adding it to the sync queue (don't need to push anything back that the server has just pushed down!)
- database.removeListener(listenerId) - Be sure to call this when a class instance is destroyed, otherwise the callback will be called but will be unavailable to respond, causing a big old error. The listener id is returned by addListener.
Realm now support fine-grained notifications internally, so at some point we should swap over to using their notification system (see the docs). The main flaw with our home-baked solution is that any changes that are made using the implicit 'assignment within a write' do not call the change listeners. Instead we rely on programmers to call database.save as described above. There is an issue for changing to use realm's solution here.
mSupply Mobile syncs with an mSupply server whenever there is internet available. It does this using virtually the same mechanism as satellite sync sites in a desktop/server based sync setup. See the mSupply manual and documentation for detailed information about that.
Synchronisation has five components: initially pulling down the required data to set up the store, recording changes, and every 10 minutes doing a regular sync.
When the app starts for the first time (or after all stored data has been deleted), the user is presented with the first use screen. When they have entered the sync site credentials and successfully authenticated, as described in the sync authentication section, the app will begin its initialisation process. This involves:
- Informing the server it requires an initial dump of records, by hitting the /sync/v2/initial_dump API endpoint. The server responds to this by generating sync records representing the full set of data belonging to/required by that site
- Beginning to pull down all of the generated records, simply by performing an ordinary sync as described in the section below.
- If there is an error
- Pausing
- Passing the error up to the UI layer
- If it is resumed by the user, it should not ask the server to regenerate all initial dump records, so that it continues where it left off rather than restarting
- If it is successful
- Recording that the app is initialised in settings, so that it does not present the first use screen again
- Enabling the sync queue, so that it begins listening for changes to records (which we keep off until it has initialised to make the first sync go a bit faster)
Every time a record is modified, mSupply Mobile adds a sync record to the SyncQueue to record the change. The sync record holds on to the:
- Type of change (create, update, delete)
- Type of database object changed
- Id of the object that was changed
- Time of the change The SyncQueue detects these changes by subscribing to be a change listener, as described above. Each time a change is detected, the sync queue:
- Checks it is a record that gets synced, and if not stops there
- If the change is a delete, remove any sync records that represent earlier creates/updates for the object being deleted - they are superseded by the fact the record no longer exists (if we left them in there things would break when sync out tried to get the now defunct database object to push to the server)
- If there is a duplicate sync record in the queue, i.e. one with the same recordId and changeType, it does nothing
- If there are no duplicates, it adds a new sync record to the queue representing the change The queue is just a set of Sync
Every ten minutes mSupply Mobile attempts to sync with the mSupply server. It first pushes any local changes, and then pulls central changes back down. It works in this push then pull order to give some responsibility to the sync server, e.g. if there was ever a conflict between local changes and central changes it would be up to the server to detect that conflict coming in from the app, resolve it, and sync the outcome back down to the app.
If any error occurs during the sync, e.g. the internet drops out, sync will stop and the error will be passed up. It will try again 10 minutes later. Some safety measures around errors:
- Change records stay in the sync queue until the server acknowledges the push HTTP request, so if it fails they will still be there for the next sync attempt
- The mSupply server dispose of sync_out records as soon as they are pulled down, instead it only considers central changes to be consumed by sync when a POST is made to the acknowledged_records endpoint. We only make that POST when the records are fully integrated, so if there is a failure due to the internet, an app crash, etc., they will still be on the server for the next sync attempt
- The integration stage of the pull is inside a database.write() transaction, so if the app crashes during that transaction, any already integrated records will not be persisted to the database, leaving it in the same state the mSupply server thinks it is in (as we will not have sent the acknowledgement HTTP request yet, as mentioned above)
- Even if the records in a pull fully integrate but the internet fails before we can acknowledge that, the worst that will happen is we will pull down duplicate sync records, which will use extra bandwidth but not cause any data problems
The process for pushing locally changed records up to the mSupply server is:
- Grab the oldest 20 sync records
- Generates JSON data based on the sync record ID, change type, changed record ID, and content of the changed record if it was not deleted
- Sends that JSON data as the body of a POST request to the /sync/v2/queued_records endpoint
- When the server acknowledges the HTTP request has got through, it deletes the successfully synced records from the sync queue, and starts this series of steps again
The process to pull centrally changed records down from the mSupply server is:
- First checks how many records a ready to sync from the server, by sending a GET request to the /sync/v2/queued_records/count endpoint. If there are none left, it stops syncing (it's finished!)
- Sends a GET request to the /sync/v2/queued_records endpoint, asking for up to 20 sync records representing central changes
- Integrates the batch of records that came in
- Each record is sanity checked, to make sure it a) is of a data type we care about, b) contains all the expected fields to map onto our database, and c) contains some value in fields that cannot be empty, e.g. foreign key ID fields.
- The sync record is then parsed and converted into a matching mSupply Mobile database object
- If the record is related to another record(s), we will attempt to fetch the related record from the app's database, and connect the two. However, if the related record doesn't exist (i.e. has not yet synced in), we will create a placeholder database to represent the related record(s). When that related record does sync in, its contents will simply override the placeholder database object we created, and it will remain connected. This allows us to maintain database relationships independent of sync order. For example, if a trans_line with a parent transact referred to in trans_line.transact_ID, and the transact has not yet synced down, we just make a stub transact with that ID. Then when a transact with that ID syncs in, the data in the stub we created is updated to match.
- Sends a POST to the /sync/v2/acknowledged_records endpoint, with the IDs of the sync records just processed in the body. This tells the server that the sync records have been consumed, so it can delete them from its queue, safe in the knowledge the changes are saved in the app
There are some things that could be improved about sync, and some places to be careful. Here is what you need to be aware of:
- Malformed records may break/crash the app. While we sanity check to ensure incoming sync records have the expected fields, we do not sanity check the value itself. If a record were to have all the appropriate fields, with some content in those that require content, but values that for some reason don't fit logically into their field, we won't detect it. For example 'true' in a field that expects a number, 'Hello' in a field that expects a boolean, or 'I like eating cake' in a field that is used as an ID will all break things. So far our sanity checking has been all that is required, but if an app is crashing every ten minutes, it may be because of malformed records coming from the the sync server.
- If mSupply ever adds any special statuses, it will break the app. We support 'nw, 'sg', 'cn', 'fn', 'wp', and 'wf'. These last two only have basic support, the app will actually convert them to one of the first four - see this discussion for details
- Data that is around from earlier versions of mSupply, including the old mobile, may not sync perfectly, e.g. see issue 381
- Stocktakes made centrally may not sync down properly, see issue 371
Pages that don't represent editable records all have a sync indicator on the far right of the navigation bar. Where a page has a finalise button, this takes precedence, so the sync indicator is not displayed. The sync indicator shows either:
- Sync Enabled - Meaning it functioned correctly on the last attempt, but is not currently syncing
- Sync In Progress - Meaning it is currently attempting to sync
- Sync Error - Meaning the last attempt to sync with the server failed. It also displays the date of this attempt
You actually don't have to wait 10 minutes for an app to sync, you can manually force it by putting the app into admin mode and then tapping on the sync icon.
This section describes the functional design of the interface. Some of the logical complexities are covered only briefly here, for more detail see the section above
Other than the root menu, each page in the app extends the generic page in the react-native-generic-table-page npm package (which is maintained by Sussol). Each page defines
- Its columns
- A function to return the data, given filtering and sorting details
- The data types that it should respond to changes for (e.g. if displaying a list of customer invoices and a new CI syncs in, the page should refresh to reflect the change and so respond to data of type 'Transaction')
- Optional functions to render particular components
- Modals, which open in response to certain buttons being pressed (e.g. edit comment)
- Page Info, which will be displayed at the top left of the page, above the data table
- The cells, if some cells need to render anything other than just the value as static text, extracted from the row's data using that column's key (e.g. are editable, checkable, etc.)
- Optionally, the render function for the page as a whole, if the layout diverges from the generic layout (e.g. has modals)
This is presented every time the app is opened from a closed state, or when a user presses logout. See user authentication for the business rules.
The menu is like the 'home page' of the app, with buttons to each of the main sections.
Interface Features
- Links to subpages are grouped in three columns, representing things related to Customers, to Suppliers, and to Stock.
- Pressing any of the buttons will take you to the relevant page
- There is a 'Logout' button on the bottom left corner - pressing this will log the current user out and bring up the login screen
Shows a list of customer invoices.
Columns
- Customer - The name of the customer the stock is being/was issued to
- Invoice Number - The unique serial number of each invoice (unique within the store only, not across all of an mSupply system)
- Status - Whether the customer invoice is 'Finalised' or 'In Progress' (any status other than finalised)
- Entered Date - Self explanatory
- Comment - Optional comment entered against the customer invoice
- Delete - Tap to select a given invoice for deletion. Can tap more than one to delete multiple invoices at one time. Disabled when the invoice is finalised. If one or more invoices are selected for deletion, a confirmation modal will appear at the bottom of the screen, with the option to confirm or cancel
Other Interface Features
- New Invoice Button - Pressing will bring up a dialog to search for and select a customer, see below
- Search Bar - Typing in here will filter the list of customer invoices on the name
- Tapping anywhere on a row (other than the delete column) will open the page for that customer invoice
Presents a list of customers to create the invoice for
Interface Features
- Can start typing in the search bar to filter the list of customers
- The cursor is automatically active in the search bar when the window opens
- Tapping on one of the customers in the list will create a new customer invoice with them as the recipient (at present, you actually need to tap twice: once anywhere to get the focus out of the search bar, and again on the customer's name to actually select them.)
Show details and allows editing of a customer invoice (unless it is finalised, when editing is disabled)
Columns
- Item Code - The code of the item
- Item Name - The name of the item
- Available Stock - The stock on hand that is available for issue, i.e. not already accounted for in other customer invoices. This is essentially the maximum quantity of each item that could be issued in this customer invoice.
- Quantity - The amount of stock being issued. Quantities in mSupply mobile are in individual items
- Remove - Tap to select a given item for deletion. Can tap more than one to delete multiple items at one time. If one or more items are selected for deletion, a confirmation modal will appear at the bottom of the screen, with the option to confirm or cancel
Other Interface Features
- Entry Date - Date the invoice was created
- Confirm Date - Generally the same as the entry date; for invoices created within mSupply mobile, we confirm them immediately so that they have an effect on inventory, making it easy to ensure that across all customer invoices, no more stock is issued than is available
- Entered By - The user logged in at the time the customer invoice was created
- Customer - The customer's name
- Their Ref - An editable text field that maps onto the their_ref field in mSupply. Usually used for entering the reference the customer might use on a physical record or similar. Tapping the field or 'edit pencil' next to it brings up a modal allowing the text to be changed
- Comment - An editable text field used for any purpose. Tapping the field or 'edit pencil' next to it brings up a modal allowing the text to be changed
- New Item Button - Brings up a modal allowing the user to select an item to add to the invoice, see add item. If the item selected is already in the invoice, it does nothing.
- Add Master List Items - Adds all items on the master lists attached to the customer this invoice is for. If any are in the invoice already, it doesn't add them again. It also does not delete any non-master list items already in the list
- Search Bar - Filters the list of items on the customer invoice by item name or code (so typing 'ace' would include an item with the name 'Acetazolamide' and code 'dia250t' and an item with the name 'Diamox' and code 'ace250t')
- Finalise - Pressing will bring up the finalise modal
Notes
- When a Customer Invoice is opened, we ensure it is either confirmed or finalised - if not we confirm it. This will only occur in rare cases when a new or suggested invoice syncs down from the central server. By confirming on opening, we enforce our policy to avoid over-issuing stock
Shows a list of all names that are visible in the mobile store and are customers
Columns
- Code - The name's code (generally unique)
- Name - The name's name
- Invoices - The number of customer invoices that have been or are intended to be issued to that customer
Other Interface Features
- Search Bar - Filters customer list on name
- Tapping any row will open the page for that customer
Details about the customer with a list of all their customer invoices
Columns
- ID - The unique serial number of the customer invoice (unique within the mobile store, not across a full mSupply installation)
- Status - Whether the customer invoice is 'Finalised' or 'In Progress' (any status other than finalised)
- Entered Date - The date the customer invoice was created
- Items - The number of item lines on the customer invoice
- Comment - An optional comment on the customer invoice
Other Interface Features
- Address - Address of the customer, if it has been entered
- Code - Code of the customer, generally unique
- New Invoice Button - Creates a new customer invoice for this customer, and takes you to the edit page
Shows a list of supplier invoices received by this store
Columns
- Invoice Number - The unique serial number of the supplier invoice (unique within the mobile store, not across a full mSupply installation)
- Status - Whether the supplier invoice is 'Finalised' or 'In Progress' (any status other than finalised)
- Entered Date - The date the supplier invoice was created
- Comment - An optional comment on the supplier invoice
Other Interface Features
- Search Bar - Filters the list of supplier invoices on the invoice number
Notes
- All supplier invoices currently come from a single supplying store set on the sync server, so we don't bother with a 'Supplier' column
- Entering supplier invoices manually is currently unsupported
Shows details and allows review and modification of a supplier invoice
Columns
- Item Code - The code of the item on that line
- Item Name - The name of the item on that line
- Number Sent - The quantity (in individual items) of the item that was recorded as being sent out on the supplier's end. Generally comes from the customer invoice made by the central supplying store, which automatically generates a supplier invoice when it is a stock transfer
- Number Received - The quantity of the item that arrived at the store. This is where the mSupply Mobile user is responsible for reviewing the stock that was delivered, and ensuring it matches the number in the sent column. If it doesn't, they can edit the number received so that their inventory is increased by the appropriate amount
Other Interface Features
- Entry Date - Date the invoice was created
- Confirm Date - Date the invoice is confirmed, which generally doesn't happen until the user presses 'finalise' (supplier invoices from stock transfers are created as 'suggested')
- Their Ref - A text field mapping on to transact.their_ref in mSupply. Most of the time will contain information about the customer invoice that is at the other end of the stock transfer that created this supplier invoice
- Comment - A text field with an optional comment. Generally says 'Stock transfer'
- Search Bar - Filters the list of items on the supplier invoice by item name or code (so typing 'ace' would include an item with the name 'Acetazolamide' and code 'dia250t' and an item with the name 'Diamox' and code 'ace250t')
- Finalise - Brings up the finalise window
Notes
- When a Supplier Invoice is opened, we ensure it is either new, suggested, or finalised - if it is 'confirmed', we finalise it. This will only occur in rare cases when a confirmed invoice syncs down from the central server. By finalising on opening, we enforce our policy to avoid 'confirmed' supplier invoices - if someone were to reduce the amount of stock on a confirmed supplier invoice, but it had already been issued in a customer invoice, we would have to deal with a tricky situation.
Shows a list of requisitions placed/to be placed by this store against the supplying store (as defined on the mSupply server)
Columns
- Requisition Number - The unique serial number of the requisition (unique within the mobile store, not across a full mSupply installation)
- Entered Date - The date the requisition was created
- Items - The number of item lines currently on the requisition
- Status - Whether the requisition is 'Finalised' or 'In Progress' (any status other than finalised)
- Delete - Tap to select a given requisition for deletion. Can tap more than one to delete multiple requisitions at one time. Disabled for any requisition that is finalised. If one or more requisitions are selected for deletion, a confirmation modal will appear at the bottom of the screen, with the option to confirm or cancel
Other Interface Features
- Search Bar - Filters the list on the requisition number
- New Requisition - Creates a new requisition and opens the edit page
Shows details and allows editing of a requisition (unless it is finalised, when editing is disabled)
Columns
- Code - The code of the item on that line
- Item Name - The name of the item on that line
- Current Stock - The stock on hand for that item, in individual items
- Monthly Use - The average monthly consumption of that item over the last three months, as determined by the dailyUsage multiplied by 30. Details described in the section on Item.dailyUsage logic
- Suggested Qty - The number of individual items mSupply Mobile thinks should be ordered in order to maintain stock availability for at least the period in the 'Months Stock' section. For details on how the suggested quantity is calculated, see the logic section
- Requested Qty - The amount of stock actually requested for that item
- Remove - Tap to select a given item for deletion. Can tap more than one to delete multiple items at one time. If one or more items are selected for deletion, a confirmation modal will appear at the bottom of the screen, with the option to confirm or cancel
Other Interface Features
- Entry Date - The date this requisition was created
- Entered By - The user who was logged in when the requisition was created
- Months Stock - Number of months of cover required in stock, used to calculate suggested quantities. Tapping on the number or drop down arrow will open a selector allowing the user to change the months stock to a number between 1 and 6
- Comment - Optional comment. Tapping on the underlined section or 'edit pencil' brings up a modal allowing the user to change the text
- Search Bar - Filters the list of items on the requisition by item name or code (so typing 'ace' would include an item with the name 'Acetazolamide' and code 'dia250t' and an item with the name 'Diamox' and code 'ace250t')
- Create Automatic Order - Generates an order with all items that would run out of stock in the period the requisition covers, with suggested quantities automatically in the requested column. More details in the logic section
- Use Suggested Quantities - Copies all of the values in the suggested quantity column to the requested quantity column
- New Item - Opens the new item modal to allow adding an item to the requisition
- Add Master List Items - Adds all items in this store's master lists to the requisition
- Finalise - Opens the finalise modal for this requisition
Shows a list of stock, with details about inventory levels etc
Columns
- Item Code - The code of the item
- Item Name - The name of the item
- Stock On Hand - The quantity of stock currently available in the store's inventory. This automatically updates whenever a stocktake is done, stock is received into the store (supplier invoice is finalised), or stock is issued (a customer invoice is created/edited). Note that it does not include stock that may be physically in the store room, but is either a) on a supplier invoice that has not yet been finalised, or b) on a customer invoice and so allocated to be issued
Other Interface Features
- Search Bar - Filters the list of items on the requisition by item name or code (so typing 'ace' would include an item with the name 'Acetazolamide' and code 'dia250t' and an item with the name 'Diamox' and code 'ace250t')
- Tapping any row will expand the row to reveal extra details about the item
- Category - The lowest level (most fine grained) category the item belongs to, if any
- Department - The lowest level (most fine grained) department the item belongs to, if any
- Number of batches - The number of batches of this item are currently in stock
- Earliest expiry - The date the batch with the earliest expiry date should be used by (note, it may be in the past if there is expired stock in the store)
Shows a list of all stocktakes that are being done/have been done in this store
Columns
- Name - The name given to the stocktake
- Created Date - The date the stocktake was created in mSupply Mobile
- Status - Whether the stocktake is 'Finalised' or 'In Progress' (any status other than finalised)
- Delete - Tap to select a given stocktake for deletion. Can tap more than one to delete multiple stocktakes at one time. Disabled for any stocktake that is finalised. If one or more stocktakes are selected for deletion, a confirmation modal will appear at the bottom of the screen, with the option to confirm or cancel
Other Interface Features
- Current/Past Toggle - Selecting Current shows all 'In Progress' stocktakes in the table and filters out all that are 'Finalised', while selecting 'Past' does the opposite
- New Stocktake - Opens the new stocktake page (does not actually create a new stocktake at this point)
Used to edit the items in and title of a stocktake, either in the process of creating it or as a management tool during the stocktake
Columns
- Item Code - The code of the item
- Item Name - The name of the item
- Selected - Tapping will fill the checkbox in this column, and mark the item to be added to/retained in the stocktake. Tapping again will unfill the checkbox, and mark the item to be either not added or removed from the stocktake (depending on whether it is already in there). If one or more items are selected, a confirm modal will appear at the bottom of the page - see 'Other Interface Features' below
Other Interface Features
- Search Bar - Filters the list of items in the table by item name or code (so typing 'ace' would include an item with the name 'Acetazolamide' and code 'dia250t' and an item with the name 'Diamox' and code 'ace250t')
- Hide Stockouts - A toggle button. When toggled on, filters out any items that are currently out of stock from the list. If they were selected before the button was toggled on, they will remain selected. However, if All Items Selected is pressed when stockouts are hidden, they won't be selected
- All Items Selected - A toggle button that will select/deselect all the items currently visible in the table.
- If there is nothing typed in the search bar and 'Hide Stockouts' is off
- If there is one or more line that is not yet selected, it will appear 'off' (no colour fill), and tapping it will select all items in the store
- If all items are selected, it will appear 'on' (background filled with orange), and tapping it will deselect all items in the store
- If there is something typed in the search bar and/or 'Hide Stockouts' is on, i.e. the table is showing a subset of items in the store
- If there is one or more item in the subset that is not yet selected, it will appear 'off' (no colour fill), and tapping it will select all items in the subset, without affecting items that are not yet selected but filtered out of view
- If all items in the subset are selected, it will appear 'on' (background filled with orange), and tapping it will deselect all items in the subset, without affecting items that are selected but filtered out of view
- If there is nothing typed in the search bar and 'Hide Stockouts' is off
- Confirm Modal - A small modal will appear at the bottom of the page whenever at least one item is selected.
- It has an editable text field to edit the title of the Stocktake
- It has a button reading either 'Create' or 'Confirm', depending on whether or not the stocktake is brand new
- On a brand new stocktake, if the user does not enter any text in the title text field before pressing 'Create', it will be created with the title 'Stocktake [current date and time]'
- A brand new stocktake is only created at the point the user presses 'Create'. If a user navigates back out of the page before that, the stocktake will not exist
- Similarly, if the user navigates back out of the manage page of an existing stocktake without pressing 'Confirm', any changes will not be applied
- It will disappear if no items are selected, so you cannot have a stocktake with no items
Shows details and allows editing of a stocktake (unless it is finalised, when editing is disabled)
Columns
- Item Code - The code of the item
- Item Name - The name of the item
- Snapshot Quantity - The stock on hand of the item at the time the item was added to the stocktake (often at the point the stocktake was created). We take a snapshot on the basis that no stock is issued or received between the time the item is added to the stocktake and the time it is counted, but stock may be issued/received between the time it is counted and the time it is entered into mSupply Mobile. By taking a snapshot, we can use the difference between the snapshot and counted quantities and apply that to the current stock levels as an inventory adjustment
- Actual Quantity - The amount of stock counted actually counted in the store, in individual items
- Difference - Shows the positive or negative difference between the snapshot and actual quantities, indicating the inventory adjustment that will be made when the stocktake is finalised. This was added to clear up confusion for users about whether to enter the total quantity or the difference in the 'Actual Quantity' column
Other Interface Features
- Search Bar - Filters the list of items on the stocktake by item name or code (so typing 'ace' would include an item with the name 'Acetazolamide' and code 'dia250t' and an item with the name 'Diamox' and code 'ace250t')
- Manage Stocktake - Opens the stocktake manager for this stocktake, allowing the user to add or remove items from the stocktake, and edit the title
- Finalise - Opens the finalise modal for this requisition
Allows the user to search for and select an item to add to the invoice or requisition.
Columns
- Item Name - The full name of each item
- Stock on Hand - The amount of stock of each item that is currently in stock
Other Interface Features
- Search Bar - Filters the list of items in the list by item name or code (so typing 'ace' would include an item with the name 'Acetazolamide' and code 'dia250t' and an item with the name 'Diamox' and code 'ace250t'). The cursor is in the search bar by default when the modal opens.
- Close X - Tapping will close the selector without adding any items
- Tapping any row will add the item to the invoice or requisition. Note that if the cursor is in the search bar, the user will need to tap twice, once to unfocus from the search bar, and again to actually select the item
Notes
- Only one item can be added at a time through this selector - it might be nice to have a multiple item selection interface
A modal that controls finalising invoices/stocktakes/requisitions. Will start by checking if the record is ready to be finalised, and if not, show an error message with information on how to correct the problem. Otherwise it shows a message about the effects of finalising.
Interface Features
- If there is an error, i.e. the record is not ready to be finalised (for the logic, see 'Notes' below)
- Error Message - a custom message depending on the issue, with information to help the user fix it
- 'Got It' Button - pressing will dismiss the modal but not affect the record
- If there is no error, i.e. the record is ready to be finalised
- Message - a custom message with information on the effects of finalising that type of record
- Cancel Button - pressing will dismiss the modal but not affect the record
- Confirm Button - pressing will finalise the record, locking it for future editing. A loading spinner will appear while this is happening, and then the modal will be dismissed, revealing the locked record behind it
- Close X - Appears whether or not there is an error. Pressing will dismiss the modal but not affect the record
Notes
- Each type of record has different criteria to be ready for finalisation
- All Types - Need at least one item line
- Customer Invoices - Needs at least one item line with a number greater than 0 in the quantity to issue column
- Supplier Invoices - No criteria! As there isn't much a mobile user can do, they are created centrally
- Requisitions - Needs at least one item line with a number greater than 0 in the required quantity column
- Stocktakes - Needs to have at least one item line with a number in the counted quantity column, and for no item to be reduced by more than is in stock at the time of finalising (we don't want to send stock negative, this situation doesn't make sense!)
This is only displayed in admin mode and provides a way to look at the raw data stored in the realm database.
- Typing the name of a data type, e.g. 'Transaction', in the search bar will show all the data of that type in the table
- The table is dynamically built using the data returned to determine the columns. This means it won't always look nice as it may be overfilled with columns
- Any field that is a list will simply show the number of children, e.g. the Transaction.items column will show the number of items in each transaction, rather than any details about the actual items
- It does not provide editing abilities, i.e. you cannot change the data in the database through Realm Explorer
The first time the user opens the app (or any time the locally stored data is wiped, see changing sync details).
Interface Features
- Primary Server URL - An editable text field to enter the URL of the mSupply server that this tablet will sync with. This should include the https:// and the :port (unless the port is the standard HTTPS 443). Obviously all syncing should be done over HTTPS
- Sync Site Name - An editable text field to enter the name of the sync site that this app represents, as set up on the server in mSupply's 'Synchronise' preferences
- Sync Site Password - An editable text field to enter the password of the sync site that this app represents, as set up on the server in mSupply's 'Synchronise' preferences
- Sync Indicator - Shows the status of sync: during sync it is darkly coloured, if there is an error it becomes lighter grey
- Connect Button
- Is disabled until some text has been entered in all three fields
- Tapping begins the app's initialisation sync, getting all data required for that site
- Once sync is underway the connect button and all fields are disabled
- While syncing, the number of records remaining will be displayed inside the button as a progress indicator
- If there is an error (did not authenticate, server stopped responding, bad sync record, etc.), the error message will display in the 'Connect' button, ending with 'Tap to retry.' Tapping this button will resume the sync from the point it got to before the error occurred, so if it had already synced 1500 out of 2000 records, it won't have to sync them all again - just the 500 still to go.
Settings are kept in the realm database along with the rest of the data. However, they have a different interface to the database, so that a.) settings are easier to add, retrieve, and edit, and b.) we could swap out where they are kept but keep the interface if we wanted (encapsulation!) The repo (and npm package) that sussol maintains, react-native-database, provides this settings interface.
In the settings module's index.js, we keep all of the settings key constants in a javascript object. This is so you can import { SETTINGS_KEYS } from './settings';
and then access the specific key you need - basically provides a centralised point to maintain a consistent, unique set of settings keys that can be changed in one place and have the changes reflected everywhere. It's good practice to use named constants rather than repeating the string literal version of a key throughout a codebase.
We extend the react-native-database Settings class to connect it to our localization module. Whenever an interface consumer (user of the Settings class) sets the CURRENT_LANGUAGE, it will ensure the localization module updates the current language. It also ensures the localization module is informed of the current language when the app first starts.
Localization works by providing the strings that will be rendered throughout the app as they are needed, in the language currently set by the user. Currently the only options are English (default) and Tetum. We use the external npm package react-native-localization to deal with actually selecting the right strings, given a set to choose from and having set a current language key.
Adding support for a new language is easy, just add it to languageKeys in the localization index.js, and a matching entry in each of the other files in the module (as well as adding the option to the LanguageModal).
The widgets module is a collection of odds and sods, all of them UI components that are self contained, not specific to a particular page, and can be reused. A few examples: PageButton, SyncState, ToggleBar, and the various Modals (ConfirmModal, FinaliseModal, etc.)
If any are needed for other projects, they can easily have their last dependencies (usually on globalStyles) broken, and be pulled out into their own repo with an associated npm package.
Here is a list of things that people may expect to work in mSupply Mobile, but at this stage are unsupported:
If one mobile site makes a customer invoice for another mobile site, the stock will not be automatically transferred. Instead, the stock will go out of the supplying site, but no supplier invoice will be generated for the receiving site. For this reason, anyone setting up mobile must be careful not to make names that are attached to stores visible in a mobile site - this makes it too easy for them to create a customer invoice without the appropriate supplier invoice showing up on the other side. A good solution to this problem would be to have the sync server respond centrally, by detecting customer invoices created by mobile that have a store as the recipient, and generating a supplier invoice at that point.
Requisitions created in the mSupply Mobile app are all directed to the supplying store, as set up in the preferences on the sync server. Mobile users cannot make requisitions for any other store.
Even if a requisition is made against a mobile store using a desktop computer, it could not be accessed in mobile. We have no UI or process for a mobile site taking the role of a supplying store, and receiving and responding to requisitions.
Supplier invoices show up in mobile as if by magic. They are all generated centrally by the server or someone on a desktop, and almost always as the result of a stock transfer to that store. Mobile users cannot enter supplier invoices manually - the workaround is to create a new stocktake and add the appropriate amount of incoming items to the current stock count.
Every quantity visible to an mSupply mobile user is in individual items, meaning individual tablets, vials, and some exceptional cases like boxes of gloves. If you must think of pack sizes, consider that every batch has a pack size of 1.
We made a conscious decision not to clutter or confuse things with batch numbers and expiries in the mobile app. It does maintain knowledge of the batch details under the hood, but no information about this is ever exposed to an mSupply Mobile user. The way batches are dealt with internally is by assuming the store follows FEFO (first expiry, first out) principles.
- Whenever a customer invoice is created, the stock issued from it will be taken from the batch(es) with the nearest expiry date.
- Supplier invoices sync down with batch information already recorded in mSupply Desktop/Server, however if the mobile user disputes how much was sent, and edits the amount being received, this will be treated pessimistically: if there is more than expected, it will be added to the batch with the shortest shelf life, if there is less, it will come off the batch(es) with the longest shelf life.
- Stocktakes will also take this pessimistic view. If a stocktake decreases stock, it will be taken from the batch(es) with the longest shelf life. If it increases stock, it will be added to the batch with the shortest shelf life, unless there are no batches of the given item, in which case a batch will be created with no batch number or expiry date. These no-expiry batches are considered to have the shortest shelf life when making the above decisions (this way they will be issued first in customer invoices, and be cleared out of the system as quickly as possible)
Patients (or other 'names' that might be issued stock from a mobile store) must be added centrally. There is no interface for adding them from within the mobile app. When added centrally, and made 'visible' in a given mobile store, they will sync down and become available within the app.
Even if a name added centrally is a patient, with details like gender, age, etc., those details cannot be viewed in mobile. Mobile doesn't make any distinction between patients and other 'names' that can have stock issued to them.
Invoices created within mSupply mobile are confirmed from the outset so that they have an effect on inventory, making it easy to ensure that across all customer invoices, no more stock is issued than is available. We don't allow for stock that is not available to be issued (i.e. no placeholder lines), and we avoid the situation where stock that is available will be issued across more than one customer invoice. There is a loophole in the logic that could create this situation: if a customer invoice is created as 'new' or 'suggested' centrally and syncs down, it could have more stock issued than is available. However, we assume that mobile stores are only ever active on a mobile app sync site, so this should never happen.
The first time the app is opened, the user is presented with the first use page. Once this initial sync has successfully completed, the Sync Server URL, Sync Site Name, and Sync Site Password are locked in, and can't be changed. This is partially what is easy, but also a considered decision: adding the ability to change sync details after the initial sync opens up the possibility of having a tablet with the data of one site/store, and then getting mucked up by changing to get the data of another site/store or even from another sync server. The safe way is therefore to wipe all data and sync the initial dump in fresh. We don't have to provide an additional interface to do this, as you can wipe all data through the app manager within Android settings, and the next time you open mSupply Mobile it will be at the first use page. Having to go through this route rather than make it part of our standard interface also prevents any user from accidentally wiping everything.
That's it! Absolutely everything else is supported. I joke, please edit and add features here that people might expect but are missing.
We have a special admin mode that allows access to
- The Realm Explorer page, for viewing the contents of the app database
- Manually syncing To get into admin mode, press and hold the mSupply logo in the middle of the navigation bar for at least 5 seconds. Do the same again to get out of it (please don't forget!)