Copy Location Search Groups : Admin UI
authorBill Erickson <berick@esilibrary.com>
Fri, 17 Feb 2012 21:14:16 +0000 (16:14 -0500)
committerDan Scott <dan@coffeecode.net>
Sun, 11 Mar 2012 00:20:54 +0000 (19:20 -0500)
Added admin UI for managing copy location groups.  It allows the user to
create/edit/delete groups and add copy locations to the groups.

The UI can be found under Admin -> Local System Administration -> Copy
Location Groups.

Signed-off-by: Bill Erickson <berick@esilibrary.com>
Signed-off-by: Dan Scott <dan@coffeecode.net>
Open-ILS/src/templates/conify/global/asset/copy_location_group.tt2 [new file with mode: 0644]
Open-ILS/web/js/ui/default/conify/global/asset/copy_location_group.js [new file with mode: 0644]
Open-ILS/web/opac/locale/en-US/lang.dtd
Open-ILS/xul/staff_client/chrome/content/main/menu.js
Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul

diff --git a/Open-ILS/src/templates/conify/global/asset/copy_location_group.tt2 b/Open-ILS/src/templates/conify/global/asset/copy_location_group.tt2
new file mode 100644 (file)
index 0000000..c62ec4a
--- /dev/null
@@ -0,0 +1,137 @@
+[% WRAPPER base.tt2 %]
+[% ctx.page_title = l('Copy Location Group') %]
+<script type="text/javascript" src='[% ctx.media_prefix %]/js/ui/default/conify/global/asset/copy_location_group.js'> </script>
+<link rel='stylesheet' type='text/css' href='[% ctx.media_prefix %]/js/dojo/dojo/resources/dnd.css'/>
+<style>
+    .acpl-content {
+        padding: 10px; 
+        padding-top: 20px;
+        min-width: 20%;
+        height: 100%;
+    }
+    .acpl-content div:first-child {
+        font-weight: bold;
+        font-size: 110%;
+        border-bottom: 1px solid #888;
+        background-color:#E7A555;
+    }
+    .acpl-content-2 {
+        border-left: 2px solid #888;
+        float:left;
+    }
+    .acplg-drag-handle { 
+        background-image: url([% ctx.media_prefix %]/images/dimple.png);
+        background-repeat: no-repeat;
+        background-position: center;
+        padding: 5px;
+        margin-left:5px;
+    }
+    .acplg-list-tbody td {
+        padding: 2px;
+    }
+    .acplg-list-tbody tr:nth-child(even) {
+        background: #EEE
+    }
+    #acplg-list li {
+        padding: 5px;
+    }
+    #acplg-header {
+        margin-top: 20px;
+        border-bottom: 2px solid #888;
+        padding-bottom: 10px;
+    }
+    #acplg-header span:first-child {
+        font-weight: bold;
+        font-size: 130%;
+    }
+    .acplg-group-selected {
+        background-color:#E7A555;
+        border: 1px solid #4A4747;
+    }
+</style>
+
+<div id='acplg-header'>
+    <span>[% l('Copy Location Groups') %]</span>
+    <select dojoType="openils.widget.OrgUnitFilteringSelect"
+            jsId='contextOrgSelector'
+            searchAttr='shortname'
+            labelAttr='shortname'>
+    </select>
+</div>
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client" style="width:100%">
+    <div class='acpl-content' style="float: left">
+        <div>
+            <table width='100%'><tr>
+                <td align='left' style='padding-left:5px;'>[% l('Location Groups') %]</td>
+                <td align='right'><button onclick='newGroup()'>[% l('New') %]</button></td>
+            </tr></table>
+        </div>
+        <ol id='acplg-list'></ol>
+    </div>
+    <div class='acpl-content acpl-content-2'>
+        <div>
+            <table width='100%'><tr>
+                <td align='left' style='padding-left:5px;'>[% l('Group Entries') %]</td>
+                <td align='right'><button onclick='editLocations("eliminate")'>[% l('Remove &rarr;') %]</button></td>
+            </tr></table>
+        </div>
+        <div style='height:400px; overflow-y:scroll'>
+            <table>
+                <tbody id='acplg-loc-map-tbody' class='acplg-list-tbody'>
+                    <tr id='acplg-loc-map-row'>
+                        <td><input type='checkbox' name='selector'/></td>
+                        <td><span name='owning_lib'></span></td>
+                        <td><span name='name'></span></td>
+                    </td>
+                </tbody>
+            </table>
+        </div>
+    </div>
+    <div class='acpl-content acpl-content-2'>
+        <div>
+            <table width='100%'><tr>
+                <td align='left'><button onclick='editLocations("create")'>[% l('&larr; Add') %]</button></td>
+                <td align='right' style='padding-right:5px;'>[% l('Copy Locations') %]</td>
+            </tr></table>
+        </div>
+        <div style='height:400px; overflow-y:scroll'>
+            <table>
+                <tbody id='acplg-loc-tbody' class='acplg-list-tbody'>
+                    <tr id='acplg-loc-row'>
+                        <td><input type='checkbox' name='selector'/></td>
+                        <td><span name='owning_lib'></span></td>
+                        <td><span name='name'></span></td>
+                    </td>
+                </tbody>
+            </table>
+        </div>
+    </div>
+</div>
+
+<div class='hidden'>
+    <div dojoType='openils.widget.ProgressDialog' jsId='progressDialog'></div>
+    <div id='dnd-drag-actions'>
+        <span>
+            <span style='padding:5px;' group="GRPID">
+                <span><a href="javascript:;" onclick="drawGroupEntries(GRPID)">GRPNAME</a></span>
+                [
+                <span>[% l('Visible') %]
+                    <span style='color:green' name='visible'>&#x2713;</span>
+                    <span style='color:red' name='invisible'>&#x2717;</span>
+                </span>
+                <span><a href="javascript:;" onclick="editGroup(GRPID)">[% l('Edit') %]</a></span>
+                <span><a href="javascript:;" onclick="deleteGroup(GRPID)">[% l('Delete') %]</a></span>
+                ]
+            </span>
+            <span></span>
+            <span class='acplg-drag-handle'></span>
+            <span class='acplg-drag-handle'></span>
+            <span class='acplg-drag-handle'></span>
+        </span>
+    </div>
+    <div id='acplg-edit-dialog'></div>
+</div>
+
+
+[% END %]
diff --git a/Open-ILS/web/js/ui/default/conify/global/asset/copy_location_group.js b/Open-ILS/web/js/ui/default/conify/global/asset/copy_location_group.js
new file mode 100644 (file)
index 0000000..9a9770b
--- /dev/null
@@ -0,0 +1,360 @@
+dojo.require('dijit.layout.ContentPane');
+dojo.require('dijit.layout.BorderContainer');
+dojo.require("dojo.dnd.Container");
+dojo.require("dojo.dnd.Source");
+dojo.require('fieldmapper.OrgUtils');
+dojo.require('openils.User');
+dojo.require('openils.Util');
+dojo.require('openils.Event');
+dojo.require('openils.PermaCrud');
+dojo.require('openils.widget.AutoGrid');
+dojo.require('openils.widget.ProgressDialog');
+dojo.require('openils.widget.OrgUnitFilteringSelect');
+dojo.require('openils.widget.EditDialog');
+
+var user;
+var groups;
+var locations;
+var source;
+var locTbody;
+var locRowTemplate;
+var locMapTbody;
+var locMapRowTemplate;
+var currentGroupId;
+var currentGroupMaps;
+var currentOrg;
+
+function init() {
+
+    user = new openils.User();
+   
+    // init the DnD environment
+    source = new dojo.dnd.Source('acplg-list');
+    dojo.connect(source, 'onDndDrop', updateGroupOrder);
+
+    // context org selector
+    user.buildPermOrgSelector(
+        'ADMIN_COPY_LOCATION_GROUP', 
+        contextOrgSelector, 
+        null, 
+        function() {
+            dojo.connect(contextOrgSelector, 'onChange', drawPage);
+        }
+    );
+
+    fetchCopyLocations();
+}
+
+function fetchCopyLocations() {
+    // the full set of copy locations can be very large.  
+    // Only retrieve the set of locations owned by orgs this user 
+    // can use for building location groups.
+    user.getPermOrgList(
+        ['ADMIN_COPY_LOCATION_GROUP'], 
+        function(list) {
+
+            var ownerOrgList = [];
+            dojo.forEach(list,
+                function(org) {
+                    // include parent orgs
+                    ownerOrgList = ownerOrgList.concat(org).concat(
+                        fieldmapper.aou.orgNodeTrail(fieldmapper.aou.findOrgUnit(org), true));
+                }
+            );
+
+            var pcrud = new openils.PermaCrud({authtoken : user.authtoken});
+            pcrud.search('acpl', // this can take some time...
+                {owning_lib : ownerOrgList},
+                {   
+                    async : true,
+                    join : 'aou',
+                    oncomplete : function(r) {
+                        locations = openils.Util.readResponse(r);
+                        sortCopyLocations();
+                        drawPage(user.user.ws_ou());
+                    }
+                }
+            );
+        },
+        true,
+        true
+    );
+}
+
+// sort the list of copy locations according the shape of 
+// the org unit tree.  apply a secondary sort on name.
+function sortCopyLocations() {
+    var newlist = [];
+
+    function addNode(node) {
+        // find locs for this org
+        var locs = locations.filter(function(loc) { return loc.owning_lib() == node.id() });
+        // sort on name and append to the new list
+        newlist = newlist.concat(locs.sort(function(a, b) { return a.name() < b.name() ? -1 : 1 }));
+        // repeat for org child nodes
+        dojo.forEach(node.children(), addNode);
+    }
+
+    addNode(fieldmapper.aou.globalOrgTree);
+    locations = newlist;
+}
+
+
+function drawPage(org) {
+    currentOrg = org;
+    currentGroupId = null;
+    currentGroupMaps = [];
+    //drawLocations();
+    drawGroupList();
+}
+
+function drawGroupList(selectedGrp) {
+    var pcrud = new openils.PermaCrud({authtoken : user.authtoken});
+    groups = pcrud.search('acplg', {owner : currentOrg}, {order_by : {acplg : 'pos'}});
+
+
+    source.selectAll();
+    source.deleteSelectedNodes();
+    source.clearItems();
+
+    dojo.forEach(groups,
+        function(group) {
+            if(!group) return;
+
+            var drag = dojo.byId('dnd-drag-actions').cloneNode(true);
+            drag.id = '';
+            var vis = openils.Util.isTrue(group.opac_visible());
+            openils.Util.hide(dojo.query('[name=' + (vis ? 'invisible' : 'visible') + ']', drag)[0]);
+
+
+            var node = source.insertNodes(false, [{ 
+                data : drag.innerHTML.replace(/GRPID/g, group.id()).replace(/GRPNAME/g, group.name()),
+                type : [group.id()+''] // use the type field to store the ID
+            }]);
+        }
+    );
+
+    if (groups.length == 0) {
+        selectedGrp = null
+    } else if (selectedGrp == null) {
+        selectedGrp = groups[0].id();
+    }
+
+    drawGroupEntries(selectedGrp);
+}
+
+function drawLocations() {
+
+    if (!locTbody) {
+        locTbody = dojo.byId('acplg-loc-tbody');
+        locRowTemplate = locTbody.removeChild(dojo.byId('acplg-loc-row'));
+    } else {
+        // clear out the previous table
+        while (node = locTbody.childNodes[0])
+            locTbody.removeChild(node);
+    }
+
+    var allMyOrgs = fieldmapper.aou.fullPath(currentOrg, true);
+
+    dojo.forEach(locations,
+        function(loc) {
+            if (allMyOrgs.indexOf(loc.owning_lib()) == -1) return;
+
+            // don't show locations contained in the current group
+            if (currentGroupMaps.length) {
+                var existing = currentGroupMaps.filter(
+                    function(map) { return (map.location() == loc.id()) });
+                if (existing.length > 0) return;
+            }
+
+            var row = locRowTemplate.cloneNode(true);
+            row.setAttribute('location', loc.id());
+            dojo.query('[name=name]', row)[0].innerHTML = loc.name();
+            dojo.query('[name=owning_lib]', row)[0].innerHTML = fieldmapper.aou.findOrgUnit(loc.owning_lib()).shortname();
+            locTbody.appendChild(row);
+        }
+    );
+}
+
+function updateGroupOrder() {
+    var pos = 0;
+    var toUpdate = [];
+
+    // find any groups that have changed position and send them off for update
+    dojo.forEach(
+        source.getAllNodes(),
+        function(node) {
+            var item = source.getItem(node.id);
+            var grpId = item.type[0];
+            var grp = groups.filter(function(g) { return g.id() == grpId })[0];
+            if (grp.pos() != pos) {
+                grp.pos(pos);
+                toUpdate.push(grp);
+            }
+            pos++;
+        }
+    );
+
+    if (toUpdate.length == 0) return;
+
+    var pcrud = new openils.PermaCrud({authtoken : user.authtoken});
+    pcrud.update(toUpdate); // run sync to prevent UI changes mid-update 
+}
+
+function newGroup() {
+
+    var dialog = new openils.widget.EditDialog({
+        fmClass : 'acplg',
+        mode : 'create',
+        parentNode : dojo.byId('acplg-edit-dialog'),
+        suppressFields : ['id'],
+        // note: when 'pos' is suppressed, the value is not propagated.
+        overrideWidgetArgs : {
+            pos : {widgetValue : groups.length, dijitArgs : {disabled : true}},
+            owner : {widgetValue : currentOrg, dijitArgs : {disabled : true}}
+        },
+        onPostSubmit : function(req, cudResults) {
+            if (cudResults && cudResults.length) {
+                // refresh the group display
+                drawGroupList(cudResults[0].id());
+            }
+        }
+    });
+
+    dialog.startup();
+    dialog.show();
+}
+
+function editGroup(grpId) {
+    var grp = groups.filter(function(g) { return g.id() == grpId })[0];
+
+    var dialog = new openils.widget.EditDialog({
+        fmObject : grp,
+        mode : 'update',
+        parentNode : dojo.byId('acplg-edit-dialog'),
+        suppressFields : ['id', 'pos', 'owner'],
+        onPostSubmit : function(req, cudResults) {
+            if (cudResults && cudResults.length) {
+                // refresh the group display
+                // pcrud.update returns ID only
+                drawGroupList(cudResults[0]);
+            }
+        }
+    });
+
+    dialog.startup();
+    dialog.show();
+}
+
+function deleteGroup(grpId) {
+    // confirm and delete
+    var pcrud = new openils.PermaCrud({authtoken : user.authtoken});
+    var grp = groups.filter(function(g) { return g.id() == grpId })[0];
+    pcrud.eliminate(grp, {oncomplete : function() { drawGroupList() }});
+}
+
+function drawGroupEntries(grpId) {
+    currentGroupId = grpId;
+
+    // init/reset the table of mapped copy locations
+    if (!locMapTbody) {
+        locMapTbody = dojo.byId('acplg-loc-map-tbody');
+        locMapRowTemplate = locMapTbody.removeChild(dojo.byId('acplg-loc-map-row'));
+    } else {
+        // clear out the previous table
+        while (node = locMapTbody.childNodes[0])
+            locMapTbody.removeChild(node);
+    }
+    
+    // update the 'selected' status
+    dojo.query('[group]').forEach(
+        function(node) {
+            if (node.getAttribute('group') == grpId) {
+                openils.Util.addCSSClass(node, 'acplg-group-selected');
+            } else {
+                openils.Util.removeCSSClass(node, 'acplg-group-selected');
+            }
+        }
+    );
+
+    currentGroupMaps = [];
+
+    // fetch the group
+    if (grpId) {
+        var pcrud = new openils.PermaCrud({authtoken : user.authtoken});
+        currentGroupMaps = pcrud.search('acplgm', {lgroup : grpId});
+    } 
+
+    // update the location selector to remove the already-selected orgs
+    drawLocations();
+
+    // draw the mapped copy locations
+    // remove any mapped locations from the location selector
+    dojo.forEach(currentGroupMaps,
+        function(map) {
+            var row = locMapRowTemplate.cloneNode(true);
+            row.setAttribute('map', map.id());
+            var loc = locations.filter(
+                function(loc) { return (loc.id() == map.location()) })[0];
+            dojo.query('[name=name]', row)[0].innerHTML = loc.name();
+            dojo.query('[name=owning_lib]', row)[0].innerHTML = 
+                fieldmapper.aou.findOrgUnit(loc.owning_lib()).shortname();
+            locMapTbody.appendChild(row);
+
+            // if the location is in the group, remove it from the location selection list
+            //removeLocationRow(loc.id());
+        }
+    );
+}
+
+function editLocations(action) {
+    var maps = [];
+    var tbody = (action == 'create') ? locTbody : locMapTbody;
+    dojo.forEach(tbody.getElementsByTagName('tr'),
+        function(row) {
+            var selector = dojo.query('[name=selector]', row)[0];
+            if (selector.checked) {
+                var map = new fieldmapper.acplgm();
+                map.lgroup(currentGroupId);
+                if (action == 'create') {
+                    map.location(row.getAttribute('location'));
+                } else {
+                    map.id(row.getAttribute('map'));
+                }
+                maps.push(map);
+            }
+        }
+    );
+
+    if (maps.length == 0) return;
+
+    // check for dupes
+    var pcrud = new openils.PermaCrud({authtoken : user.authtoken});
+    pcrud[action](maps, {
+        oncomplete : function() { 
+            drawGroupEntries(currentGroupId) 
+            /*
+            if (action != 'create') {
+                drawLocations();
+            }
+            */
+        }
+    });
+}
+
+function deSelectAll(node) {
+    dojo.query('[name=selector]', node).forEach(
+        function(selector) {
+            selector.checked = false;
+        }
+    );
+}
+
+/*
+function removeLocationRow(locId) {
+    var row = dojo.query('[location=' + locId + ']', locTbody)[0];
+    if (row) locTbody.removeChild(row);
+}
+*/
+
+openils.Util.addOnLoad(init);
index 1fa3359..3561e30 100644 (file)
 <!ENTITY staff.main.menu.admin.local_admin.patrons_due_refunds.accesskey "N">
 <!ENTITY staff.main.menu.admin.local_admin.address_alert.label "Address Alerts">
 <!ENTITY staff.main.menu.admin.local_admin.circ_limit_set.label "Circulation Limit Sets">
