ff/angular UI initial import
authorBill Erickson <berick@esilibrary.com>
Fri, 25 Oct 2013 14:23:16 +0000 (10:23 -0400)
committerBill Erickson <berick@esilibrary.com>
Fri, 25 Oct 2013 19:27:25 +0000 (15:27 -0400)
Signed-off-by: Bill Erickson <berick@esilibrary.com>
Open-ILS/src/templates/staff/fulfillment/index.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/fulfillment/t_actions.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/fulfillment/t_circulating.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/fulfillment/t_ill.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/fulfillment/t_inbound.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/fulfillment/t_item_table.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/fulfillment/t_outbound.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/fulfillment/t_pending.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/fulfillment/t_status.tt2 [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/fulfillment/app.js [new file with mode: 0644]

diff --git a/Open-ILS/src/templates/staff/fulfillment/index.tt2 b/Open-ILS/src/templates/staff/fulfillment/index.tt2
new file mode 100644 (file)
index 0000000..2d022de
--- /dev/null
@@ -0,0 +1,105 @@
+<!doctype html>
+
+<!--
+    TODO:
+    extract CSS
+    i18n 
+    use ../index.tt2 as base (pending)
+-->
+<html ng-app="ffMain" ng-controller="FFMainCtrl" lang="en">
+  <head>
+    <title>FulfILLment</title>
+    <base href="/eg/staff/" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" />
+    <style type="text/css">
+      body {
+        background-color:#b7bbc3;
+      }
+      #mainhead {
+        height:120px;
+        background-color:#1d57aa;
+      }
+      #wrap {
+        width:950px;
+        margin-left: auto;
+        margin-right: auto;
+        border:1px solid #8396d3;
+        min-height:750px;
+        background-color:white;
+        margin-top:0px;
+      }
+      .mainNav {
+        text-decoration:none;
+        color:#8396d3;
+        padding-right:1em;
+      }
+      .thispage {
+        color:white;
+      }
+      a.mainNav:hover {
+        color:white;
+        text-decoration:none;
+      }
+      #subhead{
+        background-color:#00396a;
+        padding-left:30px;
+        height:30px;
+        line-height:30px;
+        font-size:1em;
+        margin-bottom: 20px;
+      }
+      .caret { 
+        color : white;
+        border-top-color: white;
+        border-bottom-color: white;
+      }
+      .command-bar {
+        padding-left: 5px;
+        padding-right: 5px;
+      }
+    </style>
+  </head>
+  <body>
+    <div id="wrap"> 
+      <div id="mainhead"> 
+        <a href="http://fulfillment-ill.org" target="_blank">
+          <img src="/images/FulfillmentHomePageBanner.png" border="0" 
+            alt="Open Source Integrated Interlibrary Lending System" /></a> 
+      </div> 
+
+      <div id="subhead" style='width:100%'> 
+        <div style='float:left'>
+          <a href="" class="thispage mainNav">Manage ILL</a> 
+          <a href="" class="thispage mainNav">Manage Bibliographic Records</a> 
+        </div>
+        <div style='float:left'>
+          <div class="dropdown" ng-cloak>
+            <a href="javascript:;" 
+              class="dropdown-toggle mainNav thispage" data-toggle="dropdown">
+              Location: {{ffService.currentOrg().shortname()}}
+              <b class="caret"></b>
+            </a>
+            <ul class="dropdown-menu" role="menu">
+              <li role="presentation" ng-repeat="org in ffService.orgList()">
+                <a role="menuitem" tabindex="-1" ng-click="selectOrg(org.id())">
+                  <div style="padding-left:{{org.ou_type().depth() * 10}}px">
+                    {{org.shortname()}}
+                  </div>
+                </a>
+             </li>
+            </ul>
+          </div>
+        </div>
+        <div style='float:right'>
+          <a href="./login" target="_self"
+            ng-click="logout()" class="mainNav thispage">Log Out</a> 
+        </div>
+      </div> 
+
+      <div ng-view></div>
+    </div>
+  </body>
+  [% INCLUDE "staff/t_base_js.tt2" %]
+  <script src="fulfillment/app.js"></script>
+</html>
diff --git a/Open-ILS/src/templates/staff/fulfillment/t_actions.tt2 b/Open-ILS/src/templates/staff/fulfillment/t_actions.tt2
new file mode 100644 (file)
index 0000000..f0bb3ed
--- /dev/null
@@ -0,0 +1,35 @@
+<div class="btn-group text-left">
+  <button type="button" class="btn btn-default" ng-class="{disabled : action_pending}"
+    ng-show="itemList.offset" ng-click="firstPage()">Start</button>
+  <button type="button" class="btn btn-default" ng-class="{disabled : action_pending}"
+    ng-show="itemList.offset" ng-click="prevPage()">&laquo;</button>
+  <button type="button" class="btn btn-default" ng-class="{disabled : action_pending}"
+    ng-click="nextPage()">&raquo;</button>
+  <div class="btn-group">
+    <button type="button" class="btn btn-default dropdown-toggle" 
+        ng-class="{disabled : action_pending}" data-toggle="dropdown">
+      Actions <span class="caret"></span>
+    </button>
+    <ul class="dropdown-menu">
+      <li><a href="javascript:;" ng-click="checkin()"
+        ng-show="tab_pending && itemList.filterLender">Capture Item</a></li>
+      <li><a href="javascript:;" ng-click="retarget()"
+        ng-show="tab_pending">Retarget Request</a></li>
+      <li><a href="javascript:;" ng-click="cancel"
+        ng-show="(tab_pending || tab_inbound) && itemList.filterBorrower">Cancel Request</a></li>
+      <li><a href="javascript:;" ng-click="abort_transit"
+        ng-show="tab_inbound || tab_outbound">Abort Transit</a></li>
+      <li><a href="javascript:;" ng-click="checkin()"
+        ng-show="tab_inbound">Receive Item</a></li>
+      <li><a href="javascript:;" ng-click="checkin()"
+        ng-show="tab_circulating && itemList.filterBorrower">Check In</a></li>
+      <!-- We need an on-shelf tab for this action to have a home
+      <li><a href="javascript:;"
+        ng-show="">Check Out</a></li>
+      -->
+      <li><a href="javascript:;" ng-click="mark_lost()"
+        ng-show="tab_circulating && itemList.filterBorrower">Mark Lost</a></li>
+      <li><a href="javascript:;" ng-click="print()">Print</a></li>
+    </ul>
+  </div>
+</div>
diff --git a/Open-ILS/src/templates/staff/fulfillment/t_circulating.tt2 b/Open-ILS/src/templates/staff/fulfillment/t_circulating.tt2
new file mode 100644 (file)
index 0000000..8561ec6
--- /dev/null
@@ -0,0 +1,22 @@
+<div ng-controller="CircCtrl">
+  <br/>
+  <div class="row command-bar">
+    <div class="col-lg-6">
+      <ul class="nav nav-pills">
+        <li ng-class="{active : itemList.filterBorrower}">
+          <a href="javascript:;" ng-click="drawTable()">For My Patrons</a>
+        </li>
+        <li ng-class="{active : itemList.filterLender}">
+          <a href="javascript:;" ng-click="drawTable(true)">For Other Libraries</a>
+        </li>
+      </ul>
+    </div>
+    <div class="col-lg-6 text-right">
+      <div ng-include="'./fulfillment/actions'"></div>
+    </div>
+  </div>
+
+  <br/>
+
+  <div ng-include="'./fulfillment/item_table'"></div>
+</div>
diff --git a/Open-ILS/src/templates/staff/fulfillment/t_ill.tt2 b/Open-ILS/src/templates/staff/fulfillment/t_ill.tt2
new file mode 100644 (file)
index 0000000..a7e3cd4
--- /dev/null
@@ -0,0 +1,40 @@
+<ul class="nav nav-tabs">
+  <li ng-class="{active : tab_pending}">
+    <a href="./fulfillment/pending">Pending Requests</a></li>
+  <li ng-class="{active : tab_inbound}">
+    <a href="./fulfillment/inbound">Inbound Transits</a></li>
+  <li ng-class="{active : tab_outbound}">
+    <a href="./fulfillment/outbound">Outbound Transits</a></li>
+  <li ng-class="{active : tab_circulating}">
+    <a href="./fulfillment/circulating">Currently Circulating</a></li>
+  <li ng-class="{active : tab_status}">
+    <a href="./fulfillment/status">Item Status</a></li>
+</ul>
+<div class="tab-content">
+  <div class="tab-pane" ng-class="{active : tab_pending}">
+    <div ng-if="tab_pending">
+      <div ng-include="'./fulfillment/pending'"></div>
+    </div>
+  </div>
+  <div class="tab-pane" ng-class="{active : tab_inbound}">
+    <div ng-if="tab_inbound">
+      <div ng-include="'./fulfillment/inbound'"></div>
+    </div>
+  </div>
+  <div class="tab-pane" ng-class="{active : tab_outbound}">
+    <div ng-if="tab_outbound">
+      <div ng-include="'./fulfillment/outbound'"></div>
+    </div>
+  </div>
+  <div class="tab-pane" ng-class="{active : tab_circulating}">
+    <div ng-if="tab_circulating">
+      <div ng-include="'./fulfillment/circulating'"></div>
+    </div>
+  </div>
+  <div class="tab-pane" ng-class="{active : tab_status}">
+    <div ng-if="tab_status">
+      <div ng-include="'./fulfillment/status'"></div>
+    </div>
+  </div>
+</div>
+
diff --git a/Open-ILS/src/templates/staff/fulfillment/t_inbound.tt2 b/Open-ILS/src/templates/staff/fulfillment/t_inbound.tt2
new file mode 100644 (file)
index 0000000..dd11503
--- /dev/null
@@ -0,0 +1,23 @@
+<div ng-controller="TransitsCtrl">
+  <br/>
+
+  <div class="row command-bar">
+    <div class="col-lg-6">
+      <ul class="nav nav-pills">
+        <li ng-class="{active : itemList.filterBorrower}">
+          <a href="javascript:;" ng-click="drawTable()">Items For My Patrons</a>
+        </li>
+        <li ng-class="{active : itemList.filterLender}">
+          <a href="javascript:;" ng-click="drawTable(true)">My Returns</a>
+        </li>
+      </ul>
+    </div>
+    <div class="col-lg-6 text-right">
+      <div ng-include="'./fulfillment/actions'"></div>
+    </div>
+  </div>
+
+  <br/>
+
+  <div ng-include="'./fulfillment/item_table'"></div>
+</div>
diff --git a/Open-ILS/src/templates/staff/fulfillment/t_item_table.tt2 b/Open-ILS/src/templates/staff/fulfillment/t_item_table.tt2
new file mode 100644 (file)
index 0000000..8862e31
--- /dev/null
@@ -0,0 +1,47 @@
+
+<div class="container" ng-hide="itemList.items.length">
+  <div class="alert alert-info">No Items To Display</div>
+</div>
+<table class="table table-striped table-hover" ng-show="itemList.items.length">
+  <thead>
+    <tr>
+      <th><a href="javascript:;" ng-click="itemList.selectAll()">&#x2713;</a></th>
+
+      <!-- common columns -->
+      <th>#</th>
+      <th>Item Barcode</th>
+      <th>Owning Library</th>
+
+      <!-- transit columns -->
+      <th ng-show="tab_inbound || tab_outbound">Transit Date</th>
+      <th ng-show="tab_inbound || tab_outbound">Transit Source</th>
+      <th ng-show="tab_inbound || tab_outbound">Transit Destination</th>
+
+      <!-- circ columns -->
+      <th ng-show="tab_circulating">Checkout Date</th>
+      <th ng-show="tab_circulating">Due Date</th>
+      <th ng-show="tab_circulating">Circulating Library</th>
+
+      <!-- titles can get long, so plop it onto the end of the table -->
+      <th>Title</th>
+
+    </tr>
+  </thead>
+  <tbody>
+    <tr ng-repeat="item in itemList.items">
+      <td><input type='checkbox' ng-model="itemList.selected[item.index]"/></td>
+      <td>{{item.index + 1}}</td>
+      <td><a 
+        href="./fulfillment/status/{{item.item_barcode_enc}}">
+          {{item.item_barcode}}</a>
+      </td>
+      <td>{{item.source_lib}}</td>
+      <td ng-show="tab_inbound || tab_outbound">{{item.transit_time | date}}</td>
+      <td ng-show="tab_inbound || tab_outbound">{{item.transit_source}}</td>
+      <td ng-show="tab_inbound || tab_outbound">{{item.transit_dest}}</td>
+      <td ng-show="tab_circulating">{{item.circ_xact_start | date}}</td>
+      <td ng-show="tab_circulating">{{item.due_date | date}}</td>
+      <td ng-show="tab_circulating">{{item.circ_circ_lib}}</td>
+      <td>{{item.title}}</td>
+  </tbody>
+</table>
diff --git a/Open-ILS/src/templates/staff/fulfillment/t_outbound.tt2 b/Open-ILS/src/templates/staff/fulfillment/t_outbound.tt2
new file mode 100644 (file)
index 0000000..564bdbf
--- /dev/null
@@ -0,0 +1,22 @@
+<div ng-controller="TransitsCtrl">
+  <br/>
+  <div class="row command-bar">
+    <div class="col-lg-6">
+      <ul class="nav nav-pills">
+        <li ng-class="{active : itemList.filterLender}">
+          <a href="javascript:;" ng-click="drawTable(true)">Items For Other Libraries</a>
+        </li>
+        <li ng-class="{active : itemList.filterBorrower}">
+          <a href="javascript:;" ng-click="drawTable()">Returns to Other Libraries</a>
+        </li>
+      </ul>
+    </div>
+    <div class="col-lg-6 text-right">
+      <div ng-include="'./fulfillment/actions'"></div>
+    </div>
+  </div>
+
+  <br/>
+
+  <div ng-include="'./fulfillment/item_table'"></div>
+</div>
diff --git a/Open-ILS/src/templates/staff/fulfillment/t_pending.tt2 b/Open-ILS/src/templates/staff/fulfillment/t_pending.tt2
new file mode 100644 (file)
index 0000000..cac9945
--- /dev/null
@@ -0,0 +1,58 @@
+
+<div ng-controller="PendingRequestsCtrl">
+  <br/>
+
+  <div class="row command-bar">
+    <div class="col-lg-6">
+      <ul class="nav nav-pills">
+        <li ng-class="{active : itemList.filterBorrower}">
+          <a href="javascript:;" ng-click="drawTable()">Hold for My Patrons</a>
+        </li>
+        <li ng-class="{active : itemList.filterLender}">
+          <a href="javascript:;" ng-click="drawTable(true)">Holds for Other Libraries</a>
+        </li>
+      </ul>
+    </div>
+
+    <div class="col-lg-6 text-right">
+      <div ng-include="'./fulfillment/actions'"></div>
+    </div>
+  </div>
+
+  <div ng-hide="itemList.items.length" class="container">
+    <br/>
+    <div class="alert alert-info">No Items To Display</div>
+  </div>
+
+  <table class="table table-striped table-hover" ng-show="itemList.items.length">
+    <thead>
+      <tr>
+        <th><a href="javascript:;" ng-click="itemList.selectAll()">&#x2713;</a></th>
+        <th>ID</th>
+        <th>Request Date</th>
+        <th>Expire Date</th>
+        <th>Requesting Patron</th>
+        <th>Requesting Library</th>
+        <th>Current Copy</th>
+        <th>Copy Library</th>
+        <th>Title</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr ng-repeat="item in itemList.items">
+        <td><input type='checkbox' ng-model="itemList.selected[item.index]"/></td>
+        <td>{{item.id}}</td>
+        <td>{{item.request_time | date}}</td>
+        <td>{{item.expire_time | date}}</td>
+        <td>{{item.user_name}}</td>
+        <td>{{item.request_lib}}</td>
+        <td><a 
+          href="./fulfillment/status/{{item.current_copy_enc}}">
+            {{item.current_copy}}
+          </a>
+        </td>
+        <td>{{item.current_copy_lib}}</td>
+        <td>{{item.title}}</td>
+    </tbody>
+  </table>
+</div>
diff --git a/Open-ILS/src/templates/staff/fulfillment/t_status.tt2 b/Open-ILS/src/templates/staff/fulfillment/t_status.tt2
new file mode 100644 (file)
index 0000000..aca0040
--- /dev/null
@@ -0,0 +1,134 @@
+
+<div ng-controller="ItemStatusCtrl">
+  <br/>
+  <div class="row" style='padding-left: 5px; padding-right: 5px;'>
+    <div class="col-lg-3">
+      <form ng-submit="draw(barcode)">
+        <div class="input-group">
+          <input focus-me="focusMe" select-me="selectMe" ng-model="barcode" 
+            type="text" class="form-control" placeholder="Item Barcode">
+          <span class="input-group-btn">
+          <button class="btn btn-default" 
+            type="button" ng-click="draw(barcode)">Go!</button>
+          </span>
+        </div>
+      </form>
+    </div>
+    <div class="col-lg-9 text-right">
+      <div class="btn-group">
+        <button type="button" class="btn btn-default" ng-class="{disabled : action_pending}"
+          ng-click="mark_lost()" ng-show="item.can_mark_lost">Mark Lost</button>
+        <button type="button" class="btn btn-default" ng-class="{disabled : action_pending}"
+          ng-click="abort_transit()" ng-show="item.open_transit">Abort Transit</button>
+        <button type="button" class="btn btn-default" ng-class="{disabled : action_pending}"
+          ng-click="cancel()" ng-show="item.can_cancel_hold">Cancel Request</button>
+        <button type="button" class="btn btn-default" ng-class="{disabled : action_pending}"
+          ng-click="checkout()" ng-show="item.needs_checkout">Check Out</button>
+        <button type="button" class="btn btn-default" ng-class="{disabled : action_pending}"
+          ng-click="checkin()" ng-show="item.needs_checkin">Check In</button>
+        <button type="button" class="btn btn-default" ng-class="{disabled : action_pending}"
+          ng-click="checkin()" ng-show="item.needs_receive">Receive Item</button>
+        <button type="button" class="btn btn-default" ng-class="{disabled : action_pending}"
+          ng-click="retarget()" ng-show="item.can_retarget_hold">Retarget Request</button>
+        <button type="button" class="btn btn-default" ng-class="{disabled : action_pending}"
+          ng-click="checkin()"  ng-show="item.needs_capture">Capture Item</button>
+        <button type="button" class="btn btn-default" ng-class="{disabled : action_pending}"
+          ng-click="actions.print(item)">Print Details</button>
+      </div>
+    </div>
+  </div>
+  <br/>
+  <div class="row" ng-show="item">
+    <div class="col-lg-6">
+      <table class="table table-striped table-hover">
+        <thead>
+          <th>Item Details</th>
+          <th>
+            <a target="_blank" ng-show="item.source_lib"
+              href="/opac/en-US/skin/default/xml/rdetail.xml?r={{item.bib_id}}">
+              View in Catalog
+            </a>
+            <span ng-show="item.not_found" 
+              class="label label-danger">Item Not Found</span>
+          </th>
+        </thead>
+        <tbody>
+          <tr><td>Barcode:</td><td>{{item.barcode}}</td></tr>
+          <tr>
+            <td>Status:</td>
+            <td>
+              <span 
+                ng-class="{label : item.copy_status_warning, 'label-warning' : item.copy_status_warning}">
+                {{item.status_str}}
+              </span>
+            </td>
+          </tr>
+          <tr><td>Owning Lib:</td><td>{{item.source_lib}}</td></tr>
+          <tr><td>Title:</td><td>{{item.title}}</td></tr>
+          <tr><td>Author:</td><td>{{item.author}}</td></tr>
+          <tr><td>Call Number:</td><td>{{item.call_number}}</td></tr>
+          <tr><td>FulfILLment Bib ID:</td><td>{{item.bib_id}}</td></tr>
+          <tr><td>Remote ILS Bib ID:</td><td>{{item.remote_bib_id}}</td></tr>
+        </tbody>
+      </table>
+    </div>
+    <div class="col-lg-6">
+      <table class="table table-striped table-hover">
+        <thead>
+          <th>Transaction Details</th>
+          <th></th>
+        </thead>
+        <tbody>
+          <tr ng-show='item.circ'><td>Circulating Library:</td><td>{{item.circ_circ_lib}}</td></tr>
+          <tr ng-show='item.circ'><td>Circulating Patron:</td><td>{{item.circ_usr}}</td></tr>
+          <tr ng-show='item.circ'><td>Checkout date:</td><td>{{item.circ_xact_start | date}}</td></tr>
+          <tr ng-show='item.circ'><td>Due Date:</td><td>{{item.due_date | date}}</td></tr>
+          <tr ng-show='item.circ_stop_fines'><td>Circ Status:</td>
+            <td><span class="label label-warning">{{item.circ_stop_fines}}</span></td>
+          </tr>
+          <tr ng-show='item.hold'><td>Requesting Patron:</td><td>{{item.hold_request_usr}}</td></tr>
+          <tr ng-show='item.hold'><td>Requesting Library:</td><td>{{item.hold_request_lib}}</td></tr>
+          <tr ng-show='item.hold'><td>Pickup Library:</td><td>{{item.hold_pickup_lib}}</td></tr>
+          <tr ng-show='item.hold'><td>Request Date:</td><td>{{item.hold_request_time | date}}</td></tr>
+          <tr ng-show='item.hold'><td>Capture Date:</td><td>{{item.hold_capture_time | date}}</td></tr>
+          <tr ng-show='item.hold_cancel_time'>
+              <td>Hold Cancel Date:</td>
+              <td><span class="label label-warning">{{item.hold_cancel_time | date}}</span></td>
+          </tr>
+          <tr ng-show='item.hold_cancel_cause'>
+              <td>Hold Cancel Reason:</td>
+              <td><span class="label label-warning">{{item.hold_cancel_cause}}</span></td>
+          </tr>
+          <tr ng-show='item.transit'><td>Transit Source:</td><td>{{item.transit_source}}</td></tr>
+          <tr ng-show='item.transit'><td>Transit Destination:</td><td>{{item.transit_dest}}</td></tr>
+          <tr ng-show='item.transit'><td>Transit Send Date:</td><td>{{item.transit_time | date}}</td></tr>
+          <tr ng-show='item.transit'><td>Transit Receive Date:</td><td>{{item.transit_recv_time | date}}</td></tr>
+        </tbody>
+      </table>
+    </div>
+  </div>
+  <style>
+    .item-status-actions { 
+      padding-left: 5px; 
+      padding-top: 5px; 
+      border-top:2px solid #8396d3;
+    }
+    .item-status-actions a { padding-right: 5px; }
+  </style>
+  <!--
+  <div class="row" ng-show="item.source_lib">
+    <div class="col-lg-10 col-lg-offset-1 item-status-actions">
+      <div class="btn-group">
+       <button type="button" class="btn btn-default">Print Details</button>
+       <button type="button" class="btn btn-default">Capture Item</button>
+      </div>
+      <a href="javascript:;" class='action-link' ng-click="actions.print(item)">Print Details</a>
+      <a href="javascript:;" class='action-link' ng-click="checkin()"  ng-show="item.needs_capture">Capture Item</a>
+      <a href="javascript:;" class='action-link' ng-click="checkout()" ng-show="item.needs_checkout">Check Out</a>
+      <a href="javascript:;" class='action-link' ng-click="checkin()"  ng-show="item.needs_checkin">Check In</a>
+      <a href="javascript:;" class='action-link' ng-click="checkin()"  ng-show="item.needs_receive">Receive Item</a>
+      <a href="javascript:;" class='action-link' ng-click="cancel()"   ng-show="item.can_cancel_hold">Cancel Request</a>
+    </div>
+  </div>
+  -->
+</div>
diff --git a/Open-ILS/web/js/ui/default/staff/fulfillment/app.js b/Open-ILS/web/js/ui/default/staff/fulfillment/app.js
new file mode 100644 (file)
index 0000000..6759ce2
--- /dev/null
@@ -0,0 +1,799 @@
+/**
+ * TODO:
+ * Instead of making pcrud calls, followed by per-item server calls,
+ * consider a server-side API for all of this stuff for speed, etc.
+ *
+ * Consolidate the various item structures into one common,
+ * authoritative structure for display and print templates.
+ */
+
+
+angular.module('ffMain', 
+['ngRoute', 'egNetMod', 'egAuthMod', 'egStartupMod', 
+  'egUserMod', 'egUiMod', 'egFlattenerMod', 'egPCRUDMod', 'egOrgMod'])
+
+.config(function($routeProvider, $locationProvider) {
+
+    // The route-specified controller will not get instantiated 
+    // until the promise returned by this function is resolved 
+    var resolver = {delay : function(egStartup) {return egStartup.go()}};
+
+    // record management UI
+    $routeProvider.when('/fulfillment/records', {
+        templateUrl: './fulfillment/records',
+        controller: 'ILLCtrl',
+        resolve : resolver
+    });
+    
+    // Default to ILL management tabs
+    $routeProvider.when('/fulfillment/status/:barcode', {
+        templateUrl: './fulfillment/ill',
+        controller: 'ILLCtrl',
+        resolve : resolver
+    });
+
+
+    // Default to ILL management tabs
+    $routeProvider.otherwise({
+        templateUrl: './fulfillment/ill',
+        controller: 'ILLCtrl',
+        resolve : resolver
+    });
+
+    $locationProvider.html5Mode(true);
+})
+
+/**
+ * Data shared across FF controllers.
+ */
+.factory('ffService', 
+['$rootScope', 'egOrg', 'egAuth', 
+function($rootScope, egOrg, egAuth) {
+    return {
+        orgList : function() {
+            // we want all orgs
+            return egOrg.list();
+        },
+
+        // currently selected org unit
+        currentOrg : function(id) {
+            if (id) {
+                this.org = id;
+            } else if (!this.org) {
+                this.org = egAuth.user().ws_ou();
+            }
+            return egOrg.get(this.org);
+        },
+
+        /** returns list of IDs for all org units within the
+         * full path of currentOrg.  Useful for pcrud queries.
+         */
+        relatedOrgs : function(id) {
+            return egOrg.fullPath(
+                this.currentOrg()).map(function(o) {return o.id()});
+        }
+    }
+}])
+
+/**
+ * Top-level page controller.  Handles global components, like the org 
+ * selector.
+ */
+.controller('FFMainCtrl', ['$scope', '$route', 'egStartup', 'ffService', 'egAuth',
+function ($scope, $route, egStartup, ffService, egAuth) {
+
+    // run after startup so we can guarantee access to org units
+    egStartup.go().then(function() {
+        $scope.ffService = ffService;
+    });
+
+    // change the selected org unit and re-draw the page
+    $scope.selectOrg = function(id) {
+        ffService.currentOrg(id);
+        $route.reload();
+    }
+
+    $scope.logout = function() {
+        egAuth.logout();
+        return true;
+    };
+
+}])
+
+
+/**
+ * Main ILL controller.
+ * Maintains the table data / attributes.
+ * Performs actions.
+ */
+.controller('ILLCtrl', 
+['$scope', '$q', '$compile', '$timeout', '$rootScope', '$location', 
+    '$route', '$routeParams', 'egNet', 'egAuth', 'ffService', 'egOrg',
+function ($scope, $q, $compile, $timeout, $rootScope, $location, 
+    $route, $routeParams, egNet, egAuth, ffService, egOrg) {
+
+    // tabs
+    var mytab = $location.path().match(/\/fulfillment\/([^\/]+)/)[1];
+    $scope['tab_' + mytab] = true;
+
+    // so our child controllers can access our route info
+    $scope.illRouteParams = $routeParams;
+
+    // This bit of scope is used directly by all child scopes.
+    // Inherited scopes use shallow copies, hence the nested object.
+    $scope.itemList = {
+        items : [],
+        selected : {},
+        limit : 10, // TODO UI
+        offset : 0, // TODO UI
+        filter_borrwer : true,
+        filterLender : false,
+
+        toggleFilters : function(lender) {
+            $scope.itemList.filterBorrower = !lender;
+            $scope.itemList.filterLender = lender;
+        },
+
+        // select all rows in the list.  if any are 
+        // already selected, de-select all.
+        selectAll : function() {
+            var action = true;
+            angular.forEach($scope.itemList.selected, function(val) {
+                if (val) action = false;
+            });
+            angular.forEach($scope.itemList.items, function(item) {
+                if (action) {
+                    $scope.itemList.selected[item.index] = action;
+                } else {
+                    delete $scope.itemList.selected[item.index];
+                }
+            });
+        },
+
+        addItem : function(item) {
+            // TODO: id version
+            item.index = $scope.itemList.items.length;
+            $scope.itemList.items.push(item);
+            egNet.request(
+                'open-ils.circ',
+                'open-ils.circ.item.transaction.disposition',
+                egAuth.token(), 
+                ffService.currentOrg().id(), 
+                item.barcode
+            ).then(function(items) {
+                if (items[0]) {
+                    $scope.itemList.flattenItem(item, items[0]);
+                } else {
+                   $scope.itemList.items.pop().not_found = true;
+                }
+            });
+        },
+
+        // given an item disposition blob, flatten it for display
+        flattenItem : function(item, item_data) {
+            /*
+             * TODO: most of this is unnecessary, since we can access
+             * fields directly in the template.  Consider pairing
+             * this down to fields that need munging only
+             */
+            var copy = item_data.copy;
+            var transit = item_data.transit;
+            var circ = item_data.circ;
+            var hold = item_data.hold;
+            if (hold) {
+                if (!transit && hold.transit()) {
+                    transit = item_data.hold.transit();
+                }
+            } else if (transit && transit.hold_transit_copy()) {
+                hold = transit.hold_transit_copy().hold();
+            }
+
+            item.copy = item_data.copy;
+            item.item_barcode = copy.barcode();
+            item.item_barcode_enc = encodeURIComponent(copy.barcode());
+            item.source_lib = egOrg.get(copy.source_lib()).shortname();
+            item.circ_lib = egOrg.get(copy.circ_lib()).shortname();
+            item.title = copy.call_number().record().simple_record().title();
+            item.author = copy.call_number().record().simple_record().author();
+            item.call_number = copy.call_number().label();
+            item.bib_id = copy.call_number().record().id();
+            item.remote_bib_id = copy.call_number().record().remote_id();
+            item.next_action = item_data.next_action;
+            item.can_cancel_hold = (item_data.can_cancel_hold == 1);
+            item.can_retarget_hold = (item_data.can_retarget_hold == 1);
+
+            switch(item_data.next_action) {
+                // capture lender copy for hold
+                case 'ill-home-capture' :
+                    item.needs_capture = true;
+                    break; 
+                // receive item at borrower
+                case 'ill-foreign-receive':
+                // receive lender copy back home
+                case 'transit-home-receive':
+                // transit item for cancelled hold back home (or next hold)
+                case 'transit-foreign-return':
+                    item.needs_receive = true;
+                    break; 
+                // complete borrower circ, transit item back home
+                case 'ill-foreign-checkin':
+                    item.needs_checkin = true;
+                    break;
+                // check out item to borrowing patron
+                case 'ill-foreign-checkout':
+                    item.needs_checkout = true;
+                    break;
+            }
+
+            item.status_str = copy.status().name();
+            if (copy.status().id() == 8 /* holds shelf */ &&
+                hold &&
+                hold.transit() &&
+                hold.transit().dest_recv_time()) {
+                item.status_str += ' @ ' + egOrg.get(hold.transit().dest()).shortname();
+            } else if (copy.status().id() == 1 /* checked out */ && circ) {
+                item.status_str += ' @ ' + egOrg.get(circ.circ_lib()).shortname();
+            }
+
+            item.copy_status_warning = (copy.status().holdable() == 'f');
+
+            if (transit) {
+                item.transit = transit;
+                item.transit_source = transit.source().shortname();
+                item.transit_dest = transit.dest().shortname();
+                item.transit_time = transit.source_send_time();
+                item.transit_recv_time = transit.dest_recv_time();
+                item.open_transit = !Boolean(transit.dest_recv_time());
+            }
+
+            if (circ) {
+                item.circ = circ;
+                item.due_date = circ.due_date();
+                item.circ_circ_lib = egOrg.get(circ.circ_lib()).shortname();
+                item.circ_xact_start = circ.xact_start();
+                item.circ_stop_fines = circ.stop_fines();
+                item.circ_usr = circ.usr().card() ? 
+                    circ.usr().card().barcode() : circ.usr().usrname();
+                item.can_mark_lost = (item.circ && item.copy.status().id() == 1); // checked out
+            }
+
+            if (hold) {
+                item.hold = hold;
+                item.hold_request_usr = hold.usr().card() ? 
+                    hold.usr().card().barcode() : hold.usr().usrname();
+                item.hold_request_lib = egOrg.get(hold.request_lib()).shortname();
+                item.hold_pickup_lib = egOrg.get(hold.pickup_lib()).shortname();
+                item.hold_request_time = hold.request_time();
+                item.hold_capture_time = hold.capture_time();
+                if (hold.cancel_time()) {
+                    item.hold_cancel_time = hold.cancel_time();
+                    if (hold.cancel_cause()) {
+                        item.hold_cancel_cause = hold.cancel_cause().label();
+                    }
+                }
+            }
+
+            // TODO: another unnecessary layer of data munging,
+            // this time to fit the print templates.  Can be unified.
+            item.barcode = item.item_barcode;
+            item.status = item.status_str;
+            item.item_circ_lib = item.circ_lib;
+        }
+    };
+
+    /* Actions
+     * Performed on flattened items (see above)
+     */
+    $scope.actions = {
+
+        checkin : function(item) {
+            $scope.action_pending = true;
+            var deferred = $q.defer();
+            egNet.request(
+                'open-ils.circ', 
+                'open-ils.circ.checkin.override',
+                egAuth.token(), {
+                    circ_lib : ffService.currentOrg().id(),
+                    copy_id: item.copy.id(),
+                    ff_action: item.next_action
+                }
+            ).then(function(response) {
+                $scope.action_pending = false;
+                // do some basic sanity checking before passing 
+                // the response to the caller.
+                if (response) {
+                    if (angular.isArray(response))
+                        response = response[0];
+                    // TODO: check for failure events
+                    deferred.resolve(response);
+                } else {
+                    // warn that checkin failed
+                    deferred.reject();
+                }
+            });
+            return deferred.promise;
+        },
+
+        checkout : function(item) {
+            $scope.action_pending = true;
+            var deferred = $q.defer();
+            egNet.request(
+                'open-ils.circ', 
+                'open-ils.circ.checkout.full.override',
+                egAuth.token(), {
+                    circ_lib : ffService.currentOrg().id(),
+                    patron_id : item.hold.usr().id(),
+                    copy_id: item.copy.id(),
+                    ff_action: item.next_action
+                }
+            ).then(function(response) {
+                $scope.action_pending = false;
+                // do some basic sanity checking before passing 
+                // the response to the caller.
+                if (response) {
+                    if (angular.isArray(response))
+                        response = response[0];
+                    // TODO: check for failure events
+                    deferred.resolve(response);
+                } else {
+                    // warn that checkin failed
+                    deferred.reject();
+                }
+            });
+            return deferred.promise;
+        },
+
+
+        cancel : function(item) {
+            var deferred = $q.defer();
+            $scope.action_pending = true;
+            egNet.request(
+              'open-ils.circ',
+              'open-ils.circ.hold.cancel',
+              egAuth.token(), item.hold.id()
+            ).then(function() {
+                $scope.action_pending = false;
+                deferred.resolve();
+            });
+            return deferred.promise;
+        },
+
+        retarget : function(item) {
+            var deferred = $q.defer();
+            $scope.action_pending = true;
+            egNet.request(
+              'open-ils.circ',
+              'open-ils.circ.hold.reset',
+              egAuth.token(), item.hold.id()
+            ).then(function() {
+                $scope.action_pending = false;
+                deferred.resolve();
+            });
+            return deferred.promise;
+        },
+
+        abort_transit : function(item) {
+            var deferred = $q.defer();
+            $scope.action_pending = true;
+            egNet.request(
+                'open-ils.circ', 
+                'open-ils.circ.transit.abort',
+                egAuth.token(), 
+                {transitid : item.transit.id()}
+            ).then(function() {
+                $scope.action_pending = false;
+                deferred.resolve();
+            });
+            return deferred.promise;
+        },
+
+        mark_lost : function(item) {
+            var deferred = $q.defer();
+            $scope.action_pending = true;
+            egNet.request(
+                'open-ils.circ',
+                'open-ils.circ.circulation.set_lost',
+                egAuth.token(), {barcode : item.item_barcode}
+            ).then(function(resp) {
+                $scope.action_pending = false;
+                if (resp == 1) {
+                    deferred.resolve();
+                } else {
+                    console.error('mark lost failed: ' + js2JSON(resp));
+                    deferred.reject();
+                }
+            });
+            return deferred.promise;
+        },
+
+        print : function(item) {
+            var deferred = $q.defer();
+            $scope.action_pending = true;
+            var focus = item.hold ? 'hold' :
+                (item.circ ? 'circ' :
+                    (item.transit ? 'transit' : 'copy'));
+
+            egNet.request(
+                'open-ils.actor',
+                'open-ils.actor.web_action_print_template.fetch',
+                ffService.currentOrg().id(), focus
+            ).then(function(template) {
+
+                if (!template || !(template = template.template())) { // assign
+                    console.warn('unable to find template for ' + 
+                        item.copy_barcode + ' : ' + focus);
+                    return;
+                }
+
+                // NOTE: templates stored for now as dojo-style
+                // template.  mangle to angular templates manually.
+                template = template.replace(/\${([^}]+)}/g, '{{$1}}');
+
+                // compile the template w/ a temporary print scope
+                var printScope = $rootScope.$new();
+                angular.forEach(item, function(val, key) {
+                    printScope[key] = val;
+                });
+                var element = angular.element(template);
+                $compile(element)(printScope);
+
+                // append the compiled element to the new window and print
+                var w = window.open();
+                $(w.document.body).append(element);
+                w.document.close();
+                
+                // $timeout needed in some environments (Mac confirmed) 
+                // to allow the new window to fully digest before printing.
+                $timeout(
+                    function() {
+                        w.print();
+                        w.close();
+                        $scope.action_pending = false;
+                        deferred.resolve();
+                    }
+                );
+            });
+            return deferred.promise;
+        }
+    };
+
+    // default batch action handlers.
+    // when a batch is done, reload the route (unless printing).
+    angular.forEach(
+        Object.keys($scope.actions),
+        function(action) {
+            $scope[action] = function() {
+                var total = Object.keys($scope.itemList.selected).length;
+                angular.forEach(
+                    $scope.itemList.selected,
+                    function(val, idx) {
+                        var item = $scope.itemList.items.filter(
+                            function(i) {return i.index == idx})[0];
+                        console.debug(item.index + ' => ' + action);
+                        $scope.actions[action](item)
+                        .then(
+                            // when all items are done processing, reload the route
+                            function(resp) {
+                                console.debug(item.index + ' => ' + action + ' : done');
+                                if (--total == 0 && action != 'print') 
+                                    $route.reload()
+                            }, 
+                            function(resp) {
+                                console.error("error in " + action + ": " + resp);
+                            }
+                        );
+                    }
+                );
+            };
+        }
+    );
+}])
+
+.controller('TransitsCtrl',
+['$scope', '$q', 'egPCRUD', 'ffService',
+function ($scope, $q, egPCRUD, ffService) {
+
+    $scope.drawTable = function(filterLender) {
+        var deferred = $q.defer();
+        $scope.itemList.items = [];
+        $scope.itemList.toggleFilters(filterLender);
+
+        var fullPath = ffService.relatedOrgs();
+
+        var dest = fullPath; // inbound transits
+        var circ_lib = fullPath; // our copies
+
+        if ($scope.itemList.filterBorrower) {
+            // borrower always means not-our-copies
+            circ_lib = {'not in' : fullPath};
+        }
+        if ($scope.tab_outbound) {
+            // outbound transits away from "here"
+            dest = {'not in' : fullPath};
+        }
+            
+        var query = {
+            dest_recv_time : null,
+            dest : dest,
+            target_copy : {
+                'in' : {
+                    select: {acp : ['id']},
+                    from : 'acp',
+                    where : {
+                        deleted : 'f',
+                        id : {'=' : {'+atc' : 'target_copy'}},
+                        circ_lib : circ_lib
+                    }
+                }
+            }
+        };
+
+        return egPCRUD.search('atc', query,
+            {   limit : $scope.itemList.limit,
+                offset : $scope.itemList.offset,
+                flesh : 1, 
+                flesh_fields : {atc : ['target_copy']}
+            }, {atomic : true}
+        ).then(function(transits) {
+            angular.forEach(transits, function(transit) {
+                $scope.itemList.addItem(
+                  {barcode : transit.target_copy().barcode()});
+            });
+        });
+    };
+    
+    // outbound tab defaults to lender view
+    return $scope.drawTable($scope.tab_outbound == true);
+}])
+
+.controller('CircCtrl',
+['$scope', '$q', 'egPCRUD', 'ffService',
+function ($scope, $q, egPCRUD, ffService) {
+
+    $scope.drawTable = function(filterLender) {
+        var deferred = $q.defer();
+        $scope.itemList.items = [];
+        $scope.itemList.toggleFilters(filterLender);
+
+        var fullPath = ffService.relatedOrgs();
+
+        var copy_lib = fullPath; // our copies
+        var circ_lib = fullPath; // circulating here
+
+        if ($scope.itemList.filterBorrower) {
+            // borrower always means not-our-copies
+            circ_lib = {'not in' : fullPath};
+        } else {
+            copy_lib = {'not in' : fullPath};
+        }
+            
+        var query = {
+            checkin_time : null,
+            circ_lib : circ_lib,
+            target_copy : {
+                'in' : {
+                    select: {acp : ['id']},
+                    from : 'acp',
+                    where : {
+                        deleted : 'f',
+                        id : {'=' : {'+circ' : 'target_copy'}},
+                        circ_lib : copy_lib
+                    }
+                }
+            }
+        };
+
+        return egPCRUD.search('circ', query,
+            {   limit : $scope.itemList.limit,
+                offset : $scope.itemList.offset,
+                flesh : 1, 
+                flesh_fields : {circ : ['target_copy']}
+            }, {atomic : true}
+        ).then(function(circs) {
+            angular.forEach(circs, function(circ) {
+                $scope.itemList.addItem(
+                  {barcode : circ.target_copy().barcode()});
+            });
+        });
+    };
+    
+    // outbound tab defaults to lender view
+    return $scope.drawTable();
+}])
+
+
+.controller('ItemStatusCtrl',
+['$scope', '$q', '$route', '$location', 'egPCRUD', 'ffService',
+function ($scope, $q, $route, $location, egPCRUD, ffService) {
+    $scope.focusMe = true;
+
+    $scope.draw = function(barcode) {
+        if ($scope.illRouteParams.barcode != barcode) {
+            // keep the scan box and URL in sync
+            $location.path('/fulfillment/status/' + 
+              encodeURIComponent(barcode));
+        } else {
+            $scope.itemList.items = [];
+            $scope.item = {barcode : barcode};
+            $scope.itemList.addItem($scope.item);
+            $scope.selectMe = true;
+        }
+    }
+
+    // item status actions all call the parent scope's action
+    // handlers unadorned then reload the route.
+    // TODO: set selected == item; no more need for custom action handers??
+    angular.forEach(['checkin', 'checkout', 
+            'cancel', 'abort_transit', 'retarget', 'mark_lost'],
+        function(action) {
+            $scope[action] = function() {
+                $scope.actions[action]($scope.item)
+                .then(function(resp) {$route.reload()});
+            };
+        }
+    );
+
+    // barcode passed via URL
+    if ($scope.illRouteParams.barcode) {
+        $scope.barcode = $scope.illRouteParams.barcode;
+        $scope.draw($scope.illRouteParams.barcode);
+    }
+}])
+
+
+
+/**
+ * Table of pending requests 
+ * This is the only table based purely on holds instead of items,
+ * so the data is managed a little differently.
+ */
+.controller('PendingRequestsCtrl',
+['$scope', '$q', '$route', 'egNet', 'egAuth', 'egPCRUD', 'egOrg', 'ffService',
+function ($scope, $q, $route, egNet, egAuth, egPCRUD, egOrg, ffService) {
+    var self = this;
+
+    this.displayOne = function(display, hold_blob) {
+        var hold = display.hold = hold_blob.hold;
+        display.request_time = hold.hold_request_time = hold.request_time();
+        display.expire_time = hold.expire_time();
+        display.user_name = display.hold_request_usr = 
+          (hold_blob.patron_first || '') + 
+            ' ' + (hold_blob.patron_last || '');
+        display.request_lib = display.hold_request_lib = 
+            egOrg.get(hold.request_lib()).shortname();
+        display.hold_pickup_lib = 
+            egOrg.get(hold.pickup_lib()).shortname();
+        display.title = hold_blob.mvr.title(); // MVR BOO
+        display.author = hold_blob.mvr.author(); // MVR BOO
+        if (hold_blob.copy) {
+            display.copy = hold_blob.copy;
+            display.current_copy = hold_blob.copy.barcode();
+            display.current_copy_lib = 
+                egOrg.get(hold_blob.copy.source_lib()).shortname();
+            display.current_copy_enc = encodeURIComponent(hold_blob.copy.barcode());
+            display.barcode = display.copy.barcode();
+            display.status = display.copy.status();
+            display.item_circ_lib = egOrg.get(display.copy.circ_lib()).shortname();
+        }
+        if (hold_blob.volume) {
+            display.call_number = hold_blob.volume.label();
+        }
+        if ($scope.itemList.filterLender) 
+            display.next_action = 'ill-home-capture';
+    };
+
+    // call the parent scope's action handler for each selected item.
+    // when all are done, reload the route (when not printing).
+    /*
+    angular.forEach(['checkin', 'cancel', 'retarget', 'print'],
+        function(action) {
+            $scope[action] = function() {
+                var total = Object.keys($scope.itemList.selected).length;
+                angular.forEach(
+                    $scope.itemList.selected,
+                    function(idx) {
+                        var item = $scope.itemList.items.filter(
+                            function(i) {return i.index == idx})[0];
+                        $scope.actions[action](item)
+                        .then(
+                            // when all items are done processing, reload the route
+                            function(resp) {
+                                if (--total == 0 && action != 'print') 
+                                    $route.reload()
+                            }, 
+                            function(resp) {
+                                console.error("error in " + action + ": " + resp);
+                            }
+                        );
+                    }
+                );
+            };
+        }
+    );
+    */
+
+    $scope.drawTable = function(filterLender) {
+        $scope.itemList.items = [];
+        $scope.itemList.toggleFilters(filterLender);
+
+        var fullPath = ffService.relatedOrgs();
+
+        var query = {   
+            capture_time : null, 
+            cancel_time : null, 
+            frozen : 'f'
+        };
+
+        if ($scope.itemList.filterBorrower) {
+            // holds for my patrons originate "here"
+            // current_copy is not relevant
+            query.request_lib = fullPath;
+        } else {
+            // holds for other originate from not-"here" and
+            // have a current copy at "here".
+            query.request_lib = {'not in' : fullPath};
+            query.current_copy = {
+                "in" : {
+                    select: {acp : ['id']},
+                    from : 'acp',
+                    where: {
+                        deleted : 'f',
+                        circ_lib : fullPath,
+                        id : {'=' : {'+ahr' : 'current_copy'}}
+                    }
+                }
+            }
+        }
+
+        egPCRUD.search('ahr', query,
+            {   limit : $scope.itemList.limit, 
+                offset : $scope.itemList.offset,
+                order_by : {'ahr' : 'request_time, id'}
+            }, 
+            {atomic : true}
+
+        ).then(function(holds) {
+
+            // fetch the extended hold details for each hold
+            // to pick up the title, etc.
+            angular.forEach(holds, function(hold) {
+                var display = {
+                    id : hold.id(),
+                    index : $scope.itemList.items.length
+                };
+                // we don't use itemList.addItem(), 
+                // since it fetches data differently
+                $scope.itemList.items.push(display);
+                egNet.request(
+                    'open-ils.circ',
+                    'open-ils.circ.hold.details.retrieve', 
+                    egAuth.token(), hold.id()
+                ).then(function(hold_blob) {
+                    self.displayOne(display, hold_blob);
+                });
+            });
+        });
+    };
+
+
+    $scope.firstPage = function() {
+        $scope.itemList.offset = 0;
+        $scope.drawTable($scope.itemList.filterLender == true);
+    };
+
+    $scope.nextPage = function() {
+        $scope.itemList.offset += $scope.itemList.limit;
+        $scope.drawTable($scope.itemList.filterLender == true);
+    };
+
+    $scope.prevPage = function() {
+        $scope.itemList.offset -= $scope.itemList.limit;
+        $scope.drawTable($scope.itemList.filterLender == true);
+    };
+
+    $scope.drawTable();
+}])
+
+
+
+