Bug 14697: Enhance the return claims feature
authorKyle M Hall <kyle@bywatersolutions.com>
Tue, 29 Oct 2019 15:29:54 +0000 (12:29 -0300)
committerMartin Renvoize <martin.renvoize@ptfs-europe.com>
Thu, 31 Oct 2019 12:04:21 +0000 (12:04 +0000)
This adds a "Claims returned" feature that extends and enhances the claims returned lost status.
To use this feature, a new LOST status to represent an item claimed as returned needs to be created.
The value of this LOST authorised value should be set in the new syspref ClaimReturnedLostValue.
Setting this system preference turns on the feature.

Once the feature is enabled, you should be able to mark checked out items as return claims from the
checkout and patron details pages, and also modify them from the new claims tab on those pages.

Returning a claimed item will notify the librarian that the item in question has a claim on it.

Setting the ClaimReturnedWarningThreshold will add an alert to make librarians aware that this
patron has many return claims on the patron's record.

Test Plan:
1) Create a "Claims Returned" lost value
2) Create some RETURN_CLAIM_RESOLUTION authorized values
3) Set ClaimReturnedLostValue
4) Set ClaimReturnedChargeFee
5) Set ClaimReturnedWarningThreshold
6) Create some checkouts
7) Claim some returns
8) Verify ClaimReturnedChargeFee works with all 3 options
9) Verify ClaimReturnedWarningThreshold shows a warning once the threshold has been exceeded
10) Edit notes on a claim
11) Resolve a claim
12) Delete a claim

Sponsored-by: North Central Regional Library System
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Andrew Fuerste-Henry <andrew@bywatersolutions.com>
Signed-off-by: Lisette Scheer <lisetteslatah@gmail.com>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>