+<!ENTITY staff.main.menu.admin.local_admin.copy_location_group.label "Copy Location Groups">
 
 <!ENTITY staff.main.menu.admin.server_admin.label "Server Administration">
 <!ENTITY staff.main.menu.admin.server_admin.conify.org_unit_type.label "Organization Types">
index b9f5a2a..d541553 100644 (file)
@@ -1069,6 +1069,10 @@ main.menu.prototype = {
                 ['oncommand'],
                 function(event) { open_eg_web_page('conify/global/actor/address_alert', null, event); }
             ],
+            'cmd_local_admin_copy_location_group' : [
+                ['oncommand'],
+                function(event) { open_eg_web_page('conify/global/asset/copy_location_group', null, event); }
+            ],
             'cmd_acq_create_invoice' : [
                 ['oncommand'],
                 function(event) { open_eg_web_page('acq/invoice/view?create=1', 'menu.cmd_acq_create_invoice.tab', event); }
index 220c91e..c71f8e1 100644 (file)
     <command id="cmd_local_admin_circ_limit_set"
              perm="ADMIN_CIRC_MATRIX_MATCHPOINT VIEW_CIRC_MATRIX_MATCHPOINT"
              />
+    <command id="cmd_local_admin_copy_location_group"
+             perm="ADMIN_COPY_LOCATION_GROUP VIEW_COPY_LOCATION_GROUP" />
 
     <!-- server admin menu commands -->
     <command id="cmd_server_admin_org_type"
                 <menuitem label="&staff.server.admin.index.closed_dates;" command="cmd_local_admin_closed_dates"/>
                 <menuitem label="&staff.server.admin.index.copy_locations;" command="cmd_local_admin_copy_locations"/>
                 <menuitem label="&staff.main.menu.admin.local_admin.conify.copy_location_order.label;" command="cmd_local_admin_copy_location_order"/>
+                <menuitem label="&staff.main.menu.admin.local_admin.copy_location_group.label;" command="cmd_local_admin_copy_location_group"/>
                 <menuitem label="&staff.main.menu.admin.local_admin.copy_template.label;" accesskey="&staff.main.menu.admin.local_admin.copy_template.accesskey;" command="cmd_local_admin_copy_template"/>
                 <menuitem label="&staff.main.menu.admin.local_admin.conify.idl_field_doc.label;" command="cmd_local_admin_idl_field_doc"/>
                 <menuitem label="&staff.main.menu.admin.local_admin.conify.grp_penalty_threshold.label;" command="cmd_local_admin_grp_penalty_threshold"/>