Bugzilla – Attachment 154048 Details for
Bug 30719
ILL should provide the ability to create batch requests
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 30719: ILL Batches
Bug-30719-ILL-Batches.patch (text/plain), 102.69 KB, created by
Pedro Amorim
on 2023-07-28 16:29:03 UTC
(
hide
)
Description:
Bug 30719: ILL Batches
Filename:
MIME Type:
Creator:
Pedro Amorim
Created:
2023-07-28 16:29:03 UTC
Size:
102.69 KB
patch
obsolete
>From 13b6a4a34ff1b4e5c1ede573eb1c2380f80af036 Mon Sep 17 00:00:00 2001 >From: Pedro Amorim <pedro.amorim@ptfs-europe.com> >Date: Fri, 12 May 2023 17:06:01 +0000 >Subject: [PATCH] Bug 30719: ILL Batches > >- Add batch column to requests table >- Establish if there are any availability or metadata enrichment plugins and pass that to the template >- Verify if we have any backend that can support batches, if not, don't show the option >- Updates to the ILL toolbar >- New ILL batch modal >- New Koha classes >- API specs > >Co-authored-by: Andrew Isherwood <andrew.isherwood@ptfs-europe.com> >Signed-off-by: Edith Speller <Edith.Speller@ukhsa.gov.uk> >--- > Koha/Illbatch.pm | 196 +++ > Koha/Illbatches.pm | 61 + > Koha/REST/V1/Illbatches.pm | 256 ++++ > admin/columns_settings.yml | 2 + > api/v1/swagger/definitions/illbatch.yaml | 48 + > api/v1/swagger/definitions/illbatches.yaml | 5 + > api/v1/swagger/paths/illbatches.yaml | 236 ++++ > ill/ill-requests.pl | 105 +- > .../prog/css/src/staff-global.scss | 106 ++ > .../en/includes/ill-batch-modal-strings.inc | 38 + > .../prog/en/includes/ill-batch-modal.inc | 97 ++ > .../en/includes/ill-batch-table-strings.inc | 9 + > .../prog/en/includes/ill-batch.inc | 19 + > .../en/includes/ill-list-table-strings.inc | 1 + > .../prog/en/includes/ill-list-table.inc | 3 + > .../prog/en/includes/ill-toolbar.inc | 21 +- > .../prog/en/modules/ill/ill-requests.tt | 52 +- > .../intranet-tmpl/prog/js/ill-batch-modal.js | 1089 +++++++++++++++++ > .../intranet-tmpl/prog/js/ill-batch-table.js | 216 ++++ > koha-tmpl/intranet-tmpl/prog/js/ill-batch.js | 49 + > .../intranet-tmpl/prog/js/ill-list-table.js | 27 +- > 21 files changed, 2625 insertions(+), 11 deletions(-) > create mode 100644 Koha/Illbatch.pm > create mode 100644 Koha/Illbatches.pm > create mode 100644 Koha/REST/V1/Illbatches.pm > create mode 100644 api/v1/swagger/definitions/illbatch.yaml > create mode 100644 api/v1/swagger/definitions/illbatches.yaml > create mode 100644 api/v1/swagger/paths/illbatches.yaml > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal-strings.inc > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal.inc > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-table-strings.inc > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch.inc > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/ill-batch-modal.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/ill-batch-table.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/ill-batch.js > >diff --git a/Koha/Illbatch.pm b/Koha/Illbatch.pm >new file mode 100644 >index 0000000000..f0e427eec9 >--- /dev/null >+++ b/Koha/Illbatch.pm >@@ -0,0 +1,196 @@ >+package Koha::Illbatch; >+ >+# Copyright PTFS Europe 2022 >+# >+# 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, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+use Koha::Database; >+use Koha::Illrequest::Logger; >+use Koha::IllbatchStatus; >+use JSON qw( to_json ); >+use base qw(Koha::Object); >+ >+=head1 NAME >+ >+Koha::Illbatch - Koha Illbatch Object class >+ >+=head2 Class methods >+ >+=head3 status >+ >+ my $status = Koha::Illbatch->status; >+ >+Return the status object associated with this batch >+ >+=cut >+ >+sub status { >+ my ( $self ) = @_; >+ return Koha::IllbatchStatus->_new_from_dbic( >+ scalar $self->_result->statuscode >+ ); >+} >+ >+=head3 patron >+ >+ my $patron = Koha::Illbatch->patron; >+ >+Return the patron object associated with this batch >+ >+=cut >+ >+sub patron { >+ my ( $self ) = @_; >+ return Koha::Patron->_new_from_dbic( >+ scalar $self->_result->borrowernumber >+ ); >+} >+ >+=head3 branch >+ >+ my $branch = Koha::Illbatch->branch; >+ >+Return the branch object associated with this batch >+ >+=cut >+ >+sub branch { >+ my ( $self ) = @_; >+ return Koha::Library->_new_from_dbic( >+ scalar $self->_result->branchcode >+ ); >+} >+ >+=head3 requests_count >+ >+ my $requests_count = Koha::Illbatch->requests_count; >+ >+Return the number of requests associated with this batch >+ >+=cut >+ >+sub requests_count { >+ my ( $self ) = @_; >+ return Koha::Illrequests->search({ >+ batch_id => $self->id >+ })->count; >+} >+ >+=head3 create_and_log >+ >+ $batch->create_and_log; >+ >+Log batch creation following storage >+ >+=cut >+ >+sub create_and_log { >+ my ( $self ) = @_; >+ >+ $self->store; >+ >+ my $logger = Koha::Illrequest::Logger->new; >+ >+ $logger->log_something({ >+ modulename => 'ILL', >+ actionname => 'batch_create', >+ objectnumber => $self->id, >+ infos => to_json({}) >+ }); >+} >+ >+=head3 update_and_log >+ >+ $batch->update_and_log; >+ >+Log batch update following storage >+ >+=cut >+ >+sub update_and_log { >+ my ( $self, $params ) = @_; >+ >+ my $before = { >+ name => $self->name, >+ branchcode => $self->branchcode >+ }; >+ >+ $self->set( $params ); >+ my $update = $self->store; >+ >+ my $after = { >+ name => $self->name, >+ branchcode => $self->branchcode >+ }; >+ >+ my $logger = Koha::Illrequest::Logger->new; >+ >+ $logger->log_something({ >+ modulename => 'ILL', >+ actionname => 'batch_update', >+ objectnumber => $self->id, >+ infos => to_json({ >+ before => $before, >+ after => $after >+ }) >+ }); >+} >+ >+=head3 delete_and_log >+ >+ $batch->delete_and_log; >+ >+Log batch delete >+ >+=cut >+ >+sub delete_and_log { >+ my ( $self ) = @_; >+ >+ my $logger = Koha::Illrequest::Logger->new; >+ >+ $logger->log_something({ >+ modulename => 'ILL', >+ actionname => 'batch_delete', >+ objectnumber => $self->id, >+ infos => to_json({}) >+ }); >+ >+ $self->delete; >+} >+ >+=head2 Internal methods >+ >+=head3 _type >+ >+ my $type = Koha::Illbatch->_type; >+ >+Return this object's type >+ >+=cut >+ >+sub _type { >+ return 'Illbatch'; >+} >+ >+=head1 AUTHOR >+ >+Andrew Isherwood <andrew.isherwood@ptfs-europe.com> >+ >+=cut >+ >+1; >diff --git a/Koha/Illbatches.pm b/Koha/Illbatches.pm >new file mode 100644 >index 0000000000..ee65a262d6 >--- /dev/null >+++ b/Koha/Illbatches.pm >@@ -0,0 +1,61 @@ >+package Koha::Illbatches; >+ >+# Copyright PTFS Europe 2022 >+# >+# 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, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+use Koha::Database; >+use Koha::Illbatch; >+use base qw(Koha::Objects); >+ >+=head1 NAME >+ >+Koha::Illbatches - Koha Illbatches Object class >+ >+=head2 Internal methods >+ >+=head3 _type >+ >+ my $type = Koha::Illbatches->_type; >+ >+Return this object's type >+ >+=cut >+ >+sub _type { >+ return 'Illbatch'; >+} >+ >+=head3 object_class >+ >+ my $class = Koha::Illbatches->object_class; >+ >+Return this object's class name >+ >+=cut >+ >+sub object_class { >+ return 'Koha::Illbatch'; >+} >+ >+=head1 AUTHOR >+ >+Andrew Isherwood <andrew.isherwood@ptfs-europe.com> >+ >+=cut >+ >+1; >diff --git a/Koha/REST/V1/Illbatches.pm b/Koha/REST/V1/Illbatches.pm >new file mode 100644 >index 0000000000..fbadb6ecd6 >--- /dev/null >+++ b/Koha/REST/V1/Illbatches.pm >@@ -0,0 +1,256 @@ >+package Koha::REST::V1::Illbatches; >+ >+# 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, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use Mojo::Base 'Mojolicious::Controller'; >+ >+use Koha::Illbatches; >+use Koha::IllbatchStatuses; >+use Koha::Illrequests; >+ >+use Try::Tiny qw( catch try ); >+ >+=head1 NAME >+ >+Koha::REST::V1::Illbatches >+ >+=head2 Operations >+ >+=head3 list >+ >+Return a list of available ILL batches >+ >+=cut >+ >+sub list { >+ my $c = shift->openapi->valid_input; >+ >+ #FIXME: This should be $c->objects-search >+ my @batches = Koha::Illbatches->search()->as_list; >+ >+ #FIXME: Below should be coming from $c->objects accessors >+ # Get all patrons associated with all our batches >+ # in one go >+ my $patrons = {}; >+ foreach my $batch(@batches) { >+ my $patron_id = $batch->borrowernumber; >+ $patrons->{$patron_id} = 1 >+ }; >+ my @patron_ids = keys %{$patrons}; >+ my $patron_results = Koha::Patrons->search({ >+ borrowernumber => { -in => \@patron_ids } >+ }); >+ >+ # Get all branches associated with all our batches >+ # in one go >+ my $branches = {}; >+ foreach my $batch(@batches) { >+ my $branch_id = $batch->branchcode; >+ $branches->{$branch_id} = 1 >+ }; >+ my @branchcodes = keys %{$branches}; >+ my $branch_results = Koha::Libraries->search({ >+ branchcode => { -in => \@branchcodes } >+ }); >+ >+ # Get all batch statuses associated with all our batches >+ # in one go >+ my $statuses = {}; >+ foreach my $batch(@batches) { >+ my $code = $batch->statuscode; >+ $statuses->{$code} = 1 >+ }; >+ my @statuscodes = keys %{$statuses}; >+ my $status_results = Koha::IllbatchStatuses->search({ >+ code => { -in => \@statuscodes } >+ }); >+ >+ # Populate the response >+ my @to_return = (); >+ foreach my $it_batch(@batches) { >+ my $patron = $patron_results->find({ borrowernumber => $it_batch->borrowernumber}); >+ my $branch = $branch_results->find({ branchcode => $it_batch->branchcode }); >+ my $status = $status_results->find({ code => $it_batch->statuscode }); >+ push @to_return, { >+ %{$it_batch->unblessed}, >+ patron => $patron, >+ branch => $branch, >+ status => $status, >+ requests_count => $it_batch->requests_count >+ }; >+ } >+ >+ return $c->render( status => 200, openapi => \@to_return ); >+} >+ >+=head3 get >+ >+Get one batch >+ >+=cut >+ >+sub get { >+ my $c = shift->openapi->valid_input; >+ >+ my $batchid = $c->validation->param('illbatch_id'); >+ >+ my $batch = Koha::Illbatches->find($batchid); >+ >+ if (not defined $batch) { >+ return $c->render( >+ status => 404, >+ openapi => { error => "ILL batch not found" } >+ ); >+ } >+ >+ return $c->render( >+ status => 200, >+ openapi => { >+ %{$batch->unblessed}, >+ patron => $batch->patron->unblessed, >+ branch => $batch->branch->unblessed, >+ status => $batch->status->unblessed, >+ requests_count => $batch->requests_count >+ } >+ ); >+} >+ >+=head3 add >+ >+Add a new batch >+ >+=cut >+ >+sub add { >+ my $c = shift->openapi->valid_input or return; >+ >+ my $body = $c->validation->param('body'); >+ >+ # We receive cardnumber, so we need to look up the corresponding >+ # borrowernumber >+ my $patron = Koha::Patrons->find({ cardnumber => $body->{cardnumber} }); >+ >+ if ( not defined $patron ) { >+ return $c->render( >+ status => 404, >+ openapi => { error => "Patron with cardnumber " . $body->{cardnumber} . " not found" } >+ ); >+ } >+ >+ delete $body->{cardnumber}; >+ $body->{borrowernumber} = $patron->borrowernumber; >+ >+ return try { >+ my $batch = Koha::Illbatch->new( $body ); >+ $batch->create_and_log; >+ $c->res->headers->location( $c->req->url->to_string . '/' . $batch->id ); >+ >+ my $ret = { >+ %{$batch->unblessed}, >+ patron => $batch->patron->unblessed, >+ branch => $batch->branch->unblessed, >+ status => $batch->status->unblessed, >+ requests_count => 0 >+ }; >+ >+ return $c->render( >+ status => 201, >+ openapi => $ret >+ ); >+ } >+ catch { >+ if ( blessed $_ ) { >+ if ( $_->isa('Koha::Exceptions::Object::DuplicateID') ) { >+ return $c->render( >+ status => 409, >+ openapi => { error => "A batch named " . $body->{name} . " already exists" } >+ ); >+ } >+ } >+ $c->unhandled_exception($_); >+ }; >+} >+ >+=head3 update >+ >+Update a batch >+ >+=cut >+ >+sub update { >+ my $c = shift->openapi->valid_input or return; >+ >+ my $batch = Koha::Illbatches->find( $c->validation->param('illbatch_id') ); >+ >+ if ( not defined $batch ) { >+ return $c->render( >+ status => 404, >+ openapi => { error => "ILL batch not found" } >+ ); >+ } >+ >+ my $params = $c->req->json; >+ delete $params->{cardnumber}; >+ >+ return try { >+ $batch->update_and_log( $params ); >+ >+ my $ret = { >+ %{$batch->unblessed}, >+ patron => $batch->patron->unblessed, >+ branch => $batch->branch->unblessed, >+ status => $batch->status->unblessed, >+ requests_count => $batch->requests_count >+ }; >+ >+ return $c->render( >+ status => 200, >+ openapi => $ret >+ ); >+ } >+ catch { >+ $c->unhandled_exception($_); >+ }; >+} >+ >+=head3 delete >+ >+Delete a batch >+ >+=cut >+ >+sub delete { >+ >+ my $c = shift->openapi->valid_input or return; >+ >+ my $batch = Koha::Illbatches->find( $c->validation->param( 'illbatch_id' ) ); >+ >+ if ( not defined $batch ) { >+ return $c->render( status => 404, openapi => { error => "ILL batch not found" } ); >+ } >+ >+ return try { >+ $batch->delete_and_log; >+ return $c->render( status => 204, openapi => ''); >+ } >+ catch { >+ $c->unhandled_exception($_); >+ }; >+} >+ >+1; >diff --git a/admin/columns_settings.yml b/admin/columns_settings.yml >index 90fb535472..892d3c3610 100644 >--- a/admin/columns_settings.yml >+++ b/admin/columns_settings.yml >@@ -825,6 +825,8 @@ modules: > columns: > - > columnname: ill_request_id >+ - >+ columnname: batch > - > columnname: metadata_author > - >diff --git a/api/v1/swagger/definitions/illbatch.yaml b/api/v1/swagger/definitions/illbatch.yaml >new file mode 100644 >index 0000000000..d3146a3303 >--- /dev/null >+++ b/api/v1/swagger/definitions/illbatch.yaml >@@ -0,0 +1,48 @@ >+--- >+type: object >+properties: >+ id: >+ type: string >+ description: Internal ILL batch identifier >+ name: >+ type: string >+ description: Name of the ILL batch >+ backend: >+ type: string >+ description: Backend name >+ cardnumber: >+ type: string >+ description: Card number of the patron of the ILL batch >+ borrowernumber: >+ type: string >+ description: Borrower number of the patron of the ILL batch >+ branchcode: >+ type: string >+ description: Branch code of the branch of the ILL batch >+ patron: >+ type: >+ - object >+ - "null" >+ description: The patron associated with the batch >+ branch: >+ type: >+ - object >+ - "null" >+ description: The branch associated with the batch >+ statuscode: >+ type: string >+ description: Code of the status of the ILL batch >+ status: >+ type: >+ - object >+ - "null" >+ description: The status associated with the batch >+ requests_count: >+ type: string >+ description: The number of requests in this batch >+additionalProperties: false >+required: >+ - name >+ - backend >+ - branchcode >+ - statuscode >\ No newline at end of file >diff --git a/api/v1/swagger/definitions/illbatches.yaml b/api/v1/swagger/definitions/illbatches.yaml >new file mode 100644 >index 0000000000..4db20f480d >--- /dev/null >+++ b/api/v1/swagger/definitions/illbatches.yaml >@@ -0,0 +1,5 @@ >+--- >+type: array >+items: >+ $ref: "illbatch.yaml" >+additionalProperties: false >diff --git a/api/v1/swagger/paths/illbatches.yaml b/api/v1/swagger/paths/illbatches.yaml >new file mode 100644 >index 0000000000..30a67355d8 >--- /dev/null >+++ b/api/v1/swagger/paths/illbatches.yaml >@@ -0,0 +1,236 @@ >+--- >+/illbatches: >+ get: >+ x-mojo-to: Illbatches#list >+ operationId: listIllbatches >+ tags: >+ - illbatches >+ summary: List ILL batches >+ parameters: [] >+ produces: >+ - application/json >+ responses: >+ "200": >+ description: A list of ILL batches >+ schema: >+ $ref: "../swagger.yaml#/definitions/illbatches" >+ "401": >+ description: Authentication required >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "403": >+ description: Access forbidden >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "404": >+ description: ILL batches not found >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "500": >+ description: | >+ Internal server error. Possible `error_code` attribute values: >+ >+ * `internal_server_error` >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "503": >+ description: Under maintenance >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ x-koha-authorization: >+ permissions: >+ ill: "1" >+ post: >+ x-mojo-to: Illbatches#add >+ operationId: addIllbatch >+ tags: >+ - illbatches >+ summary: Add ILL batch >+ parameters: >+ - name: body >+ in: body >+ description: A JSON object containing informations about the new batch >+ required: true >+ schema: >+ $ref: "../swagger.yaml#/definitions/illbatch" >+ produces: >+ - application/json >+ responses: >+ "201": >+ description: Batch added >+ schema: >+ $ref: "../swagger.yaml#/definitions/illbatch" >+ "400": >+ description: Bad request >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "401": >+ description: Authentication required >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "403": >+ description: Access forbidden >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "404": >+ description: Patron with given cardnumber not found >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "409": >+ description: Conflict in creating resource >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "500": >+ description: | >+ Internal server error. Possible `error_code` attribute values: >+ >+ * `internal_server_error` >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "503": >+ description: Under maintenance >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ x-koha-authorization: >+ permissions: >+ ill: "1" >+"/illbatches/{illbatch_id}": >+ get: >+ x-mojo-to: Illbatches#get >+ operationId: getIllbatches >+ tags: >+ - illbatches >+ summary: Get ILL batch >+ parameters: >+ - name: illbatch_id >+ in: path >+ description: ILL batch id/name/contents >+ required: true >+ type: string >+ produces: >+ - application/json >+ responses: >+ "200": >+ description: An ILL batch >+ schema: >+ $ref: "../swagger.yaml#/definitions/illbatch" >+ "401": >+ description: Authentication required >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "403": >+ description: Access forbidden >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "404": >+ description: ILL batch not found >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "500": >+ description: | >+ Internal server error. Possible `error_code` attribute values: >+ >+ * `internal_server_error` >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "503": >+ description: Under maintenance >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ x-koha-authorization: >+ permissions: >+ ill: "1" >+ put: >+ x-mojo-to: Illbatches#update >+ operationId: updateIllBatch >+ tags: >+ - illbatches >+ summary: Update batch >+ parameters: >+ - $ref: "../swagger.yaml#/parameters/illbatch_id_pp" >+ - name: body >+ in: body >+ description: A JSON object containing information on the batch >+ required: true >+ schema: >+ $ref: "../swagger.yaml#/definitions/illbatch" >+ consumes: >+ - application/json >+ produces: >+ - application/json >+ responses: >+ "200": >+ description: An ILL batch >+ schema: >+ $ref: "../swagger.yaml#/definitions/illbatch" >+ "400": >+ description: Bad request >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "401": >+ description: Authentication required >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "403": >+ description: Access forbidden >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "404": >+ description: ILL batch not found >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "500": >+ description: | >+ Internal server error. Possible `error_code` attribute values: >+ >+ * `internal_server_error` >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "503": >+ description: Under maintenance >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ x-koha-authorization: >+ permissions: >+ ill: "1" >+ delete: >+ x-mojo-to: Illbatches#delete >+ operationId: deleteBatch >+ tags: >+ - illbatches >+ summary: Delete ILL batch >+ parameters: >+ - $ref: "../swagger.yaml#/parameters/illbatch_id_pp" >+ produces: >+ - application/json >+ responses: >+ "204": >+ description: ILL batch deleted >+ schema: >+ type: string >+ "401": >+ description: Authentication required >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "403": >+ description: Access forbidden >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "404": >+ description: ILL batch not found >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "500": >+ description: | >+ Internal server error. Possible `error_code` attribute values: >+ >+ * `internal_server_error` >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "503": >+ description: Under maintenance >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ x-koha-authorization: >+ permissions: >+ ill: "1" >diff --git a/ill/ill-requests.pl b/ill/ill-requests.pl >index a549d9cd5b..c10bfa5caf 100755 >--- a/ill/ill-requests.pl >+++ b/ill/ill-requests.pl >@@ -27,10 +27,12 @@ use Koha::Notice::Templates; > use Koha::AuthorisedValues; > use Koha::Illcomment; > use Koha::Illrequests; >+use Koha::Illbatches; > use Koha::Illrequest::Workflow::Availability; > use Koha::Illrequest::Workflow::TypeDisclaimer; > use Koha::Libraries; > use Koha::Token; >+use Koha::Plugins; > > use Try::Tiny qw( catch try ); > use URI::Escape qw( uri_escape_utf8 ); >@@ -66,10 +68,27 @@ my $has_branch = $cfg->has_branch; > my $backends_available = ( scalar @{$backends} > 0 ); > $template->param( > backends_available => $backends_available, >- has_branch => $has_branch >+ has_branch => $has_branch, >+ have_batch => have_batch_backends($backends) > ); > > if ( $backends_available ) { >+ # Establish what metadata enrichment plugins we have available >+ my $enrichment_services = get_metadata_enrichment(); >+ if (scalar @{$enrichment_services} > 0) { >+ $template->param( >+ metadata_enrichment_services => encode_json($enrichment_services) >+ ); >+ } >+ # Establish whether we have any availability services that can provide availability >+ # for the batch identifier types we support >+ my $batch_availability_services = get_ill_availability($enrichment_services); >+ if (scalar @{$batch_availability_services} > 0) { >+ $template->param( >+ batch_availability_services => encode_json($batch_availability_services) >+ ); >+ } >+ > if ( $op eq 'illview' ) { > # View the details of an ILL > my $request = Koha::Illrequests->find($params->{illrequest_id}); >@@ -136,8 +155,8 @@ if ( $backends_available ) { > } > > $template->param( >- whole => $backend_result, >- request => $request >+ whole => $backend_result, >+ request => $request > ); > handle_commit_maybe($backend_result, $request); > } >@@ -203,6 +222,9 @@ if ( $backends_available ) { > # We simulate the API for backend requests for uniformity. > # So, init: > my $request = Koha::Illrequests->find($params->{illrequest_id}); >+ my $batches = Koha::Illbatches->search(undef, { >+ order_by => { -asc => 'name' } >+ }); > if ( !$params->{stage} ) { > my $backend_result = { > error => 0, >@@ -215,13 +237,15 @@ if ( $backends_available ) { > }; > $template->param( > whole => $backend_result, >- request => $request >+ request => $request, >+ batches => $batches > ); > } else { > # Commit: > # Save the changes > $request->borrowernumber($params->{borrowernumber}); > $request->biblio_id($params->{biblio_id}); >+ $request->batch_id($params->{batch_id}); > $request->branchcode($params->{branchcode}); > $request->price_paid($params->{price_paid}); > $request->notesopac($params->{notesopac}); >@@ -353,7 +377,7 @@ if ( $backends_available ) { > } elsif ( $op eq 'illlist') { > > # If we receive a pre-filter, make it available to the template >- my $possible_filters = ['borrowernumber']; >+ my $possible_filters = ['borrowernumber', 'batch_id']; > my $active_filters = {}; > foreach my $filter(@{$possible_filters}) { > if ($params->{$filter}) { >@@ -372,6 +396,17 @@ if ( $backends_available ) { > $template->param( > prefilters => join("&", @tpl_arr) > ); >+ >+ if ($active_filters->{batch_id}) { >+ my $batch_id = $active_filters->{batch_id}; >+ if ($batch_id) { >+ my $batch = Koha::Illbatches->find($batch_id); >+ $template->param( >+ batch => $batch >+ ); >+ } >+ } >+ > } elsif ( $op eq "save_comment" ) { > die "Wrong CSRF token" unless Koha::Token->new->check_csrf({ > session_id => scalar $cgi->cookie('CGISESSID'), >@@ -406,6 +441,10 @@ if ( $backends_available ) { > scalar $params->{illrequest_id} . $append > ); > exit; >+ } elsif ( $op eq "batch_list" ) { >+ # Do not remove, it prevents us falling through to the 'else' >+ } elsif ( $op eq "batch_create" ) { >+ # Do not remove, it prevents us falling through to the 'else' > } else { > my $request = Koha::Illrequests->find($params->{illrequest_id}); > my $backend_result = $request->custom_capability($op, $params); >@@ -463,3 +502,59 @@ sub redirect_to_list { > print $cgi->redirect('/cgi-bin/koha/ill/ill-requests.pl'); > exit; > } >+ >+# Do any of the available backends provide batch requesting >+sub have_batch_backends { >+ my ( $backends ) = @_; >+ >+ my @have_batch = (); >+ >+ foreach my $backend(@{$backends}) { >+ my $can_batch = can_batch($backend); >+ if ($can_batch) { >+ push @have_batch, $backend; >+ } >+ } >+ return \@have_batch; >+} >+ >+# Does a given backend provide batch requests >+# FIXME: This should be moved to Koha::Illbackend >+sub can_batch { >+ my ( $backend ) = @_; >+ my $request = Koha::Illrequest->new->load_backend( $backend ); >+ return $request->_backend_capability( 'provides_batch_requests' ); >+} >+ >+# Get available metadata enrichment plugins >+sub get_metadata_enrichment { >+ my @candidates = Koha::Plugins->new()->GetPlugins({ >+ method => 'provides_api' >+ }); >+ my @services = (); >+ foreach my $plugin(@candidates) { >+ my $supported = $plugin->provides_api(); >+ if ($supported->{type} eq 'search') { >+ push @services, $supported; >+ } >+ } >+ return \@services; >+} >+ >+# Get ILL availability plugins that can help us with the batch identifier types >+# we support >+sub get_ill_availability { >+ my ( $services ) = @_; >+ >+ my $id_types = {}; >+ foreach my $service(@{$services}) { >+ foreach my $id_supported(keys %{$service->{identifiers_supported}}) { >+ $id_types->{$id_supported} = 1; >+ } >+ } >+ >+ my $availability = Koha::Illrequest::Workflow::Availability->new($id_types); >+ return $availability->get_services({ >+ ui_context => 'staff' >+ }); >+} >diff --git a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >index abbc4a82b7..f1ae7fc983 100644 >--- a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >+++ b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >@@ -3729,6 +3729,112 @@ input.renew { > } > > #interlibraryloans { >+ >+ .ill-toolbar { >+ display: flex; >+ } >+ >+ #ill-batch { >+ flex-grow: 1; >+ display: flex; >+ justify-content: flex-end; >+ gap: 5px; >+ } >+ >+ #ill-batch-requests { >+ .action-buttons { >+ display: flex; >+ gap: 5px; >+ justify-content: center; >+ } >+ } >+ >+ #ill-batch-modal { >+ .modal-footer { >+ display: flex; >+ & > * { >+ flex: 1; >+ } >+ #lhs { >+ text-align: left; >+ } >+ } >+ #create-progress { >+ margin-top: 17px; >+ } >+ .fetch-failed { >+ background-color: rgba(255,0,0,0.1); >+ & > * { >+ background-color: inherit; >+ } >+ } >+ .progress { >+ margin-bottom: 0; >+ margin-top: 17px; >+ } >+ #create-requests { >+ display: flex; >+ justify-content: flex-end; >+ } >+ .action-column { >+ text-align: center; >+ & > * { >+ margin-left: 5px; >+ } >+ & > *:first-child { >+ margin-left: 0; >+ } >+ } >+ .metadata-row:not(:first-child) { >+ margin-top: 0.5em; >+ } >+ .metadata-label { >+ font-weight: 600; >+ } >+ .more-less { >+ text-align: right; >+ margin: 2px 0; >+ } >+ >+ } >+ >+ #batch-form { >+ legend { >+ margin-bottom: 2em; >+ } >+ textarea { >+ width: 100%; >+ min-height: 100px; >+ padding: 5px; >+ resize: vertical; >+ } >+ #new-batch-form { >+ display: flex; >+ gap: 20px; >+ } >+ li#process-button { >+ display: flex; >+ justify-content: flex-end; >+ } >+ #textarea-metadata { >+ padding: 0 15px; >+ display: flex; >+ justify-content: space-between; >+ } >+ #textarea-errors { >+ display: flex; >+ flex-direction: column; >+ gap: 10px; >+ padding: 20px 15px 10px >+ } >+ .batch-modal-actions { >+ text-align: center; >+ } >+ fieldset { >+ border: 2px solid #b9d8d9; >+ } >+ } >+ > #dataPreviewLabel { > margin: .3em 0; > } >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal-strings.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal-strings.inc >new file mode 100644 >index 0000000000..69737a6259 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal-strings.inc >@@ -0,0 +1,38 @@ >+<!-- ill-batch-table-strings.inc --> >+<script> >+ var ill_batch_add = _("Add new batch"); >+ var ill_batch_update = _("Update batch"); >+ var ill_batch_none = _("None"); >+ var ill_batch_retrieving_metadata = _("Retrieving metadata"); >+ var ill_batch_api_fail = _("Unable to retrieve batch details"); >+ var ill_batch_statuses_api_fail = _("Unable to retrieve batch statuses"); >+ var ill_batch_api_request_fail = _("Unable to create local request"); >+ var ill_batch_requests_api_fail = _("Unable to retrieve batch requests"); >+ var ill_batch_unknown = _("Unknown"); >+ var ill_batch_doi = _("DOI"); >+ var ill_batch_pmid = _("PubMed ID"); >+ var ill_populate_waiting = _("Retrieving..."); >+ var ill_populate_failed = _("Failed to retrieve"); >+ var ill_button_remove = _("Remove"); >+ var ill_batch_create_api_fail = _("Unable to create batch request"); >+ var ill_batch_update_api_fail = _("Unable to updatecreate batch request"); >+ var ill_batch_item_remove = _("Are you sure you want to remove this item from the batch"); >+ var ill_batch_create_cancel_button = _("Close"); >+ var ill_batch_metadata_more = _("More"); >+ var ill_batch_metadata_less = _("Less"); >+ var ill_batch_available_via = _("Available via"); >+ var ill_batch_metadata = { >+ 'doi': _("DOI"), >+ 'pmid': _("PubMed ID"), >+ 'issn': _("ISSN"), >+ 'title': _("Title"), >+ 'year': _("Year"), >+ 'issue': _("Issue"), >+ 'pages': _("Pages"), >+ 'publisher': _("Publisher"), >+ 'article_title': _("Article title"), >+ 'article_author': _("Article author"), >+ 'volume': _("Volume") >+ }; >+</script> >+<!-- / ill-batch-table-strings.inc --> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal.inc >new file mode 100644 >index 0000000000..72903bb48d >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal.inc >@@ -0,0 +1,97 @@ >+<div id="ill-batch-details" style="display:none"></div> >+<div id="ill-batch-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="ill-batch-modal-label" aria-hidden="true"> >+ <div class="modal-dialog modal-lg"> >+ <div class="modal-content"> >+ <div class="modal-header"> >+ <button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">Ã</button> >+ <h3 id="ill-batch-modal-label"></h3> >+ </div> >+ <div class="modal-body"> >+ <div id="batch-form"> >+ <form id="new-batch-form"> >+ <fieldset class="rows"> >+ <legend id="legend">Batch details</legend> >+ <ol> >+ <li id="batch_name"> >+ <label class="required" for="name">Batch name:</label> >+ <input type="text" autocomplete="off" name="name" id="name" type="text" >+ value="" /> >+ </li> >+ <li id="batch_patron"> >+ <label class="required" for="batchcardnumber">Card number, username or surname:</label> >+ <input type="text" autocomplete="off" name="batchcardnumber" id="batchcardnumber" type="text" value="" /> >+ <span id="patron_link"></span> >+ </li> >+ <li id="batch_branch"> >+ <label class="required" for="branchcode">Library:</label> >+ <select id="branchcode" name="branchcode"> >+ [% PROCESS options_for_libraries libraries => Branches.all( selected => branchcode ) %] >+ </select> >+ </li> >+ <li id="batch_statuscode" style="display:none"> >+ <label class="required" for="statuscode">Status:</label> >+ <select id="statuscode" name="statuscode"></select> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset id="add_batch_items" class="rows" style="display:none"> >+ <legend id="legend">Add batch items</legend> >+ <div id="textarea-metadata"> >+ <div id="supported">Supported identifiers: <span id="supported_identifiers"></span></div> >+ <div id="row_count">Row count: <span id="row_count_value"></span></div> >+ </div> >+ <div id="textarea-errors" style="display:none" class="error"> >+ <div id="duplicates" style="display:none">The following duplicates were found, these have been de-duplicated: <span id="dupelist"></span></div> >+ <div id="badidentifiers" style="display:none">The following unknown identifiers were found, it was not possible to establish their type: <span id="badids"></span></div> >+ </div> >+ <ol> >+ <li> >+ <textarea id="identifiers_input" placeholder="Enter identifiers, one per line"></textarea> >+ </li> >+ <li id="process-button"> >+ <button id="process_button" disabled aria-disabled="true" type="button">Process identifiers</button> >+ </li> >+ </ol> >+ </fieldset> >+ </form> >+ <div id="create-progress" class="alert alert-info" role="alert" style="display:none"> >+ <span id="progress-label"><strong></strong></span> - >+ Items processed: <span id="processed_count">0</span> out of <span id="processed_total">0</span>. >+ Items failed: <span id="processed_failed">0</span>. >+ <div class="progress"> >+ <div id="processed_progress_bar" class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="min-width: 2em;"> >+ 0% >+ </div> >+ </div> >+ </div> >+ <table id="identifier-table" style="display:none"> >+ <thead> >+ <tr id="identifier-table-header"> >+ <th scope="col">Identifier type</th> >+ <th scope="col">Identifier value</th> >+ <th scope="col">Metadata</th> >+ <th scope="col">Request ID</th> >+ <th scope="col">Request Status</th> >+ <th scope="col"></th> >+ </tr> >+ </thead> >+ <tbody id="identifier-table-body"> >+ </tbody> >+ </table> >+ </div> >+ <div id="create-requests" style="display:none"> >+ <button id="create-requests-button" type="button" class="btn btn-primary" aria-label="Add items to batch">Add items to batch</button> >+ </div> >+ </div> >+ <div class="modal-footer"> >+ <div id="lhs"> >+ <button class="btn btn-default" data-dismiss="modal" aria-hidden="false">Cancel</button> >+ </div> >+ <div id="rhs"> >+ <button id="button_create_batch" class="btn btn-default" aria-hidden="true" disabled>Continue</button> >+ <button id="button_finish" disabled type="button" class="btn btn-default" aria-hidden="true">Finish and view batch</button> >+ </div> >+ </div> >+ </div> >+ </div> >+</div> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-table-strings.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-table-strings.inc >new file mode 100644 >index 0000000000..2a8d4051e5 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-table-strings.inc >@@ -0,0 +1,9 @@ >+<!-- ill-batch-table-strings.inc --> >+<script> >+ var ill_batch_borrower_details = _("View borrower details"); >+ var ill_batch_edit = _("Edit"); >+ var ill_batch_delete = _("Delete"); >+ var ill_batch_confirm_delete = _("Are you sure you want to delete this batch? All attached requests will be detached."); >+ var ill_batch_delete_fail = _("Unable to delete batch"); >+</script> >+<!-- / ill-batch-table-strings.inc --> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch.inc >new file mode 100644 >index 0000000000..8c0ca21e74 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch.inc >@@ -0,0 +1,19 @@ >+[% IF query_type == "batch_list" %] >+<div> >+ <table id="ill-batch-requests"> >+ <thead> >+ <tr id="ill-batch-header"> >+ <th scope="col">Batch ID</th> >+ <th scope="col">Name</th> >+ <th scope="col">Number of requests</th> >+ <th scope="col">Status</th> >+ <th scope="col">Patron</th> >+ <th scope="col">Branch</th> >+ <th scope="col"></th> >+ </tr> >+ </thead> >+ <tbody id="ill-batch-body"> >+ </tbody> >+ </table> >+</div> >+[% END %] >\ No newline at end of file >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-list-table-strings.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-list-table-strings.inc >index f4327f40eb..bf1ef077f9 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-list-table-strings.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-list-table-strings.inc >@@ -1,5 +1,6 @@ > <!-- ill-list-table-strings.inc --> > <script> >+ var ill_borrower_details = _("View patron details"); > var ill_manage = _("Manage request"); > var ill_manage_select_backend_first = _("Select a backend first"); > var ill_all_statuses = _("All statuses"); >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-list-table.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-list-table.inc >index 910f3a4f2a..0ffe57d4dd 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-list-table.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-list-table.inc >@@ -1,11 +1,14 @@ > [% IF patron.borrowernumber %] > <table id="ill-requests-patron-[% patron.borrowernumber | html %]"> >+[% ELSIF batch.id %] >+<table id="ill-requests-batch-[% batch.id | html %]"> > [% ELSE %] > <table id="ill-requests"> > [% END %] > <thead> > <tr id="ill_requests_header"> > <th scope="col">Request ID</th> >+ <th scope="col">Batch</th> > <th scope="col" data-datatype="related-object" data-related="extended_attributes" data-related-key="type" data-related-value="author" data-related-search-on="value">Author</th> > <th scope="col" data-datatype="related-object" data-related="extended_attributes" data-related-key="type" data-related-value="title" data-related-search-on="value">Title</th> > <th scope="col" data-datatype="related-object" data-related="extended_attributes" data-related-key="type" data-related-value="article_title" data-related-search-on="value">Article title</th> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc >index 03fec59ce1..cbb6bfd13e 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc >@@ -1,6 +1,6 @@ > [% USE Koha %] >-[% IF Koha.Preference('ILLModule ') && CAN_user_ill %] >- <div id="toolbar" class="btn-toolbar"> >+[% IF Koha.Preference('ILLModule') && CAN_user_ill %] >+ <div id="toolbar" class="btn-toolbar ill-toolbar"> > [% IF backends_available %] > [% IF backends.size > 1 %] > <div class="dropdown btn-group"> >@@ -32,5 +32,22 @@ > <i class="fa fa-list"></i> List requests > </a> > [% END %] >+ [% IF have_batch.size > 0 && metadata_enrichment_services %] >+ <div id="ill-batch"> >+ <div class="dropdown btn-group"> >+ <button class="btn btn-default dropdown-toggle" type="button" id="ill-batch-backend-dropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> >+ <i class="fa fa-plus"></i> New ILL batch request <span class="caret"></span> >+ </button> >+ <ul class="dropdown-menu" aria-labelledby="ill-batch-backend-dropdown"> >+ [% FOREACH backend IN have_batch %] >+ <li><a href="#" role="button" onclick="window.openBatchModal(null, '[% backend | html %]')">[% backend | html %]</a></li> >+ [% END %] >+ </ul> >+ </div> >+ <a class="btn btn-default" type="button" href="/cgi-bin/koha/ill/ill-requests.pl?method=batch_list" %]"> >+ <i class="fa fa-tasks"></i> Batch requests</span> >+ </a> >+ </div> >+ [% END %] > </div> > [% END %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt >index 95bcd61909..e33c0f26a7 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt >@@ -121,6 +121,7 @@ > <div class="dialog message">ILL module configuration problem. Take a look at the <a href="/cgi-bin/koha/about.pl#sysinfo">about page</a></div> > [% ELSE %] > [% INCLUDE 'ill-toolbar.inc' %] >+ [% INCLUDE 'ill-batch-modal.inc' %] > > [% IF whole.error %] > <h1>Error performing operation</h1> >@@ -434,6 +435,23 @@ > [% END %] > </select> > </li> >+ [% IF batches.count > 0 %] >+ <li class="batch"> >+ <label class="batch_label">Batch:</label> >+ <select id="batch_id" name="batch_id"> >+ <option value=""> >+ [% FOREACH batch IN batches %] >+ [% IF batch.id == request.batch_id %] >+ <option value="[% batch.id | html %]" selected> >+ [% ELSE %] >+ <option value="[% batch.id | html %]"> >+ [% END %] >+ [% batch.name | html %] >+ </option> >+ [% END %] >+ </select> >+ </li> >+ [% END %] > <li class="updated"> > <label class="updated">Last updated:</label> > [% request.updated | $KohaDates with_hours => 1 %] >@@ -645,6 +663,14 @@ > [% END %] > [% END %] > </li> >+ [% IF request.batch > 0 %] >+ <li class="batch"> >+ <span class="label batch">Batch:</span> >+ <a href="/cgi-bin/koha/ill/ill-requests.pl?batch_id=[% request.batch.id | html %]"> >+ [% request.batch.name | html %] >+ </a> >+ </li> >+ [% END %] > <li class="updated"> > <span class="label updated">Last updated:</span> > [% request.updated | $KohaDates with_hours => 1 %] >@@ -776,7 +802,12 @@ > > [% ELSIF query_type == 'illlist' %] > <!-- illlist --> >- <h1>View ILL requests</h1> >+ <h1> >+ View ILL requests >+ [% IF batch %] >+ for batch "[% batch.name | html %]" >+ [% END %] >+ </h1> > <div id="results" class="page-section"> > <h2>Details for all requests</h2> > [% INCLUDE 'ill-list-table.inc' %] >@@ -855,6 +886,8 @@ > </fieldset> > </form> > </div> >+ [% ELSIF query_type == 'batch_list' || query_type == 'batch_create' %] >+ [% INCLUDE 'ill-batch.inc' %] > [% ELSE %] > <!-- Custom Backend Action --> > [% PROCESS $whole.template %] >@@ -874,6 +907,18 @@ > [% INCLUDE 'columns_settings.inc' %] > [% INCLUDE 'calendar.inc' %] > [% INCLUDE 'select2.inc' %] >+ [% IF metadata_enrichment_services %] >+ <script> >+ var metadata_enrichment_services = [% metadata_enrichment_services | $raw %]; >+ </script> >+ <script> >+ [% IF batch_availability_services %] >+ var batch_availability_services = [% batch_availability_services | $raw %]; >+ [% ELSE %] >+ var batch_availability_services = []; >+ [% END %] >+ </script> >+ [% END %] > <script> > var prefilters = '[% prefilters | $raw %]'; > // Set column settings >@@ -898,7 +943,12 @@ > }); > </script> > [% INCLUDE 'ill-list-table-strings.inc' %] >+ [% INCLUDE 'ill-batch-table-strings.inc' %] >+ [% INCLUDE 'ill-batch-modal-strings.inc' %] > [% Asset.js("js/ill-list-table.js") | $raw %] >+ [% Asset.js("js/ill-batch.js") | $raw %] >+ [% Asset.js("js/ill-batch-table.js") | $raw %] >+ [% Asset.js("js/ill-batch-modal.js") | $raw %] > [% IF (query_type == 'availability' || query_type == 'generic_confirm') && Koha.Preference('ILLCheckAvailability') %] > [% Asset.js("js/ill-availability.js") | $raw %] > [% END %] >diff --git a/koha-tmpl/intranet-tmpl/prog/js/ill-batch-modal.js b/koha-tmpl/intranet-tmpl/prog/js/ill-batch-modal.js >new file mode 100644 >index 0000000000..9a913a9ee2 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/ill-batch-modal.js >@@ -0,0 +1,1089 @@ >+(function () { >+ // Bail if there aren't any metadata enrichment plugins installed >+ if (typeof metadata_enrichment_services === 'undefined') { >+ console.log('No metadata enrichment plugins found.') >+ return; >+ } >+ >+ window.addEventListener('load', onload); >+ >+ // Delay between API requests >+ var debounceDelay = 1000; >+ >+ // Elements we work frequently with >+ var textarea = document.getElementById("identifiers_input"); >+ var nameInput = document.getElementById("name"); >+ var cardnumberInput = document.getElementById("batchcardnumber"); >+ var branchcodeSelect = document.getElementById("branchcode"); >+ var processButton = document.getElementById("process_button"); >+ var createButton = document.getElementById("button_create_batch"); >+ var finishButton = document.getElementById("button_finish"); >+ var batchItemsDisplay = document.getElementById("add_batch_items"); >+ var createProgressTotal = document.getElementById("processed_total"); >+ var createProgressCount = document.getElementById("processed_count"); >+ var createProgressFailed = document.getElementById("processed_failed"); >+ var createProgressBar = document.getElementById("processed_progress_bar"); >+ var identifierTable = document.getElementById('identifier-table'); >+ var createRequestsButton = document.getElementById('create-requests-button'); >+ var statusesSelect = document.getElementById('statuscode'); >+ var cancelButton = document.getElementById('lhs').querySelector('button'); >+ var cancelButtonOriginalText = cancelButton.innerHTML; >+ >+ // We need a data structure keyed on identifier type, which tells us how to parse that >+ // identifier type and what services can get its metadata. We receive an array of >+ // available services >+ var supportedIdentifiers = {}; >+ metadata_enrichment_services.forEach(function (service) { >+ // Iterate the identifiers that this service supports >+ Object.keys(service.identifiers_supported).forEach(function (idType) { >+ if (!supportedIdentifiers[idType]) { >+ supportedIdentifiers[idType] = []; >+ } >+ supportedIdentifiers[idType].push(service); >+ }); >+ }); >+ >+ // An object for when we're creating a new batch >+ var emptyBatch = { >+ name: '', >+ backend: null, >+ cardnumber: '', >+ branchcode: '', >+ statuscode: 'NEW' >+ }; >+ >+ // The object that holds the batch we're working with >+ // It's a proxy so we can update portions of the UI >+ // upon changes >+ var batch = new Proxy( >+ { data: {} }, >+ { >+ get: function (obj, prop) { >+ return obj[prop]; >+ }, >+ set: function (obj, prop, value) { >+ obj[prop] = value; >+ manageBatchItemsDisplay(); >+ updateBatchInputs(); >+ disableCardnumberInput(); >+ displayPatronName(); >+ updateStatusesSelect(); >+ } >+ } >+ ); >+ >+ // The object that holds the contents of the table >+ // It's a proxy so we can make it automatically redraw the >+ // table upon changes >+ var tableContent = new Proxy( >+ { data: [] }, >+ { >+ get: function (obj, prop) { >+ return obj[prop]; >+ }, >+ set: function (obj, prop, value) { >+ obj[prop] = value; >+ updateTable(); >+ updateRowCount(); >+ updateProcessTotals(); >+ checkAvailability(); >+ } >+ } >+ ); >+ >+ // The object that holds the contents of the table >+ // It's a proxy so we can update portions of the UI >+ // upon changes >+ var statuses = new Proxy( >+ { data: [] }, >+ { >+ get: function (obj, prop) { >+ return obj[prop]; >+ }, >+ set: function (obj, prop, value) { >+ obj[prop] = value; >+ updateStatusesSelect(); >+ } >+ } >+ ); >+ >+ var progressTotals = new Proxy( >+ { >+ data: {} >+ }, >+ { >+ get: function (obj, prop) { >+ return obj[prop]; >+ }, >+ set: function (obj, prop, value) { >+ obj[prop] = value; >+ showCreateRequestsButton(); >+ } >+ } >+ ); >+ >+ // Keep track of submission API calls that are in progress >+ // so we don't duplicate them >+ var submissionSent = {}; >+ >+ // Keep track of availability API calls that are in progress >+ // so we don't duplicate them >+ var availabilitySent = {}; >+ >+ // Are we updating an existing batch >+ var isUpdate = false; >+ >+ // The datatable >+ var table; >+ var tableEl = document.getElementById('identifier-table'); >+ >+ // The element that potentially holds the ID of the batch >+ // we're working with >+ var idEl = document.getElementById('ill-batch-details'); >+ var batchId = null; >+ var backend = null; >+ >+ function onload() { >+ $('#ill-batch-modal').on('shown.bs.modal', function () { >+ init(); >+ patronAutocomplete(); >+ batchInputsEventListeners(); >+ createButtonEventListener(); >+ createRequestsButtonEventListener(); >+ moreLessEventListener(); >+ removeRowEventListener(); >+ }); >+ $('#ill-batch-modal').on('hidden.bs.modal', function () { >+ // Reset our state when we close the modal >+ // TODO: need to also reset progress bar and already processed identifiers >+ delete idEl.dataset.batchId; >+ delete idEl.dataset.backend; >+ batchId = null; >+ tableEl.style.display = 'none'; >+ tableContent.data = []; >+ progressTotals.data = { >+ total: 0, >+ count: 0, >+ failed: 0 >+ }; >+ textarea.value = ''; >+ batch.data = {}; >+ cancelButton.innerHTML = cancelButtonOriginalText; >+ // Remove event listeners we created >+ removeEventListeners(); >+ }); >+ }; >+ >+ function init() { >+ batchId = idEl.dataset.batchId; >+ backend = idEl.dataset.backend; >+ emptyBatch.backend = backend; >+ progressTotals.data = { >+ total: 0, >+ count: 0, >+ failed: 0 >+ }; >+ if (batchId) { >+ fetchBatch(); >+ isUpdate = true; >+ setModalHeading(); >+ } else { >+ batch.data = emptyBatch; >+ setModalHeading(); >+ } >+ fetchStatuses(); >+ finishButtonEventListener(); >+ processButtonEventListener(); >+ identifierTextareaEventListener(); >+ displaySupportedIdentifiers(); >+ createButtonEventListener(); >+ updateRowCount(); >+ }; >+ >+ function initPostCreate() { >+ disableCreateButton(); >+ cancelButton.innerHTML = ill_batch_create_cancel_button; >+ }; >+ >+ function setFinishButton() { >+ if (batch.data.patron) { >+ finishButton.removeAttribute('disabled'); >+ } >+ }; >+ >+ function setModalHeading() { >+ var heading = document.getElementById('ill-batch-modal-label'); >+ heading.textContent = isUpdate ? ill_batch_update : ill_batch_add; >+ } >+ >+ // Identify items that have metadata and therefore can have a local request >+ // created, and do so >+ function requestRequestable() { >+ createRequestsButton.setAttribute('disabled', true); >+ createRequestsButton.setAttribute('aria-disabled', true); >+ setFinishButton(); >+ var toCheck = tableContent.data; >+ toCheck.forEach(function (row) { >+ if ( >+ !row.requestId && >+ Object.keys(row.metadata).length > 0 && >+ !submissionSent[row.value] >+ ) { >+ submissionSent[row.value] = 1; >+ makeLocalSubmission(row.value, row.metadata); >+ } >+ }); >+ }; >+ >+ // Identify items that can have their availability checked, and do it >+ function checkAvailability() { >+ // Only proceed if we've got services that can check availability >+ if (!batch_availability_services || batch_availability_services.length === 0) return; >+ var toCheck = tableContent.data; >+ toCheck.forEach(function (row) { >+ if ( >+ !row.url && >+ Object.keys(row.metadata).length > 0 && >+ !availabilitySent[row.value] >+ ) { >+ availabilitySent[row.value] = 1; >+ getAvailability(row.value, row.metadata); >+ } >+ }); >+ }; >+ >+ // Check availability services for immediate availability, if found, >+ // create a link in the table linking to the item >+ function getAvailability(identifier, metadata) { >+ // Prep the metadata for passing to the availability plugins >+ let availability_object = {}; >+ if (metadata.issn) availability_object['issn'] = metadata.issn; >+ if (metadata.doi) availability_object['doi'] = metadata.doi; >+ if (metadata.pubmedid) availability_object['pubmedid'] = metadata.pubmedid; >+ var prepped = encodeURIComponent(base64EncodeUnicode(JSON.stringify(availability_object))); >+ for (i = 0; i < batch_availability_services.length; i++) { >+ var service = batch_availability_services[i]; >+ window.doApiRequest( >+ service.endpoint + prepped >+ ) >+ .then(function (response) { >+ return response.json(); >+ }) >+ .then(function (data) { >+ if (data.results.search_results && data.results.search_results.length > 0) { >+ var result = data.results.search_results[0]; >+ tableContent.data = tableContent.data.map(function (row) { >+ if (row.value === identifier) { >+ row.url = result.url; >+ row.availabilitySupplier = service.name; >+ } >+ return row; >+ }); >+ } >+ }); >+ } >+ }; >+ >+ // Help btoa with > 8 bit strings >+ // Shamelessly grabbed from: https://www.base64encoder.io/javascript/ >+ function base64EncodeUnicode(str) { >+ // First we escape the string using encodeURIComponent to get the UTF-8 encoding of the characters, >+ // then we convert the percent encodings into raw bytes, and finally feed it to btoa() function. >+ utf8Bytes = encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) { >+ return String.fromCharCode('0x' + p1); >+ }); >+ >+ return btoa(utf8Bytes); >+ }; >+ >+ // Create a local submission and update our local state >+ // upon success >+ function makeLocalSubmission(identifier, metadata) { >+ >+ // Prepare extended_attributes in array format for POST >+ var extended_attributes = []; >+ for (const [key, value] of Object.entries(metadata)) { >+ extended_attributes.push({"type":key, "value":value}); >+ } >+ >+ var payload = { >+ batch_id: batchId, >+ ill_backend_id: batch.data.backend, >+ patron_id: batch.data.patron.borrowernumber, >+ library_id: batch.data.branchcode, >+ extended_attributes: extended_attributes >+ }; >+ window.doCreateSubmission(payload) >+ .then(function (response) { >+ return response.json(); >+ }) >+ .then(function (data) { >+ tableContent.data = tableContent.data.map(function (row) { >+ if (row.value === identifier) { >+ row.requestId = data.ill_request_id; >+ row.requestStatus = data.status; >+ } >+ return row; >+ }); >+ }) >+ .catch(function () { >+ window.handleApiError(ill_batch_api_request_fail); >+ }); >+ }; >+ >+ function updateProcessTotals() { >+ var init = { >+ total: 0, >+ count: 0, >+ failed: 0 >+ }; >+ progressTotals.data = init; >+ var toUpdate = progressTotals.data; >+ tableContent.data.forEach(function (row) { >+ toUpdate.total++; >+ if (Object.keys(row.metadata).length > 0 || row.failed.length > 0) { >+ toUpdate.count++; >+ } >+ if (Object.keys(row.failed).length > 0) { >+ toUpdate.failed++; >+ } >+ }); >+ createProgressTotal.innerHTML = toUpdate.total; >+ createProgressCount.innerHTML = toUpdate.count; >+ createProgressFailed.innerHTML = toUpdate.failed; >+ var percentDone = Math.ceil((toUpdate.count / toUpdate.total) * 100); >+ createProgressBar.setAttribute('aria-valuenow', percentDone); >+ createProgressBar.innerHTML = percentDone + '%'; >+ createProgressBar.style.width = percentDone + '%'; >+ progressTotals.data = toUpdate; >+ }; >+ >+ function displayPatronName() { >+ var span = document.getElementById('patron_link'); >+ if (batch.data.patron) { >+ var link = createPatronLink(); >+ span.appendChild(link); >+ } else { >+ if (span.children.length > 0) { >+ span.removeChild(span.firstChild); >+ } >+ } >+ }; >+ >+ function updateStatusesSelect() { >+ while (statusesSelect.options.length > 0) { >+ statusesSelect.remove(0); >+ } >+ statuses.data.forEach(function (status) { >+ var option = document.createElement('option') >+ option.value = status.code; >+ option.text = status.name; >+ if (batch.data.id && batch.data.statuscode === status.code) { >+ option.selected = true; >+ } >+ statusesSelect.add(option); >+ }); >+ if (isUpdate) { >+ statusesSelect.parentElement.style.display = 'block'; >+ } >+ }; >+ >+ function removeEventListeners() { >+ textarea.removeEventListener('paste', processButtonState); >+ textarea.removeEventListener('keyup', processButtonState); >+ processButton.removeEventListener('click', processIdentifiers); >+ nameInput.removeEventListener('keyup', createButtonState); >+ cardnumberInput.removeEventListener('keyup', createButtonState); >+ branchcodeSelect.removeEventListener('change', createButtonState); >+ createButton.removeEventListener('click', createBatch); >+ identifierTable.removeEventListener('click', toggleMetadata); >+ identifierTable.removeEventListener('click', removeRow); >+ createRequestsButton.remove('click', requestRequestable); >+ }; >+ >+ function finishButtonEventListener() { >+ finishButton.addEventListener('click', doFinish); >+ }; >+ >+ function identifierTextareaEventListener() { >+ textarea.addEventListener('paste', textareaUpdate); >+ textarea.addEventListener('keyup', textareaUpdate); >+ }; >+ >+ function processButtonEventListener() { >+ processButton.addEventListener('click', processIdentifiers); >+ }; >+ >+ function createRequestsButtonEventListener() { >+ createRequestsButton.addEventListener('click', requestRequestable); >+ }; >+ >+ function createButtonEventListener() { >+ createButton.addEventListener('click', createBatch); >+ }; >+ >+ function batchInputsEventListeners() { >+ nameInput.addEventListener('keyup', createButtonState); >+ cardnumberInput.addEventListener('keyup', createButtonState); >+ branchcodeSelect.addEventListener('change', createButtonState); >+ }; >+ >+ function moreLessEventListener() { >+ identifierTable.addEventListener('click', toggleMetadata); >+ }; >+ >+ function removeRowEventListener() { >+ identifierTable.addEventListener('click', removeRow); >+ }; >+ >+ function textareaUpdate() { >+ processButtonState(); >+ updateRowCount(); >+ }; >+ >+ function processButtonState() { >+ if (textarea.value.length > 0) { >+ processButton.removeAttribute('disabled'); >+ processButton.removeAttribute('aria-disabled'); >+ } else { >+ processButton.setAttribute('disabled', true); >+ processButton.setAttribute('aria-disabled', true); >+ } >+ }; >+ >+ function disableCardnumberInput() { >+ if (batch.data.patron) { >+ cardnumberInput.setAttribute('disabled', true); >+ cardnumberInput.setAttribute('aria-disabled', true); >+ } else { >+ cardnumberInput.removeAttribute('disabled'); >+ cardnumberInput.removeAttribute('aria-disabled'); >+ } >+ }; >+ >+ function createButtonState() { >+ if ( >+ nameInput.value.length > 0 && >+ cardnumberInput.value.length > 0 && >+ branchcodeSelect.selectedOptions.length === 1 >+ ) { >+ createButton.removeAttribute('disabled'); >+ createButton.setAttribute('display', 'inline-block'); >+ } else { >+ createButton.setAttribute('disabled', 1); >+ createButton.setAttribute('display', 'none'); >+ } >+ }; >+ >+ function doFinish() { >+ updateBatch() >+ .then(function () { >+ $('#ill-batch-modal').modal({ show: false }); >+ location.href = '/cgi-bin/koha/ill/ill-requests.pl?batch_id=' + batch.data.id; >+ }); >+ }; >+ >+ // Get all batch statuses >+ function fetchStatuses() { >+ window.doApiRequest('/api/v1/illbatchstatuses') >+ .then(function (response) { >+ return response.json(); >+ }) >+ .then(function (jsoned) { >+ statuses.data = jsoned; >+ }) >+ .catch(function (e) { >+ window.handleApiError(ill_batch_statuses_api_fail); >+ }); >+ }; >+ >+ // Get the batch >+ function fetchBatch() { >+ window.doBatchApiRequest("/" + batchId) >+ .then(function (response) { >+ return response.json(); >+ }) >+ .then(function (jsoned) { >+ batch.data = { >+ id: jsoned.id, >+ name: jsoned.name, >+ backend: jsoned.backend, >+ cardnumber: jsoned.cardnumber, >+ branchcode: jsoned.branchcode, >+ statuscode: jsoned.statuscode >+ } >+ return jsoned; >+ }) >+ .then(function (data) { >+ batch.data = data; >+ }) >+ .catch(function () { >+ window.handleApiError(ill_batch_api_fail); >+ }); >+ }; >+ >+ function createBatch() { >+ var selectedBranchcode = branchcodeSelect.selectedOptions[0].value; >+ var selectedStatuscode = statusesSelect.selectedOptions[0].value; >+ return doBatchApiRequest('', { >+ method: 'POST', >+ headers: { >+ 'Content-type': 'application/json' >+ }, >+ body: JSON.stringify({ >+ name: nameInput.value, >+ backend: backend, >+ cardnumber: cardnumberInput.value, >+ branchcode: selectedBranchcode, >+ statuscode: selectedStatuscode >+ }) >+ }) >+ .then(function (response) { >+ if ( response.ok ) { >+ return response.json(); >+ } >+ return Promise.reject(response); >+ }) >+ .then(function (body) { >+ batchId = body.id; >+ batch.data = { >+ id: body.id, >+ name: body.name, >+ backend: body.backend, >+ cardnumber: body.patron.cardnumber, >+ branchcode: body.branchcode, >+ statuscode: body.statuscode, >+ patron: body.patron, >+ status: body.status >+ }; >+ initPostCreate(); >+ }) >+ .catch(function (response) { >+ response.json().then((json) => { >+ if( json.error ) { >+ handleApiError(json.error); >+ } else { >+ handleApiError(ill_batch_create_api_fail); >+ } >+ }) >+ }); >+ }; >+ >+ function updateBatch() { >+ var selectedBranchcode = branchcodeSelect.selectedOptions[0].value; >+ var selectedStatuscode = statusesSelect.selectedOptions[0].value; >+ return doBatchApiRequest('/' + batch.data.id, { >+ method: 'PUT', >+ headers: { >+ 'Content-type': 'application/json' >+ }, >+ body: JSON.stringify({ >+ name: nameInput.value, >+ backend: batch.data.backend, >+ cardnumber: batch.data.patron.cardnumber, >+ branchcode: selectedBranchcode, >+ statuscode: selectedStatuscode >+ }) >+ }) >+ .catch(function () { >+ handleApiError(ill_batch_update_api_fail); >+ }); >+ }; >+ >+ function displaySupportedIdentifiers() { >+ var names = Object.keys(supportedIdentifiers).map(function (identifier) { >+ return window['ill_batch_' + identifier]; >+ }); >+ var displayEl = document.getElementById('supported_identifiers'); >+ displayEl.textContent = names.length > 0 ? names.join(', ') : ill_batch_none; >+ } >+ >+ function updateRowCount() { >+ var textEl = document.getElementById('row_count_value'); >+ var val = textarea.value.trim(); >+ var cnt = 0; >+ if (val.length > 0) { >+ cnt = val.split(/\n/).length; >+ } >+ textEl.textContent = cnt; >+ } >+ >+ function showProgress() { >+ var el = document.getElementById('create-progress'); >+ el.style.display = 'block'; >+ } >+ >+ function showCreateRequestsButton() { >+ var data = progressTotals.data; >+ var el = document.getElementById('create-requests'); >+ el.style.display = (data.total > 0 && data.count === data.total) ? 'flex' : 'none'; >+ } >+ >+ async function processIdentifiers() { >+ var content = textarea.value; >+ hideErrors(); >+ if (content.length === 0) return; >+ >+ disableProcessButton(); >+ var label = document.getElementById('progress-label').firstChild; >+ label.innerHTML = ill_batch_retrieving_metadata; >+ showProgress(); >+ >+ // Errors encountered when processing >+ var processErrors = {}; >+ >+ // Prepare the content, including trimming each row >+ var contentArr = content.split(/\n/); >+ var trimmed = contentArr.map(function (row) { >+ return row.trim(); >+ }); >+ >+ var parsed = []; >+ >+ trimmed.forEach(function (identifier) { >+ var match = identifyIdentifier(identifier); >+ // If this identifier is not identifiable or >+ // looks like more than one type, we can't be sure >+ // what it is >+ if (match.length != 1) { >+ parsed.push({ >+ type: 'unknown', >+ value: identifier >+ }); >+ } else { >+ parsed.push(match[0]); >+ } >+ }); >+ >+ var unknownIdentifiers = parsed >+ .filter(function (parse) { >+ if (parse.type == 'unknown') { >+ return parse; >+ } >+ }) >+ .map(function (filtered) { >+ return filtered.value; >+ }); >+ >+ if (unknownIdentifiers.length > 0) { >+ processErrors.badidentifiers = { >+ element: 'badids', >+ values: unknownIdentifiers.join(', ') >+ }; >+ }; >+ >+ // Deduping >+ var deduped = []; >+ var dupes = {}; >+ parsed.forEach(function (row) { >+ var value = row.value; >+ var alreadyInDeduped = deduped.filter(function (d) { >+ return d.value === value; >+ }); >+ if (alreadyInDeduped.length > 0 && !dupes[value]) { >+ dupes[value] = 1; >+ } else if (alreadyInDeduped.length === 0) { >+ row.metadata = {}; >+ row.failed = {}; >+ row.requestId = null; >+ deduped.push(row); >+ } >+ }); >+ // Update duplicate error if dupes were found >+ if (Object.keys(dupes).length > 0) { >+ processErrors.duplicates = { >+ element: 'dupelist', >+ values: Object.keys(dupes).join(', ') >+ }; >+ } >+ >+ // Display any errors >+ displayErrors(processErrors); >+ >+ // Now build and display the table >+ if (!table) { >+ buildTable(); >+ } >+ >+ // We may be appending new values to an existing table, >+ // in which case, ensure we don't create duplicates >+ var tabIdentifiers = tableContent.data.map(function (tabId) { >+ return tabId.value; >+ }); >+ var notInTable = deduped.filter(function (ded) { >+ if (!tabIdentifiers.includes(ded.value)) { >+ return ded; >+ } >+ }); >+ if (notInTable.length > 0) { >+ tableContent.data = tableContent.data.concat(notInTable); >+ } >+ >+ // Populate metadata for those records that need it >+ var newData = tableContent.data; >+ for (var i = 0; i < tableContent.data.length; i++) { >+ var row = tableContent.data[i]; >+ // Skip rows that don't need populating >+ if ( >+ Object.keys(tableContent.data[i].metadata).length > 0 || >+ Object.keys(tableContent.data[i].failed).length > 0 >+ ) continue; >+ var identifier = { type: row.type, value: row.value }; >+ try { >+ var populated = await populateMetadata(identifier); >+ row.metadata = populated.results.result || {}; >+ } catch (e) { >+ row.failed = ill_populate_failed; >+ } >+ newData[i] = row; >+ tableContent.data = newData; >+ } >+ } >+ >+ function disableProcessButton() { >+ processButton.setAttribute('disabled', true); >+ processButton.setAttribute('aria-disabled', true); >+ } >+ >+ function disableCreateButton() { >+ createButton.setAttribute('disabled', true); >+ createButton.setAttribute('aria-disabled', true); >+ } >+ >+ async function populateMetadata(identifier) { >+ // All services that support this identifier type >+ var services = supportedIdentifiers[identifier.type]; >+ // Check each service and use the first results we get, if any >+ for (var i = 0; i < services.length; i++) { >+ var service = services[i]; >+ var endpoint = '/api/v1/contrib/' + service.api_namespace + service.search_endpoint + '?' + identifier.type + '=' + identifier.value; >+ var metadata = await getMetadata(endpoint); >+ if (metadata.errors.length === 0) { >+ var parsed = await parseMetadata(metadata, service); >+ if (parsed.errors.length > 0) { >+ throw Error(metadata.errors.map(function (error) { >+ return error.message; >+ }).join(', ')); >+ } >+ return parsed; >+ } >+ } >+ }; >+ >+ async function getMetadata(endpoint) { >+ var response = await debounce(doApiRequest)(endpoint); >+ return response.json(); >+ }; >+ >+ async function parseMetadata(metadata, service) { >+ var endpoint = '/api/v1/contrib/' + service.api_namespace + service.ill_parse_endpoint; >+ var response = await doApiRequest(endpoint, { >+ method: 'POST', >+ headers: { >+ 'Content-type': 'application/json' >+ }, >+ body: JSON.stringify(metadata) >+ }); >+ return response.json(); >+ } >+ >+ // A render function for identifier type >+ function createIdentifierType(data) { >+ return window['ill_batch_' + data]; >+ }; >+ >+ // Get an item's title >+ function getTitle(meta) { >+ if (meta.article_title && meta.article_title.length > 0) { >+ return 'article_title'; >+ return { >+ prop: 'article_title', >+ value: meta.article_title >+ }; >+ } else if (meta.title && meta.title.length > 0) { >+ return 'title'; >+ return { >+ prop: 'title', >+ value: meta.title >+ }; >+ } >+ }; >+ >+ // Create a metadata row >+ function createMetadataRow(data, meta, prop) { >+ if (!meta[prop]) return; >+ >+ var div = document.createElement('div'); >+ div.classList.add('metadata-row'); >+ var label = document.createElement('span'); >+ label.classList.add('metadata-label'); >+ label.innerText = ill_batch_metadata[prop] + ': '; >+ >+ // Add a link to the availability URL if appropriate >+ var value; >+ if (!data.url) { >+ value = document.createElement('span'); >+ } else { >+ value = document.createElement('a'); >+ value.setAttribute('href', data.url); >+ value.setAttribute('target', '_blank'); >+ value.setAttribute('title', ill_batch_available_via + ' ' + data.availabilitySupplier); >+ } >+ value.classList.add('metadata-value'); >+ value.innerText = meta[prop]; >+ div.appendChild(label); >+ div.appendChild(value); >+ >+ return div; >+ } >+ >+ // A render function for displaying metadata >+ function createMetadata(x, y, data) { >+ // If the fetch failed >+ if (data.failed.length > 0) { >+ return data.failed; >+ } >+ >+ // If we've not yet got any metadata back >+ if (Object.keys(data.metadata).length === 0) { >+ return ill_populate_waiting; >+ } >+ >+ var core = ['doi', 'pmid', 'issn', 'title', 'year', 'issue', 'pages', 'publisher', 'article_title', 'article_author', 'volume']; >+ var meta = data.metadata; >+ >+ var container = document.createElement('div'); >+ container.classList.add('metadata-container'); >+ >+ // Create the title row >+ var title = getTitle(meta); >+ if (title) { >+ // Remove the title element from the props >+ // we're about to iterate >+ core = core.filter(function (i) { >+ return i !== title; >+ }); >+ var titleRow = createMetadataRow(data, meta, title); >+ container.appendChild(titleRow); >+ } >+ >+ var remainder = document.createElement('div'); >+ remainder.classList.add('metadata-remainder'); >+ remainder.style.display = 'none'; >+ // Create the remaining rows >+ core.sort().forEach(function (prop) { >+ var div = createMetadataRow(data, meta, prop); >+ if (div) { >+ remainder.appendChild(div); >+ } >+ }); >+ container.appendChild(remainder); >+ >+ // Add a more/less toggle >+ var firstField = container.firstChild; >+ var moreLess = document.createElement('div'); >+ moreLess.classList.add('more-less'); >+ var moreLessLink = document.createElement('a'); >+ moreLessLink.setAttribute('href', '#'); >+ moreLessLink.classList.add('more-less-link'); >+ moreLessLink.innerText = ' [' + ill_batch_metadata_more + ']'; >+ moreLess.appendChild(moreLessLink); >+ firstField.appendChild(moreLess); >+ >+ return container.outerHTML; >+ }; >+ >+ function removeRow(ev) { >+ if (ev.target.className.includes('remove-row')) { >+ if (!confirm(ill_batch_item_remove)) return; >+ // Find the parent row >+ var ancestor = ev.target.closest('tr'); >+ var identifier = ancestor.querySelector('.identifier').innerText; >+ tableContent.data = tableContent.data.filter(function (row) { >+ return row.value !== identifier; >+ }); >+ } >+ } >+ >+ function toggleMetadata(ev) { >+ if (ev.target.className === 'more-less-link') { >+ // Find the element we need to show >+ var ancestor = ev.target.closest('.metadata-container'); >+ var meta = ancestor.querySelector('.metadata-remainder'); >+ >+ // Display or hide based on its current state >+ var display = window.getComputedStyle(meta).display; >+ >+ meta.style.display = display === 'block' ? 'none' : 'block'; >+ >+ // Update the More / Less text >+ ev.target.innerText = ' [ ' + (display === 'none' ? ill_batch_metadata_less : ill_batch_metadata_more) + ' ]'; >+ } >+ } >+ >+ // A render function for the link to a request ID >+ function createRequestId(x, y, data) { >+ return data.requestId || '-'; >+ } >+ >+ function createRequestStatus(x, y, data) { >+ return data.requestStatus || '-'; >+ } >+ >+ function buildTable(identifiers) { >+ table = KohaTable('identifier-table', { >+ processing: true, >+ deferRender: true, >+ ordering: false, >+ paging: false, >+ searching: false, >+ autoWidth: false, >+ columns: [ >+ { >+ data: 'type', >+ width: '13%', >+ render: createIdentifierType >+ }, >+ { >+ data: 'value', >+ width: '25%', >+ className: 'identifier' >+ }, >+ { >+ data: 'metadata', >+ render: createMetadata >+ }, >+ { >+ data: 'requestId', >+ width: '6.5%', >+ render: createRequestId >+ }, >+ { >+ data: 'requestStatus', >+ width: '6.5%', >+ render: createRequestStatus >+ }, >+ { >+ width: '18%', >+ render: createActions, >+ className: 'action-column' >+ } >+ ], >+ createdRow: function (row, data) { >+ if (data.failed.length > 0) { >+ row.classList.add('fetch-failed'); >+ } >+ } >+ }); >+ } >+ >+ function createActions(x, y, data) { >+ return '<button type="button" aria-label='+ ill_button_remove + (data.requestId ? ' disabled aria-disabled="true"' : '') + ' class="btn btn-xs btn-danger remove-row">' + ill_button_remove + '</button>'; >+ } >+ >+ // Redraw the table >+ function updateTable() { >+ if (!table) return; >+ tableEl.style.display = tableContent.data.length > 0 ? 'table' : 'none'; >+ tableEl.style.width = '100%'; >+ table.api() >+ .clear() >+ .rows.add(tableContent.data) >+ .draw(); >+ }; >+ >+ function identifyIdentifier(identifier) { >+ var matches = []; >+ >+ // Iterate our available services to see if any can identify this identifier >+ Object.keys(supportedIdentifiers).forEach(function (identifierType) { >+ // Since all the services supporting this identifier type should use the same >+ // regex to identify it, we can just use the first >+ var service = supportedIdentifiers[identifierType][0]; >+ var regex = new RegExp(service.identifiers_supported[identifierType].regex); >+ var match = identifier.match(regex); >+ if (match && match.groups && match.groups.identifier) { >+ matches.push({ >+ type: identifierType, >+ value: match.groups.identifier >+ }); >+ } >+ }); >+ return matches; >+ } >+ >+ function displayErrors(errors) { >+ var keys = Object.keys(errors); >+ if (keys.length > 0) { >+ keys.forEach(function (key) { >+ var el = document.getElementById(errors[key].element); >+ el.textContent = errors[key].values; >+ el.style.display = 'inline'; >+ var container = document.getElementById(key); >+ container.style.display = 'block'; >+ }); >+ var el = document.getElementById('textarea-errors'); >+ el.style.display = 'flex'; >+ } >+ } >+ >+ function hideErrors() { >+ var dupelist = document.getElementById('dupelist'); >+ var badids = document.getElementById('badids'); >+ dupelist.textContent = ''; >+ dupelist.parentElement.style.display = 'none'; >+ badids.textContent = ''; >+ badids.parentElement.style.display = 'none'; >+ var tae = document.getElementById('textarea-errors'); >+ tae.style.display = 'none'; >+ } >+ >+ function manageBatchItemsDisplay() { >+ batchItemsDisplay.style.display = batch.data.id ? 'block' : 'none' >+ }; >+ >+ function updateBatchInputs() { >+ nameInput.value = batch.data.name || ''; >+ cardnumberInput.value = batch.data.cardnumber || ''; >+ branchcodeSelect.value = batch.data.branchcode || ''; >+ } >+ >+ function debounce(func) { >+ var timeout; >+ return function (...args) { >+ return new Promise(function (resolve) { >+ if (timeout) { >+ clearTimeout(timeout); >+ } >+ timeout = setTimeout(function () { >+ return resolve(func(...args)); >+ }, debounceDelay); >+ }); >+ } >+ } >+ >+ function patronAutocomplete() { >+ patron_autocomplete( >+ $('#batch-form #batchcardnumber'), >+ { >+ 'on-select-callback': function( event, ui ) { >+ $("#batch-form #batchcardnumber").val( ui.item.cardnumber ); >+ return false; >+ } >+ } >+ ); >+ }; >+ >+ function createPatronLink() { >+ if (!batch.data.patron) return; >+ var patron = batch.data.patron; >+ var a = document.createElement('a'); >+ var href = '/cgi-bin/koha/members/moremember.pl?borrowernumber=' + patron.borrowernumber; >+ var text = patron.surname + ' (' + patron.cardnumber + ')'; >+ a.setAttribute('title', ill_borrower_details); >+ a.setAttribute('href', href); >+ a.textContent = text; >+ return a; >+ }; >+ >+})(); >diff --git a/koha-tmpl/intranet-tmpl/prog/js/ill-batch-table.js b/koha-tmpl/intranet-tmpl/prog/js/ill-batch-table.js >new file mode 100644 >index 0000000000..605efba436 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/ill-batch-table.js >@@ -0,0 +1,216 @@ >+(function () { >+ var table; >+ var batchesProxy; >+ >+ window.addEventListener('load', onload); >+ >+ function onload() { >+ // Only proceed if appropriate >+ if (!document.getElementById('ill-batch-requests')) return; >+ >+ // A proxy that will give us some element of reactivity to >+ // changes in our list of batches >+ batchesProxy = new Proxy( >+ { data: [] }, >+ { >+ get: function (obj, prop) { >+ return obj[prop]; >+ }, >+ set: function (obj, prop, value) { >+ obj[prop] = value; >+ updateTable(); >+ } >+ } >+ ); >+ >+ // Initialise the Datatable, binding it to our proxy object >+ table = initTable(); >+ >+ // Do the initial data population >+ window.doBatchApiRequest() >+ .then(function (response) { >+ return response.json(); >+ }) >+ .then(function (data) { >+ batchesProxy.data = data; >+ }); >+ >+ // Clean up any event listeners we added >+ window.addEventListener('beforeunload', removeEventListeners); >+ }; >+ >+ // Initialise the Datatable >+ // FIXME: This should be a kohaTable not KohaTable >+ var initTable = function () { >+ return KohaTable("ill-batch-requests", { >+ data: batchesProxy.data, >+ columns: [ >+ { >+ data: 'id', >+ width: '10%' >+ }, >+ { >+ data: 'name', >+ render: createName, >+ width: '30%' >+ }, >+ { >+ data: 'requests_count', >+ width: '10%' >+ }, >+ { >+ data: 'status', >+ render: createStatus, >+ width: '10%' >+ }, >+ { >+ data: 'patron', >+ render: createPatronLink, >+ width: '10%' >+ }, >+ { >+ data: 'branch', >+ render: createBranch, >+ width: '20%' >+ }, >+ { >+ render: createActions, >+ width: '10%', >+ orderable: false >+ } >+ ], >+ processing: true, >+ deferRender: true, >+ drawCallback: addEventListeners >+ }); >+ } >+ >+ // A render function for branch name >+ var createBranch = function (data) { >+ return data.branchname; >+ }; >+ >+ // A render function for batch name >+ var createName = function (x, y, data) { >+ var a = document.createElement('a'); >+ a.setAttribute('href', '/cgi-bin/koha/ill/ill-requests.pl?batch_id=' + data.id); >+ a.setAttribute('title', data.name); >+ a.textContent = data.name; >+ return a.outerHTML; >+ }; >+ >+ // A render function for batch status >+ var createStatus = function (x, y, data) { >+ return data.status.name; >+ }; >+ >+ // A render function for our patron link >+ var createPatronLink = function (data) { >+ var link = document.createElement('a'); >+ link.setAttribute('title', ill_batch_borrower_details); >+ link.setAttribute('href', '/cgi-bin/koha/members/moremember.pl?borrowernumber=' + data.borrowernumber); >+ var displayText = [data.firstname, data.surname].join(' ') + ' ( ' + data.cardnumber + ' )'; >+ link.appendChild(document.createTextNode(displayText)); >+ >+ return link.outerHTML; >+ }; >+ >+ // A render function for our row action buttons >+ var createActions = function (data, type, row) { >+ var div = document.createElement('div'); >+ div.setAttribute('class', 'action-buttons'); >+ >+ var editButton = document.createElement('button'); >+ editButton.setAttribute('type', 'button'); >+ editButton.setAttribute('class', 'editButton btn btn-xs btn-default'); >+ editButton.setAttribute('data-batch-id', row.id); >+ editButton.appendChild(document.createTextNode(ill_batch_edit)); >+ >+ var deleteButton = document.createElement('button'); >+ deleteButton.setAttribute('type', 'button'); >+ deleteButton.setAttribute('class', 'deleteButton btn btn-xs btn-danger'); >+ deleteButton.setAttribute('data-batch-id', row.id); >+ deleteButton.appendChild(document.createTextNode(ill_batch_delete)); >+ >+ div.appendChild(editButton); >+ div.appendChild(deleteButton); >+ >+ return div.outerHTML; >+ }; >+ >+ // Add event listeners to our row action buttons >+ var addEventListeners = function () { >+ var del = document.querySelectorAll('.deleteButton'); >+ del.forEach(function (el) { >+ el.addEventListener('click', handleDeleteClick); >+ }); >+ >+ var edit = document.querySelectorAll('.editButton'); >+ edit.forEach(function (elEdit) { >+ elEdit.addEventListener('click', handleEditClick); >+ }); >+ }; >+ >+ // Remove all added event listeners >+ var removeEventListeners = function () { >+ var del = document.querySelectorAll('.deleteButton'); >+ del.forEach(function (el) { >+ el.removeEventListener('click', handleDeleteClick); >+ }); >+ window.removeEventListener('load', onload); >+ window.removeEventListener('beforeunload', removeEventListeners); >+ }; >+ >+ // Handle "Delete" clicks >+ var handleDeleteClick = function(e) { >+ var el = e.srcElement; >+ if (confirm(ill_batch_confirm_delete)) { >+ deleteBatch(el); >+ } >+ }; >+ >+ // Handle "Edit" clicks >+ var handleEditClick = function(e) { >+ var el = e.srcElement; >+ var id = el.dataset.batchId; >+ window.openBatchModal(id); >+ }; >+ >+ // Delete a batch >+ // - Make the API call >+ // - Handle errors >+ // - Update our proxy data >+ var deleteBatch = function (el) { >+ var id = el.dataset.batchId; >+ doBatchApiRequest( >+ '/' + id, >+ { method: 'DELETE' } >+ ) >+ .then(function (response) { >+ if (!response.ok) { >+ window.handleApiError(ill_batch_delete_fail); >+ } else { >+ removeBatch(el.dataset.batchId); >+ } >+ }) >+ .catch(function (response) { >+ window.handleApiError(ill_batch_delete_fail); >+ }) >+ }; >+ >+ // Remove a batch from our proxy data >+ var removeBatch = function(id) { >+ batchesProxy.data = batchesProxy.data.filter(function (batch) { >+ return batch.id != id; >+ }); >+ }; >+ >+ // Redraw the table >+ var updateTable = function () { >+ table.api() >+ .clear() >+ .rows.add(batchesProxy.data) >+ .draw(); >+ }; >+ >+})(); >diff --git a/koha-tmpl/intranet-tmpl/prog/js/ill-batch.js b/koha-tmpl/intranet-tmpl/prog/js/ill-batch.js >new file mode 100644 >index 0000000000..491c0a4374 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/ill-batch.js >@@ -0,0 +1,49 @@ >+(function () { >+ // Enable the modal to be opened from anywhere >+ // If we're working with an existing batch, set the ID so the >+ // modal can access it >+ window.openBatchModal = function (id, backend) { >+ var idEl = document.getElementById('ill-batch-details'); >+ idEl.dataset.backend = backend; >+ if (id) { >+ idEl.dataset.batchId = id; >+ } >+ $('#ill-batch-modal').modal({ show: true }); >+ }; >+ >+ // Make a batch API call, returning the resulting promise >+ window.doBatchApiRequest = function (url, options) { >+ var batchListApi = '/api/v1/illbatches'; >+ var fullUrl = batchListApi + (url ? url : ''); >+ return doApiRequest(fullUrl, options); >+ }; >+ >+ // Make a "create local ILL submission" call >+ window.doCreateSubmission = function (body, options) { >+ options = Object.assign( >+ options || {}, >+ { >+ headers: { >+ 'Content-Type': 'application/json' >+ }, >+ method: 'POST', >+ body: JSON.stringify(body) >+ } >+ ); >+ return doApiRequest( >+ '/api/v1/ill/requests', >+ options >+ ) >+ } >+ >+ // Make an API call, returning the resulting promise >+ window.doApiRequest = function (url, options) { >+ return fetch(url, options); >+ }; >+ >+ // Display an API error >+ window.handleApiError = function (error) { >+ alert(error); >+ }; >+ >+})(); >diff --git a/koha-tmpl/intranet-tmpl/prog/js/ill-list-table.js b/koha-tmpl/intranet-tmpl/prog/js/ill-list-table.js >index 9419b0fa5d..41b3184c92 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/ill-list-table.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/ill-list-table.js >@@ -51,7 +51,7 @@ $(document).ready(function() { > return ''; > } > >- // At the moment, the only prefilter possible is borrowernumber >+ // Possible prefilters: borrowernumber, batch_id > // see ill/ill-requests.pl and members/ill-requests.pl > let additional_prefilters = {}; > if(prefilters){ >@@ -63,6 +63,7 @@ $(document).ready(function() { > } > > let borrower_prefilter = additional_prefilters['borrowernumber'] || null; >+ let batch_id_prefilter = additional_prefilters['batch_id'] || null; > > let additional_filters = { > "me.backend": function(){ >@@ -78,6 +79,9 @@ $(document).ready(function() { > "me.borrowernumber": function(){ > return borrower_prefilter ? { "=": borrower_prefilter } : ""; > }, >+ "me.batch_id": function(){ >+ return batch_id_prefilter ? { "=": batch_id_prefilter } : ""; >+ }, > "-or": function(){ > let patron = $("#illfilter_patron").val(); > let status = $("#illfilter_status").val(); >@@ -119,7 +123,7 @@ $(document).ready(function() { > return filters; > }, > "me.placed": function(){ >- if ( Object.keys(additional_prefilters).length ) return ""; >+ if (Object.keys(additional_prefilters).length && borrower_prefilter) return ""; > let placed_start = $('#illfilter_dateplaced_start').get(0)._flatpickr.selectedDates[0]; > let placed_end = $('#illfilter_dateplaced_end').get(0)._flatpickr.selectedDates[0]; > if (!placed_start && !placed_end) return ""; >@@ -129,7 +133,7 @@ $(document).ready(function() { > } > }, > "me.updated": function(){ >- if (Object.keys(additional_prefilters).length) return ""; >+ if (Object.keys(additional_prefilters).length && borrower_prefilter) return ""; > let updated_start = $('#illfilter_datemodified_start').get(0)._flatpickr.selectedDates[0]; > let updated_end = $('#illfilter_datemodified_end').get(0)._flatpickr.selectedDates[0]; > if (!updated_start && !updated_end) return ""; >@@ -175,8 +179,11 @@ $(document).ready(function() { > }; > > let table_id = "#ill-requests"; >+ > if (borrower_prefilter) { > table_id += "-patron-" + borrower_prefilter; >+ } else if ( batch_id_prefilter ){ >+ table_id += "-batch-" + batch_id_prefilter; > } > > var ill_requests_table = $(table_id).kohaTable({ >@@ -188,6 +195,7 @@ $(document).ready(function() { > 'biblio', > 'comments+count', > 'extended_attributes', >+ 'batch', > 'library', > 'id_prefix', > 'patron' >@@ -206,6 +214,19 @@ $(document).ready(function() { > '">' + escape_str(row.id_prefix) + escape_str(data) + '</a>'; > } > }, >+ { >+ "data": "batch.name", // batch >+ "orderable": false, >+ "render": function(data, type, row, meta) { >+ return row.batch ? >+ '<a href="/cgi-bin/koha/ill/ill-requests.pl?batch_id=' + >+ row.batch_id + >+ '">' + >+ row.batch.name + >+ '</a>' >+ : ""; >+ } >+ }, > { > "data": "", // author > "orderable": false, >-- >2.30.2
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 30719
:
144281
|
144285
|
144286
|
144287
|
144288
|
144289
|
144290
|
144291
|
144292
|
144293
|
144294
|
144295
|
144296
|
144297
|
144298
|
144299
|
144300
|
144301
|
144302
|
145760
|
145869
|
145870
|
145876
|
145934
|
146152
|
147658
|
147929
|
147930
|
147931
|
147932
|
147933
|
147934
|
147952
|
147953
|
147954
|
147955
|
147956
|
151152
|
151153
|
151154
|
151155
|
151156
|
151284
|
151285
|
151286
|
151287
|
151288
|
151303
|
151304
|
151305
|
151306
|
151307
|
151308
|
151309
|
151310
|
151311
|
151312
|
151513
|
151514
|
151515
|
151516
|
151517
|
151519
|
151520
|
151521
|
151522
|
151523
|
151571
|
151572
|
151573
|
151574
|
151575
|
151611
|
151612
|
151613
|
151614
|
151615
|
151804
|
151805
|
151806
|
151807
|
151808
|
151809
|
154046
|
154047
|
154048
|
154049
|
154050
|
154051
|
154052
|
155421
|
155422
|
155423
|
155424
|
155425
|
155426
|
155427
|
155428
|
155429
|
155430
|
155431
|
155432
|
155433
|
155434
|
155435
|
156556
|
156748
|
156749
|
156752
|
156783
|
156785
|
156811
|
156812
|
156827
|
156828