14 files changed:
circ/circulation.pl
circ/returns.pl
koha-tmpl/intranet-tmpl/prog/en/includes/checkouts-table.inc
koha-tmpl/intranet-tmpl/prog/en/includes/patron-return-claims.inc [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/includes/strings.inc
koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref
koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt
koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt
koha-tmpl/intranet-tmpl/prog/en/modules/circ/returns.tt
koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt
koha-tmpl/intranet-tmpl/prog/js/checkouts.js
members/moremember.pl
svc/checkouts
svc/return_claims [new file with mode: 0755]

index 60e5455..5bc227d 100755 (executable)
@@ -643,6 +643,7 @@ $template->param(
     override_high_holds       => $override_high_holds,
     nopermission              => scalar $query->param('nopermission'),
     autoswitched              => $autoswitched,
+    logged_in_user            => $logged_in_user,
 );
 
 output_html_with_http_headers $query, $cookie, $template->output;
index 42226b4..ddce95a 100755 (executable)
@@ -542,7 +542,9 @@ foreach my $code ( keys %$messages ) {
     elsif ( $code eq 'DataCorrupted' ) {
         $err{data_corrupted} = 1;
     }
-    else {
+    elsif ( $code eq 'ReturnClaims' ) {
+        $template->param( ReturnClaims => $messages->{ReturnClaims} );
+    } else {
         die "Unknown error code $code";    # note we need all the (empty) elsif's above, or we die.
         # This forces the issue of staying in sync w/ Circulation.pm
     }
index 1276e70..f1bc60f 100644 (file)
@@ -28,6 +28,7 @@
                         <th scope="col">Price</th>
                         <th scope="col">Renew <p class="column-tool"><a href="#" id="CheckAllRenewals">select all</a> | <a href="#" id="UncheckAllRenewals">none</a></p></th>
                         <th scope="col">Check in <p class="column-tool"><a href="#" id="CheckAllCheckins">select all</a> | <a href="#" id="UncheckAllCheckins">none</a></p></th>
+                        <th scope="col">Return claims</th>
                         <th scope="col">Export <p class="column-tool"><a href="#" id="CheckAllExports">select all</a> | <a href="#" id="UncheckAllExports">none</a></p></th>
                     </tr>
                 </thead>
         <p>Patron has nothing checked out.</p>
     [% END %]
 </div>
+
+<!-- Claims Returned Modal -->
+<div class="modal fade" id="claims-returned-modal" tabindex="-1" role="dialog" aria-labelledby="claims-returned-modal-label">
+  <div class="modal-dialog" role="document">
+    <div class="modal-content">
+      <div class="modal-header">
+        <h4 class="modal-title" id="claims-returned-modal-label">Claim returned</h4>
+      </div>
+      <div class="modal-body">
+
+          <div class="form-group">
+            <label for="claims-returned-notes" class="control-label">Notes</label>
+            <div>
+              <textarea id="claims-returned-notes" class="form-control" rows="3"></textarea>
+            </div>
+          </div>
+
+          [% IF Koha.Preference('ClaimReturnedChargeFee') == 'ask' %]
+            <div class="form-group">
+              <div class="checkbox">
+                <label for="claims-returned-charge-lost-fee">
+                  <input id="claims-returned-charge-lost-fee" type="checkbox" value="1">
+                  Charge lost fee
+                </label>
+              </div>
+            </div>
+          [% END %]
+
+          <input type="hidden" id="claims-returned-itemnumber" />
+      </div>
+      <div class="modal-footer">
+        <button id="claims-returned-modal-btn-submit" type="button" class="btn btn-primary"><i class="fa fa-exclamation-circle"></i> Make claim</button>
+        <button class="btn btn-default deny cancel" href="#" data-dismiss="modal" aria-hidden="true"><i class="fa fa-times"></i> Cancel</button>
+      </div>
+    </div>
+  </div>
+</div>
+
+<!-- Resolve Return Claim Modal -->
+<div class="modal fade" id="claims-returned-resolved-modal" tabindex="-1" role="dialog" aria-labelledby="claims-returned-resolved-modal-label">
+  <div class="modal-dialog" role="document">
+    <div class="modal-content">
+      <div class="modal-header">
+        <h4 class="modal-title" id="claims-returned-resolved-modal-label">Resolve return claim</h4>
+      </div>
+      <div class="modal-body">
+
+          <div class="form-group">
+            <label for="claims-returned-resolved-code">Resolution</label>
+            [% SET resolutions = AuthorisedValues.GetAuthValueDropbox('RETURN_CLAIM_RESOLUTION') %]
+            <select class="form-control" id="claims-returned-resolved-modal-resolved-code">
+              [% FOREACH r IN resolutions %]
+                  <option value="[% r.authorised_value | html %]">[% r.lib | html %]</option>
+              [% END %]
+            </select>
+          </div>
+
+          <input type="hidden" id="claims-returned-resolved-modal-id"/>
+      </div>
+      <div class="modal-footer">
+        <button id="claims-returned-resolved-modal-btn-submit" type="button" class="btn btn-primary">
+          <i id="claims-returned-resolved-modal-btn-submit-icon" class="fa fa-exclamation-circle"></i>
+          <i id="claims-returned-resolved-modal-btn-submit-spinner" class="fa fa-spinner fa-pulse fa-fw" style="display:none"></i>
+          Resolve claim
+         </button>
+        <button class="btn btn-default deny cancel" href="#" data-dismiss="modal" aria-hidden="true"><i class="fa fa-times"></i> Cancel</button>
+      </div>
+    </div>
+  </div>
+</div>
diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/patron-return-claims.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/patron-return-claims.inc
new file mode 100644 (file)
index 0000000..63f1dad
--- /dev/null
@@ -0,0 +1,15 @@
+<div id="return-claims">
+  <table id="return-claims-table" class="table table-bordered table-striped">
+      <thead>
+          <tr>
+              <th class="return-claim-id">Claim ID</th>
+              <th class="return-claim-record-title anti-the">Title</th>
+              <th class="return-claim-notes">Notes</th>
+              <th class="return-claim-created-on">Created on</th>
+              <th class="return-claim-updated-on">Updated on</th>
+              <th class="return-claim-resolution">Resolution</th>
+              <th class="return-claim-actions">&nbsp;</th>
+          </tr>
+      </thead>
+  </table>
+</div>
index e88a90f..7eba12b 100644 (file)
@@ -5,6 +5,10 @@
     var NOT_RENEWABLE_RESTRICTION = _("Not allowed: patron restricted");
     var CIRCULATION_RENEWED_DUE = _("Renewed, due:");
     var CIRCULATION_RENEW_FAILED = _("Renew failed:");
+    var RETURN_CLAIMED = _("Return claimed");
+    var RETURN_CLAIMED_FAILURE = _("Unable to claim as returned");
+    var RETURN_CLAIMED_MAKE = _("Claim returned");
+    var RETURN_CLAIMED_NOTES = _("Notes about return claim");
     var NOT_CHECKED_OUT = _("not checked out");
     var TOO_MANY_RENEWALS = _("too many renewals");
     var ON_RESERVE = _("on hold");
@@ -47,4 +51,5 @@
     var MSG_NO_ITEMTYPE = _("No itemtype");
     var MSG_CHECKOUTS_BY_ITEMTYPE = _("Number of checkouts by item type");
     var PATRON_NOTE = _("Patron note");
+    var CONFIRM_DELETE_RETURN_CLAIM = _("Are you sure you want to delete this return claim?");
 </script>
index 53669d7..52fdc24 100644 (file)
@@ -1085,3 +1085,22 @@ Circulation:
                 pages: Pages
                 chapters: Chapters
             -
+    Return Claims:
+        -
+            - When marking a checkout as "claims returned",
+            - pref: ClaimReturnedChargeFee
+              default: ask
+              choices:
+                  ask: ask if a lost fee should be charged
+                  charge: charge a lost fee
+                  no_charge: don't charge a lost fee
+            - .
+        -
+            - Use the LOST authorised value
+            - pref: ClaimReturnedLostValue
+            - to represent returns claims
+        -
+            - Warn librarians that a patron has excessive return cliams if the patron has claimed the return of more than
+            - pref: ClaimReturnedWarningThreshold
+              class: integer
+            - items.
index 0a6d060..8d5e712 100644 (file)
                 <li><span class="label">Lost status:</span>
                     [% IF ( CAN_user_circulate ) %]
                         <form action="updateitem.pl" method="post">
-                        <input type="hidden" name="biblionumber" value="[% ITEM_DAT.biblionumber | html %]" />
-                        <input type="hidden" name="biblioitemnumber" value="[% ITEM_DAT.biblioitemnumber | html %]" />
-                        <input type="hidden" name="itemnumber" value="[% ITEM_DAT.itemnumber | html %]" />
-                        <select name="itemlost" >
-                                    <option value="">Choose</option>
-                        [% FOREACH itemlostloo IN itemlostloop %]
-                            [% IF itemlostloo.authorised_value == ITEM_DAT.itemlost %]
-                                    <option value="[% itemlostloo.authorised_value | html %]" selected="selected">[% itemlostloo.lib | html %]</option>
+                            <input type="hidden" name="biblionumber" value="[% ITEM_DAT.biblionumber | html %]" />
+                            <input type="hidden" name="biblioitemnumber" value="[% ITEM_DAT.biblioitemnumber | html %]" />
+                            <input type="hidden" name="itemnumber" value="[% ITEM_DAT.itemnumber | html %]" />
+                            <select name="itemlost" >
+                                <option value="">Choose</option>
+                                [% FOREACH itemlostloo IN itemlostloop %]
+                                    [% IF itemlostloo.authorised_value == ITEM_DAT.itemlost %]
+                                        <option value="[% itemlostloo.authorised_value | html %]" selected="selected">[% itemlostloo.lib | html %]</option>
+                                    [% ELSE %]
+                                        <option value="[% itemlostloo.authorised_value | html %]">[% itemlostloo.lib | html %]</option>
+                                    [% END %]
+                                [% END %]
+                            </select>
+                            <input type="hidden" name="withdrawn" value="[% ITEM_DAT.withdrawn | html %]" />
+                            <input type="hidden" name="damaged" value="[% ITEM_DAT.damaged | html %]" />
+
+                            [% SET ClaimReturnedLostValue = Koha.Preference('ClaimReturnedLostValue') %]
+                            [% IF ClaimReturnedLostValue && ITEM_DAT.itemlost == ClaimReturnedLostValue %]
+                                <input type="submit" name="submit" class="submit" value="Set status" disabled="disabled"/>
+                                <p class="help-block">Item has been claimed as returned.</p>
                             [% ELSE %]
-                                    <option value="[% itemlostloo.authorised_value | html %]">[% itemlostloo.lib | html %]</option>
+                                <input type="hidden" name="op" value="set_lost" />
+                                <input type="submit" name="submit" class="submit" value="Set status" /></form>
                             [% END %]
-                        [% END %]
-                        </select>
-                        <input type="hidden" name="op" value="set_lost" />
-                        <input type="submit" name="submit" class="submit" value="Set status" /></form>
+                        </form>
                     [% ELSE %]
                         [% FOREACH itemlostloo IN itemlostloop %]
                             [% IF ( itemlostloo.selected ) %]
index 8680344..acbe85a 100644 (file)
                                         <li><span class="circ-hlt">Overdues: Patron has ITEMS OVERDUE.</span> <a href="#checkouts">See highlighted items below</a></li>
                                     [% END %]
 
+                                    [% SET ClaimReturnedWarningThreshold = Koha.Preference('ClaimReturnedWarningThreshold') %]
+                                    [% SET return_claims = patron.return_claims %]
+                                    [% IF return_claims.count %]
+                                        <li><span class="circ-hlt return-claims">Return claims: Patron has [% return_claims.count | html %] RETURN CLAIMS.</span>
+                                    [% END %]
+
                                     [% IF ( charges ) %]
                                         [% INCLUDE 'blocked-fines.inc' fines = chargesamount %]
                                     [% END %]
                                 </li>
                             [% END %]
 
+                            <li>
+                                [% IF ( patron.return_claims.count ) %]
+                                    <a href="#return-claims" id="return-claims-tab">
+                                        <span id="return-claims-count-resolved">[% patron.return_claims.resolved.count | html %]</span>
+                                        /
+                                        <span id="return-claims-count-unresolved">[% patron.return_claims.unresolved.count | html %]</span>
+                                        Claim(s)
+                                    </a>
+                                [% ELSE %]
+                                    <a href="#return-claims" id="return-claims-tab">
+                                        <span id="return-claims-count-resolved">0</span>
+                                        /
+                                        <span id="return-claims-count-unresolved">0</span>
+                                        Claim(s)
+                                    </a>
+                                [% END %]
+                            </li>
+
+                            [% IF Koha.Preference('ArticleRequests') %]
+                                <li>
+                                    <a href="#article-requests" id="article-requests-tab"> [% patron.article_requests_current.count | html %] Article requests</a>
+                                </li>
+                            [% END %]
+
                             <li><a id="debarments-tab-link" href="#reldebarments">[% debarments.count | html %] Restrictions</a></li>
 
                             [% SET enrollments = patron.get_club_enrollments(1) %]
                             [% END # /IF holds_count %]
                         </div> <!-- /#reserves -->
 
+                        [% INCLUDE 'patron-return-claims.inc' %]
+
                         [% IF Koha.Preference('ArticleRequests') %]
                             [% INCLUDE 'patron-article-requests.inc' %]
                         [% END %]
     [% Asset.js("js/circ-patron-search-results.js") | $raw %]
     <script type="text/javascript">
         /* Set some variable needed in circulation.js */
+        var logged_in_user_borrowernumber = "[% logged_in_user.borrowernumber | html %]";
+        var ClaimReturnedLostValue = "[% Koha.Preference('ClaimReturnedLostValue') | html %]";
+        var ClaimReturnedChargeFee = "[% Koha.Preference('ClaimReturnedChargeFee') | html %]";
+        var ClaimReturnedWarningThreshold = "[% Koha.Preference('ClaimReturnedWarningThreshold') | html %]";
         var MSG_DT_LOADING_RECORDS = _("Loading... you may continue scanning.");
         var interface = "[% interface | html %]";
         var theme = "[% theme | html %]";
index dd359dd..6294c06 100644 (file)
                                 </div>
                             [% END %]
 
+                                                       <!-- Item has return claim(s) -->
+                                                       [% IF ( ReturnClaims ) %]
+                                                               <div class="dialog alert return-claim">
+                                                                       <h3>
+                                                                               This item has been claimed as returned by:
+                                                                               <ul>
+                                                                               [% FOREACH rc IN ReturnClaims %]
+                                                                                       [% SET patron = rc.patron %]
+                                                                                       <li>
+                                                                                               <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% patron.borrowernumber | uri %]">
+                                                                                                       [% patron.firstname | html %] [% patron.surname | html %]
+                                                                                               </a>
+                                                                                       </li>
+                                                                               [% END %]
+                                                                               </ul>
+                                                                       </h3>
+                                                               </div>
+                                                       [% END %]
+
                             <!-- Patron has waiting holds -->
                             [% IF ( waiting_holds ) %]
                                 <div id="awaiting-pickup" class="dialog message">
index ac14659..bf6dac9 100644 (file)
                                     <a href="#article-requests" id="article-requests-tab"> [% patron.article_requests_current.count | html %] Article requests</a>
                                 </li>
                             [% END %]
+
+                            <li>
+                                [% IF ( patron.return_claims.count ) %]
+                                    <a href="#return-claims" id="return-claims-tab">
+                                        <span id="return-claims-count-resolved">[% patron.return_claims.resolved.count | html %]</span>
+                                        /
+                                        <span id="return-claims-count-unresolved">[% patron.return_claims.unresolved.count | html %]</span>
+                                        Claim(s)
+                                    </a>
+                                [% ELSE %]
+                                    <a href="#return-claims" id="return-claims-tab">
+                                        <span id="return-claims-count-resolved">0</span>
+                                        /
+                                        <span id="return-claims-count-unresolved">0</span>
+                                        Claim(s)
+                                    </a>
+                                [% END %]
+                            </li>
+
                             <li>
                                 <a id="debarments-tab-link" href="#reldebarments">[% debarments.size | html %] Restrictions</a>
                             </li>
                             </div> [% # /div#reserves %]
                         [% END %]
 
+                        [% INCLUDE 'patron-return-claims.inc' %]
+
                         [% IF Koha.Preference('ArticleRequests') %]
                             [% INCLUDE 'patron-article-requests.inc' %]
                         [% END %]
     [% Asset.js("js/messaging-preference-form.js") | $raw %]
     <script>
         /* Set some variable needed in circulation.js */
+        var logged_in_user_borrowernumber = "[% logged_in_user.borrowernumber | html %]";
+        var ClaimReturnedLostValue = "[% Koha.Preference('ClaimReturnedLostValue') | html %]";
+        var ClaimReturnedChargeFee = "[% Koha.Preference('ClaimReturnedChargeFee') | html %]";
+        var ClaimReturnedWarningThreshold = "[% Koha.Preference('ClaimReturnedWarningThreshold') | html %]";
         var interface = "[% interface | html %]";
         var theme = "[% theme | html %]";
         var borrowernumber = "[% patron.borrowernumber | html %]";
index e317f57..2bfd18e 100644 (file)
@@ -270,7 +270,9 @@ $(document).ready(function() {
 
                         due = "<span id='date_due_" + oObj.itemnumber + "' class='date_due'>" + due + "</span>";
 
-                        if ( oObj.lost ) {
+                        if ( oObj.lost && oObj.claims_returned ) {
+                            due += "<span class='lost claims_returned'>" + oObj.lost.escapeHtml() + "</span>";
+                        } else if ( oObj.lost ) {
                             due += "<span class='lost'>" + oObj.lost.escapeHtml() + "</span>";
                         }
 
@@ -541,6 +543,20 @@ $(document).ready(function() {
                     }
                 },
                 {
+                    "bVisible": ClaimReturnedLostValue ? true : false,
+                    "bSortable": false,
+                    "mDataProp": function ( oObj ) {
+                        let content = "";
+
+                        if ( oObj.return_claim_id ) {
+                          content = `<span class="badge">${oObj.return_claim_created_on_formatted}</span>`;
+                        } else {
+                          content = `<a class="btn btn-default btn-xs claim-returned-btn" data-itemnumber="${oObj.itemnumber}"><i class="fa fa-exclamation-circle"></i> ${RETURN_CLAIMED_MAKE}</a>`;
+                        }
+                        return content;
+                    }
+                },
+                {
                     "bVisible": exports_enabled == 1 ? true : false,
                     "bSortable": false,
                     "mDataProp": function ( oObj ) {
@@ -810,4 +826,268 @@ $(document).ready(function() {
             }
         } ).prop('checked', false);
     }
+
+    // Handle return claims
+    $(document).on("click", '.claim-returned-btn', function(e){
+        e.preventDefault();
+        itemnumber = $(this).data('itemnumber');
+
+        $('#claims-returned-itemnumber').val(itemnumber);
+        $('#claims-returned-notes').val("");
+        $('#claims-returned-charge-lost-fee').attr('checked', false)
+        $('#claims-returned-modal').modal()
+    });
+    $(document).on("click", '#claims-returned-modal-btn-submit', function(e){
+        let itemnumber = $('#claims-returned-itemnumber').val();
+        let notes = $('#claims-returned-notes').val();
+        let fee = $('#claims-returned-charge-lost-fee').attr('checked') ? true : false;
+
+        $('#claims-returned-modal').modal('hide')
+
+        $(`.claim-returned-btn[data-itemnumber='${itemnumber}']`).replaceWith(`<img id='return_claim_spinner_${itemnumber}' src='${interface}/${theme}/img/spinner-small.gif' />`);
+
+        params = {
+            item_id: itemnumber,
+            notes: notes,
+            charge_lost_fee: fee,
+            created_by: logged_in_user_borrowernumber,
+        };
+
+        $.post( '/api/v1/return_claims', JSON.stringify(params), function( data ) {
+
+            id = "#return_claim_spinner_" + data.item_id;
+
+            let created_on = new Date(data.created_on);
+
+            let content = "";
+            if ( data.claim_id ) {
+                content = `<span class="badge">${created_on.toLocaleDateString()}</span>`;
+                $(id).parent().parent().addClass('ok');
+            } else {
+                content = RETURN_CLAIMED_FAILURE;
+                $(id).parent().parent().addClass('warn');
+            }
+
+            $(id).replaceWith( content );
+
+            refreshReturnClaimsTable();
+        }, "json")
+
+    });
+
+
+    // Don't load return claims table unless it is clicked on
+    var returnClaimsTable;
+    $("#return-claims-tab").click( function() {
+        refreshReturnClaimsTable();
+    });
+
+    function refreshReturnClaimsTable(){
+        loadReturnClaimsTable();
+        $("#return-claims-table").DataTable().ajax.reload();
+    }
+    function loadReturnClaimsTable() {
+        if ( ! returnClaimsTable ) {
+            returnClaimsTable = $("#return-claims-table").dataTable({
+                "bAutoWidth": false,
+                "sDom": "rt",
+                "aaSorting": [],
+                "aoColumns": [
+                    {
+                        "mDataProp": "id",
+                        "bVisible": false,
+                    },
+                    {
+                        "mDataProp": function ( oObj ) {
+                              let title = `<a class="return-claim-title strong" href="/cgi-bin/koha/circ/request-rcticle.pl?biblionumber=[% rc.checkout.item.biblionumber | html %]">
+                                  ${oObj.title}
+                                  ${oObj.enumchron || ""}
+                              </a>`;
+                              if ( oObj.author ) {
+                                title += `by ${oObj.author}`;
+                              }
+                              title += `<a href="/cgi-bin/koha/catalogue/moredetail.pl?biblionumber=${oObj.biblionumber}&itemnumber=${oObj.itemnumber}">${oObj.barcode}</a>`;
+
+                              return title;
+                        }
+                    },
+                    {
+                        "sClass": "return-claim-notes-td",
+                        "mDataProp": function ( oObj ) {
+                            return `
+                                <span id="return-claim-notes-static-${oObj.id}" class="return-claim-notes" data-return-claim-id="${oObj.id}">${oObj.notes}</span>
+                                <i style="float:right" class="fa fa-pencil-square-o" title="Double click to edit"></i>
+                            `;
+                        }
+                    },
+                    {
+                        "mDataProp": function ( oObj ) {
+                            let created_on = new Date( oObj.created_on );
+                            return created_on.toLocaleDateString();
+                        }
+                    },
+                    {
+                        "mDataProp": function ( oObj ) {
+                            let updated_on = new Date( oObj.updated_on );
+                            return updated_on.toLocaleDateString();
+                        }
+                    },
+                    {
+                        "mDataProp": function ( oObj ) {
+                            if ( ! oObj.resolution ) return "";
+
+                            let desc = `<strong>${oObj.resolution_data.lib}</strong> on <i>${oObj.resolved_on}</i>`;
+                            if (oObj.resolved_by_data) desc += ` by <a href="/cgi-bin/koha/circ/circulation.pl?borrowernumber=${oObj.resolved_by_data.borrowernumber}">${oObj.resolved_by_data.firstname || ""} ${oObj.resolved_by_data.surname || ""}</a>`;
+                            return desc;
+                        }
+                    },
+                    {
+                        "mDataProp": function ( oObj ) {
+                            let delete_html = oObj.resolved_on
+                                ? `<li><a href="#" class="return-claim-tools-delete" data-return-claim-id="${oObj.id}"><i class="fa fa-trash"></i> Delete</a></li>`
+                                : "";
+                            let resolve_html = ! oObj.resolution
+                                ? `<li><a href="#" class="return-claim-tools-resolve" data-return-claim-id="${oObj.id}"><i class="fa fa-check-square"></i> Resolve</a></li>`
+                                : "";
+
+                            return `
+                                <div class="btn-group">
+                                  <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                                    Actions <span class="caret"></span>
+                                  </button>
+                                  <ul class="dropdown-menu">
+                                    <li><a href="#" class="return-claim-tools-editnotes" data-return-claim-id="${oObj.id}"><i class="fa fa-edit"></i> Edit notes</a></li>
+                                    ${resolve_html}
+                                    ${delete_html}
+                                  </ul>
+                                </div>
+                            `;
+                        }
+                    },
+                ],
+                "bPaginate": false,
+                "bProcessing": true,
+                "bServerSide": false,
+                "sAjaxSource": '/cgi-bin/koha/svc/return_claims',
+                "fnServerData": function ( sSource, aoData, fnCallback ) {
+                    aoData.push( { "name": "borrowernumber", "value": borrowernumber } );
+
+                    $.getJSON( sSource, aoData, function (json) {
+                        let resolved = json.resolved;
+                        let unresolved = json.unresolved;
+
+                        $('#return-claims-count-resolved').text(resolved);
+                        $('#return-claims-count-unresolved').text(unresolved);
+
+                        fnCallback(json)
+                    } );
+                },
+            });
+        }
+    }
+
+    $('body').on('click', '.return-claim-tools-editnotes', function() {
+        let id = $(this).data('return-claim-id');
+        $(`#return-claim-notes-static-${id}`).parent().dblclick();
+    });
+    $('body').on('dblclick', '.return-claim-notes-td', function() {
+        let elt = $(this).children('.return-claim-notes');
+        let id = elt.data('return-claim-id');
+        if ( $(`#return-claim-notes-editor-textarea-${id}`).length == 0 ) {
+            let note = elt.text();
+            let editor = `
+                <span id="return-claim-notes-editor-${id}">
+                    <textarea id="return-claim-notes-editor-textarea-${id}">${note}</textarea>
+                    <br/>
+                    <a class="btn btn-default btn-xs claim-returned-notes-editor-submit" data-return-claim-id="${id}"><i class="fa fa-save"></i> Update</a>
+                    <a class="claim-returned-notes-editor-cancel" data-return-claim-id="${id}" href="#">Cancel</a>
+                </span>
+            `;
+            elt.hide();
+            $(editor).insertAfter( elt );
+        }
+    });
+
+    $('body').on('click', '.claim-returned-notes-editor-submit', function(){
+        let id = $(this).data('return-claim-id');
+        let notes = $(`#return-claim-notes-editor-textarea-${id}`).val();
+
+        let params = {
+            notes: notes,
+            updated_by: logged_in_user_borrowernumber
+        };
+
+        $(this).parent().remove();
+
+        $.ajax({
+            url: `/api/v1/return_claims/${id}/notes`,
+            type: 'PUT',
+            data: JSON.stringify(params),
+            success: function( data ) {
+                let notes = $(`#return-claim-notes-static-${id}`);
+                notes.text(data.notes);
+                notes.show();
+            },
+            contentType: "json"
+        });
+    });
+
+    $('body').on('click', '.claim-returned-notes-editor-cancel', function(){
+        let id = $(this).data('return-claim-id');
+        $(this).parent().remove();
+        $(`#return-claim-notes-static-${id}`).show();
+    });
+
+    // Hanld return claim deletion
+    $('body').on('click', '.return-claim-tools-delete', function() {
+        let confirmed = confirm(CONFIRM_DELETE_RETURN_CLAIM);
+        if ( confirmed ) {
+            let id = $(this).data('return-claim-id');
+
+            $.ajax({
+                url: `/api/v1/return_claims/${id}`,
+                type: 'DELETE',
+                success: function( data ) {
+                    refreshReturnClaimsTable();
+                }
+            });
+        }
+    });
+
+    // Handle return claim resolution
+    $('body').on('click', '.return-claim-tools-resolve', function() {
+        let id = $(this).data('return-claim-id');
+
+        $('#claims-returned-resolved-modal-id').val(id);
+        $('#claims-returned-resolved-modal').modal()
+    });
+
+    $(document).on('click', '#claims-returned-resolved-modal-btn-submit', function(e) {
+        let resolution = $('#claims-returned-resolved-modal-resolved-code').val();
+        let id = $('#claims-returned-resolved-modal-id').val();
+
+        $('#claims-returned-resolved-modal-btn-submit-spinner').show();
+        $('#claims-returned-resolved-modal-btn-submit-icon').hide();
+
+        params = {
+          resolution: resolution,
+          updated_by: logged_in_user_borrowernumber
+        };
+
+        $.ajax({
+            url: `/api/v1/return_claims/${id}/resolve`,
+            type: 'PUT',
+            data: JSON.stringify(params),
+            success: function( data ) {
+                $('#claims-returned-resolved-modal-btn-submit-spinner').hide();
+                $('#claims-returned-resolved-modal-btn-submit-icon').show();
+                $('#claims-returned-resolved-modal').modal('hide')
+
+                refreshReturnClaimsTable();
+            },
+            contentType: "json"
+        });
+
+    });
+
  });
index 593b0d1..a2ba396 100755 (executable)
@@ -209,6 +209,7 @@ $template->param(
     housebound_role => scalar $patron->housebound_role,
     relatives_issues_count => $relatives_issues_count,
     relatives_borrowernumbers => \@relatives,
+    logged_in_user => $logged_in_user,
 );
 
 output_html_with_http_headers $input, $cookie, $template->output;
index 892eab8..053b589 100755 (executable)
@@ -64,28 +64,28 @@ print $input->header( -type => 'text/plain', -charset => 'UTF-8' );
 my @parameters;
 my $sql = '
     SELECT
-        issuedate,
-        date_due,
-        date_due < now() as date_due_overdue,
+        issues.issuedate,
+        issues.date_due,
+        issues.date_due < now() as date_due_overdue,
         issues.timestamp,
 
-        onsite_checkout,
+        issues.onsite_checkout,
 
-        biblionumber,
+        biblio.biblionumber,
         biblio.title,
         biblio.subtitle,
         biblio.medium,
         biblio.part_number,
         biblio.part_name,
-        author,
+        biblio.author,
 
-        itemnumber,
-        barcode,
+        items.itemnumber,
+        items.barcode,
         branches2.branchname AS homebranch,
-        itemnotes,
-        itemnotes_nonpublic,
-        itemcallnumber,
-        replacementprice,
+        items.itemnotes,
+        items.itemnotes_nonpublic,
+        items.itemcallnumber,
+        items.replacementprice,
 
         issues.branchcode,
         branches.branchname,
@@ -95,17 +95,23 @@ my $sql = '
 
         items.ccode AS collection,
 
-        borrowernumber,
-        surname,
-        firstname,
-        cardnumber,
+        borrowers.borrowernumber,
+        borrowers.surname,
+        borrowers.firstname,
+        borrowers.cardnumber,
 
-        itemlost,
-        damaged,
-        location,
+        items.itemlost,
+        items.damaged,
+        items.location,
         items.enumchron,
 
-        DATEDIFF( issuedate, CURRENT_DATE() ) AS not_issued_today
+        DATEDIFF( issues.issuedate, CURRENT_DATE() ) AS not_issued_today,
+
+        return_claims.id AS return_claim_id,
+        return_claims.notes AS return_claim_notes,
+        return_claims.created_on AS return_claim_created_on,
+        return_claims.updated_on AS return_claim_updated_on
+
     FROM issues
         LEFT JOIN items USING ( itemnumber )
         LEFT JOIN biblio USING ( biblionumber )
@@ -113,7 +119,8 @@ my $sql = '
         LEFT JOIN borrowers USING ( borrowernumber )
         LEFT JOIN branches ON ( issues.branchcode = branches.branchcode )
         LEFT JOIN branches branches2 ON ( items.homebranch = branches2.branchcode )
-    WHERE borrowernumber
+        LEFT JOIN return_claims USING ( issue_id )
+    WHERE issues.borrowernumber
 ';
 
 if ( @borrowernumber == 1 ) {
@@ -131,6 +138,7 @@ my $sth = $dbh->prepare($sql);
 $sth->execute(@parameters);
 
 my $item_level_itypes = C4::Context->preference('item-level_itypes');
+my $claims_returned_lost_value = C4::Context->preference('ClaimReturnedLostValue');
 
 my @checkouts_today;
 my @checkouts_previous;
@@ -165,9 +173,11 @@ while ( my $c = $sth->fetchrow_hashref() ) {
         $collection = $av->count ? $av->next->lib : '';
     }
     my $lost;
+    my $claims_returned;
     if ( $c->{itemlost} ) {
         my $av = Koha::AuthorisedValues->search({ category => 'LOST', authorised_value => $c->{itemlost} });
         $lost = $av->count ? $av->next->lib : '';
+        $claims_returned = $c->{itemlost} eq $claims_returned_lost_value;
     }
     my $damaged;
     if ( $c->{damaged} ) {
@@ -212,6 +222,14 @@ while ( my $c = $sth->fetchrow_hashref() ) {
         renewals_count      => $renewals_count,
         renewals_allowed    => $renewals_allowed,
         renewals_remaining  => $renewals_remaining,
+
+        return_claim_id         => $c->{return_claim_id},
+        return_claim_notes      => $c->{return_claim_notes},
+        return_claim_created_on => $c->{return_claim_created_on},
+        return_claim_updated_on => $c->{return_claim_updated_on},
+        return_claim_created_on_formatted => $c->{return_claim_created_on} ? output_pref({ dt => dt_from_string( $c->{return_claim_created_on} ) }) : undef,
+        return_claim_updated_on_formatted => $c->{return_claim_updated_on} ? output_pref({ dt => dt_from_string( $c->{return_claim_updated_on} ) }) : undef,
+
         issuedate_formatted => output_pref(
             {
                 dt          => dt_from_string( $c->{issuedate} ),
@@ -225,6 +243,7 @@ while ( my $c = $sth->fetchrow_hashref() ) {
             }
         ),
         lost    => $lost,
+        claims_returned => $claims_returned,
         damaged => $damaged,
         borrower => {
             surname    => $c->{surname},
diff --git a/svc/return_claims b/svc/return_claims
new file mode 100755 (executable)
index 0000000..e10d5f1
--- /dev/null
@@ -0,0 +1,127 @@
+#!/usr/bin/perl
+
+# Copyright 2019 ByWater Solutions
+#
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with Koha; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use Modern::Perl;
+
+use CGI;
+use JSON qw(to_json);
+
+use C4::Auth qw(check_cookie_auth haspermission get_session);
+use C4::Context;
+
+use Koha::AuthorisedValues;
+use Koha::DateUtils;
+use Koha::Patrons;
+
+my $input = new CGI;
+
+my ( $auth_status, $sessionID ) =
+  check_cookie_auth( $input->cookie('CGISESSID') );
+
+my $session = get_session($sessionID);
+my $userid  = $session->param('id');
+
+unless (
+    haspermission(
+        $userid, { circulate => 'circulate_remaining_permissions' }
+    )
+    || haspermission( $userid, { borrowers => 'edit_borrowers' } )
+  )
+{
+    exit 0;
+}
+
+my @sort_columns = qw/title notes created_on updated_on/;
+
+my $borrowernumber   = $input->param('borrowernumber');
+my $offset           = $input->param('iDisplayStart');
+my $results_per_page = $input->param('iDisplayLength') || -1;
+
+my $sorting_column = $input->param('iSortCol_0') || q{};
+$sorting_column =
+  ( $sorting_column && $sort_columns[$sorting_column] )
+  ? $sort_columns[$sorting_column]
+  : 'created_on';
+
+my $sorting_direction = $input->param('sSortDir_0') || q{};
+$sorting_direction = $sorting_direction eq 'asc' ? 'asc' : 'desc';
+
+$results_per_page = undef if ( $results_per_page == -1 );
+
+binmode STDOUT, ":encoding(UTF-8)";
+print $input->header( -type => 'text/plain', -charset => 'UTF-8' );
+
+my $sql = qq{
+    SELECT
+        return_claims.*,
+
+        biblio.biblionumber,
+        biblio.title,
+        biblio.author,
+
+        items.enumchron,
+        items.barcode
+    FROM return_claims
+        LEFT JOIN items USING ( itemnumber )
+        LEFT JOIN biblio USING ( biblionumber )
+        LEFT JOIN biblioitems USING ( biblionumber )
+    WHERE return_claims.borrowernumber = ?
+    ORDER BY $sorting_column $sorting_direction
+};
+
+my $dbh = C4::Context->dbh();
+my $sth = $dbh->prepare($sql);
+$sth->execute($borrowernumber);
+
+my $resolved = 0;
+my $unresolved = 0;
+my @return_claims;
+while ( my $claim = $sth->fetchrow_hashref() ) {
+    $claim->{created_on_formatted}  = output_pref( { dt => dt_from_string( $claim->{created_on} ) } ) if $claim->{created_on};
+    $claim->{updated_on_formatted}  = output_pref( { dt => dt_from_string( $claim->{updated_on} ) } ) if $claim->{updated_on};
+    $claim->{resolved_on_formatted} = output_pref( { dt => dt_from_string( $claim->{resolved_on} ) } ) if $claim->{resolved_on};
+
+    my $patron = $claim->{resolved_by} ? Koha::Patrons->find( $claim->{resolved_by} ) : undef;
+    $claim->{resolved_by_data} = $patron->unblessed if $patron;
+
+    my $resolution = $claim->{resolution}
+      ? Koha::AuthorisedValues->find(
+        {
+            category         => 'RETURN_CLAIM_RESOLUTION',
+            authorised_value => $claim->{resolution},
+        }
+      )
+      : undef;
+    $claim->{resolution_data} = $resolution->unblessed if $resolution;
+
+    $claim->{resolved_on} ? $resolved++ : $unresolved++;
+
+    push( @return_claims, $claim );
+}
+
+my $data = {
+    iTotalRecords        => scalar @return_claims,
+    iTotalDisplayRecords => scalar @return_claims,
+    sEcho                => $input->param('sEcho') || undef,
+    aaData               => \@return_claims,
+    resolved             => $resolved,
+    unresolved           => $unresolved
+};
+
+print to_json($data);