Samples / Controls / sap.m

Browse Orders

Master-detail app for browsing orders. - the UI5 demo app "Browse Orders", rebuilt as one self-contained abap2UI5 class.

Class
z2ui5_cl_smpc_demo_002
Repository
abap2UI5/samples-controls
UI5 libraries
sap.f, sap.m, sap.ui.core, sap.ui.layout
Lowest UI5 release
1.71
In the browser
Runs in the playground
Controls it builds
sap.f.FlexibleColumnLayout, sap.f.semantic.CloseAction, sap.f.semantic.ExitFullScreenAction, sap.f.semantic.FullScreenAction, sap.f.semantic.SemanticPage, sap.f.semantic.SendEmailAction, sap.m.App, sap.m.Button, sap.m.Column, sap.m.ColumnListItem, sap.m.IconTabBar, sap.m.IconTabFilter, sap.m.Image, sap.m.Label, sap.m.Link, sap.m.List, sap.m.MessagePage, sap.m.ObjectAttribute, sap.m.ObjectIdentifier, sap.m.ObjectListItem, sap.m.ObjectNumber, sap.m.ObjectStatus, sap.m.OverflowToolbar, sap.m.OverflowToolbarLayoutData, sap.m.Page, sap.m.SearchField, sap.m.Table, sap.m.Text, sap.m.Title, sap.m.Toolbar, sap.m.ToolbarSpacer, sap.m.VBox, sap.m.ViewSettingsDialog, sap.m.ViewSettingsFilterItem, sap.m.ViewSettingsItem, sap.ui.core.Title, sap.ui.core.mvc.View, sap.ui.layout.HorizontalLayout, sap.ui.layout.VerticalLayout, sap.ui.layout.form.SimpleForm

The class

" @keywords browse orders app flexiblecolumnlayout viewsettingsdialog viewsettingsfilteritem viewsettingsitem title list toolbar overflowtoolbar searchfield
" @summary Master-detail app for browsing orders. - the UI5 demo app "Browse Orders", rebuilt as one self-contained abap2UI5 class.
" @origin demo app Browse Orders (sap.m/orderbrowser) - https://sdk.openui5.org/demoapps (status: generated - machine-written, not yet reviewed)
"! <p class="shorttext">demo app - Browse Orders</p>
"!
"! The UI5 demo app Browse Orders - the master-detail showcase of the demo kit
"! - rebuilt as ONE abap2UI5 class, view for view and control for control:
"! the FlexibleColumnLayout with the order list in the begin column and the
"! order in the mid column, the search, the ViewSettingsDialog that filters
"! and groups (with its group order), the info bar, the line-item table with
"! its totals, the shipping and processor tabs, the share, close and full
"! screen actions, and the NotFound and DetailObjectNotFound pages.
"!
"! The original's router is kept, and so is its URL: the app owns the hash
"! (cs_event-hash_attach_changed) and route_hash( ) matches it against the
"! patterns of manifest.json - "" is the master route,
"! "Orders/{objectId}/:?query:" the object route, anything else is bypassed
"! to NotFound. Each route shows the same pages in the same columns and sets
"! the layouts the route-matched handlers set. navTo is hash_set( ) or - where
"! the original passes bReplace - hash_replace( ): a selection replaces the
"! hash on a desktop and pushes it on a phone, a tab switch replaces it, an
"! object route without a valid ?tab= is rewritten to tab=shipping, and the
"! Back buttons of the not-found pages are onNavBack (cs_event-hash_back,
"! falling back to the master route when the app has no history of its own).
"!
"! Where it differs from the original, and why - only what an app without a
"! browser-side model layer cannot do the same way:
"!
"!  - the OData V2 service and its mock server become ABAP data - the five
"!    mock files verbatim, the Edm.DateTime values as the epoch milliseconds
"!    the mock carries. What the service does is done where the data is,
"!    with the mock server's semantics: the search is its case-sensitive
"!    substringof on the order's own CustomerName (so order 3115, whose
"!    CustomerName the mock leaves empty, is never found), the two filters
"!    are ShippedDate ne/eq null, and the list's sort order is the one the
"!    binding asks for - OrderID descending until the dialog is first
"!    confirmed, then the grouper's sorter (a stable sort on Customer/
"!    CompanyName, OrderDate or ShippedDate, ascending or descending), and no
"!    sorter at all - the mock's own row order - once a confirm carries no
"!    grouping, because _applyGrouper replaces the binding's sorters on every
"!    confirm.
"!  - a GROUP HEADER is made by the list binding, from a Sorter with a group
"!    function the original passes per grouping. A backend cannot hand over a
"!    function, so the rows are bound as JSON the class composes
"!    (_bind( json = abap_true )): each row carries its group as an object
"!    { key, text } - exactly what the original's group functions return - and
"!    cs_event-binding_call sorts on it with group = X. The default group
"!    function then yields that object as the group, and the default
"!    comparator ranks two objects as equal, so the sort leaves the order the
"!    backend chose. A view rebuild loses the sorter; view_display re-issues it.
"!  - the formatter module is business logic and moves to the backend: the
"!    delivery state and text, the unit price's two digits, the order total and
"!    the employee picture's fallback. The dates and amounts keep the
"!    original's DateType and CurrencyType bindings, so they format in the
"!    browser's locale and time zone exactly as there. The group header texts
"!    ("Ordered in November 2016") are composed in ABAP from the UTC date,
"!    where the original's group function reads the month in the browser's
"!    time zone - so east of UTC one order (2686, shipped 2016-10-31 23:00
"!    UTC) is listed under the other month of its shipping date.
"!  - the share action's mail body loses its line breaks: abap2UI5's URLHELPER
"!    action refuses CR/LF in its parameters (a header-injection guard), and
"!    the i18n text carries three of them, so they become spaces. Subject,
"!    recipient-less mailto: and the link to the order are the original's.
"!  - the processor's picture is the original's fallback image
"!    (images/Employee.png, as every Photo of the mock is empty), served from
"!    the demo kit host, because this archive carries no binaries.
"!  - the IllustratedMessage is @since 1.98 and this package holds the 1.71
"!    floor: the empty list shows the same text as a noDataText (the List's
"!    noData aggregation is @since 1.101), and the two not-found pages show
"!    theirs in a MessagePage, the 1.71 empty-page control.
"!  - the i18n resource bundle becomes literals. The busy indicators the
"!    original shows until its OData metadata and data have arrived, and the
"!    ErrorHandler's MessageBox for a failed request, have no service to wait
"!    for or to fail here. The content density needs nothing: abap2UI5's page
"!    already carries sapUiSizeCompact on its body, which is the case in which
"!    the original's getContentDensityClass adds no class either.
"!
"! Original: src/sap.m/test/sap/m/demokit/orderbrowser in OpenUI5, archived
"! under ui5/demoapps/sap.m/orderbrowser.
"! Demo apps: https://sdk.openui5.org/demoapps
CLASS z2ui5_cl_smpc_demo_002 DEFINITION PUBLIC.

  PUBLIC SECTION.
    INTERFACES z2ui5_if_app.

    " Edm.DateTime as the epoch milliseconds of the mock - what the
    " DateType bindings read with source pattern 'timestamp'
    TYPES ty_ms TYPE p LENGTH 8 DECIMALS 0.
    TYPES ty_amount TYPE p LENGTH 12 DECIMALS 2.
    TYPES:
      BEGIN OF ty_s_item,
        productname TYPE string,
        productid   TYPE string,
        unitprice   TYPE string,
        quantity    TYPE string,
        item_total  TYPE ty_amount,
      END OF ty_s_item.
    TYPES:
      BEGIN OF ty_s_mail,
        email   TYPE string,
        subject TYPE string,
        body    TYPE string,
      END OF ty_s_mail.
    TYPES:
      BEGIN OF ty_s_tel,
        tel TYPE string,
      END OF ty_s_tel.

    " the master list - JSON composed in rows_build( ), see the header
    DATA rows_json          TYPE string VALUE `[]`.
    DATA t_items            TYPE STANDARD TABLE OF ty_s_item WITH EMPTY KEY.
    " appView>/layout and appView>/actionButtonsInfo/midColumn/fullScreen
    DATA layout             TYPE string VALUE `OneColumn`.
    DATA check_fullscreen   TYPE abap_bool.
    " masterView
    DATA title_count        TYPE string.
    DATA no_data_text       TYPE string VALUE `No orders are currently available`.
    DATA filter_bar_visible TYPE abap_bool.
    DATA filter_bar_label   TYPE string.
    DATA search_value       TYPE string.
    " the dialog's selections, bound so a rebuilt view shows them again
    DATA flt_shipped        TYPE abap_bool.
    DATA flt_not_shipped    TYPE abap_bool.
    DATA grp_customer       TYPE abap_bool.
    DATA grp_order          TYPE abap_bool.
    DATA grp_shipped        TYPE abap_bool.
    DATA group_descending   TYPE abap_bool.
    " detailView and the bound order
    DATA selected_tab       TYPE string VALUE `shipping`.
    DATA det_title          TYPE string.
    DATA det_customer       TYPE string.
    DATA det_orderdate      TYPE p LENGTH 8 DECIMALS 0.
    DATA det_shippeddate    TYPE p LENGTH 8 DECIMALS 0.
    DATA det_total          TYPE ty_amount.
    DATA det_currency       TYPE string VALUE `EUR`.
    DATA det_items_title    TYPE string VALUE `Line Items`.
    DATA det_shipname       TYPE string.
    DATA det_shipaddress    TYPE string.
    DATA det_shipzipcity    TYPE string.
    DATA det_shipregion     TYPE string.
    DATA det_shipcountry    TYPE string.
    DATA det_employee       TYPE string.
    DATA det_employeeid     TYPE string.
    DATA det_jobtitle       TYPE string.
    DATA det_photo          TYPE string.
    DATA s_tel              TYPE ty_s_tel.
    DATA s_mail             TYPE ty_s_mail.

  PROTECTED SECTION.
    TYPES:
      BEGIN OF ty_s_order,
        orderid      TYPE i,
        customerid   TYPE string,
        customername TYPE string,
        companyname  TYPE string,
        employeeid   TYPE i,
        orderdate    TYPE ty_ms,
        requireddate TYPE ty_ms,
        shippeddate  TYPE ty_ms,
        shipname     TYPE string,
        shipaddress  TYPE string,
        shipcity     TYPE string,
        shipregion   TYPE string,
        shippostal   TYPE string,
        shipcountry  TYPE string,
      END OF ty_s_order.
    TYPES ty_t_order TYPE STANDARD TABLE OF ty_s_order WITH EMPTY KEY.
    TYPES:
      BEGIN OF ty_s_customer,
        customerid  TYPE string,
        companyname TYPE string,
      END OF ty_s_customer.
    TYPES:
      BEGIN OF ty_s_employee,
        employeeid TYPE i,
        firstname  TYPE string,
        lastname   TYPE string,
        title      TYPE string,
        homephone  TYPE string,
      END OF ty_s_employee.
    TYPES:
      BEGIN OF ty_s_product,
        productid   TYPE i,
        productname TYPE string,
      END OF ty_s_product.
    TYPES:
      BEGIN OF ty_s_detail,
        orderid   TYPE i,
        productid TYPE i,
        unitprice TYPE ty_amount,
        quantity  TYPE i,
      END OF ty_s_detail.

    DATA client          TYPE REF TO z2ui5_if_client.
    DATA t_orders        TYPE ty_t_order.
    DATA t_customers     TYPE STANDARD TABLE OF ty_s_customer WITH EMPTY KEY.
    DATA t_employees     TYPE STANDARD TABLE OF ty_s_employee WITH EMPTY KEY.
    DATA t_products      TYPE STANDARD TABLE OF ty_s_product WITH EMPTY KEY.
    DATA t_details       TYPE STANDARD TABLE OF ty_s_detail WITH EMPTY KEY.
    " Master.controller's _oListFilterState and the binding's sorters
    DATA search_term     TYPE string.
    DATA filter_key      TYPE string.
    DATA group_key       TYPE string.
    DATA check_sorted    TYPE abap_bool.
    " the route's objectId, the order bound to the detail page and the one
    " the ListSelector marks in the master list
    DATA route_objectid  TYPE string.
    DATA order_shown     TYPE i.
    DATA order_selected  TYPE i.
    DATA previous_layout TYPE string.
    " the page each column shows, and what the client shows right now -
    " bookkeeping the view never binds
    DATA page_begin      TYPE string VALUE `page`.
    DATA page_mid        TYPE string VALUE `detailPage`.
    DATA page_begin_live TYPE string.
    DATA page_mid_live   TYPE string.
    DATA group_live      TYPE string.

    METHODS view_display.
    METHODS view_dialog
      IMPORTING
        view TYPE REF TO z2ui5_cl_ui5_view_builder.
    METHODS view_master
      IMPORTING
        fcl TYPE REF TO z2ui5_cl_ui5_view_builder.
    METHODS view_detail
      IMPORTING
        fcl TYPE REF TO z2ui5_cl_ui5_view_builder.
    METHODS view_detail_tabs
      IMPORTING
        column TYPE REF TO z2ui5_cl_ui5_view_builder.
    METHODS view_detail_items
      IMPORTING
        column TYPE REF TO z2ui5_cl_ui5_view_builder.
    METHODS on_event.
    METHODS on_select
      IMPORTING
        orderid TYPE string.
    METHODS on_search.
    METHODS on_view_settings.
    METHODS on_toggle_fullscreen.
    METHODS route_hash.
    METHODS route_master.
    METHODS route_object
      IMPORTING
        objectid TYPE string
        tab      TYPE string.
    METHODS route_bypassed.
    METHODS detail_bind
      IMPORTING
        order TYPE ty_s_order.
    METHODS pages_sync.
    METHODS group_sync
      IMPORTING
        check_force TYPE abap_bool DEFAULT abap_false.
    METHODS rows_build.
    METHODS rows_sort
      CHANGING
        t_order TYPE ty_t_order.
    METHODS row_json
      IMPORTING
        order         TYPE ty_s_order
        pos           TYPE i
      RETURNING
        VALUE(result) TYPE string.
    METHODS json_text
      IMPORTING
        val           TYPE string
      RETURNING
        VALUE(result) TYPE string.
    METHODS utc_date
      IMPORTING
        val           TYPE ty_ms
      RETURNING
        VALUE(result) TYPE d.
    METHODS month_name
      IMPORTING
        val           TYPE d
      RETURNING
        VALUE(result) TYPE string.
    METHODS amount
      IMPORTING
        val           TYPE ty_amount
      RETURNING
        VALUE(result) TYPE string.
    METHODS model_init.

  PRIVATE SECTION.
ENDCLASS.


CLASS z2ui5_cl_smpc_demo_002 IMPLEMENTATION.

  METHOD z2ui5_if_app~main.

    me->client = client.

    IF client->check_on_init( ).
      model_init( ).
      view_display( ).
    ELSEIF client->check_on_navigated( ).
      view_display( ).
    ELSEIF client->check_on_event( ).
      on_event( ).
    ENDIF.

  ENDMETHOD.


  METHOD view_display.

    " the app owns the hash: the browser Back/Forward buttons and a
    " hand-edited URL round-trip as HASH_CHANGED. Queued FIRST - a route
    " below may already rewrite the hash, and without the listener in place
    " the frontend would append that value instead of writing it. The
    " registration dies with an app switch, so it is re-issued per render
    client->follow_up_action( val = client->cs_event-hash_attach_changed t_arg = VALUE #( ( `HASH_CHANGED` ) ) ).

    " a start, a reload or a shared link: the live hash is matched like the
    " router's initialize( ) matches it
    route_hash( ).
    rows_build( ).

    DATA(view) = z2ui5_cl_ui5_view_builder=>factory(
        )->ele( n = `View` ns = `mvc`
            )->a( n = `displayBlock`   v = `true`
            )->a( n = `height`         v = `100%`
            )->a( n = `xmlns`          v = `sap.m`
            )->a( n = `xmlns:mvc`      v = `sap.ui.core.mvc`
            )->a( n = `xmlns:f`        v = `sap.f`
            )->a( n = `xmlns:semantic` v = `sap.f.semantic`
            )->a( n = `xmlns:core`     v = `sap.ui.core`
            )->a( n = `xmlns:form`     v = `sap.ui.layout.form`
            )->a( n = `xmlns:l`        v = `sap.ui.layout` ).

    view_dialog( view ).

    " App.view.xml. The layout is bound two-way, as appView>/layout is: the
    " FCL's own arrows change it, and toggleFullScreen stores what they left
    DATA(fcl) = view->ele( `App`
        )->a( n = `id` v = `app`

        )->ele( n = `FlexibleColumnLayout` ns = `f`
            )->a( n = `id`               v = `layout`
            )->a( n = `layout`           v = client->_bind( layout )
            )->a( n = `backgroundDesign` v = `Translucent` ).

    view_master( fcl ).
    view_detail( fcl ).

    client->view_display( view->stringify( ) ).

    " a rebuilt FlexibleColumnLayout starts on the first page of each column
    " and a rebuilt list binding without a sorter - both re-issued from the
    " state that survived
    page_begin_live = `page`.
    page_mid_live   = `detailPage`.
    pages_sync( ).
    group_live = ``.
    group_sync( ).

  ENDMETHOD.


  METHOD view_dialog.

    " ViewSettingsDialog.fragment.xml, which Master.onOpenViewSettings loads
    " into the view's dependents - declared there and opened by id without a
    " round-trip. The confirm hands over what _onConfirm reads: the text and
    " key of the filter item, the group item's key and the group order. The
    " selections are bound as well, so a rebuilt view restores them
    view->ele( n = `dependents` ns = `mvc`
        )->ele( `ViewSettingsDialog`
            )->a( n = `id`              v = `viewSettingsDialog`
            )->a( n = `groupDescending` v = client->_bind( group_descending )
            )->a( n = `confirm`         v = client->_event( val   = `VIEW_SETTINGS`
                                                            t_arg = VALUE #( ( `${$parameters>/filterItems}.length ? ${$parameters>/filterItems}[0].getText() : ''` )
                                                                             ( `${$parameters>/filterItems}.length ? ${$parameters>/filterItems}[0].getKey() : ''` )
                                                                             ( `${$parameters>/groupItem} ? ${$parameters>/groupItem}.getKey() : ''` )
                                                                             ( `${$parameters>/groupDescending} ? 'X' : ''` ) ) )

            )->ele( `filterItems`
                )->ele( `ViewSettingsFilterItem`
                    )->a( n = `id`          v = `filterItems`
                    )->a( n = `text`        v = `Orders`
                    )->a( n = `key`         v = `Orders`
                    )->a( n = `multiSelect` b = abap_false

                    )->ele( `items`
                        )->tag( `ViewSettingsItem`
                            )->a( n = `id`       v = `viewFilter1`
                            )->a( n = `text`     v = `Only Shipped Orders`
                            )->a( n = `key`      v = `Shipped`
                            )->a( n = `selected` v = client->_bind( flt_shipped )
                        )->tag( `ViewSettingsItem`
                            )->a( n = `id`       v = `viewFilter2`
                            )->a( n = `text`     v = `Only Orders without Shipment`
                            )->a( n = `key`      v = `NotShipped`
                            )->a( n = `selected` v = client->_bind( flt_not_shipped )

                    )->end(
                )->end(
            )->end(
            )->ele( `groupItems`
                )->tag( `ViewSettingsItem`
                    )->a( n = `text`     v = `Group by Customer`
                    )->a( n = `key`      v = `CompanyName`
                    )->a( n = `selected` v = client->_bind( grp_customer )
                )->tag( `ViewSettingsItem`
                    )->a( n = `text`     v = `Group by Order Period`
                    )->a( n = `key`      v = `OrderDate`
                    )->a( n = `selected` v = client->_bind( grp_order )
                )->tag( `ViewSettingsItem`
                    )->a( n = `text`     v = `Group by Shipped Period`
                    )->a( n = `key`      v = `ShippedDate`
                    )->a( n = `selected` v = client->_bind( grp_shipped ) ).

  ENDMETHOD.


  METHOD view_master.

    DATA(open_filter) = client->follow_up_action( val   = client->cs_event-control_by_id
                                                  t_arg = VALUE #( ( `viewSettingsDialog` ) ( `open` ) ( `filter` ) ) ).

    " the begin column: Master.view.xml and the NotFound target
    DATA(column) = fcl->ele( n = `beginColumnPages` ns = `f` ).

    DATA(list) = column->ele( n = `SemanticPage` ns = `semantic`
        )->a( n = `id`           v = `page`
        )->a( n = `core:require` v = `{DateType: 'sap/ui/model/type/Date'}`

        )->ele( n = `titleHeading` ns = `semantic`
            )->tag( `Title`
                )->a( n = `id`   v = `masterHeaderTitle`
                )->a( n = `text` v = client->_bind( title_count )

        )->end(
        )->ele( n = `content` ns = `semantic`
            )->ele( `List`
                )->a( n = `id`                  v = `list`
                )->a( n = `width`               v = `auto`
                )->a( n = `class`               v = `sapFDynamicPageAlignContent`
                )->a( n = `items`               v = client->_bind( val = rows_json json = abap_true )
                )->a( n = `noDataText`          v = client->_bind( no_data_text )
                )->a( n = `mode`                v = `{= ${device>/system/phone} ? 'None' : 'SingleSelectMaster'}`
                )->a( n = `growing`             b = abap_true
                )->a( n = `growingScrollToLoad` b = abap_true
                " a click in SingleSelectMaster mode fires the list's
                " selectionChange and never the item's press
                )->a( n = `selectionChange`     v = client->_event( val = `SELECT`
                                                                    arg = `${$parameters>/listItem}.getBindingContext().getProperty('orderid')` ) ).

    " the info bar and the three header controls open the dialog on the page
    " onOpenViewSettings picks: "group" for the group button, "filter" for
    " everything else
    list->ele( `infoToolbar`
        )->ele( `Toolbar`
            )->a( n = `active`  b = abap_true
            )->a( n = `id`      v = `filterBar`
            )->a( n = `visible` v = client->_bind( filter_bar_visible )
            )->a( n = `press`   v = open_filter

            )->tag( `Title`
                )->a( n = `id`   v = `filterBarLabel`
                )->a( n = `text` v = client->_bind( filter_bar_label ) ).

    list->ele( `headerToolbar`
        )->ele( `OverflowToolbar`

            )->ele( `SearchField`
                )->a( n = `id`                v = `searchField`
                )->a( n = `showRefreshButton` b = abap_true
                )->a( n = `tooltip`           v = `Enter an order name or a part of it.`
                )->a( n = `width`             v = `100%`
                )->a( n = `value`             v = client->_bind( search_value )
                )->a( n = `search`            v = client->_event( val   = `SEARCH`
                                                                  t_arg = VALUE #( ( `${$parameters>/query}` )
                                                                                   ( `${$parameters>/refreshButtonPressed} ? 'X' : ''` ) ) )

                )->ele( `layoutData`
                    )->tag( `OverflowToolbarLayoutData`
                        )->a( n = `minWidth`   v = `150px`
                        )->a( n = `maxWidth`   v = `240px`
                        )->a( n = `shrinkable` b = abap_true
                        )->a( n = `priority`   v = `NeverOverflow`

                )->end(
            )->end(
            )->tag( `ToolbarSpacer`
            )->tag( `Button`
                )->a( n = `id`    v = `filterButton`
                )->a( n = `press` v = open_filter
                )->a( n = `icon`  v = `sap-icon://filter`
                )->a( n = `type`  v = `Transparent`
            )->tag( `Button`
                )->a( n = `id`    v = `groupButton`
                )->a( n = `press` v = client->follow_up_action( val   = client->cs_event-control_by_id
                                                                t_arg = VALUE #( ( `viewSettingsDialog` ) ( `open` ) ( `group` ) ) )
                )->a( n = `icon`  v = `sap-icon://group-2`
                )->a( n = `type`  v = `Transparent` ).

    " the rows are the JSON of rows_build( ), so their fields are the lower
    " case keys composed there
    list->ele( `items`
        )->ele( `ObjectListItem`
            )->a( n = `type`     v = `{= ${device>/system/phone} ? 'Active' : 'Inactive'}`
            )->a( n = `press`    v = client->_event( val = `SELECT` arg = `${orderid}` )
            )->a( n = `selected` v = `{selected}`
            )->a( n = `title`    v = `{title}`
            )->a( n = `number`   v = `{ path: 'orderdate', type: 'DateType', formatOptions: { style: 'short', source: { pattern: 'timestamp' } } }`

            )->ele( `firstStatus`
                )->tag( `ObjectStatus`
                    )->a( n = `state` v = `{state}`
                    )->a( n = `text`  v = `{text}`

            )->end(
            )->ele( `attributes`
                )->tag( `ObjectAttribute`
                    )->a( n = `id`   v = `companyName`
                    )->a( n = `text` v = `{companyname}`
                )->tag( `ObjectAttribute`
                    )->a( n = `title` v = `Shipped`
                    )->a( n = `text`  v = `{= ${shippeddate} ? ${ path: 'shippeddate', type: 'DateType', ` &&
                                          `formatOptions: { style: 'medium', source: { pattern: 'timestamp' } } } : 'Not shipped yet' }` ).

    " NotFound.view.xml - the bypassed target
    column->ele( `Page`
        )->a( n = `id`             v = `notFoundPage`
        )->a( n = `title`          v = `Not Found`
        )->a( n = `showNavButton`  b = abap_true
        )->a( n = `navButtonPress` v = client->_event( `NAV_BACK` )

        )->tag( `MessagePage`
            )->a( n = `showHeader`  b = abap_false
            )->a( n = `text`        v = `The requested resource was not found`
            )->a( n = `description` v = ``
            )->a( n = `icon`        v = `sap-icon://documents` ).

  ENDMETHOD.


  METHOD view_detail.


    " the mid column: Detail.view.xml and the DetailObjectNotFound target
    DATA(column) = fcl->ele( n = `midColumnPages` ns = `f` ).

    DATA(detail) = column->ele( n = `SemanticPage` ns = `semantic`
        )->a( n = `id`           v = `detailPage`
        )->a( n = `core:require` v = `{DateType: 'sap/ui/model/type/Date', CurrencyType: 'sap/ui/model/type/Currency'}` ).

    detail->ele( n = `titleHeading` ns = `semantic`
        )->tag( `Title`
            )->a( n = `text` v = client->_bind( det_title ) ).

    DATA(header) = detail->ele( n = `headerContent` ns = `semantic`
        )->ele( n = `HorizontalLayout` ns = `l` ).

    header->ele( n = `VerticalLayout` ns = `l`
        )->a( n = `class` v = `sapUiMediumMarginEnd`

        )->tag( `ObjectAttribute`
            )->a( n = `title` v = `Customer`
            )->a( n = `text`  v = client->_bind( det_customer )
        )->tag( `ObjectAttribute`
            )->a( n = `title` v = `Ordered`
            )->a( n = `text`  v = |\{ path: '{ client->_bind( val = det_orderdate path = abap_true ) }', type: 'DateType', | &&
                                  |formatOptions: \{ style: 'medium', source: \{ pattern: 'timestamp' \} \} \}|
        )->tag( `ObjectAttribute`
            )->a( n = `title` v = `Shipped`
            )->a( n = `text`  v = |\{= ${ client->_bind( det_shippeddate ) } ? $\{ path: '{ client->_bind( val = det_shippeddate path = abap_true ) }', | &&
                                  |type: 'DateType', formatOptions: \{ style: 'medium', source: \{ pattern: 'timestamp' \} \} \} : 'Not shipped yet' \}| ).

    header->ele( n = `VerticalLayout` ns = `l`
        )->tag( `Label`
            )->a( n = `text` v = `Price`
        )->tag( `ObjectNumber`
            )->a( n = `number` v = |\{ parts: [ \{ path: '{ client->_bind( val = det_total path = abap_true ) }' \}, | &&
                                   |\{ path: '{ client->_bind( val = det_currency path = abap_true ) }' \} ], | &&
                                   |type: 'CurrencyType', formatOptions: \{ showMeasure: false \} \}|
            )->a( n = `unit`   v = client->_bind( det_currency ) ).

    DATA(content) = detail->ele( n = `content` ns = `semantic`
        )->ele( n = `VerticalLayout` ns = `l` ).
    view_detail_tabs( content ).
    view_detail_items( content ).

    " the share action is URLHelper.triggerEmail - a frontend action that
    " reads the bound mail structure when it is pressed, no round-trip
    detail->ele( n = `sendEmailAction` ns = `semantic`
        )->tag( n = `SendEmailAction` ns = `semantic`
            )->a( n = `id`    v = `shareEmail`
            )->a( n = `press` v = client->follow_up_action( val   = client->cs_event-urlhelper
                                                            t_arg = VALUE #( ( `TRIGGER_EMAIL` ) ( |${ client->_bind( s_mail ) }| ) ) ) ).

    detail->ele( n = `closeAction` ns = `semantic`
        )->tag( n = `CloseAction` ns = `semantic`
            )->a( n = `id`    v = `closeColumn`
            )->a( n = `press` v = client->_event( `CLOSE_DETAIL` ) ).

    detail->ele( n = `fullScreenAction` ns = `semantic`
        )->tag( n = `FullScreenAction` ns = `semantic`
            )->a( n = `id`      v = `enterFullScreen`
            )->a( n = `visible` v = |\{= !$\{device>/system/phone\} && !${ client->_bind( check_fullscreen ) } \}|
            )->a( n = `press`   v = client->_event( `TOGGLE_FULLSCREEN` ) ).

    detail->ele( n = `exitFullScreenAction` ns = `semantic`
        )->tag( n = `ExitFullScreenAction` ns = `semantic`
            )->a( n = `id`      v = `exitFullScreen`
            )->a( n = `visible` v = |\{= !$\{device>/system/phone\} && ${ client->_bind( check_fullscreen ) } \}|
            )->a( n = `press`   v = client->_event( `TOGGLE_FULLSCREEN` ) ).

    " DetailObjectNotFound.view.xml - the target _onBindingChange displays
    " when the route names an order the service does not have
    column->ele( `Page`
        )->a( n = `id`             v = `detailObjectNotFoundPage`
        )->a( n = `title`          v = `Order Details`
        )->a( n = `showNavButton`  v = `{= ${device>/system/phone} || ${device>/system/tablet} && ${device>/orientation/portrait} }`
        )->a( n = `navButtonPress` v = client->_event( `NAV_BACK` )

        )->tag( `MessagePage`
            )->a( n = `showHeader`  b = abap_false
            )->a( n = `text`        v = `This order is not available`
            )->a( n = `description` v = ``
            )->a( n = `icon`        v = `sap-icon://search` ).

  ENDMETHOD.


  METHOD view_detail_tabs.

    " the shipping and processor targets are displayed INTO these two tabs;
    " a tab switch is onTabSelect, which rewrites the route's ?tab= query
    DATA(tabs) = column->ele( `IconTabBar`
        )->a( n = `id`                     v = `iconTabBar`
        )->a( n = `headerBackgroundDesign` v = `Transparent`
        )->a( n = `select`                 v = client->_event( val = `TAB_SELECT` arg = `${$parameters>/selectedKey}` )
        )->a( n = `selectedKey`            v = client->_bind( selected_tab )

        )->ele( `items` ).

    " Shipping.view.xml
    tabs->ele( `IconTabFilter`
        )->a( n = `id`      v = `iconTabFilterShipping`
        )->a( n = `icon`    v = `sap-icon://shipping-status`
        )->a( n = `tooltip` v = `Shipping Info`
        )->a( n = `key`     v = `shipping`

        )->ele( `VBox`
            )->ele( n = `SimpleForm` ns = `form`
                )->a( n = `id`         v = `SimpleFormShipAddress`
                )->a( n = `editable`   b = abap_false
                )->a( n = `layout`     v = `ResponsiveGridLayout`
                )->a( n = `title`      v = `Shipping Address`
                )->a( n = `labelSpanL` v = `3`
                )->a( n = `labelSpanM` v = `3`
                )->a( n = `emptySpanL` v = `4`
                )->a( n = `emptySpanM` v = `4`
                )->a( n = `columnsL`   v = `1`
                )->a( n = `columnsM`   v = `1`

                )->ele( n = `content` ns = `form`
                    )->tag( `Label`
                        )->a( n = `text` v = `Name`
                    )->tag( `Text`
                        )->a( n = `text` v = client->_bind( det_shipname )
                    )->tag( `Label`
                        )->a( n = `text` v = `Street`
                    )->tag( `Text`
                        )->a( n = `text` v = client->_bind( det_shipaddress )
                    )->tag( `Label`
                        )->a( n = `text` v = `ZIP Code / City`
                    )->tag( `Text`
                        )->a( n = `text` v = client->_bind( det_shipzipcity )
                    )->tag( `Label`
                        )->a( n = `text` v = `Region`
                    )->tag( `Text`
                        )->a( n = `text` v = client->_bind( det_shipregion )
                    )->tag( `Label`
                        )->a( n = `text` v = `Country`
                    )->tag( `Text`
                        )->a( n = `text` v = client->_bind( det_shipcountry ) ).

    " Processor.view.xml. The phone link is URLHelper.triggerTel - a frontend
    " action reading the bound number, no round-trip
    tabs->ele( `IconTabFilter`
        )->a( n = `id`      v = `iconTabFilterProcessor`
        )->a( n = `icon`    v = `sap-icon://employee`
        )->a( n = `tooltip` v = `Processor`
        )->a( n = `key`     v = `processor`

        )->ele( `VBox`
            )->ele( n = `SimpleForm` ns = `form`
                )->a( n = `id`         v = `SimpleFormProcessorInfo`
                )->a( n = `editable`   b = abap_false
                )->a( n = `layout`     v = `ResponsiveGridLayout`
                )->a( n = `title`      v = `Processor Information`
                )->a( n = `labelSpanL` v = `3`
                )->a( n = `labelSpanM` v = `3`
                )->a( n = `emptySpanL` v = `4`
                )->a( n = `emptySpanM` v = `4`
                )->a( n = `columnsL`   v = `2`
                )->a( n = `columnsM`   v = `2`

                )->ele( n = `content` ns = `form`
                    )->tag( n = `Title` ns = `core`
                        )->a( n = `text` v = `Details`
                    )->tag( `Label`
                        )->a( n = `text` v = `Name`
                    )->tag( `Text`
                        )->a( n = `text` v = client->_bind( det_employee )
                    )->tag( `Label`
                        )->a( n = `text` v = `Employee ID`
                    )->tag( `Text`
                        )->a( n = `text` v = client->_bind( det_employeeid )
                    )->tag( `Label`
                        )->a( n = `text` v = `Job Title`
                    )->tag( `Text`
                        )->a( n = `text` v = client->_bind( det_jobtitle )
                    )->tag( `Label`
                        )->a( n = `text` v = `Phone`
                    )->tag( `Link`
                        )->a( n = `text`  v = client->_bind( s_tel-tel )
                        )->a( n = `press` v = client->follow_up_action( val   = client->cs_event-urlhelper
                                                                        t_arg = VALUE #( ( `TRIGGER_TEL` ) ( |${ client->_bind( s_tel ) }| ) ) )
                    )->tag( n = `Title` ns = `core`
                        )->a( n = `text` v = `Picture`
                    )->tag( `Image`
                        )->a( n = `src`    v = client->_bind( det_photo )
                        )->a( n = `width`  v = `50%`
                        )->a( n = `height` v = `50%` ).

  ENDMETHOD.


  METHOD view_detail_items.

    DATA(items) = column->ele( `Table`
        )->a( n = `id`         v = `lineItemsList`
        )->a( n = `class`      v = `sapUiSmallMarginTop`
        )->a( n = `width`      v = `auto`
        )->a( n = `items`      v = client->_bind( t_items )
        )->a( n = `noDataText` v = `No line items` ).

    items->ele( `headerToolbar`
        )->ele( `Toolbar`
            )->a( n = `id` v = `lineItemsToolbar`

            )->tag( `Title`
                )->a( n = `id`   v = `lineItemsHeader`
                )->a( n = `text` v = client->_bind( det_items_title ) ).

    items->ele( `columns`
        )->ele( `Column`

            )->tag( `Text`
                )->a( n = `text` v = `Product`

        )->end(
        )->ele( `Column`
            )->a( n = `minScreenWidth` v = `Tablet`
            )->a( n = `demandPopin`    b = abap_true
            )->a( n = `hAlign`         v = `End`

            )->tag( `Text`
                )->a( n = `text` v = `Unit Price`

        )->end(
        )->ele( `Column`
            )->a( n = `minScreenWidth` v = `Tablet`
            )->a( n = `demandPopin`    b = abap_true
            )->a( n = `hAlign`         v = `End`

            )->tag( `Text`
                )->a( n = `text` v = `Quantity`

        )->end(
        )->ele( `Column`
            )->a( n = `minScreenWidth` v = `Tablet`
            )->a( n = `demandPopin`    b = abap_true
            )->a( n = `hAlign`         v = `End`

            )->tag( `Text`
                )->a( n = `text` v = `Total` ).

    items->ele( `items`
        )->ele( `ColumnListItem`

            )->ele( `cells`
                )->tag( `ObjectIdentifier`
                    )->a( n = `title` v = `{PRODUCTNAME}`
                    )->a( n = `text`  v = `{PRODUCTID}`
                )->tag( `ObjectNumber`
                    )->a( n = `number` v = `{UNITPRICE}`
                    )->a( n = `unit`   v = client->_bind( det_currency )
                )->tag( `ObjectAttribute`
                    )->a( n = `text` v = `{QUANTITY}`
                )->tag( `ObjectNumber`
                    )->a( n = `number` v = |\{ parts: [ \{ path: 'ITEM_TOTAL' \}, \{ path: '{ client->_bind( val = det_currency path = abap_true ) }' \} ], | &&
                                           |type: 'CurrencyType', formatOptions: \{ showMeasure: false \} \}|
                    )->a( n = `unit`   v = client->_bind( det_currency ) ).

  ENDMETHOD.


  METHOD on_event.

    CASE client->get_event( ).

      WHEN `SELECT`.
        on_select( client->get_event_arg( ) ).

      WHEN `SEARCH`.
        on_search( ).

      WHEN `VIEW_SETTINGS`.
        on_view_settings( ).

      WHEN `HASH_CHANGED`.
        route_hash( ).

      WHEN `TAB_SELECT`.
        " onTabSelect: navTo( "object", { objectId, query: { tab } }, true )
        selected_tab = client->get_event_arg( ).
        client->hash_replace( |/Orders/{ route_objectid }/?tab={ selected_tab }| ).

      WHEN `CLOSE_DETAIL`.
        " onCloseDetailPress: out of full screen, no selection left in the
        " master list, and navTo( "master" ) - a pushed history entry
        check_fullscreen = abap_false.
        order_selected   = 0.
        client->hash_set( `/` ).
        route_master( ).

      WHEN `TOGGLE_FULLSCREEN`.
        on_toggle_fullscreen( ).

      WHEN `NAV_BACK`.
        " BaseController.onNavBack: one step back in the history, or - when
        " the app has none of its own - navTo( "master", {}, true )
        client->follow_up_action( val = client->cs_event-hash_back t_arg = VALUE #( ( `/` ) ) ).

    ENDCASE.

    rows_build( ).
    pages_sync( ).

  ENDMETHOD.


  METHOD on_select.

    " Master._showDetail: two columns, and navTo( "object" ) - replacing the
    " hash on a desktop, pushing it on a phone. The object route has no ?tab=
    " then, so _onObjectMatched rewrites it to tab=shipping in place: the net
    " effect is one write of the final hash
    layout = `TwoColumnsMidExpanded`.
    DATA(hash) = |/Orders/{ orderid }/?tab=shipping|.
    IF client->get( )-s_device-system = z2ui5_if_client=>cs_device-system-phone.
      client->hash_set( hash ).
    ELSE.
      client->hash_replace( hash ).
    ENDIF.
    route_object( objectid = orderid tab = `shipping` ).

  ENDMETHOD.


  METHOD on_search.

    " onSearch: the refresh button only refreshes the binding - the query in
    " the field is NOT applied then
    IF client->get_event_arg( 2 ) = `X`.
      RETURN.
    ENDIF.
    search_term = client->get_event_arg( ).
    group_sync( abap_true ).

  ENDMETHOD.


  METHOD on_view_settings.

    " onConfirmViewSettingsDialog: the filter and the info bar - which shows
    " only while a filter is set, but is labelled either way
    filter_bar_label   = |Filtered by { client->get_event_arg( ) }|.
    filter_key         = client->get_event_arg( 2 ).
    filter_bar_visible = xsdbool( filter_key IS NOT INITIAL ).
    flt_shipped        = xsdbool( filter_key = `Shipped` ).
    flt_not_shipped    = xsdbool( filter_key = `NotShipped` ).

    " _applyGrouper: the grouper's sorter REPLACES the binding's sorters -
    " the OrderID sorter of the view is gone after the first confirm
    group_key        = client->get_event_arg( 3 ).
    group_descending = xsdbool( client->get_event_arg( 4 ) = `X` ).
    grp_customer     = xsdbool( group_key = `CompanyName` ).
    grp_order        = xsdbool( group_key = `OrderDate` ).
    grp_shipped      = xsdbool( group_key = `ShippedDate` ).
    check_sorted     = abap_true.
    group_sync( abap_true ).

  ENDMETHOD.


  METHOD on_toggle_fullscreen.

    " toggleFullScreen: remember the layout on the way in, restore it on the
    " way out
    check_fullscreen = xsdbool( check_fullscreen = abap_false ).
    IF check_fullscreen = abap_true.
      previous_layout = layout.
      layout = `MidColumnFullScreen`.
    ELSE.
      layout = previous_layout.
    ENDIF.

  ENDMETHOD.


  METHOD route_hash.

    " the router's parse of the hash, against the patterns of manifest.json:
    " "" -> master, "Orders/{objectId}/:?query:" -> object, else bypassed.
    " crossroads ignores the case of the pattern's literals, as CP does
    DATA(hash) = client->get( )-s_config-hash.
    IF hash CS `#`.
      hash = substring_after( val = hash sub = `#` ).
    ENDIF.
    SHIFT hash LEFT DELETING LEADING `/`.
    " the framework's own app-state hash is none of the app's routes
    IF hash CP `z2ui5-*`.
      RETURN.
    ENDIF.

    IF hash IS INITIAL.
      route_master( ).
      RETURN.
    ENDIF.
    IF hash NP `Orders/*`.
      route_bypassed( ).
      RETURN.
    ENDIF.

    DATA(objectid) = substring_after( val = hash sub = `/` ).
    DATA(query) = ``.
    IF objectid CS `?`.
      query    = substring_after( val = objectid sub = `?` ).
      objectid = substring_before( val = objectid sub = `?` ).
    ENDIF.
    IF objectid CP `*/`.
      objectid = substring( val = objectid len = strlen( objectid ) - 1 ).
    ENDIF.
    IF objectid IS INITIAL OR objectid CS `/`.
      route_bypassed( ).
      RETURN.
    ENDIF.

    DATA(tab) = ``.
    SPLIT query AT `&` INTO TABLE DATA(t_param).
    LOOP AT t_param INTO DATA(param).
      IF substring_before( val = param sub = `=` ) = `tab`.
        tab = substring_after( val = param sub = `=` ).
      ENDIF.
    ENDLOOP.
    route_object( objectid = objectid tab = tab ).

  ENDMETHOD.


  METHOD route_master.

    " the master target, and Master._onMasterMatched
    page_begin = `page`.
    layout     = `OneColumn`.

  ENDMETHOD.


  METHOD route_object.

    " the targets master and object, and Detail._onObjectMatched: two columns
    " unless the detail is in full screen
    route_objectid = objectid.
    page_begin     = `page`.
    IF layout <> `MidColumnFullScreen`.
      layout = `TwoColumnsMidExpanded`.
    ENDIF.

    " a missing or unknown ?tab= becomes tab=shipping, in place
    IF tab = `shipping` OR tab = `processor`.
      selected_tab = tab.
    ELSE.
      selected_tab = `shipping`.
      client->hash_replace( |/Orders/{ objectid }/?tab=shipping| ).
    ENDIF.

    " _bindView and _onBindingChange: the order, or - for an id the service
    " does not have - the DetailObjectNotFound target and no selection
    DATA(order) = VALUE ty_s_order( ).
    IF objectid CO `0123456789` AND strlen( objectid ) <= 9.
      order = VALUE #( t_orders[ orderid = CONV i( objectid ) ] OPTIONAL ).
    ENDIF.
    IF order IS INITIAL.
      page_mid       = `detailObjectNotFoundPage`.
      order_selected = 0.
      RETURN.
    ENDIF.
    page_mid       = `detailPage`.
    order_selected = order-orderid.
    detail_bind( order ).

  ENDMETHOD.


  METHOD route_bypassed.

    " the notFound target, NotFound._onNotFoundDisplayed and
    " Master.onBypassed
    page_begin     = `notFoundPage`.
    layout         = `OneColumn`.
    order_selected = 0.

  ENDMETHOD.


  METHOD detail_bind.

    DATA total      TYPE ty_amount.
    DATA line_total TYPE ty_amount.

    order_shown     = order-orderid.
    det_title       = |Order { order-orderid }|.
    det_customer    = order-companyname.
    det_orderdate   = order-orderdate.
    det_shippeddate = order-shippeddate.
    det_shipname    = order-shipname.
    det_shipaddress = order-shipaddress.
    det_shipzipcity = |{ order-shippostal } { order-shipcity }|.
    det_shipregion  = order-shipregion.
    det_shipcountry = order-shipcountry.

    DATA(employee) = VALUE ty_s_employee( t_employees[ employeeid = order-employeeid ] OPTIONAL ).
    det_employee   = |{ employee-firstname } { employee-lastname }|.
    det_employeeid = |{ employee-employeeid }|.
    det_jobtitle   = employee-title.
    s_tel-tel      = employee-homephone.
    " formatter.handleBinaryContent: every Photo of the mock is empty, so the
    " original shows its fallback, webapp/images/Employee.png
    det_photo = `https://sdk.openui5.org/test-resources/sap/m/demokit/orderbrowser/webapp/images/Employee.png`.

    " the line items, and onListUpdateFinished's title and order total
    t_items = VALUE #( ).
    LOOP AT t_details INTO DATA(item) WHERE orderid = order-orderid.
      line_total = item-unitprice * item-quantity.
      total = total + line_total.
      INSERT VALUE #( productname = VALUE #( t_products[ productid = item-productid ]-productname OPTIONAL )
                      productid   = |{ item-productid }|
                      unitprice   = amount( item-unitprice )
                      quantity    = |{ item-quantity }|
                      item_total  = line_total ) INTO TABLE t_items.
    ENDLOOP.
    det_total       = total.
    det_items_title = COND #( WHEN t_items IS INITIAL THEN `Line Items` ELSE |Line Items ({ lines( t_items ) })| ).

    " _onBindingChange: the share texts, with the link to this order
    DATA(config) = client->get( )-s_config.
    s_mail-subject = |Order { order-orderid }|.
    s_mail-body    = |Please take a look at order { order-orderid } (id: { order-orderid }) | &&
                     |{ config-origin }{ config-pathname }{ config-search }#/Orders/{ order-orderid }/?tab={ selected_tab } | &&
                     |More Information on this order: - Customer Name: { order-shipname } | &&
                     |- Customer ID: { order-customerid } - Processor ID: { order-employeeid }|.

  ENDMETHOD.


  METHOD pages_sync.

    " the router's target display: FlexibleColumnLayout.to( ) moves the
    " column that owns the page - issued only where the page changes
    IF page_begin <> page_begin_live.
      client->follow_up_action( val = client->cs_event-control_by_id t_arg = VALUE #( ( `layout` ) ( `to` ) ( page_begin ) ) ).
      page_begin_live = page_begin.
    ENDIF.
    IF page_mid <> page_mid_live.
      client->follow_up_action( val = client->cs_event-control_by_id t_arg = VALUE #( ( `layout` ) ( `to` ) ( page_mid ) ) ).
      page_mid_live = page_mid.
    ENDIF.

  ENDMETHOD.


  METHOD group_sync.

    " the grouper's sorter on the LIST BINDING: sorting on each row's group
    " object with group = X draws the original's group headers, and keeps the
    " order rows_build( ) chose. Without a grouping, a sorter on the row
    " position takes the headers away again - the original's sort( [] )
    IF group_key IS INITIAL.
      IF group_live IS NOT INITIAL.
        client->follow_up_action( val   = client->cs_event-binding_call
                                  t_arg = VALUE #( ( `list` ) ( `items` ) ( `sort` ) ( `pos` ) ) ).
      ENDIF.
    ELSEIF group_key <> group_live OR check_force = abap_true.
      client->follow_up_action( val   = client->cs_event-binding_call
                                t_arg = VALUE #( ( `list` ) ( `items` ) ( `sort` ) ( `grp` ) ( `` ) ( `X` ) ) ).
    ENDIF.
    group_live = group_key.

  ENDMETHOD.


  METHOD rows_build.

    DATA t_order TYPE ty_t_order.

    " _applyFilterSearch: the search and the filter, ANDed as the binding
    " ANDs filters on different paths
    LOOP AT t_orders INTO DATA(order).
      IF    ( filter_key = `Shipped` AND order-shippeddate IS INITIAL )
         OR ( filter_key = `NotShipped` AND order-shippeddate IS NOT INITIAL ).
        CONTINUE.
      ENDIF.
      IF search_term IS NOT INITIAL AND find( val = order-customername sub = search_term ) < 0.
        CONTINUE.
      ENDIF.
      INSERT order INTO TABLE t_order.
    ENDLOOP.
    rows_sort( CHANGING t_order = t_order ).

    DATA(json) = ``.
    LOOP AT t_order INTO order.
      DATA(pos) = sy-tabix.
      IF pos > 1.
        json = json && `,`.
      ENDIF.
      json = json && row_json( order = order pos = pos ).
    ENDLOOP.
    rows_json = |[{ json }]|.

    " _updateListItemCount, and the noDataText _applyFilterSearch sets once a
    " filter or search is applied - and, as there, never sets back
    title_count = |Orders ({ lines( t_order ) })|.
    IF filter_key IS NOT INITIAL OR search_term IS NOT INITIAL.
      no_data_text = `No matching order found`.
    ENDIF.

  ENDMETHOD.


  METHOD rows_sort.

    " the binding's sorter: the view's OrderID descending until a confirm
    " replaced it, then the grouper's sorter - or none, the mock's row order.
    " The mock server sorts stably, so equal values keep the mock's order
    IF check_sorted = abap_false.
      SORT t_order BY orderid DESCENDING.
      RETURN.
    ENDIF.
    CASE group_key.
      WHEN `CompanyName`.
        IF group_descending = abap_true.
          SORT t_order STABLE BY companyname DESCENDING.
        ELSE.
          SORT t_order STABLE BY companyname.
        ENDIF.
      WHEN `OrderDate`.
        IF group_descending = abap_true.
          SORT t_order STABLE BY orderdate DESCENDING.
        ELSE.
          SORT t_order STABLE BY orderdate.
        ENDIF.
      WHEN `ShippedDate`.
        IF group_descending = abap_true.
          SORT t_order STABLE BY shippeddate DESCENDING.
        ELSE.
          SORT t_order STABLE BY shippeddate.
        ENDIF.
    ENDCASE.

  ENDMETHOD.


  METHOD row_json.

    " typed, not inline: a difference of packed values has no length and
    " decimals a declaration could take over, so DATA( ) would be P(8,0)
    DATA margin TYPE ty_ms.

    " the deliveryState / deliveryText formatters: an unshipped order has
    " none, a delivery up to five days before the required date is urgent,
    " one after it too late
    DATA(state) = `None`.
    DATA(text) = `None`.
    IF order-shippeddate IS NOT INITIAL.
      margin = order-requireddate - order-shippeddate.
      IF margin > 0 AND margin <= 432000000.
        state = `Warning`.
        text  = `Urgent`.
      ELSEIF order-requireddate < order-shippeddate.
        state = `Error`.
        text  = `Too late`.
      ELSE.
        state = `Success`.
        text  = `In time`.
      ENDIF.
    ENDIF.

    " the group the original's group function returns for the row
    DATA(key) = ``.
    DATA(group) = ``.
    CASE group_key.
      WHEN `CompanyName`.
        key   = order-companyname.
        group = order-companyname.
      WHEN `OrderDate`.
        DATA(ordered) = utc_date( order-orderdate ).
        key   = |{ ordered(4) }-{ CONV i( ordered+4(2) ) }|.
        group = |Ordered in { month_name( ordered ) } { ordered(4) }|.
      WHEN `ShippedDate`.
        IF order-shippeddate IS INITIAL.
          key   = `0`.
          group = `Not Shipped Yet`.
        ELSE.
          DATA(shipped) = utc_date( order-shippeddate ).
          key   = |{ shipped(4) }-{ CONV i( shipped+4(2) ) }|.
          group = |Shipped in { month_name( shipped ) } { shipped(4) }|.
        ENDIF.
    ENDCASE.

    result = |\{"pos":{ pos },"orderid":{ order-orderid },"title":{ json_text( |Order { order-orderid }| ) },| &&
             |"orderdate":{ order-orderdate },| &&
             |"shippeddate":{ COND string( WHEN order-shippeddate IS INITIAL THEN `null` ELSE |{ order-shippeddate }| ) },| &&
             |"companyname":{ json_text( order-companyname ) },"state":"{ state }","text":"{ text }",| &&
             |"selected":{ COND string( WHEN order-orderid = order_selected THEN `true` ELSE `false` ) },| &&
             |"grp":\{"key":{ json_text( key ) },"text":{ json_text( group ) }\}\}|.

  ENDMETHOD.


  METHOD json_text.

    " a JSON string literal: the backslash and the quote escaped
    DATA(escaped) = replace( val = val sub = `\` with = `\\` occ = 0 ).
    result = |"{ replace( val = escaped sub = `"` with = `\"` occ = 0 ) }"|.

  ENDMETHOD.


  METHOD utc_date.

    " the calendar day of an epoch millisecond value, in UTC
    result = CONV d( `19700101` ).
    result = result + val DIV 86400000.

  ENDMETHOD.


  METHOD month_name.

    " the month name the original's DateFormat "MMMM" writes into a group
    " header
    DATA(months) = VALUE string_table( ( `January` ) ( `February` ) ( `March` ) ( `April` )
                                       ( `May` ) ( `June` ) ( `July` ) ( `August` )
                                       ( `September` ) ( `October` ) ( `November` ) ( `December` ) ).
    result = months[ CONV i( val+4(2) ) ].

  ENDMETHOD.


  METHOD amount.

    " formatter.currencyValue: two digits, no grouping
    result = |{ val DECIMALS = 2 NUMBER = RAW }|.

  ENDMETHOD.


  METHOD model_init.

    " localService/mockdata/Orders.json - the Edm.DateTime values are the
    " mock's /Date(ms)/ milliseconds, CustomerName the order's own column
    t_orders = VALUE #(
        ( orderid = 7918 customerid = `TORTU` customername = `Tortuga Restaurante` employeeid = 7424
          orderdate = `1474149600000` requireddate = `1475186400000` shippeddate = `1474840800000`
          shipname = `ExcellentParcel` shipaddress = `Tottenham Court Road`
          shipcity = `London` shipregion = `Greater London` shippostal = `N170AA` shipcountry = `United Kingdom` )
        ( orderid = 7311 customerid = `ALFKI` customername = `Alfreds Futterkiste` employeeid = 7827
          orderdate = `1479682800000` requireddate = `1481151600000` shippeddate = `1480028400000`
          shipname = `ExcellentParcel` shipaddress = `Tottenham Court Road`
          shipcity = `London` shipregion = `Greater London` shippostal = `N170AA` shipcountry = `United Kingdom` )
        ( orderid = 7375 customerid = `AROUT` customername = `Around the Horn` employeeid = 7424
          orderdate = `1475704800000` requireddate = `1479337200000` shippeddate = `1476396000000`
          shipname = `ExcellentParcel` shipaddress = `Tottenham Court Road`
          shipcity = `London` shipregion = `Greater London` shippostal = `N170AA` shipcountry = `United Kingdom` )
        ( orderid = 6189 customerid = `BERGS` customername = `Berglunds snabbköp` employeeid = 7829
          orderdate = `1480287600000` requireddate = `1482274800000` shippeddate = `1480460400000`
          shipname = `1A Paket- und Lieferservice` shipaddress = `Bismarckstraße 5`
          shipcity = `Berlin` shipregion = `Berlin` shippostal = `10179` shipcountry = `Deutschland` )
        ( orderid = 3115 customerid = `BERGS` customername = `` employeeid = 7830
          orderdate = `1480806000000` requireddate = `1480978800000` shippeddate = `1482447600000`
          shipname = `1A Paket- und Lieferservice` shipaddress = `Bismarckstraße 5`
          shipcity = `Berlin` shipregion = `Berlin` shippostal = `10179` shipcountry = `Deutschland` )
        ( orderid = 2686 customerid = `BOTTM` customername = `Bottom-Dollar Markets` employeeid = 7840
          orderdate = `1477519200000` requireddate = `1478646000000` shippeddate = `1477954800000`
          shipname = `1A Paket- und Lieferservice` shipaddress = `Bismarckstraße 5`
          shipcity = `Berlin` shipregion = `Berlin` shippostal = `10179` shipcountry = `Deutschland` )
        ( orderid = 6858 customerid = `TORTU` customername = `Tortuga Restaurante` employeeid = 7840
          orderdate = `1478991600000` requireddate = `1480460400000` shippeddate = `1479078000000`
          shipname = `ExcellentParcel` shipaddress = `Tottenham Court Road`
          shipcity = `London` shipregion = `Greater London` shippostal = `N170AA` shipcountry = `United Kingdom` )
        ( orderid = 6368 customerid = `ALFKI` customername = `Alfreds Futterkiste` employeeid = 7424
          orderdate = `1478991600000` requireddate = `1480460400000` shippeddate = `1479510000000`
          shipname = `ExcellentParcel` shipaddress = `Tottenham Court Road`
          shipcity = `London` shipregion = `Greater London` shippostal = `N170AA` shipcountry = `United Kingdom` )
        ( orderid = 828  customerid = `AROUT` customername = `Around the Horn` employeeid = 7829
          orderdate = `1479682800000` requireddate = `1481151600000` shippeddate = `1480028400000`
          shipname = `ShipEx` shipaddress = `5th Avenue 610`
          shipcity = `New York` shipregion = `New Jersey` shippostal = `10020` shipcountry = `United Stated of America` )
        ( orderid = 7991 customerid = `BERGS` customername = `Berglunds snabbköp` employeeid = 7830
          orderdate = `1479682800000` requireddate = `1481151600000` shippeddate = `1480028400000`
          shipname = `ShipEx` shipaddress = `5th Avenue 610`
          shipcity = `New York` shipregion = `New Jersey` shippostal = `10020` shipcountry = `United Stated of America` ) ).

    " localService/mockdata/Customer.json - the list's expand of Customer
    t_customers = VALUE #(
        ( customerid = `TORTU` companyname = `Tortuga Restaurante` )
        ( customerid = `ALFKI` companyname = `Alfreds Futterkiste` )
        ( customerid = `AROUT` companyname = `Around the Horn` )
        ( customerid = `BERGS` companyname = `Berglunds snabbköp` )
        ( customerid = `BOTTM` companyname = `Bottom-Dollar Markets` ) ).
    LOOP AT t_orders REFERENCE INTO DATA(order).
      order->companyname = VALUE #( t_customers[ customerid = order->customerid ]-companyname OPTIONAL ).
    ENDLOOP.

    " localService/mockdata/Employee.json - the Photo column is empty in the
    " mock for every row
    t_employees = VALUE #(
        ( employeeid = 7424 firstname = `Jack`   lastname = `Smith`     title = `Developer` homephone = `01781163487` )
        ( employeeid = 7827 firstname = `Loura`  lastname = `Hajjar`    title = `Developer` homephone = `01781237845` )
        ( employeeid = 7829 firstname = `Steven` lastname = `Buchanan`  title = `Manager`   homephone = `01787650439` )
        ( employeeid = 7830 firstname = `Andrew` lastname = `Fuller`    title = `Designer`  homephone = `01781234598` )
        ( employeeid = 7840 firstname = `Anne`   lastname = `Dodsworth` title = `Developer` homephone = `01796577660` ) ).

    " localService/mockdata/Product.json
    t_products = VALUE #(
        ( productid = 1412 productname = `Aniseed Syrup` )
        ( productid = 5267 productname = `Uncle Bob's Organic Dried Pears` )
        ( productid = 5046 productname = `Northwoods Cranberry Sauce` )
        ( productid = 1114 productname = `Grandma's Boysenberry Spread` )
        ( productid = 5079 productname = `Chef Anton's Cajun Seasoning` )
        ( productid = 4008 productname = `Sir Rodney's Marmalade` )
        ( productid = 5672 productname = `Tunnbröd` )
        ( productid = 8486 productname = `Mascarpone Fabioli` )
        ( productid = 9505 productname = `Camembert Pierrot` )
        ( productid = 4663 productname = `Louisiana Hot Spiced Okra` ) ).

    " localService/mockdata/Order_Details.json - the Discount column is in
    " the mock and in no view of the app
    t_details = VALUE #(
        ( orderid = 7918 productid = 1412 unitprice = `65.89` quantity = 899 )
        ( orderid = 7918 productid = 5672 unitprice = `14.77` quantity = 2367 )
        ( orderid = 7918 productid = 5046 unitprice = `61.69` quantity = 867 )
        ( orderid = 7918 productid = 9505 unitprice = `3.07`  quantity = 1060 )
        ( orderid = 2686 productid = 5267 unitprice = `24.34` quantity = 7783 )
        ( orderid = 2686 productid = 1114 unitprice = `20.50` quantity = 523 )
        ( orderid = 2686 productid = 5046 unitprice = `61.69` quantity = 336 )
        ( orderid = 6858 productid = 1114 unitprice = `20.50` quantity = 2960 )
        ( orderid = 6858 productid = 4663 unitprice = `9.31`  quantity = 9491 )
        ( orderid = 6858 productid = 5672 unitprice = `14.77` quantity = 547 )
        ( orderid = 6858 productid = 4008 unitprice = `47.10` quantity = 5780 )
        ( orderid = 7311 productid = 5046 unitprice = `61.69` quantity = 6636 )
        ( orderid = 7311 productid = 1412 unitprice = `65.89` quantity = 3436 )
        ( orderid = 7311 productid = 5672 unitprice = `14.77` quantity = 8076 )
        ( orderid = 7991 productid = 4663 unitprice = `9.31`  quantity = 9491 )
        ( orderid = 7991 productid = 5672 unitprice = `14.77` quantity = 547 )
        ( orderid = 7991 productid = 4008 unitprice = `47.10` quantity = 5780 )
        ( orderid = 7991 productid = 9505 unitprice = `3.07`  quantity = 3239 )
        ( orderid = 7991 productid = 8486 unitprice = `6.31`  quantity = 5039 )
        ( orderid = 6189 productid = 5672 unitprice = `14.77` quantity = 552 )
        ( orderid = 6189 productid = 4663 unitprice = `9.31`  quantity = 1052 )
        ( orderid = 6189 productid = 5267 unitprice = `24.34` quantity = 852 )
        ( orderid = 6189 productid = 1412 unitprice = `65.89` quantity = 6752 )
        ( orderid = 828  productid = 8486 unitprice = `6.31`  quantity = 1204 )
        ( orderid = 828  productid = 1114 unitprice = `20.50` quantity = 2960 )
        ( orderid = 828  productid = 4663 unitprice = `9.31`  quantity = 9491 )
        ( orderid = 828  productid = 5672 unitprice = `14.77` quantity = 547 )
        ( orderid = 828  productid = 4008 unitprice = `47.10` quantity = 5780 )
        ( orderid = 3115 productid = 1114 unitprice = `20.50` quantity = 1530 )
        ( orderid = 3115 productid = 5079 unitprice = `62.40` quantity = 2370 )
        ( orderid = 3115 productid = 8486 unitprice = `6.31`  quantity = 2567 )
        ( orderid = 3115 productid = 4663 unitprice = `9.31`  quantity = 1809 )
        ( orderid = 3115 productid = 4008 unitprice = `47.10` quantity = 1310 )
        ( orderid = 7375 productid = 4008 unitprice = `47.10` quantity = 5780 )
        ( orderid = 7375 productid = 9505 unitprice = `3.07`  quantity = 3239 )
        ( orderid = 7375 productid = 8486 unitprice = `6.31`  quantity = 5039 )
        ( orderid = 6368 productid = 9505 unitprice = `3.07`  quantity = 3239 )
        ( orderid = 6368 productid = 8486 unitprice = `6.31`  quantity = 5039 ) ).

  ENDMETHOD.

ENDCLASS.